diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..7adf043 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,47 @@ +name: Build and Push Docker Image + +on: + release: + types: [published] + +jobs: + docker: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ secrets.DOCKERHUB_USERNAME }}/${{ gitea.event.repository.name }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/${{ gitea.event.repository.name }}:buildcache + cache-to: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/${{ gitea.event.repository.name }}:buildcache,mode=max \ No newline at end of file diff --git a/.gitignore b/.gitignore index 18b44e7..a8dbef7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ .env -config/config.yaml storage -quay config \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..9e8fa74 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + sqlite.xerial + true + org.sqlite.JDBC + jdbc:sqlite:$PROJECT_DIR$/backend/config/db.sqlite + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..c0e01ca --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 94a25f7..317ebc3 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,5 +2,6 @@ + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 3f46c28..db9763a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,28 +1,53 @@ -FROM golang:1.25-alpine AS builder +# Stage 1: Build frontend +FROM node:20-alpine AS frontend-builder WORKDIR /app -RUN apk add --no-cache build-base libwebp-dev +COPY frontend/package.json frontend/pnpm-lock.yaml ./ + +RUN npm install -g pnpm \ + && pnpm install --frozen-lockfile + +COPY frontend . + +RUN pnpm run build + +# Stage 2: Build backend +FROM golang:1.25-alpine AS backend-builder + +WORKDIR /backend + +ARG TARGETOS +ARG TARGETARCH + +COPY backend/go.mod backend/go.sum ./ -COPY go.mod go.sum ./ RUN go mod download -COPY . . +COPY backend . -RUN go build -tags "fts5" -o quay +RUN CGO_ENABLED=0 \ + GOOS=$TARGETOS \ + GOARCH=$TARGETARCH \ + go build -o server ./ -FROM alpine:3.19 +# Stage 3: Final image +FROM alpine:latest +RUN apk add --no-cache ca-certificates tzdata -WORKDIR /app +# backend +COPY --from=backend-builder /backend/server /server -RUN apk add --no-cache libwebp libstdc++ +# frontend +COPY --from=frontend-builder /app/dist /app/dist -COPY --from=builder /app/quay . +RUN mkdir -p /config ENV PORT=4321 ENV CONFIG_DIR=/config ENV STORAGE_PATH=/storage +ENV STATIC_DASHBOARD_PATH=/app/dist EXPOSE 4321 -CMD ["./quay"] \ No newline at end of file +CMD ["/server"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2e71899 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Quay diff --git a/app/handlers/static.go b/app/handlers/static.go deleted file mode 100644 index a1349be..0000000 --- a/app/handlers/static.go +++ /dev/null @@ -1,86 +0,0 @@ -package handlers - -import ( - "os" - "path" - "path/filepath" - "quay/internal/config" - - "github.com/gofiber/fiber/v3" -) - -func NewStaticHandler(storagePath string, siteMap map[string]config.SiteConfig) fiber.Handler { - return func(c fiber.Ctx) error { - site, ok := siteMap[c.Hostname()] - if !ok { - return c.Status(fiber.StatusNotFound).SendString("Site not found") - } - - if !site.Enabled { - return c.Status(fiber.StatusServiceUnavailable).SendString("Site is currently unavailable") - } - - urlPath := filepath.Clean(c.Path()) - - for _, header := range site.CustomHeaders { - var matched bool - if header.Regex && header.Compiled != nil { - matched = header.Compiled.MatchString(urlPath) - } else { - matched, _ = path.Match(header.Source, urlPath) - } - if matched { - for key, value := range header.Headers { - c.Set(key, value) - } - } - } - - for _, rule := range site.ForwardRules { - if rule.Regex && rule.Compiled != nil { - match := rule.Compiled.FindStringSubmatchIndex(urlPath) - if match != nil { - dest := rule.Compiled.ExpandString([]byte{}, rule.Destination, urlPath, match) - return c.Redirect().Status(rule.StatusCode).To(string(dest)) - } - } else if rule.Source == urlPath { - return c.Redirect().Status(rule.StatusCode).To(rule.Destination) - } - } - - if urlPath == "/" || urlPath == "." { - urlPath = "/index.html" - } - - basePath := filepath.Join(storagePath, site.Name) - filePath := filepath.Join(basePath, urlPath) - - info, err := os.Stat(filePath) - if err == nil { - if info.IsDir() { - indexPath := filepath.Join(filePath, "index.html") - if _, err := os.Stat(indexPath); err == nil { - return c.SendFile(indexPath) - } - } else { - return c.SendFile(filePath) - } - } - - if site.SPA { - indexPath := filepath.Join(basePath, "index.html") - if _, err := os.Stat(indexPath); err == nil { - return c.SendFile(indexPath) - } - } - - if site.NotFoundFile != "" { - notFoundPath := filepath.Join(basePath, site.NotFoundFile) - if _, err := os.Stat(notFoundPath); err == nil { - return c.SendFile(notFoundPath) - } - } - - return c.SendStatus(fiber.StatusNotFound) - } -} diff --git a/app/handlers/update.go b/app/handlers/update.go deleted file mode 100644 index 623871c..0000000 --- a/app/handlers/update.go +++ /dev/null @@ -1,85 +0,0 @@ -package handlers - -import ( - "path/filepath" - "quay/app/github" - "quay/app/models" - "quay/internal/config" - "quay/internal/envconfig" - "strings" - - "crypto/subtle" - - "github.com/gofiber/fiber/v3" -) - -func NewUpdateSiteHandler(cfg *config.Config, envCfg *envconfig.EnvConfig) fiber.Handler { - return func(c fiber.Ctx) error { - sitename := c.Query("site") - if sitename == "" { - return c.Status(400).JSON(models.APIError{ - Error: "Missing 'site' query parameter", - }) - } - - var siteConfig *config.SiteConfig - for _, site := range cfg.Sites { - if site.Name == sitename { - siteConfig = &site - break - } - } - - if siteConfig == nil { - return c.Status(404).JSON(models.APIError{ - Error: "Site not found", - }) - } - - deployToken := siteConfig.DeployToken - if deployToken == "" { - return c.Status(500).JSON(models.APIError{ - Error: "Deploy token not configured for this site", - }) - } - - providedToken := c.Get("Authorization") - if strings.HasPrefix(providedToken, "Bearer ") { - providedToken = strings.TrimPrefix(providedToken, "Bearer ") - } - if providedToken == "" { - return c.Status(401).JSON(models.APIError{ - Error: "Missing Authorization header", - }) - } - - if subtle.ConstantTimeCompare([]byte(providedToken), []byte(deployToken)) != 1 { - return c.Status(403).JSON(models.APIError{ - Error: "Invalid deploy token", - }) - } - - sitePath := filepath.Join(envCfg.StoragePath, siteConfig.Name) - if _, err := filepath.Abs(sitePath); err != nil { - return c.Status(500).JSON(models.APIError{ - Error: "Failed to resolve site path", - }) - } - - err := github.FetchAndDeployBranch( - siteConfig.Owner, - siteConfig.Repo, - siteConfig.Branch, - envCfg.GithubPat, - sitePath, - ) - - if err != nil { - return c.Status(500).JSON(models.APIError{ - Error: "Failed to deploy site: " + err.Error(), - }) - } - - return c.SendStatus(201) - } -} diff --git a/app/routes/routes.go b/app/routes/routes.go deleted file mode 100644 index 71e4e7c..0000000 --- a/app/routes/routes.go +++ /dev/null @@ -1,30 +0,0 @@ -package routes - -import ( - "log" - "path/filepath" - "quay/app/handlers" - "quay/internal/config" - "quay/internal/envconfig" - - "github.com/gofiber/fiber/v3" -) - -func Register(app *fiber.App, cfg *config.Config, envCfg *envconfig.EnvConfig) { - api := app.Group("/api") - api.Get("/health", handlers.HealthCheck) - - api.Post("/update", handlers.NewUpdateSiteHandler(cfg, envCfg)) - - storagePath, err := filepath.Abs(envCfg.StoragePath) - if err != nil { - log.Fatalf("Failed to resolve storage path: %v", err) - } - - siteMap := make(map[string]config.SiteConfig) - for _, site := range cfg.Sites { - siteMap[site.Domain] = site - } - - app.Use(handlers.NewStaticHandler(storagePath, siteMap)) -} diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..18b44e7 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,5 @@ +.env +config/config.yaml +storage +quay +config \ No newline at end of file diff --git a/backend/.idea/.gitignore b/backend/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/backend/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/backend/.idea/backend.iml b/backend/.idea/backend.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/backend/.idea/backend.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/backend/.idea/dataSources.xml b/backend/.idea/dataSources.xml new file mode 100644 index 0000000..8b5266e --- /dev/null +++ b/backend/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + sqlite.xerial + true + org.sqlite.JDBC + jdbc:sqlite:$PROJECT_DIR$/config/db.sqlite + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/backend/.idea/go.imports.xml b/backend/.idea/go.imports.xml new file mode 100644 index 0000000..644cdf0 --- /dev/null +++ b/backend/.idea/go.imports.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/backend/.idea/modules.xml b/backend/.idea/modules.xml new file mode 100644 index 0000000..e066844 --- /dev/null +++ b/backend/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/backend/.idea/vcs.xml b/backend/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/backend/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/backend/app/cachedrepo/site.go b/backend/app/cachedrepo/site.go new file mode 100644 index 0000000..e8a77f3 --- /dev/null +++ b/backend/app/cachedrepo/site.go @@ -0,0 +1,268 @@ +package cachedrepo + +import ( + "quay/app/models" + "quay/app/repository" + + "github.com/maypok86/otter/v2" +) + +type CachedSiteRepository struct { + inner repository.SiteRepository + sites *otter.Cache[string, *models.Site] + list *otter.Cache[string, []models.Site] + forwardRules *otter.Cache[string, *models.ForwardRule] + customHeaders *otter.Cache[string, *models.CustomHeaders] + headers *otter.Cache[string, *models.Header] +} + +const siteListKey = "__list__" + +func NewCachedSiteRepository(inner repository.SiteRepository) *CachedSiteRepository { + return &CachedSiteRepository{ + inner: inner, + sites: otter.Must(&otter.Options[string, *models.Site]{ + MaximumSize: 1_000, + }), + list: otter.Must(&otter.Options[string, []models.Site]{ + MaximumSize: 1, + }), + forwardRules: otter.Must(&otter.Options[string, *models.ForwardRule]{ + MaximumSize: 10_000, + }), + customHeaders: otter.Must(&otter.Options[string, *models.CustomHeaders]{ + MaximumSize: 10_000, + }), + headers: otter.Must(&otter.Options[string, *models.Header]{ + MaximumSize: 10_000, + }), + } +} + +var _ repository.SiteRepository = (*CachedSiteRepository)(nil) + +// Sites + +func (c *CachedSiteRepository) GetSite(id string) (*models.Site, error) { + if s, ok := c.sites.GetIfPresent(id); ok { + return s, nil + } + s, err := c.inner.GetSite(id) + if err != nil { + return nil, err + } + c.sites.Set(id, s) + return s, nil +} + +func (c *CachedSiteRepository) GetSiteByDomain(domain string) (*models.Site, error) { + if s, ok := c.sites.GetIfPresent(domain); ok { + return s, nil + } + s, err := c.inner.GetSiteByDomain(domain) + if err != nil { + return nil, err + } + c.sites.Set(domain, s) + return s, nil +} + +func (c *CachedSiteRepository) ListSites() ([]models.Site, error) { + if sites, ok := c.list.GetIfPresent(siteListKey); ok { + return sites, nil + } + sites, err := c.inner.ListSites() + if err != nil { + return nil, err + } + c.list.Set(siteListKey, sites) + for i := range sites { + c.sites.Set(sites[i].ID, &sites[i]) + } + return sites, nil +} + +func (c *CachedSiteRepository) CreateSite(s *models.Site) error { + if err := c.inner.CreateSite(s); err != nil { + return err + } + c.sites.Set(s.ID, s) + c.list.Invalidate(siteListKey) + return nil +} + +func (c *CachedSiteRepository) UpdateSite(s *models.Site) error { + if err := c.inner.UpdateSite(s); err != nil { + return err + } + c.sites.Set(s.ID, s) + c.list.Invalidate(siteListKey) + return nil +} + +func (c *CachedSiteRepository) ToggleEnabled(id string) (bool, error) { + enabled, err := c.inner.ToggleEnabled(id) + if err != nil { + return false, err + } + c.sites.Invalidate(id) + c.list.Invalidate(siteListKey) + return enabled, nil +} + +func (c *CachedSiteRepository) DeleteSite(id string) error { + if err := c.inner.DeleteSite(id); err != nil { + return err + } + c.sites.Invalidate(id) + c.list.Invalidate(siteListKey) + return nil +} + +// Forward Rules + +func (c *CachedSiteRepository) GetForwardRule(id string) (*models.ForwardRule, error) { + if fr, ok := c.forwardRules.GetIfPresent(id); ok { + return fr, nil + } + fr, err := c.inner.GetForwardRule(id) + if err != nil { + return nil, err + } + c.forwardRules.Set(id, fr) + return fr, nil +} + +func (c *CachedSiteRepository) CreateForwardRule(siteID string, fr *models.ForwardRule) error { + if err := c.inner.CreateForwardRule(siteID, fr); err != nil { + return err + } + c.forwardRules.Set(fr.ID, fr) + c.sites.Invalidate(siteID) + c.list.Invalidate(siteListKey) + return nil +} + +func (c *CachedSiteRepository) UpdateForwardRule(siteID string, fr *models.ForwardRule) error { + if err := c.inner.UpdateForwardRule(siteID, fr); err != nil { + return err + } + c.forwardRules.Set(fr.ID, fr) + c.sites.Invalidate(siteID) + c.list.Invalidate(siteListKey) + return nil +} + +func (c *CachedSiteRepository) DeleteForwardRule(siteID string, id string) error { + if err := c.inner.DeleteForwardRule(siteID, id); err != nil { + return err + } + c.forwardRules.Invalidate(id) + c.sites.Invalidate(siteID) + c.list.Invalidate(siteListKey) + return nil +} + +// Custom Headers + +func (c *CachedSiteRepository) GetCustomHeaders(id string) (*models.CustomHeaders, error) { + if ch, ok := c.customHeaders.GetIfPresent(id); ok { + return ch, nil + } + ch, err := c.inner.GetCustomHeaders(id) + if err != nil { + return nil, err + } + c.customHeaders.Set(id, ch) + return ch, nil +} + +func (c *CachedSiteRepository) CreateCustomHeaders(siteID string, ch *models.CustomHeaders) error { + if err := c.inner.CreateCustomHeaders(siteID, ch); err != nil { + return err + } + c.customHeaders.Set(ch.ID, ch) + c.sites.Invalidate(siteID) + c.list.Invalidate(siteListKey) + return nil +} + +func (c *CachedSiteRepository) UpdateCustomHeaders(siteID string, ch *models.CustomHeaders) error { + if err := c.inner.UpdateCustomHeaders(siteID, ch); err != nil { + return err + } + c.customHeaders.Set(ch.ID, ch) + c.sites.Invalidate(siteID) + c.list.Invalidate(siteListKey) + return nil +} + +func (c *CachedSiteRepository) DeleteCustomHeaders(siteID string, id string) error { + if err := c.inner.DeleteCustomHeaders(siteID, id); err != nil { + return err + } + c.customHeaders.Invalidate(id) + c.sites.Invalidate(siteID) + c.list.Invalidate(siteListKey) + return nil +} + +// Headers + +func (c *CachedSiteRepository) GetHeader(id string) (*models.Header, error) { + if h, ok := c.headers.GetIfPresent(id); ok { + return h, nil + } + h, err := c.inner.GetHeader(id) + if err != nil { + return nil, err + } + c.headers.Set(id, h) + return h, nil +} + +func (c *CachedSiteRepository) CreateHeader(siteID string, customHeaderID string, h *models.Header) error { + if err := c.inner.CreateHeader(siteID, customHeaderID, h); err != nil { + return err + } + c.headers.Set(h.ID, h) + c.customHeaders.Invalidate(customHeaderID) + c.sites.Invalidate(siteID) + c.list.Invalidate(siteListKey) + return nil +} + +func (c *CachedSiteRepository) UpdateHeader(siteID string, h *models.Header) error { + if err := c.inner.UpdateHeader(siteID, h); err != nil { + return err + } + c.headers.Set(h.ID, h) + c.sites.Invalidate(siteID) + c.list.Invalidate(siteListKey) + return nil +} + +func (c *CachedSiteRepository) DeleteHeader(siteID string, id string) error { + if err := c.inner.DeleteHeader(siteID, id); err != nil { + return err + } + c.headers.Invalidate(id) + c.sites.Invalidate(siteID) + c.list.Invalidate(siteListKey) + return nil +} + +// Invalidate explicitly evicts all cached data. +func (c *CachedSiteRepository) Invalidate() { + c.sites.InvalidateAll() + c.list.InvalidateAll() + c.forwardRules.InvalidateAll() + c.customHeaders.InvalidateAll() + c.headers.InvalidateAll() +} + +// InvalidateSite evicts a single site and marks the list stale. +func (c *CachedSiteRepository) InvalidateSite(id string) { + c.sites.Invalidate(id) + c.list.Invalidate(siteListKey) +} diff --git a/backend/app/gitprovider/common.go b/backend/app/gitprovider/common.go new file mode 100644 index 0000000..a250a19 --- /dev/null +++ b/backend/app/gitprovider/common.go @@ -0,0 +1,105 @@ +package gitprovider + +import ( + "archive/zip" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "strings" +) + +// unzip extracts the zip at src into the dest directory, +// guarding against zip-slip path traversal attacks. +func unzip(src, dest string) error { + r, err := zip.OpenReader(src) + if err != nil { + return err + } + defer r.Close() + + destClean := filepath.Clean(dest) + string(os.PathSeparator) + + for _, f := range r.File { + target := filepath.Join(dest, filepath.Clean("/"+f.Name)) + if !strings.HasPrefix(target, destClean) { + return fmt.Errorf("zip slip attempt: %q resolves outside destination", f.Name) + } + + if f.FileInfo().IsDir() { + if err = os.MkdirAll(target, f.Mode()); err != nil { + return err + } + continue + } + + if err = os.MkdirAll(filepath.Dir(target), 0o755); err != nil { + return err + } + + if err = writeZipEntry(f, target); err != nil { + return err + } + } + return nil +} + +func writeZipEntry(f *zip.File, target string) error { + rc, err := f.Open() + if err != nil { + return err + } + defer rc.Close() + + out, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, rc) + return err +} + +// copyDir recursively copies src to dst, preserving file modes. +// Used as a cross-device fallback when os.Rename fails. +func copyDir(src, dst string) error { + return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + rel, err := filepath.Rel(src, path) + if err != nil { + return err + } + target := filepath.Join(dst, rel) + + if d.IsDir() { + return os.MkdirAll(target, 0o755) + } + + info, err := d.Info() + if err != nil { + return err + } + return copyFile(path, target, info.Mode()) + }) +} + +func copyFile(src, dst string, mode fs.FileMode) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, in) + return err +} diff --git a/backend/app/gitprovider/factory.go b/backend/app/gitprovider/factory.go new file mode 100644 index 0000000..cc2a7fb --- /dev/null +++ b/backend/app/gitprovider/factory.go @@ -0,0 +1,31 @@ +package gitprovider + +import ( + "fmt" + "strings" +) + +func NewProvider(serverType, protocol, baseUrl, authToken string) (GitProvider, error) { + switch strings.ToLower(serverType) { + case "github": + return &GitHubProvider{ + protocol: protocol, + baseUrl: baseUrl, + authToken: authToken, + }, nil + case "gitea": + return &GiteaProvider{ + protocol: protocol, + baseUrl: baseUrl, + authToken: authToken, + }, nil + case "gitlab": + return &GitLabProvider{ + protocol: protocol, + baseUrl: baseUrl, + authToken: authToken, + }, nil + default: + return nil, fmt.Errorf("unsupported git provider: %s", serverType) + } +} diff --git a/backend/app/gitprovider/gitea.go b/backend/app/gitprovider/gitea.go new file mode 100644 index 0000000..a51265f --- /dev/null +++ b/backend/app/gitprovider/gitea.go @@ -0,0 +1,141 @@ +package gitprovider + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" +) + +type GiteaProvider struct { + protocol string + baseUrl string + authToken string +} + +var _ GitProvider = (*GiteaProvider)(nil) + +func (p *GiteaProvider) FetchAndDeployBranch(owner, repo, branch, destPath string) (*DeployResult, error) { + apiUrl := fmt.Sprintf("%s://%s/api/v1/", p.protocol, strings.TrimSuffix(p.baseUrl, "/")) + + result, err := fetchGiteaBranchHead(apiUrl, owner, repo, branch, p.authToken) + if err != nil { + return nil, fmt.Errorf("fetching branch head: %w", err) + } + + if err = downloadAndExtractGitea(apiUrl, owner, repo, branch, p.authToken, destPath); err != nil { + return nil, err + } + + return result, nil +} + +func fetchGiteaBranchHead(apiUrl, owner, repo, branch, token string) (*DeployResult, error) { + url := fmt.Sprintf("%srepos/%s/%s/branches/%s", apiUrl, owner, repo, branch) + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("creating request: %w", err) + } + req.Header.Set("Authorization", "token "+token) + req.Header.Set("Accept", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("fetching branch: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("Gitea returned %s for %s", resp.Status, url) + } + + // GET /repos/{owner}/{repo}/branches/{branch} returns a Branch object + // whose commit field is a PayloadCommit. + var payload struct { + Commit struct { + ID string `json:"id"` + Message string `json:"message"` + } `json:"commit"` + } + if err = json.NewDecoder(resp.Body).Decode(&payload); err != nil { + return nil, fmt.Errorf("decoding branch response: %w", err) + } + + return &DeployResult{ + CommitHash: payload.Commit.ID, + CommitMessage: strings.SplitN(payload.Commit.Message, "\n", 2)[0], + }, nil +} + +func downloadAndExtractGitea(apiUrl, owner, repo, branch, token, destDir string) error { + // GET /repos/{owner}/{repo}/archive/{sha}.zip + archiveURL := fmt.Sprintf("%srepos/%s/%s/archive/%s.zip", apiUrl, owner, repo, branch) + + req, err := http.NewRequest(http.MethodGet, archiveURL, nil) + if err != nil { + return fmt.Errorf("creating request: %w", err) + } + req.Header.Set("Authorization", "token "+token) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("fetching archive: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("Gitea returned %s for %s", resp.Status, archiveURL) + } + + storageRoot := filepath.Dir(destDir) + + tmpZip, err := os.CreateTemp(storageRoot, "giteabranch-*.zip") + if err != nil { + return fmt.Errorf("creating temp zip: %w", err) + } + defer os.Remove(tmpZip.Name()) + + if _, err = io.Copy(tmpZip, resp.Body); err != nil { + tmpZip.Close() + return fmt.Errorf("writing zip: %w", err) + } + tmpZip.Close() + + tmpDir, err := os.MkdirTemp(storageRoot, "giteabranch-unpack-*") + if err != nil { + return fmt.Errorf("creating temp dir: %w", err) + } + defer os.RemoveAll(tmpDir) + + if err = unzip(tmpZip.Name(), tmpDir); err != nil { + return fmt.Errorf("unzipping: %w", err) + } + + // Gitea wraps everything in {repo}-{branch}/ (all lowercase). + entries, err := os.ReadDir(tmpDir) + if err != nil { + return fmt.Errorf("reading temp dir: %w", err) + } + if len(entries) != 1 || !entries[0].IsDir() { + return fmt.Errorf("unexpected archive layout: expected a single root directory") + } + extractedRoot := filepath.Join(tmpDir, entries[0].Name()) + + if err = os.MkdirAll(filepath.Dir(destDir), 0o755); err != nil { + return fmt.Errorf("creating destination parent dirs: %w", err) + } + os.RemoveAll(destDir) + + if err = os.Rename(extractedRoot, destDir); err == nil { + return nil + } + + if err = copyDir(extractedRoot, destDir); err != nil { + return fmt.Errorf("cross-device copy to destination: %w", err) + } + return nil +} diff --git a/app/github/github.go b/backend/app/gitprovider/github.go similarity index 50% rename from app/github/github.go rename to backend/app/gitprovider/github.go index 2bb90b5..7b02bd1 100644 --- a/app/github/github.go +++ b/backend/app/gitprovider/github.go @@ -1,17 +1,81 @@ -package github +package gitprovider import ( - "archive/zip" + "encoding/json" "fmt" "io" - "io/fs" "net/http" "os" "path/filepath" "strings" ) -func FetchAndDeployBranch(repoOwner, repoName, branch, pat, destDir string) error { +type GitHubProvider struct { + protocol string + baseUrl string + authToken string +} + +var _ GitProvider = (*GitHubProvider)(nil) + +func (p *GitHubProvider) FetchAndDeployBranch(owner, repo, branch, destPath string) (*DeployResult, error) { + baseUrl := strings.TrimSuffix(p.baseUrl, "/") + apiUrl := fmt.Sprintf("%s://api.%s/", p.protocol, baseUrl) + + result, err := fetchBranchHead(apiUrl, owner, repo, branch, p.authToken) + if err != nil { + return nil, fmt.Errorf("fetching branch head: %w", err) + } + + if err = downloadAndExtract(owner, repo, branch, p.authToken, destPath); err != nil { + return nil, err + } + + return result, nil +} + +// fetchBranchHead returns the SHA and commit message for the tip of the given branch. +func fetchBranchHead(apiUrl, owner, repo, branch, pat string) (*DeployResult, error) { + url := fmt.Sprintf( + "%srepos/%s/%s/commits/%s", + apiUrl, owner, repo, branch, + ) + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("creating request: %w", err) + } + req.Header.Set("Authorization", "Bearer "+pat) + req.Header.Set("Accept", "application/vnd.github+json") + req.Header.Set("X-GitHub-Api-Version", "2022-11-28") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("fetching commit: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("GitHub returned %s for %s", resp.Status, url) + } + + var payload struct { + SHA string `json:"sha"` + Commit struct { + Message string `json:"message"` + } `json:"commit"` + } + if err = json.NewDecoder(resp.Body).Decode(&payload); err != nil { + return nil, fmt.Errorf("decoding commit response: %w", err) + } + + return &DeployResult{ + CommitHash: payload.SHA, + CommitMessage: strings.SplitN(payload.Commit.Message, "\n", 2)[0], + }, nil +} + +func downloadAndExtract(repoOwner, repoName, branch, pat, destDir string) error { archiveURL := fmt.Sprintf( "https://api.github.com/repos/%s/%s/zipball/%s", repoOwner, repoName, branch, @@ -59,7 +123,7 @@ func FetchAndDeployBranch(repoOwner, repoName, branch, pat, destDir string) erro return fmt.Errorf("unzipping: %w", err) } - // github wraps everything in one top-level folder ({owner}-{repo}-{sha}/). + // GitHub wraps everything in one top-level folder ({owner}-{repo}-{sha}/). entries, err := os.ReadDir(tmpDir) if err != nil { return fmt.Errorf("reading temp dir: %w", err) @@ -83,97 +147,3 @@ func FetchAndDeployBranch(repoOwner, repoName, branch, pat, destDir string) erro } return nil } - -// unzip extracts the zip at src into the dest directory, -// guarding against zip-slip path traversal attacks. -func unzip(src, dest string) error { - r, err := zip.OpenReader(src) - if err != nil { - return err - } - defer r.Close() - - destClean := filepath.Clean(dest) + string(os.PathSeparator) - - for _, f := range r.File { - target := filepath.Join(dest, filepath.Clean("/"+f.Name)) - if !strings.HasPrefix(target, destClean) { - return fmt.Errorf("zip slip attempt: %q resolves outside destination", f.Name) - } - - if f.FileInfo().IsDir() { - if err = os.MkdirAll(target, f.Mode()); err != nil { - return err - } - continue - } - - if err = os.MkdirAll(filepath.Dir(target), 0o755); err != nil { - return err - } - - if err = writeZipEntry(f, target); err != nil { - return err - } - } - return nil -} - -func writeZipEntry(f *zip.File, target string) error { - rc, err := f.Open() - if err != nil { - return err - } - defer rc.Close() - - out, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) - if err != nil { - return err - } - defer out.Close() - - _, err = io.Copy(out, rc) - return err -} - -// copyDir recursively copies src to dst, preserving file modes. -// Used as a cross-device fallback when os.Rename fails. -func copyDir(src, dst string) error { - return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - rel, err := filepath.Rel(src, path) - if err != nil { - return err - } - target := filepath.Join(dst, rel) - - if d.IsDir() { - return os.MkdirAll(target, 0o755) - } - - info, err := d.Info() - if err != nil { - return err - } - return copyFile(path, target, info.Mode()) - }) -} - -func copyFile(src, dst string, mode fs.FileMode) error { - in, err := os.Open(src) - if err != nil { - return err - } - defer in.Close() - - out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode) - if err != nil { - return err - } - defer out.Close() - - _, err = io.Copy(out, in) - return err -} diff --git a/backend/app/gitprovider/gitlab.go b/backend/app/gitprovider/gitlab.go new file mode 100644 index 0000000..70ee225 --- /dev/null +++ b/backend/app/gitprovider/gitlab.go @@ -0,0 +1,143 @@ +package gitprovider + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" +) + +type GitLabProvider struct { + protocol string + baseUrl string + authToken string +} + +var _ GitProvider = (*GitLabProvider)(nil) + +func (p *GitLabProvider) FetchAndDeployBranch(owner, repo, branch, destPath string) (*DeployResult, error) { + apiUrl := fmt.Sprintf("%s://%s/api/v4/", p.protocol, strings.TrimSuffix(p.baseUrl, "/")) + + result, err := fetchGitLabBranchHead(apiUrl, owner, repo, branch, p.authToken) + if err != nil { + return nil, fmt.Errorf("fetching branch head: %w", err) + } + + if err = downloadAndExtractGitLab(apiUrl, owner, repo, branch, p.authToken, destPath); err != nil { + return nil, err + } + + return result, nil +} + +func fetchGitLabBranchHead(apiUrl, owner, repo, branch, token string) (*DeployResult, error) { + // GitLab identifies projects by URL-encoded "namespace/repo" path. + projectID := url.PathEscape(owner + "/" + repo) + encodedBranch := url.PathEscape(branch) + reqUrl := fmt.Sprintf("%sprojects/%s/repository/branches/%s", apiUrl, projectID, encodedBranch) + + req, err := http.NewRequest(http.MethodGet, reqUrl, nil) + if err != nil { + return nil, fmt.Errorf("creating request: %w", err) + } + req.Header.Set("PRIVATE-TOKEN", token) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("fetching branch: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("GitLab returned %s for %s", resp.Status, reqUrl) + } + + var payload struct { + Commit struct { + ID string `json:"id"` + Title string `json:"title"` // first line of the commit message + } `json:"commit"` + } + if err = json.NewDecoder(resp.Body).Decode(&payload); err != nil { + return nil, fmt.Errorf("decoding branch response: %w", err) + } + + return &DeployResult{ + CommitHash: payload.Commit.ID, + CommitMessage: payload.Commit.Title, + }, nil +} + +func downloadAndExtractGitLab(apiUrl, owner, repo, branch, token, destDir string) error { + // GET /projects/:id/repository/archive.zip?sha= + projectID := url.PathEscape(owner + "/" + repo) + archiveURL := fmt.Sprintf("%sprojects/%s/repository/archive.zip?sha=%s", apiUrl, projectID, url.QueryEscape(branch)) + + req, err := http.NewRequest(http.MethodGet, archiveURL, nil) + if err != nil { + return fmt.Errorf("creating request: %w", err) + } + req.Header.Set("PRIVATE-TOKEN", token) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("fetching archive: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("GitLab returned %s for %s", resp.Status, archiveURL) + } + + storageRoot := filepath.Dir(destDir) + + tmpZip, err := os.CreateTemp(storageRoot, "gitlabbranch-*.zip") + if err != nil { + return fmt.Errorf("creating temp zip: %w", err) + } + defer os.Remove(tmpZip.Name()) + + if _, err = io.Copy(tmpZip, resp.Body); err != nil { + tmpZip.Close() + return fmt.Errorf("writing zip: %w", err) + } + tmpZip.Close() + + tmpDir, err := os.MkdirTemp(storageRoot, "gitlabbranch-unpack-*") + if err != nil { + return fmt.Errorf("creating temp dir: %w", err) + } + defer os.RemoveAll(tmpDir) + + if err = unzip(tmpZip.Name(), tmpDir); err != nil { + return fmt.Errorf("unzipping: %w", err) + } + + // GitLab names the root folder {repo}-{branch}-{sha}/. + entries, err := os.ReadDir(tmpDir) + if err != nil { + return fmt.Errorf("reading temp dir: %w", err) + } + if len(entries) != 1 || !entries[0].IsDir() { + return fmt.Errorf("unexpected archive layout: expected a single root directory") + } + extractedRoot := filepath.Join(tmpDir, entries[0].Name()) + + if err = os.MkdirAll(filepath.Dir(destDir), 0o755); err != nil { + return fmt.Errorf("creating destination parent dirs: %w", err) + } + os.RemoveAll(destDir) + + if err = os.Rename(extractedRoot, destDir); err == nil { + return nil + } + + if err = copyDir(extractedRoot, destDir); err != nil { + return fmt.Errorf("cross-device copy to destination: %w", err) + } + return nil +} diff --git a/backend/app/gitprovider/provider.go b/backend/app/gitprovider/provider.go new file mode 100644 index 0000000..ba945db --- /dev/null +++ b/backend/app/gitprovider/provider.go @@ -0,0 +1,10 @@ +package gitprovider + +type DeployResult struct { + CommitHash string + CommitMessage string +} + +type GitProvider interface { + FetchAndDeployBranch(owner, repo, branch, destPath string) (*DeployResult, error) +} diff --git a/backend/app/handlers/auth.go b/backend/app/handlers/auth.go new file mode 100644 index 0000000..a5d19da --- /dev/null +++ b/backend/app/handlers/auth.go @@ -0,0 +1,52 @@ +package handlers + +import ( + "log" + "quay/app/repository" + "quay/internal/security" + "time" + + "github.com/gofiber/fiber/v3" +) + +type AuthHandler struct { + Repo repository.UserRepository +} + +func NewAuthHandler(repo repository.UserRepository) *AuthHandler { + return &AuthHandler{Repo: repo} +} + +type LoginRequest struct { + Name string `json:"name"` + Password string `json:"password"` +} + +type LoginResponse struct { + Token string `json:"token"` +} + +func (h *AuthHandler) Login(c fiber.Ctx) error { + var req LoginRequest + if err := c.Bind().Body(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request"}) + } + + user, err := h.Repo.GetUserByName(req.Name) + if err != nil || user == nil { + log.Println("login: user lookup failed", err) + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid credentials"}) + } + + if !security.CheckPasswordHash(req.Password, user.HashedPassword) { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid credentials"}) + } + + token, err := security.GenerateToken(user.ID, user.Role, 24*time.Hour) + if err != nil { + log.Println("login: token generation failed", err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to generate token"}) + } + + return c.JSON(LoginResponse{Token: token}) +} diff --git a/backend/app/handlers/deploy.go b/backend/app/handlers/deploy.go new file mode 100644 index 0000000..65e8f0a --- /dev/null +++ b/backend/app/handlers/deploy.go @@ -0,0 +1,185 @@ +package handlers + +import ( + "database/sql" + "errors" + "log" + "path/filepath" + "quay/app/gitprovider" + "quay/app/models" + "quay/app/repository" + "quay/internal/envconfig" + "quay/internal/security" + "strings" + "time" + + "github.com/gofiber/fiber/v3" +) + +type DeploySiteHandler struct { + EnvCfg *envconfig.EnvConfig + SiteRepo repository.SiteRepository + GitServerRepo repository.GitServerRepository + DeploymentRepo repository.DeploymentRepository +} + +func NewDeploySiteHandler( + envCfg *envconfig.EnvConfig, + siteRepo repository.SiteRepository, + gitServerRepo repository.GitServerRepository, + deploymentRepo repository.DeploymentRepository) *DeploySiteHandler { + return &DeploySiteHandler{ + EnvCfg: envCfg, + SiteRepo: siteRepo, + GitServerRepo: gitServerRepo, + DeploymentRepo: deploymentRepo, + } +} + +func failDeployment(deployId string, repo repository.DeploymentRepository) { + deployUpdateErr := repo.UpdateDeploymentStatus(deployId, models.DeploymentStatusFailed) + if deployUpdateErr != nil { + log.Println("Error updating deployment status to failed: ", deployUpdateErr) + } + deployUpdateErr = repo.UpdateDeploymentFinishTime(deployId, time.Now().UTC()) + if deployUpdateErr != nil { + log.Println("Error updating deployment finish time: ", deployUpdateErr) + } +} + +func (h *DeploySiteHandler) PostDeploy(c fiber.Ctx) error { + siteId := c.Query("site") + if siteId == "" { + return c.Status(400).JSON(models.APIError{ + Message: "Missing 'site' query parameter", + }) + } + + site, err := h.SiteRepo.GetSite(siteId) + + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{ + Message: "Site not found", + }) + } + log.Println("Error getting site: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{ + Message: "Unexpected error while getting site", + }) + } + + if site == nil { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{ + Message: "Site not found", + }) + } + + gitServer, err := h.GitServerRepo.GetGitServer(site.GitServer) + + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{ + Message: "Git server not found", + }) + } + log.Println("Error getting git server: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{ + Message: "Unexpected error while getting git server", + }) + } + + if gitServer == nil { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{ + Message: "Git server not found", + }) + } + + deployToken := site.DeployToken + if deployToken == "" { + log.Println("No deploy token configured for site " + siteId) + return c.Status(500).JSON(models.APIError{ + Message: "Deploy token not configured for this site", + }) + } + + providedToken := c.Get("Authorization") + if strings.HasPrefix(providedToken, "Bearer ") { + providedToken = strings.TrimPrefix(providedToken, "Bearer ") + } + if providedToken == "" { + return c.Status(401).JSON(models.APIError{ + Message: "Missing Authorization header", + }) + } + + if security.CompareDeployTokens(providedToken, deployToken) == false { + return c.Status(403).JSON(models.APIError{ + Message: "Invalid deploy token", + }) + } + + sitePath := filepath.Join(h.EnvCfg.StoragePath, site.ID) + if _, err := filepath.Abs(sitePath); err != nil { + log.Println("Error getting absolute path of site: ", err) + return c.Status(500).JSON(models.APIError{ + Message: "Failed to resolve site path", + }) + } + + deployId, err := h.DeploymentRepo.CreateDeployment(&models.Deployment{ + SiteId: siteId, + Status: models.DeploymentStatusRunning, + StartTime: time.Now().UTC().Format(time.DateTime), + }) + + if err != nil { + log.Println("Error creating deployment: ", err) + failDeployment(deployId, h.DeploymentRepo) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{ + Message: "Failed to create deployment", + }) + } + + provider, err := gitprovider.NewProvider(gitServer.Type, gitServer.Protocol, gitServer.BaseUrl, gitServer.AuthToken) + if err != nil { + failDeployment(deployId, h.DeploymentRepo) + return c.Status(400).JSON(models.APIError{ + Message: "Unsupported git provider: " + gitServer.Type, + }) + } + + res, err := provider.FetchAndDeployBranch( + site.Owner, + site.Repository, + site.Branch, + sitePath, + ) + + if res != nil { + deployUpdateErr := h.DeploymentRepo.UpdateDeploymentGitInfo(deployId, res.CommitHash, res.CommitMessage) + if deployUpdateErr != nil { + log.Println("Error updating deployment git info: ", err) + } + } + + if err != nil { + log.Println("Error deploying site: ", err) + failDeployment(deployId, h.DeploymentRepo) + + return c.Status(500).JSON(models.APIError{ + Message: "Failed to deploy site: " + err.Error(), + }) + } + + deployUpdateErr := h.DeploymentRepo.UpdateDeploymentStatus(deployId, models.DeploymentStatusSuccess) + if deployUpdateErr != nil { + log.Println("Error updating deployment status to success: ", err) + } + deployUpdateErr = h.DeploymentRepo.UpdateDeploymentFinishTime(deployId, time.Now().UTC()) + if deployUpdateErr != nil { + log.Println("Error updating deployment finish time: ", err) + } + + return c.SendStatus(201) +} diff --git a/backend/app/handlers/deployment.go b/backend/app/handlers/deployment.go new file mode 100644 index 0000000..086532e --- /dev/null +++ b/backend/app/handlers/deployment.go @@ -0,0 +1,91 @@ +package handlers + +import ( + "database/sql" + "errors" + "log" + "quay/app/models" + "quay/app/repository" + "strconv" + + "github.com/gofiber/fiber/v3" +) + +type DeploymentHandler struct { + Repo repository.DeploymentRepository +} + +func NewDeploymentHandler(repo repository.DeploymentRepository) *DeploymentHandler { + return &DeploymentHandler{Repo: repo} +} + +// GetDeployment godoc +// @Summary Get deployment by ID +// @Description Get a single deployment by its ID +// @Tags Deployments +// @Accept json +// @Produce json +// @Param id path string true "Deployment ID" +// @Success 200 {object} models.Deployment +// @Failure 404 {object} models.APIError +// @Failure 500 {object} models.APIError +// @Router /deployments/{id} [get] +func (h *DeploymentHandler) GetDeployment(c fiber.Ctx) error { + id := c.Params("id") + + deployment, err := h.Repo.GetDeploymentByID(id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{ + Message: "Deployment not found", + }) + } + log.Println("Error getting deployment by id: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{ + Message: "Unexpected error while getting deployment by id", + }) + } + + return c.Status(fiber.StatusOK).JSON(&deployment) +} + +// GetDeploymentsBySite godoc +// @Summary Get deployments for a site +// @Description Get a list of deployments for a specific site +// @Tags Deployments +// @Accept json +// @Produce json +// @Param siteId path string true "Site ID" +// @Param limit query int false "Maximum number of deployments to return" default(100) +// @Success 200 {object} models.GetDeploymentsBySiteResponse +// @Failure 500 {object} models.APIError +// @Router /sites/{siteId}/deployments [get] +func (h *DeploymentHandler) GetDeploymentsBySite(c fiber.Ctx) error { + siteId := c.Params("id") + limit, err := strconv.Atoi(c.Query("limit", "100")) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{ + Message: "Invalid limit", + }) + } + + deployments, err := h.Repo.GetDeploymentsForSite(siteId, limit) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.JSON(models.GetDeploymentsBySiteResponse{ + Deployments: []models.Deployment{}, + Total: 0, + }) + } + + log.Println("Error getting deployments: ", err) + return c.JSON(models.APIError{ + Message: "Unexpected error while getting deployments: ", + }) + } + + return c.JSON(models.GetDeploymentsBySiteResponse{ + Deployments: deployments, + Total: len(deployments), + }) +} diff --git a/backend/app/handlers/gitserver.go b/backend/app/handlers/gitserver.go new file mode 100644 index 0000000..04c28d5 --- /dev/null +++ b/backend/app/handlers/gitserver.go @@ -0,0 +1,123 @@ +package handlers + +import ( + "database/sql" + "errors" + "log" + "quay/app/models" + "quay/app/repository" + + "github.com/gofiber/fiber/v3" +) + +type GitServerHandler struct { + Repo repository.GitServerRepository +} + +func NewGitServerHandler(repo repository.GitServerRepository) *GitServerHandler { + return &GitServerHandler{Repo: repo} +} + +func (h *GitServerHandler) GetGitServers(c fiber.Ctx) error { + gs, err := h.Repo.ListGitServers() + if err != nil { + log.Println("Error listing gitservers: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while listing git servers"}) + } + if gs == nil { + gs = []models.GitServer{} + } + return c.JSON(gs) +} + +func (h *GitServerHandler) GetGitServer(c fiber.Ctx) error { + id := c.Params("id") + g, err := h.Repo.GetGitServer(id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Git server not found"}) + } + log.Println("Error getting gitserver: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while getting git server"}) + } + if g == nil { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Git server not found"}) + } + return c.JSON(g) +} + +func validateIncomingGitServer(gs *models.GitServer) error { + return models.ValidateGitServer(gs) +} + +func (h *GitServerHandler) PostGitServer(c fiber.Ctx) error { + var gs models.GitServer + if err := c.Bind().Body(&gs); err != nil { + log.Println("Error parsing body: ", err) + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body"}) + } + + if err := validateIncomingGitServer(&gs); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body: " + err.Error()}) + } + + if err := h.Repo.CreateGitServer(&gs); err != nil { + log.Println("Error creating gitserver: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while creating git server"}) + } + + return c.JSON(gs) +} + +func (h *GitServerHandler) PutGitServer(c fiber.Ctx) error { + id := c.Params("id") + if _, err := h.Repo.GetGitServer(id); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Git server not found"}) + } + log.Println("Error checking gitserver before update: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while updating git server"}) + } + + var gs models.GitServer + if err := c.Bind().Body(&gs); err != nil { + log.Println("Error parsing body: ", err) + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body"}) + } + + if err := validateIncomingGitServer(&gs); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body: " + err.Error()}) + } + + gs.ID = id + if err := h.Repo.UpdateGitServer(&gs); err != nil { + log.Println("Error updating gitserver: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while updating git server"}) + } + + updated, err := h.Repo.GetGitServer(id) + if err != nil { + log.Println("Error getting updated gitserver: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Git server was updated but could not be retrieved"}) + } + + return c.JSON(updated) +} + +func (h *GitServerHandler) DeleteGitServer(c fiber.Ctx) error { + id := c.Params("id") + if _, err := h.Repo.GetGitServer(id); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Git server not found"}) + } + log.Println("Error checking gitserver before delete: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while deleting git server"}) + } + + if err := h.Repo.DeleteGitServer(id); err != nil { + log.Println("Error deleting gitserver: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while deleting git server"}) + } + + return c.SendStatus(fiber.StatusNoContent) +} diff --git a/app/handlers/health.go b/backend/app/handlers/health.go similarity index 100% rename from app/handlers/health.go rename to backend/app/handlers/health.go diff --git a/backend/app/handlers/site.go b/backend/app/handlers/site.go new file mode 100644 index 0000000..b4fa9a0 --- /dev/null +++ b/backend/app/handlers/site.go @@ -0,0 +1,334 @@ +package handlers + +import ( + "database/sql" + "errors" + "log" + "quay/app/models" + "quay/app/repository" + "quay/internal/security" + + "github.com/gofiber/fiber/v3" +) + +type SiteHandler struct { + Repo repository.SiteRepository +} + +func NewSiteHandler(repo repository.SiteRepository) *SiteHandler { + return &SiteHandler{Repo: repo} +} + +// GetSites godoc +// @Summary Get all sites +// @Description Get a list of all sites +// @Tags Sites +// @Accept json +// @Produce json +// @Success 200 {object} models.GetAllSitesResponse +// @Failure 500 {object} models.APIError +// @Router /sites [get] +func (h *SiteHandler) GetSites(c fiber.Ctx) error { + sites, err := h.Repo.ListSites() + + if err != nil { + log.Println("Error listing sites: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{ + Message: "Unexpected error while listing sites", + }) + } + + if sites == nil { + sites = []models.Site{} + } + + return c.JSON(models.GetAllSitesResponse{ + Sites: sites, + Total: len(sites), + }) +} + +// GetSite godoc +// @Summary Get site by ID +// @Description Get a single site by its ID +// @Tags Sites +// @Accept json +// @Produce json +// @Param id path string true "Site ID" +// @Success 200 {object} models.Site +// @Failure 404 {object} models.APIError +// @Failure 500 {object} models.APIError +// @Router /sites/{id} [get] +func (h *SiteHandler) GetSite(c fiber.Ctx) error { + id := c.Params("id") + + site, err := h.Repo.GetSite(id) + + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{ + Message: "Site not found", + }) + } + log.Println("Message getting site: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{ + Message: "Unexpected error while getting site", + }) + } + + if site == nil { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{ + Message: "Site not found", + }) + } + + return c.JSON(site) +} + +func validateIncomingSite(site *models.Site) error { + if site == nil { + return errors.New("site is required") + } + if site.Name == "" { + return errors.New("site name is required") + } + if site.GitServer == "" { + return errors.New("git server required") + } + if site.Owner == "" { + return errors.New("owner required") + } + if site.Repository == "" { + return errors.New("repository required") + } + if site.Branch == "" { + return errors.New("branch required") + } + if site.Domain == "" { + return errors.New("domain required") + } + if site.NotFoundFile == "" { + site.NotFoundFile = "404.html" + } + if site.IndexFile == "" { + site.IndexFile = "index.html" + } + if site.ForwardRules == nil { + site.ForwardRules = []models.ForwardRule{} + } + if site.ForwardRules != nil { + for _, r := range site.ForwardRules { + if r.Source == "" { + return errors.New("forward rule source is required") + } + if r.Destination == "" { + return errors.New("forward rule destination is required") + } + if r.StatusCode < 300 || r.StatusCode > 399 { + return errors.New("forward rule status code must be between 300 and 399") + } + } + } + if site.CustomHeaders == nil { + site.CustomHeaders = []models.CustomHeaders{} + } + if site.CustomHeaders != nil { + for _, h := range site.CustomHeaders { + if h.Source == "" { + return errors.New("custom header source required") + } + if h.Headers == nil { + return errors.New("custom header is required") + } + } + } + return nil +} + +// PostSite godoc +// @Summary Create a new site +// @Description Create a new site with the provided details +// @Tags Sites +// @Accept json +// @Produce json +// @Param site body models.Site true "Site details" +// @Success 200 {object} models.CreateSiteResponse +// @Failure 400 {object} models.APIError +// @Failure 500 {object} models.APIError +// @Router /sites [post] +func (h *SiteHandler) PostSite(c fiber.Ctx) error { + var site models.Site + + if err := c.Bind().Body(&site); err != nil { + log.Println("Error parsing body: ", err) + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{ + Message: "Invalid request body", + }) + } + + if err := validateIncomingSite(&site); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{ + Message: "Invalid request body: " + err.Error(), + }) + } + + rawDeployToken, hashedDeployToken, err := security.CreateDeployToken() + + if err != nil { + log.Println("Error creating deploy token: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{ + Message: "Unexpected error while creating deploy token: " + err.Error(), + }) + } + + site.DeployToken = hashedDeployToken + + if err := h.Repo.CreateSite(&site); err != nil { + log.Println("Error creating site: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{ + Message: "Unexpected error while creating site", + }) + } + + return c.JSON(&models.CreateSiteResponse{ + Site: site, + RawDeployToken: rawDeployToken, + }) +} + +// PutSite godoc +// @Summary Update an existing site +// @Description Update an existing site by its ID +// @Tags Sites +// @Accept json +// @Produce json +// @Param id path string true "Site ID" +// @Param site body models.Site true "Updated site details" +// @Success 200 {object} models.Site +// @Failure 400 {object} models.APIError +// @Failure 404 {object} models.APIError +// @Failure 500 {object} models.APIError +// @Router /sites/{id} [put] +func (h *SiteHandler) PutSite(c fiber.Ctx) error { + id := c.Params("id") + + if _, err := h.Repo.GetSite(id); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{ + Message: "Site not found", + }) + } + log.Println("Error checking site before update: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{ + Message: "Unexpected error while updating site", + }) + } + + var site models.Site + if err := c.Bind().Body(&site); err != nil { + log.Println("Error parsing body: ", err) + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{ + Message: "Invalid request body", + }) + } + + if err := validateIncomingSite(&site); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{ + Message: "Invalid request body: " + err.Error(), + }) + } + + site.ID = id + if err := h.Repo.UpdateSite(&site); err != nil { + log.Println("Error updating site: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{ + Message: "Unexpected error while updating site", + }) + } + + updatedSite, err := h.Repo.GetSite(id) + if err != nil { + log.Println("Error getting updated site: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{ + Message: "Site was updated but could not be retrieved", + }) + } + + return c.JSON(updatedSite) +} + +// ToggleEnabled godoc +// @Summary Toggle site enabled status +// @Description Enable or disable a site by its ID +// @Tags Sites +// @Accept json +// @Produce json +// @Param id path string true "Site ID" +// @Success 200 {object} models.ToggleEnabledResponse +// @Failure 404 {object} models.APIError +// @Failure 500 {object} models.APIError +// @Router /sites/{id}/enabled [patch] +func (h *SiteHandler) ToggleEnabled(c fiber.Ctx) error { + id := c.Params("id") + + if _, err := h.Repo.GetSite(id); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{ + Message: "Site not found", + }) + } + log.Println("Error checking site before update: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{ + Message: "Unexpected error while updating site", + }) + } + + enabled, err := h.Repo.ToggleEnabled(id) + + if err != nil { + log.Println("Error while toggling enabled: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{ + Message: "Unexpected error while toggling enabled", + }) + } + + return c.Status(fiber.StatusOK).JSON(&models.ToggleEnabledResponse{ + Enabled: enabled, + }) +} + +// DeleteSite godoc +// @Summary Delete a site +// @Description Delete a site by its ID +// @Tags Sites +// @Accept json +// @Produce json +// @Param id path string true "Site ID" +// @Success 204 +// @Failure 404 {object} models.APIError +// @Failure 500 {object} models.APIError +// @Router /sites/{id} [delete] +func (h *SiteHandler) DeleteSite(c fiber.Ctx) error { + id := c.Params("id") + + if _, err := h.Repo.GetSite(id); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{ + Message: "Site not found", + }) + } + log.Println("Error checking site before delete: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{ + Message: "Unexpected error while deleting site", + }) + } + + if err := h.Repo.DeleteSite(id); err != nil { + log.Println("Error deleting site: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{ + Message: "Unexpected error while deleting site", + }) + } + + return c.SendStatus(fiber.StatusNoContent) +} diff --git a/backend/app/handlers/site_relations.go b/backend/app/handlers/site_relations.go new file mode 100644 index 0000000..8de8b2b --- /dev/null +++ b/backend/app/handlers/site_relations.go @@ -0,0 +1,598 @@ +package handlers + +import ( + "database/sql" + "errors" + "log" + "quay/app/models" + + "github.com/gofiber/fiber/v3" +) + +func validateForwardRule(rule *models.ForwardRule) error { + if rule == nil { + return errors.New("forward rule is required") + } + if rule.Source == "" { + return errors.New("forward rule source is required") + } + if rule.Destination == "" { + return errors.New("forward rule destination is required") + } + if rule.StatusCode < 300 || rule.StatusCode > 399 { + return errors.New("forward rule status code must be between 300 and 399") + } + return nil +} + +func validateHeader(header *models.Header) error { + if header == nil { + return errors.New("header is required") + } + if header.Key == "" { + return errors.New("header key is required") + } + if header.Value == "" { + return errors.New("header value is required") + } + return nil +} + +func validateCustomHeaders(customHeaders *models.CustomHeaders) error { + if customHeaders == nil { + return errors.New("custom headers are required") + } + if customHeaders.Source == "" { + return errors.New("custom header source required") + } + if customHeaders.Headers == nil { + customHeaders.Headers = []models.Header{} + } + for i := range customHeaders.Headers { + if err := validateHeader(&customHeaders.Headers[i]); err != nil { + return err + } + } + return nil +} + +// GetSiteForwardRules godoc +// +// @Summary List forward rules for a site +// @Description Returns all forward rules associated with the given site +// @Tags Forward-Rules +// @Produce json +// @Param id path string true "Site ID" +// @Success 200 {array} models.ForwardRule "List of forward rules" +// @Failure 404 {object} models.APIError "Site not found" +// @Failure 500 {object} models.APIError "Internal server error" +// @Router /sites/{id}/forward-rules [get] +func (h *SiteHandler) GetSiteForwardRules(c fiber.Ctx) error { + siteID := c.Params("id") + + site, err := h.Repo.GetSite(siteID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Site not found"}) + } + log.Println("Error getting site forward rules: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while listing forward rules"}) + } + + if site.ForwardRules == nil { + site.ForwardRules = []models.ForwardRule{} + } + return c.JSON(site.ForwardRules) +} + +// GetForwardRule godoc +// +// @Summary Get a forward rule +// @Description Returns a single forward rule by ID +// @Tags Forward-Rules +// @Produce json +// @Param id path string true "Forward Rule ID" +// @Success 200 {object} models.ForwardRule "Forward rule" +// @Failure 404 {object} models.APIError "Forward rule not found" +// @Failure 500 {object} models.APIError "Internal server error" +// @Router /forward-rules/{id} [get] +func (h *SiteHandler) GetForwardRule(c fiber.Ctx) error { + id := c.Params("ruleId") + + rule, err := h.Repo.GetForwardRule(id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Forward rule not found"}) + } + log.Println("Error getting forward rule: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while getting forward rule"}) + } + + return c.JSON(rule) +} + +// PostForwardRule godoc +// +// @Summary Create a forward rule +// @Description Creates a new forward rule for the given site +// @Tags Forward-Rules +// @Accept json +// @Produce json +// @Param id path string true "Site ID" +// @Param rule body models.ForwardRule true "Forward rule to create" +// @Success 200 {object} models.ForwardRule "Created forward rule" +// @Failure 400 {object} models.APIError "Invalid request body" +// @Failure 404 {object} models.APIError "Site not found" +// @Failure 500 {object} models.APIError "Internal server error" +// @Router /sites/{id}/forward-rules [post] +func (h *SiteHandler) PostForwardRule(c fiber.Ctx) error { + siteID := c.Params("id") + + if _, err := h.Repo.GetSite(siteID); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Site not found"}) + } + log.Println("Error checking site before creating forward rule: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while creating forward rule"}) + } + + var rule models.ForwardRule + if err := c.Bind().Body(&rule); err != nil { + log.Println("Error parsing body: ", err) + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body"}) + } + + if err := validateForwardRule(&rule); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body: " + err.Error()}) + } + + if err := h.Repo.CreateForwardRule(siteID, &rule); err != nil { + log.Println("Error creating forward rule: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while creating forward rule"}) + } + + return c.JSON(rule) +} + +// PutForwardRule godoc +// +// @Summary Update a forward rule +// @Description Replaces an existing forward rule by ID +// @Tags Forward-Rules +// @Accept json +// @Produce json +// @Param id path string true "Forward Rule ID" +// @Param rule body models.ForwardRule true "Updated forward rule" +// @Success 200 {object} models.ForwardRule "Updated forward rule" +// @Failure 400 {object} models.APIError "Invalid request body" +// @Failure 404 {object} models.APIError "Forward rule not found" +// @Failure 500 {object} models.APIError "Internal server error" +// @Router /forward-rules/{id} [put] +func (h *SiteHandler) PutForwardRule(c fiber.Ctx) error { + siteID := c.Params("id") + id := c.Params("ruleId") + + if _, err := h.Repo.GetForwardRule(id); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Forward rule not found"}) + } + log.Println("Error checking forward rule before update: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while updating forward rule"}) + } + + var rule models.ForwardRule + if err := c.Bind().Body(&rule); err != nil { + log.Println("Error parsing body: ", err) + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body"}) + } + + if err := validateForwardRule(&rule); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body: " + err.Error()}) + } + + rule.ID = id + if err := h.Repo.UpdateForwardRule(siteID, &rule); err != nil { + log.Println("Error updating forward rule: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while updating forward rule"}) + } + + updated, err := h.Repo.GetForwardRule(id) + if err != nil { + log.Println("Error getting updated forward rule: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Forward rule was updated but could not be retrieved"}) + } + + return c.JSON(updated) +} + +// DeleteForwardRule godoc +// +// @Summary Delete a forward rule +// @Description Deletes a forward rule by ID +// @Tags Forward-Rules +// @Produce json +// @Param id path string true "Forward Rule ID" +// @Success 204 "No content" +// @Failure 404 {object} models.APIError "Forward rule not found" +// @Failure 500 {object} models.APIError "Internal server error" +// @Router /forward-rules/{id} [delete] +func (h *SiteHandler) DeleteForwardRule(c fiber.Ctx) error { + siteID := c.Params("id") + id := c.Params("ruleId") + + if _, err := h.Repo.GetForwardRule(id); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Forward rule not found"}) + } + log.Println("Error checking forward rule before delete: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while deleting forward rule"}) + } + + if err := h.Repo.DeleteForwardRule(siteID, id); err != nil { + log.Println("Error deleting forward rule: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while deleting forward rule"}) + } + + return c.SendStatus(fiber.StatusNoContent) +} + +// GetSiteCustomHeaders godoc +// +// @Summary List custom header groups for a site +// @Description Returns all custom header groups associated with the given site +// @Tags Custom-Headers +// @Produce json +// @Param id path string true "Site ID" +// @Success 200 {array} models.CustomHeaders "List of custom header groups" +// @Failure 404 {object} models.APIError "Site not found" +// @Failure 500 {object} models.APIError "Internal server error" +// @Router /sites/{id}/custom-headers [get] +func (h *SiteHandler) GetSiteCustomHeaders(c fiber.Ctx) error { + siteID := c.Params("id") + + site, err := h.Repo.GetSite(siteID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Site not found"}) + } + log.Println("Error getting site custom headers: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while listing custom headers"}) + } + + if site.CustomHeaders == nil { + site.CustomHeaders = []models.CustomHeaders{} + } + return c.JSON(site.CustomHeaders) +} + +// GetCustomHeaders godoc +// +// @Summary Get a custom header group +// @Description Returns a single custom header group by ID +// @Tags Custom-Headers +// @Produce json +// @Param id path string true "Custom Headers ID" +// @Success 200 {object} models.CustomHeaders "Custom header group" +// @Failure 404 {object} models.APIError "Custom headers not found" +// @Failure 500 {object} models.APIError "Internal server error" +// @Router /custom-headers/{id} [get] +func (h *SiteHandler) GetCustomHeaders(c fiber.Ctx) error { + id := c.Params("customHeaderId") + + customHeaders, err := h.Repo.GetCustomHeaders(id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Custom headers not found"}) + } + log.Println("Error getting custom headers: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while getting custom headers"}) + } + + return c.JSON(customHeaders) +} + +// PostCustomHeaders godoc +// +// @Summary Create a custom header group +// @Description Creates a new custom header group for the given site +// @Tags Custom-Headers +// @Accept json +// @Produce json +// @Param id path string true "Site ID" +// @Param customHeaders body models.CustomHeaders true "Custom header group to create" +// @Success 200 {object} models.CustomHeaders "Created custom header group" +// @Failure 400 {object} models.APIError "Invalid request body" +// @Failure 404 {object} models.APIError "Site not found" +// @Failure 500 {object} models.APIError "Internal server error" +// @Router /sites/{id}/custom-headers [post] +func (h *SiteHandler) PostCustomHeaders(c fiber.Ctx) error { + siteID := c.Params("id") + + if _, err := h.Repo.GetSite(siteID); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Site not found"}) + } + log.Println("Error checking site before creating custom headers: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while creating custom headers"}) + } + + var customHeaders models.CustomHeaders + if err := c.Bind().Body(&customHeaders); err != nil { + log.Println("Error parsing body: ", err) + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body"}) + } + + if err := validateCustomHeaders(&customHeaders); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body: " + err.Error()}) + } + + if err := h.Repo.CreateCustomHeaders(siteID, &customHeaders); err != nil { + log.Println("Error creating custom headers: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while creating custom headers"}) + } + + return c.JSON(customHeaders) +} + +// PutCustomHeaders godoc +// +// @Summary Update a custom header group +// @Description Replaces an existing custom header group by ID +// @Tags Custom-Headers +// @Accept json +// @Produce json +// @Param id path string true "Custom Headers ID" +// @Param customHeaders body models.CustomHeaders true "Updated custom header group" +// @Success 200 {object} models.CustomHeaders "Updated custom header group" +// @Failure 400 {object} models.APIError "Invalid request body" +// @Failure 404 {object} models.APIError "Custom headers not found" +// @Failure 500 {object} models.APIError "Internal server error" +// @Router /custom-headers/{id} [put] +func (h *SiteHandler) PutCustomHeaders(c fiber.Ctx) error { + siteID := c.Params("id") + id := c.Params("customHeaderId") + + if _, err := h.Repo.GetCustomHeaders(id); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Custom headers not found"}) + } + log.Println("Error checking custom headers before update: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while updating custom headers"}) + } + + var customHeaders models.CustomHeaders + if err := c.Bind().Body(&customHeaders); err != nil { + log.Println("Error parsing body: ", err) + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body"}) + } + + if err := validateCustomHeaders(&customHeaders); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body: " + err.Error()}) + } + + customHeaders.ID = id + if err := h.Repo.UpdateCustomHeaders(siteID, &customHeaders); err != nil { + log.Println("Error updating custom headers: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while updating custom headers"}) + } + + updated, err := h.Repo.GetCustomHeaders(id) + if err != nil { + log.Println("Error getting updated custom headers: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Custom headers were updated but could not be retrieved"}) + } + + return c.JSON(updated) +} + +// DeleteCustomHeaders godoc +// +// @Summary Delete a custom header group +// @Description Deletes a custom header group by ID +// @Tags Custom-Headers +// @Produce json +// @Param id path string true "Custom Headers ID" +// @Success 204 "No content" +// @Failure 404 {object} models.APIError "Custom headers not found" +// @Failure 500 {object} models.APIError "Internal server error" +// @Router /custom-headers/{id} [delete] +func (h *SiteHandler) DeleteCustomHeaders(c fiber.Ctx) error { + siteID := c.Params("id") + id := c.Params("customHeaderId") + + if _, err := h.Repo.GetCustomHeaders(id); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Custom headers not found"}) + } + log.Println("Error checking custom headers before delete: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while deleting custom headers"}) + } + + if err := h.Repo.DeleteCustomHeaders(siteID, id); err != nil { + log.Println("Error deleting custom headers: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while deleting custom headers"}) + } + + return c.SendStatus(fiber.StatusNoContent) +} + +// GetCustomHeaderHeaders godoc +// +// @Summary List headers in a custom header group +// @Description Returns all individual headers belonging to the given custom header group +// @Tags Custom-Headers +// @Produce json +// @Param id path string true "Custom Headers ID" +// @Success 200 {array} models.Header "List of headers" +// @Failure 404 {object} models.APIError "Custom headers not found" +// @Failure 500 {object} models.APIError "Internal server error" +// @Router /custom-headers/{id}/headers [get] +func (h *SiteHandler) GetCustomHeaderHeaders(c fiber.Ctx) error { + customHeaderID := c.Params("customHeaderId") + + customHeaders, err := h.Repo.GetCustomHeaders(customHeaderID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Custom headers not found"}) + } + log.Println("Error listing headers: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while listing headers"}) + } + + if customHeaders.Headers == nil { + customHeaders.Headers = []models.Header{} + } + return c.JSON(customHeaders.Headers) +} + +// GetHeader godoc +// +// @Summary Get a header +// @Description Returns a single header by ID +// @Tags Headers +// @Produce json +// @Param id path string true "Header ID" +// @Success 200 {object} models.Header "Header" +// @Failure 404 {object} models.APIError "Header not found" +// @Failure 500 {object} models.APIError "Internal server error" +// @Router /headers/{id} [get] +func (h *SiteHandler) GetHeader(c fiber.Ctx) error { + id := c.Params("headerId") + + header, err := h.Repo.GetHeader(id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Header not found"}) + } + log.Println("Error getting header: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while getting header"}) + } + + return c.JSON(header) +} + +// PostHeader godoc +// +// @Summary Create a header +// @Description Creates a new header within the given custom header group +// @Tags Headers +// @Accept json +// @Produce json +// @Param id path string true "Custom Headers ID" +// @Param header body models.Header true "Header to create" +// @Success 200 {object} models.Header "Created header" +// @Failure 400 {object} models.APIError "Invalid request body" +// @Failure 404 {object} models.APIError "Custom headers not found" +// @Failure 500 {object} models.APIError "Internal server error" +// @Router /custom-headers/{id}/headers [post] +func (h *SiteHandler) PostHeader(c fiber.Ctx) error { + siteID := c.Params("id") + customHeaderID := c.Params("customHeaderId") + + if _, err := h.Repo.GetCustomHeaders(customHeaderID); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Custom headers not found"}) + } + log.Println("Error checking custom headers before creating header: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while creating header"}) + } + + var header models.Header + if err := c.Bind().Body(&header); err != nil { + log.Println("Error parsing body: ", err) + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body"}) + } + + if err := validateHeader(&header); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body: " + err.Error()}) + } + + if err := h.Repo.CreateHeader(siteID, customHeaderID, &header); err != nil { + log.Println("Error creating header: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while creating header"}) + } + + return c.JSON(header) +} + +// PutHeader godoc +// +// @Summary Update a header +// @Description Replaces an existing header by ID +// @Tags Headers +// @Accept json +// @Produce json +// @Param id path string true "Header ID" +// @Param header body models.Header true "Updated header" +// @Success 200 {object} models.Header "Updated header" +// @Failure 400 {object} models.APIError "Invalid request body" +// @Failure 404 {object} models.APIError "Header not found" +// @Failure 500 {object} models.APIError "Internal server error" +// @Router /headers/{id} [put] +func (h *SiteHandler) PutHeader(c fiber.Ctx) error { + siteID := c.Params("id") + id := c.Params("headerId") + + if _, err := h.Repo.GetHeader(id); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Header not found"}) + } + log.Println("Error checking header before update: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while updating header"}) + } + + var header models.Header + if err := c.Bind().Body(&header); err != nil { + log.Println("Error parsing body: ", err) + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body"}) + } + + if err := validateHeader(&header); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body: " + err.Error()}) + } + + header.ID = id + if err := h.Repo.UpdateHeader(siteID, &header); err != nil { + log.Println("Error updating header: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while updating header"}) + } + + updated, err := h.Repo.GetHeader(id) + if err != nil { + log.Println("Error getting updated header: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Header was updated but could not be retrieved"}) + } + + return c.JSON(updated) +} + +// DeleteHeader godoc +// +// @Summary Delete a header +// @Description Deletes a header by ID +// @Tags Headers +// @Produce json +// @Param id path string true "Header ID" +// @Success 204 "No content" +// @Failure 404 {object} models.APIError "Header not found" +// @Failure 500 {object} models.APIError "Internal server error" +// @Router /headers/{id} [delete] +func (h *SiteHandler) DeleteHeader(c fiber.Ctx) error { + siteID := c.Params("id") + id := c.Params("headerId") + + if _, err := h.Repo.GetHeader(id); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Header not found"}) + } + log.Println("Error checking header before delete: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while deleting header"}) + } + + if err := h.Repo.DeleteHeader(siteID, id); err != nil { + log.Println("Error deleting header: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while deleting header"}) + } + + return c.SendStatus(fiber.StatusNoContent) +} diff --git a/backend/app/handlers/static.go b/backend/app/handlers/static.go new file mode 100644 index 0000000..26db961 --- /dev/null +++ b/backend/app/handlers/static.go @@ -0,0 +1,112 @@ +package handlers + +import ( + "os" + "path" + "path/filepath" + "quay/app/repository" + "regexp" + + "github.com/gofiber/fiber/v3" +) + +func NewStaticHandler(storagePath string, siteRepo repository.SiteRepository, staticDashboardPath, dashboardHost string) fiber.Handler { + return func(c fiber.Ctx) error { + domain := c.Hostname() + + if staticDashboardPath != "" && domain == dashboardHost { + urlPath := c.Path() + filePath := filepath.Join(staticDashboardPath, filepath.Clean(urlPath)) + + info, err := os.Stat(filePath) + if err == nil && !info.IsDir() { + return c.SendFile(filePath) + } + + return c.SendFile(filepath.Join(staticDashboardPath, "index.html")) + } + + site, err := siteRepo.GetSiteByDomain(domain) + if err != nil { + return c.Status(fiber.StatusInternalServerError).SendString("Failed to resolve site") + } + + if site == nil { + return c.Status(fiber.StatusNotFound).SendString("Site not found") + } + + if !site.Enabled { + return c.Status(fiber.StatusServiceUnavailable).SendString("Site is currently unavailable") + } + + urlPath := filepath.Clean(c.Path()) + + for _, rule := range site.ForwardRules { + if rule.Regex { + re, err := regexp.Compile(rule.Source) + if err != nil { + continue + } + match := re.FindStringSubmatchIndex(urlPath) + if match != nil { + dest := re.ExpandString([]byte{}, rule.Destination, urlPath, match) + return c.Redirect().Status(rule.StatusCode).To(string(dest)) + } + } else if rule.Source == urlPath { + return c.Redirect().Status(rule.StatusCode).To(rule.Destination) + } + } + + for _, customHeader := range site.CustomHeaders { + var matched bool + if customHeader.Regex { + re, err := regexp.Compile(customHeader.Source) + if err == nil { + matched = re.MatchString(urlPath) + } + } else { + matched, _ = path.Match(customHeader.Source, urlPath) + } + if matched { + for _, header := range customHeader.Headers { + c.Set(header.Key, header.Value) + } + } + } + + if urlPath == "/" || urlPath == "." { + urlPath = "/" + site.IndexFile + } + + basePath := filepath.Join(storagePath, site.ID) + filePath := filepath.Join(basePath, urlPath) + + info, err := os.Stat(filePath) + if err == nil { + if info.IsDir() { + indexPath := filepath.Join(filePath, site.IndexFile) + if _, err := os.Stat(indexPath); err == nil { + return c.SendFile(indexPath) + } + } else { + return c.SendFile(filePath) + } + } + + if site.Spa { + indexPath := filepath.Join(basePath, site.IndexFile) + if _, err := os.Stat(indexPath); err == nil { + return c.SendFile(indexPath) + } + } + + if site.NotFoundFile != "" { + notFoundPath := filepath.Join(basePath, site.NotFoundFile) + if _, err := os.Stat(notFoundPath); err == nil { + return c.SendFile(notFoundPath) + } + } + + return c.SendStatus(fiber.StatusNotFound) + } +} diff --git a/backend/app/handlers/user.go b/backend/app/handlers/user.go new file mode 100644 index 0000000..ebf9ca6 --- /dev/null +++ b/backend/app/handlers/user.go @@ -0,0 +1,289 @@ +package handlers + +import ( + "database/sql" + "errors" + "log" + "quay/app/models" + "quay/app/repository" + "quay/internal/security" + "time" + + "github.com/gofiber/fiber/v3" +) + +type UserHandler struct { + Repo repository.UserRepository +} + +func NewUserHandler(repo repository.UserRepository) *UserHandler { + return &UserHandler{Repo: repo} +} + +func (h *UserHandler) GetAllUsers(c fiber.Ctx) error { + users, err := h.Repo.GetAllUsers() + + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{ + Message: "User not found", + }) + } + log.Println("Error getting user: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{ + Message: "Unexpected error while getting site", + }) + } + + if users == nil { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{ + Message: "User not found", + }) + } + + for i := range users { + users[i].HashedPassword = "" + } + + return c.JSON(users) +} + +// GetUserById godoc +// @Summary Get user by ID +// @Description Get a single user by its ID +// @Tags Users +// @Accept json +// @Produce json +// @Param id path string true "User ID" +// @Success 200 {object} models.User +// @Failure 404 {object} models.APIError +// @Failure 500 {object} models.APIError +func (h *UserHandler) GetUserById(c fiber.Ctx) error { + id := c.Params("id") + + user, err := h.Repo.GetUserById(id) + + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{ + Message: "User not found", + }) + } + log.Println("Error getting user: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{ + Message: "Unexpected error while getting site", + }) + } + + if user == nil { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{ + Message: "User not found", + }) + } + + user.HashedPassword = "" + + return c.JSON(user) +} + +// GetUserByName godoc +// @Summary Get user by Name +// @Description Get a single user by its Name +// @Tags Users +// @Accept json +// @Produce json +// @Param name path string true "User Name" +// @Success 200 {object} models.User +// @Failure 404 {object} models.APIError +// @Failure 500 {object} models.APIError +func (h *UserHandler) GetUserByName(c fiber.Ctx) error { + name := c.Params("name") + + user, err := h.Repo.GetUserByName(name) + + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{ + Message: "User not found", + }) + } + log.Println("Error getting user: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{ + Message: "Unexpected error while getting site", + }) + } + + if user == nil { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{ + Message: "User not found", + }) + } + + user.HashedPassword = "" + + return c.JSON(user) +} + +// CreateUser godoc +// @Summary Create a new user +// @Description Create a new user with the provided name, role, and password +// @Tags Users +// @Accept json +// @Produce json +// @Success 200 {object} models.User +// @Failure 400 {object} models.APIError +// @Failure 409 {object} models.APIError +// @Failure 500 {object} models.APIError +func (h *UserHandler) CreateUser(c fiber.Ctx) error { + var createUserRequest models.CreateUserRequest + + if err := c.Bind().Body(&createUserRequest); err != nil { + log.Println("Error parsing body: ", err) + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{ + Message: "Invalid request body", + }) + } + + if err := models.ValidateCreateUserRequest(&createUserRequest); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{ + Message: "Invalid request body: " + err.Error(), + }) + } + + hashedPassword, err := security.HashPassword(createUserRequest.Password) + if err != nil { + log.Println("Error hashing password: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{ + Message: "Unexpected error while hashing password", + }) + } + + user := models.User{ + Name: createUserRequest.Name, + Role: createUserRequest.Role, + HashedPassword: hashedPassword, + CreatedAt: time.Now().UTC().Format(time.DateTime), + } + + err = h.Repo.CreateUser(&user) + if err != nil { + log.Println("Error creating user: ", err) + if errors.Is(err, repository.ErrUserAlreadyExists) { + return c.Status(fiber.StatusConflict).JSON(&models.APIError{ + Message: "User name already taken", + }) + } + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{ + Message: "Unexpected error while creating user", + }) + } + + return c.JSON(user) +} + +// UpdateUser godoc +// @Summary Update an existing user +// @Description Update an existing user with the provided name, role, and password +// @Tags Users +// @Accept json +// @Produce json +// @Param id path string true "User ID" +// @Success 200 {object} models.User +// @Failure 400 {object} models.APIError +// @Failure 404 {object} models.APIError +// @Failure 409 {object} models.APIError +// @Failure 500 {object} models.APIError +func (h *UserHandler) UpdateUser(c fiber.Ctx) error { + id := c.Params("id") + var updateUserRequest models.UpdateUserRequest + + if err := c.Bind().Body(&updateUserRequest); err != nil { + log.Println("Error parsing body: ", err) + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{ + Message: "Invalid request body", + }) + } + + if err := models.ValidateUpdateUserRequest(&updateUserRequest); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{ + Message: "Invalid request body: " + err.Error(), + }) + } + + user := models.User{ + ID: id, + Name: updateUserRequest.Name, + Role: updateUserRequest.Role, + } + + err := h.Repo.UpdateUser(&user) + if err != nil { + log.Println("Error updating user: ", err) + if errors.Is(err, repository.ErrUserAlreadyExists) { + return c.Status(fiber.StatusConflict).JSON(&models.APIError{ + Message: "User name already taken", + }) + } + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{ + Message: "Unexpected error while updating user", + }) + } + + return c.SendStatus(fiber.StatusNoContent) +} + +// DeleteUser godoc +// @Summary Delete a user +// @Description Delete a user by its ID +// @Tags Users +// @Accept json +// @Produce json +// @Param id path string true "User ID" +// @Success 204 "No Content" +// @Failure 404 {object} models.APIError +// @Failure 500 {object} models.APIError +func (h *UserHandler) DeleteUser(c fiber.Ctx) error { + id := c.Params("id") + + if _, err := h.Repo.GetUserById(id); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{ + Message: "User not found", + }) + } + log.Println("Error checking user before delete: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{ + Message: "Unexpected error while deleting user", + }) + } + + err := h.Repo.DeleteUser(id) + if err != nil { + log.Println("Error deleting user: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{ + Message: "Unexpected error while deleting user", + }) + } + + return c.SendStatus(fiber.StatusNoContent) +} + +// GetMe returns the currently authenticated user's details +func (h *UserHandler) GetMe(c fiber.Ctx) error { + uid, ok := c.Locals("user_id").(string) + if !ok || uid == "" { + return c.Status(fiber.StatusUnauthorized).JSON(&models.APIError{Message: "Unauthorized"}) + } + + user, err := h.Repo.GetUserById(uid) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "User not found"}) + } + log.Println("Error getting user: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while getting user"}) + } + + user.HashedPassword = "" + return c.JSON(user) +} diff --git a/backend/app/middleware/api_host_guard.go b/backend/app/middleware/api_host_guard.go new file mode 100644 index 0000000..8a6555d --- /dev/null +++ b/backend/app/middleware/api_host_guard.go @@ -0,0 +1,15 @@ +package middleware + +import "github.com/gofiber/fiber/v3" + +func APIHostGuard(allowedHost string, alternativeHandler fiber.Handler) fiber.Handler { + return func(c fiber.Ctx) error { + if c.Hostname() != allowedHost { + if alternativeHandler != nil { + return alternativeHandler(c) + } + return c.Status(fiber.StatusForbidden).SendString("Forbidden: Invalid API host") + } + return c.Next() + } +} diff --git a/backend/app/middleware/auth.go b/backend/app/middleware/auth.go new file mode 100644 index 0000000..7cdde61 --- /dev/null +++ b/backend/app/middleware/auth.go @@ -0,0 +1,32 @@ +package middleware + +import ( + "strings" + + "quay/internal/security" + + "github.com/gofiber/fiber/v3" +) + +func RequireAuth() fiber.Handler { + return func(c fiber.Ctx) error { + auth := c.Get("Authorization") + if auth == "" { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "missing authorization header"}) + } + + parts := strings.SplitN(auth, " ", 2) + if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid authorization header"}) + } + + claims, err := security.ValidateToken(parts[1]) + if err != nil { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid token"}) + } + + c.Locals("user_id", claims.UserID) + c.Locals("role", claims.Role) + return c.Next() + } +} diff --git a/app/models/api_error.go b/backend/app/models/api_error.go similarity index 55% rename from app/models/api_error.go rename to backend/app/models/api_error.go index 9934619..e11cf2e 100644 --- a/app/models/api_error.go +++ b/backend/app/models/api_error.go @@ -1,5 +1,5 @@ package models type APIError struct { - Error string `json:"error"` + Message string `json:"message"` } diff --git a/backend/app/models/deployment.go b/backend/app/models/deployment.go new file mode 100644 index 0000000..127d0fe --- /dev/null +++ b/backend/app/models/deployment.go @@ -0,0 +1,35 @@ +package models + +type DeploymentStatus string + +const ( + DeploymentStatusPending DeploymentStatus = "pending" + DeploymentStatusRunning DeploymentStatus = "running" + DeploymentStatusSuccess DeploymentStatus = "success" + DeploymentStatusFailed DeploymentStatus = "failed" +) + +func (s DeploymentStatus) IsValid() bool { + switch s { + case DeploymentStatusPending, DeploymentStatusRunning, + DeploymentStatusSuccess, DeploymentStatusFailed: + return true + } + return false +} + +type Deployment struct { + Id string `json:"id"` + SiteId string `json:"site_id"` + CommitHash string `json:"commit_hash"` + Message string `json:"message"` + Status DeploymentStatus `json:"status"` + StartTime string `json:"start_time"` + FinishTime string `json:"finish_time"` + CreatedAt string `json:"created_at"` +} + +type GetDeploymentsBySiteResponse struct { + Deployments []Deployment `json:"deployments"` + Total int `json:"total"` +} diff --git a/backend/app/models/gitserver.go b/backend/app/models/gitserver.go new file mode 100644 index 0000000..614e9ed --- /dev/null +++ b/backend/app/models/gitserver.go @@ -0,0 +1,35 @@ +package models + +import "errors" + +type GitServer struct { + ID string `json:"id"` + Name string `json:"name"` + Protocol string `json:"protocol"` + BaseUrl string `json:"baseUrl"` + Type string `json:"type"` + AuthToken string `json:"auth_token"` + CreatedAt string `json:"created_at"` +} + +func ValidateGitServer(gs *GitServer) error { + if gs.Name == "" { + return errors.New("name is required") + } + if gs.Protocol == "" { + return errors.New("protocol is required") + } + if gs.Protocol != "http" && gs.Protocol != "https" { + return errors.New("protocol must be either 'http' or 'https'") + } + if gs.BaseUrl == "" { + return errors.New("baseUrl is required") + } + if gs.Type == "" { + return errors.New("type is required") + } + if gs.Type != "github" && gs.Type != "gitlab" && gs.Type != "gitea" { + return errors.New("type must be either 'github', 'gitlab' or 'gitea'") + } + return nil +} diff --git a/backend/app/models/site.go b/backend/app/models/site.go new file mode 100644 index 0000000..a807cea --- /dev/null +++ b/backend/app/models/site.go @@ -0,0 +1,55 @@ +package models + +type ForwardRule struct { + ID string `json:"id"` + Source string `json:"source"` + Destination string `json:"destination"` + StatusCode int `json:"status_code"` + Regex bool `json:"regex"` +} + +type Header struct { + ID string `json:"id"` + Key string `json:"key"` + Value string `json:"value"` +} + +type CustomHeaders struct { + ID string `json:"id"` + Source string `json:"source"` + Regex bool `json:"regex"` + Headers []Header `json:"headers"` +} + +type Site struct { + ID string `json:"id"` + Name string `json:"name"` + GitServer string `json:"git_server"` + Owner string `json:"owner"` + Repository string `json:"repository"` + Branch string `json:"branch"` + Domain string `json:"domain"` + DeployToken string `json:"deploy_token"` + Enabled bool `json:"enabled"` + Spa bool `json:"spa"` + NotFoundFile string `json:"not_found_file"` + IndexFile string `json:"index_file"` + TrailingSlash *bool `json:"trailing_slash"` + CreatedAt string `json:"created_at"` + ForwardRules []ForwardRule `json:"forward_rules"` + CustomHeaders []CustomHeaders `json:"custom_headers"` +} + +type GetAllSitesResponse struct { + Sites []Site `json:"sites"` + Total int `json:"total"` +} + +type CreateSiteResponse struct { + Site Site `json:"site"` + RawDeployToken string `json:"raw_deploy_token"` +} + +type ToggleEnabledResponse struct { + Enabled bool `json:"enabled"` +} diff --git a/backend/app/models/user.go b/backend/app/models/user.go new file mode 100644 index 0000000..d09dca3 --- /dev/null +++ b/backend/app/models/user.go @@ -0,0 +1,51 @@ +package models + +import "errors" + +type User struct { + ID string `json:"id"` + Name string `json:"name"` + Role string `json:"role"` + HashedPassword string `json:"hashed_password"` + CreatedAt string `json:"created_at"` +} + +type CreateUserRequest struct { + Name string `json:"name"` + Role string `json:"role"` + Password string `json:"password"` +} + +func ValidateCreateUserRequest(req *CreateUserRequest) error { + if req.Name == "" { + return errors.New("name is required") + } + if req.Role == "" { + return errors.New("role is required") + } + if req.Role != "admin" && req.Role != "user" { + return errors.New("role must be either 'admin' or 'user'") + } + if req.Password == "" { + return errors.New("password is required") + } + return nil +} + +type UpdateUserRequest struct { + Name string `json:"name"` + Role string `json:"role"` +} + +func ValidateUpdateUserRequest(req *UpdateUserRequest) error { + if req.Name == "" { + return errors.New("name is required") + } + if req.Role == "" { + return errors.New("role is required") + } + if req.Role != "admin" && req.Role != "user" { + return errors.New("role must be either 'admin' or 'user'") + } + return nil +} diff --git a/backend/app/repository/deployment_repository.go b/backend/app/repository/deployment_repository.go new file mode 100644 index 0000000..8f594fe --- /dev/null +++ b/backend/app/repository/deployment_repository.go @@ -0,0 +1,17 @@ +package repository + +import ( + "quay/app/models" + "time" +) + +type DeploymentRepository interface { + GetDeploymentByID(id string) (*models.Deployment, error) + GetDeploymentsForSite(deploymentId string, limit int) ([]models.Deployment, error) + CreateDeployment(deployment *models.Deployment) (string, error) + UpdateDeployment(deployment *models.Deployment) error + UpdateDeploymentStatus(deploymentId string, status models.DeploymentStatus) error + UpdateDeploymentFinishTime(deploymentId string, finishTime time.Time) error + UpdateDeploymentGitInfo(deploymentId, commitHash, message string) error + DeleteDeployment(id string) error +} diff --git a/backend/app/repository/gitserver_repository.go b/backend/app/repository/gitserver_repository.go new file mode 100644 index 0000000..443d4ce --- /dev/null +++ b/backend/app/repository/gitserver_repository.go @@ -0,0 +1,16 @@ +package repository + +import ( + "errors" + "quay/app/models" +) + +var ErrGitServerAlreadyExists = errors.New("gitserver already exists") + +type GitServerRepository interface { + ListGitServers() ([]models.GitServer, error) + GetGitServer(id string) (*models.GitServer, error) + CreateGitServer(gs *models.GitServer) error + UpdateGitServer(gs *models.GitServer) error + DeleteGitServer(id string) error +} diff --git a/backend/app/repository/site_repository.go b/backend/app/repository/site_repository.go new file mode 100644 index 0000000..2f70ef6 --- /dev/null +++ b/backend/app/repository/site_repository.go @@ -0,0 +1,25 @@ +package repository + +import "quay/app/models" + +type SiteRepository interface { + GetSite(id string) (*models.Site, error) + GetSiteByDomain(domain string) (*models.Site, error) + ListSites() ([]models.Site, error) + CreateSite(s *models.Site) error + UpdateSite(s *models.Site) error + ToggleEnabled(id string) (enabled bool, err error) + DeleteSite(id string) error + GetForwardRule(id string) (*models.ForwardRule, error) + CreateForwardRule(siteID string, fr *models.ForwardRule) error + UpdateForwardRule(siteID string, fr *models.ForwardRule) error + DeleteForwardRule(siteID string, id string) error + GetCustomHeaders(id string) (*models.CustomHeaders, error) + CreateCustomHeaders(siteID string, ch *models.CustomHeaders) error + UpdateCustomHeaders(siteID string, ch *models.CustomHeaders) error + DeleteCustomHeaders(siteID string, id string) error + GetHeader(id string) (*models.Header, error) + CreateHeader(siteID string, customHeaderID string, h *models.Header) error + UpdateHeader(siteID string, h *models.Header) error + DeleteHeader(siteID string, id string) error +} diff --git a/backend/app/repository/user_repository.go b/backend/app/repository/user_repository.go new file mode 100644 index 0000000..f9bdcce --- /dev/null +++ b/backend/app/repository/user_repository.go @@ -0,0 +1,17 @@ +package repository + +import ( + "errors" + "quay/app/models" +) + +var ErrUserAlreadyExists = errors.New("user already exists") + +type UserRepository interface { + GetAllUsers() ([]models.User, error) + GetUserById(id string) (*models.User, error) + GetUserByName(name string) (*models.User, error) + CreateUser(user *models.User) error + UpdateUser(user *models.User) error + DeleteUser(id string) error +} diff --git a/backend/app/routes/routes.go b/backend/app/routes/routes.go new file mode 100644 index 0000000..3b75256 --- /dev/null +++ b/backend/app/routes/routes.go @@ -0,0 +1,137 @@ +package routes + +import ( + "database/sql" + "log" + "path/filepath" + "quay/app/handlers" + "quay/app/middleware" + "quay/app/models" + "quay/internal/database" + "quay/internal/envconfig" + "quay/internal/security" + + "github.com/Flussen/swagger-fiber-v3" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/static" +) + +const BootstrapUserUsername = "admin" +const BootstrapUserPassword = "admin" + +func Register(app *fiber.App, envCfg *envconfig.EnvConfig, db *sql.DB) { + siteRepository := database.NewSQLiteSiteRepository(db) + deploymentRepository := database.NewSQLiteDeploymentRepository(db) + userRepository := database.NewSQLiteUserRepository(db) + gitServerRepository := database.NewSQLiteGitServerRepository(db) + + if uList, err := userRepository.GetAllUsers(); err != nil { + log.Printf("Warning checking users: %v", err) + } else if len(uList) == 0 { + pwd := BootstrapUserUsername + hashedPassword, err := security.HashPassword(pwd) + if err != nil { + log.Println("Error hashing default user password: ", err) + } + defaultUser := models.User{ + Name: BootstrapUserPassword, + HashedPassword: hashedPassword, + Role: "admin", + } + if err := userRepository.CreateUser(&defaultUser); err != nil { + log.Printf("Warning creating default user: %v", err) + } else { + log.Printf("Created default user: username '%s' with password '%s'", BootstrapUserUsername, pwd) + log.Printf("Please log in and change the default password immediately") + } + } + + storagePath, err := filepath.Abs(envCfg.StoragePath) + if err != nil { + log.Fatalf("Failed to resolve storage path: %v", err) + } + + siteHandler := handlers.NewSiteHandler(siteRepository) + deploySiteHandler := handlers.NewDeploySiteHandler(envCfg, siteRepository, gitServerRepository, deploymentRepository) + userHandler := handlers.NewUserHandler(userRepository) + gitServerHandler := handlers.NewGitServerHandler(gitServerRepository) + deploymentsHandler := handlers.NewDeploymentHandler(deploymentRepository) + staticSiteHandler := handlers.NewStaticHandler(storagePath, siteRepository, envCfg.StaticDashboardPath, envCfg.DashboardHost) + + api := app.Group("/api/v1", middleware.APIHostGuard(envCfg.DashboardHost, staticSiteHandler)) + + public := api.Group("") + public.Get("/health", handlers.HealthCheck) + + authHandler := handlers.NewAuthHandler(userRepository) + public.Post("/login", authHandler.Login) + + public.Post("/deploy", deploySiteHandler.PostDeploy) + + public.Get("/docs.json", static.New("./docs/swagger.json")) + + public.Get("/swagger/*", swagger.New(swagger.Config{ + URL: "/api/v1/docs.json", + Title: "Quay API Documentation", + })) + + // Protected routes - require auth for everything by default + protected := api.Group("", middleware.RequireAuth()) + + // Sites + protected.Get("/sites", siteHandler.GetSites) + protected.Get("/sites/:id", siteHandler.GetSite) + protected.Post("/sites", siteHandler.PostSite) + protected.Put("/sites/:id", siteHandler.PutSite) + protected.Delete("/sites/:id", siteHandler.DeleteSite) + protected.Patch("/sites/:id/enabled", siteHandler.ToggleEnabled) + + // Git servers + protected.Get("/gitservers", gitServerHandler.GetGitServers) + protected.Get("/gitservers/:id", gitServerHandler.GetGitServer) + protected.Post("/gitservers", gitServerHandler.PostGitServer) + protected.Put("/gitservers/:id", gitServerHandler.PutGitServer) + protected.Delete("/gitservers/:id", gitServerHandler.DeleteGitServer) + + // Forward rules + protected.Get("/sites/:id/forward-rules", siteHandler.GetSiteForwardRules) + protected.Post("/sites/:id/forward-rules", siteHandler.PostForwardRule) + protected.Get("/sites/:id/forward-rules/:ruleId", siteHandler.GetForwardRule) + protected.Put("/sites/:id/forward-rules/:ruleId", siteHandler.PutForwardRule) + protected.Delete("/sites/:id/forward-rules/:ruleId", siteHandler.DeleteForwardRule) + + // Custom headers (header rules) + protected.Get("/sites/:id/custom-headers", siteHandler.GetSiteCustomHeaders) + protected.Post("/sites/:id/custom-headers", siteHandler.PostCustomHeaders) + protected.Get("/sites/:id/custom-headers/:customHeaderId", siteHandler.GetCustomHeaders) + protected.Put("/sites/:id/custom-headers/:customHeaderId", siteHandler.PutCustomHeaders) + protected.Delete("/sites/:id/custom-headers/:customHeaderId", siteHandler.DeleteCustomHeaders) + + // Headers + protected.Get("/sites/:id/custom-headers/:customHeaderId/headers", siteHandler.GetCustomHeaderHeaders) + protected.Post("/sites/:id/custom-headers/:customHeaderId/headers", siteHandler.PostHeader) + protected.Get("/sites/:id/headers/:headerId", siteHandler.GetHeader) + protected.Put("/sites/:id/headers/:headerId", siteHandler.PutHeader) + protected.Delete("/sites/:id/headers/:headerId", siteHandler.DeleteHeader) + + // Deployments + protected.Get("/deployments/:id", deploymentsHandler.GetDeployment) + protected.Get("/sites/:id/deployments", deploymentsHandler.GetDeploymentsBySite) + + // Users + protected.Get("/users", userHandler.GetAllUsers) + protected.Get("/users/:id", userHandler.GetUserById) + protected.Get("/users/by-name/:name", userHandler.GetUserByName) + protected.Get("/me", userHandler.GetMe) + protected.Post("/users", userHandler.CreateUser) + protected.Put("/users/:id", userHandler.UpdateUser) + protected.Delete("/users/:id", userHandler.DeleteUser) + + api.Use(func(c fiber.Ctx) error { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{ + Message: "Endpoint not found", + }) + }) + + app.Use(staticSiteHandler) +} diff --git a/backend/docs/docs.go b/backend/docs/docs.go new file mode 100644 index 0000000..9b2b3ee --- /dev/null +++ b/backend/docs/docs.go @@ -0,0 +1,1320 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/custom-headers/{id}": { + "get": { + "description": "Returns a single custom header group by ID", + "produces": [ + "application/json" + ], + "tags": [ + "Custom-Headers" + ], + "summary": "Get a custom header group", + "parameters": [ + { + "type": "string", + "description": "Custom Headers ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Custom header group", + "schema": { + "$ref": "#/definitions/models.CustomHeaders" + } + }, + "404": { + "description": "Custom headers not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + }, + "put": { + "description": "Replaces an existing custom header group by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Custom-Headers" + ], + "summary": "Update a custom header group", + "parameters": [ + { + "type": "string", + "description": "Custom Headers ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated custom header group", + "name": "customHeaders", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.CustomHeaders" + } + } + ], + "responses": { + "200": { + "description": "Updated custom header group", + "schema": { + "$ref": "#/definitions/models.CustomHeaders" + } + }, + "400": { + "description": "Invalid request body", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "404": { + "description": "Custom headers not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + }, + "delete": { + "description": "Deletes a custom header group by ID", + "produces": [ + "application/json" + ], + "tags": [ + "Custom-Headers" + ], + "summary": "Delete a custom header group", + "parameters": [ + { + "type": "string", + "description": "Custom Headers ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No content" + }, + "404": { + "description": "Custom headers not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + } + }, + "/custom-headers/{id}/headers": { + "get": { + "description": "Returns all individual headers belonging to the given custom header group", + "produces": [ + "application/json" + ], + "tags": [ + "Custom-Headers" + ], + "summary": "List headers in a custom header group", + "parameters": [ + { + "type": "string", + "description": "Custom Headers ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "List of headers", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Header" + } + } + }, + "404": { + "description": "Custom headers not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + }, + "post": { + "description": "Creates a new header within the given custom header group", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Headers" + ], + "summary": "Create a header", + "parameters": [ + { + "type": "string", + "description": "Custom Headers ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Header to create", + "name": "header", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Header" + } + } + ], + "responses": { + "200": { + "description": "Created header", + "schema": { + "$ref": "#/definitions/models.Header" + } + }, + "400": { + "description": "Invalid request body", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "404": { + "description": "Custom headers not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + } + }, + "/deployments/{id}": { + "get": { + "description": "Get a single deployment by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Deployments" + ], + "summary": "Get deployment by ID", + "parameters": [ + { + "type": "string", + "description": "Deployment ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Deployment" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + } + }, + "/forward-rules/{id}": { + "get": { + "description": "Returns a single forward rule by ID", + "produces": [ + "application/json" + ], + "tags": [ + "Forward-Rules" + ], + "summary": "Get a forward rule", + "parameters": [ + { + "type": "string", + "description": "Forward Rule ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Forward rule", + "schema": { + "$ref": "#/definitions/models.ForwardRule" + } + }, + "404": { + "description": "Forward rule not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + }, + "put": { + "description": "Replaces an existing forward rule by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Forward-Rules" + ], + "summary": "Update a forward rule", + "parameters": [ + { + "type": "string", + "description": "Forward Rule ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated forward rule", + "name": "rule", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.ForwardRule" + } + } + ], + "responses": { + "200": { + "description": "Updated forward rule", + "schema": { + "$ref": "#/definitions/models.ForwardRule" + } + }, + "400": { + "description": "Invalid request body", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "404": { + "description": "Forward rule not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + }, + "delete": { + "description": "Deletes a forward rule by ID", + "produces": [ + "application/json" + ], + "tags": [ + "Forward-Rules" + ], + "summary": "Delete a forward rule", + "parameters": [ + { + "type": "string", + "description": "Forward Rule ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No content" + }, + "404": { + "description": "Forward rule not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + } + }, + "/headers/{id}": { + "get": { + "description": "Returns a single header by ID", + "produces": [ + "application/json" + ], + "tags": [ + "Headers" + ], + "summary": "Get a header", + "parameters": [ + { + "type": "string", + "description": "Header ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Header", + "schema": { + "$ref": "#/definitions/models.Header" + } + }, + "404": { + "description": "Header not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + }, + "put": { + "description": "Replaces an existing header by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Headers" + ], + "summary": "Update a header", + "parameters": [ + { + "type": "string", + "description": "Header ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated header", + "name": "header", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Header" + } + } + ], + "responses": { + "200": { + "description": "Updated header", + "schema": { + "$ref": "#/definitions/models.Header" + } + }, + "400": { + "description": "Invalid request body", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "404": { + "description": "Header not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + }, + "delete": { + "description": "Deletes a header by ID", + "produces": [ + "application/json" + ], + "tags": [ + "Headers" + ], + "summary": "Delete a header", + "parameters": [ + { + "type": "string", + "description": "Header ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No content" + }, + "404": { + "description": "Header not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + } + }, + "/sites": { + "get": { + "description": "Get a list of all sites", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Sites" + ], + "summary": "Get all sites", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.GetAllSitesResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + }, + "post": { + "description": "Create a new site with the provided details", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Sites" + ], + "summary": "Create a new site", + "parameters": [ + { + "description": "Site details", + "name": "site", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Site" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.CreateSiteResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + } + }, + "/sites/{id}": { + "get": { + "description": "Get a single site by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Sites" + ], + "summary": "Get site by ID", + "parameters": [ + { + "type": "string", + "description": "Site ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Site" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + }, + "put": { + "description": "Update an existing site by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Sites" + ], + "summary": "Update an existing site", + "parameters": [ + { + "type": "string", + "description": "Site ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated site details", + "name": "site", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Site" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Site" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + }, + "delete": { + "description": "Delete a site by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Sites" + ], + "summary": "Delete a site", + "parameters": [ + { + "type": "string", + "description": "Site ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + } + }, + "/sites/{id}/custom-headers": { + "get": { + "description": "Returns all custom header groups associated with the given site", + "produces": [ + "application/json" + ], + "tags": [ + "Custom-Headers" + ], + "summary": "List custom header groups for a site", + "parameters": [ + { + "type": "string", + "description": "Site ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "List of custom header groups", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.CustomHeaders" + } + } + }, + "404": { + "description": "Site not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + }, + "post": { + "description": "Creates a new custom header group for the given site", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Custom-Headers" + ], + "summary": "Create a custom header group", + "parameters": [ + { + "type": "string", + "description": "Site ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Custom header group to create", + "name": "customHeaders", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.CustomHeaders" + } + } + ], + "responses": { + "200": { + "description": "Created custom header group", + "schema": { + "$ref": "#/definitions/models.CustomHeaders" + } + }, + "400": { + "description": "Invalid request body", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "404": { + "description": "Site not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + } + }, + "/sites/{id}/enabled": { + "patch": { + "description": "Enable or disable a site by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Sites" + ], + "summary": "Toggle site enabled status", + "parameters": [ + { + "type": "string", + "description": "Site ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ToggleEnabledResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + } + }, + "/sites/{id}/forward-rules": { + "get": { + "description": "Returns all forward rules associated with the given site", + "produces": [ + "application/json" + ], + "tags": [ + "Forward-Rules" + ], + "summary": "List forward rules for a site", + "parameters": [ + { + "type": "string", + "description": "Site ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "List of forward rules", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ForwardRule" + } + } + }, + "404": { + "description": "Site not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + }, + "post": { + "description": "Creates a new forward rule for the given site", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Forward-Rules" + ], + "summary": "Create a forward rule", + "parameters": [ + { + "type": "string", + "description": "Site ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Forward rule to create", + "name": "rule", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.ForwardRule" + } + } + ], + "responses": { + "200": { + "description": "Created forward rule", + "schema": { + "$ref": "#/definitions/models.ForwardRule" + } + }, + "400": { + "description": "Invalid request body", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "404": { + "description": "Site not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + } + }, + "/sites/{siteId}/deployments": { + "get": { + "description": "Get a list of deployments for a specific site", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Deployments" + ], + "summary": "Get deployments for a site", + "parameters": [ + { + "type": "string", + "description": "Site ID", + "name": "siteId", + "in": "path", + "required": true + }, + { + "type": "integer", + "default": 100, + "description": "Maximum number of deployments to return", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.GetDeploymentsBySiteResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + } + } + }, + "definitions": { + "models.APIError": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + }, + "models.CreateSiteResponse": { + "type": "object", + "properties": { + "raw_deploy_token": { + "type": "string" + }, + "site": { + "$ref": "#/definitions/models.Site" + } + } + }, + "models.CustomHeaders": { + "type": "object", + "properties": { + "headers": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Header" + } + }, + "id": { + "type": "string" + }, + "regex": { + "type": "boolean" + }, + "source": { + "type": "string" + } + } + }, + "models.Deployment": { + "type": "object", + "properties": { + "commit_hash": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "finish_time": { + "type": "string" + }, + "id": { + "type": "string" + }, + "message": { + "type": "string" + }, + "site_id": { + "type": "string" + }, + "start_time": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/models.DeploymentStatus" + } + } + }, + "models.DeploymentStatus": { + "type": "string", + "enum": [ + "pending", + "running", + "success", + "failed" + ], + "x-enum-varnames": [ + "DeploymentStatusPending", + "DeploymentStatusRunning", + "DeploymentStatusSuccess", + "DeploymentStatusFailed" + ] + }, + "models.ForwardRule": { + "type": "object", + "properties": { + "destination": { + "type": "string" + }, + "id": { + "type": "string" + }, + "regex": { + "type": "boolean" + }, + "source": { + "type": "string" + }, + "status_code": { + "type": "integer" + } + } + }, + "models.GetAllSitesResponse": { + "type": "object", + "properties": { + "sites": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Site" + } + }, + "total": { + "type": "integer" + } + } + }, + "models.GetDeploymentsBySiteResponse": { + "type": "object", + "properties": { + "deployments": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Deployment" + } + }, + "total": { + "type": "integer" + } + } + }, + "models.Header": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "models.Site": { + "type": "object", + "properties": { + "branch": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "custom_headers": { + "type": "array", + "items": { + "$ref": "#/definitions/models.CustomHeaders" + } + }, + "deploy_token": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "forward_rules": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ForwardRule" + } + }, + "git_server": { + "type": "string" + }, + "id": { + "type": "string" + }, + "index_file": { + "type": "string" + }, + "name": { + "type": "string" + }, + "not_found_file": { + "type": "string" + }, + "owner": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "spa": { + "type": "boolean" + }, + "trailing_slash": { + "type": "boolean" + } + } + }, + "models.ToggleEnabledResponse": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + } + } + }, + "tags": [ + { + "description": "Manage sites", + "name": "Sites" + }, + { + "description": "Manage redirect rules for a site", + "name": "Forward-Rules" + }, + { + "description": "Manage custom header groups for a site", + "name": "Custom-Headers" + }, + { + "description": "Manage individual headers within a custom header group", + "name": "Headers" + } + ] +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "localhost:4321", + BasePath: "/api/v1", + Schemes: []string{}, + Title: "Quay API", + Description: "Self-hosted static site deployment service", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json new file mode 100644 index 0000000..ec708cc --- /dev/null +++ b/backend/docs/swagger.json @@ -0,0 +1,1296 @@ +{ + "swagger": "2.0", + "info": { + "description": "Self-hosted static site deployment service", + "title": "Quay API", + "contact": {}, + "version": "1.0" + }, + "host": "localhost:4321", + "basePath": "/api/v1", + "paths": { + "/custom-headers/{id}": { + "get": { + "description": "Returns a single custom header group by ID", + "produces": [ + "application/json" + ], + "tags": [ + "Custom-Headers" + ], + "summary": "Get a custom header group", + "parameters": [ + { + "type": "string", + "description": "Custom Headers ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Custom header group", + "schema": { + "$ref": "#/definitions/models.CustomHeaders" + } + }, + "404": { + "description": "Custom headers not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + }, + "put": { + "description": "Replaces an existing custom header group by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Custom-Headers" + ], + "summary": "Update a custom header group", + "parameters": [ + { + "type": "string", + "description": "Custom Headers ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated custom header group", + "name": "customHeaders", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.CustomHeaders" + } + } + ], + "responses": { + "200": { + "description": "Updated custom header group", + "schema": { + "$ref": "#/definitions/models.CustomHeaders" + } + }, + "400": { + "description": "Invalid request body", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "404": { + "description": "Custom headers not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + }, + "delete": { + "description": "Deletes a custom header group by ID", + "produces": [ + "application/json" + ], + "tags": [ + "Custom-Headers" + ], + "summary": "Delete a custom header group", + "parameters": [ + { + "type": "string", + "description": "Custom Headers ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No content" + }, + "404": { + "description": "Custom headers not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + } + }, + "/custom-headers/{id}/headers": { + "get": { + "description": "Returns all individual headers belonging to the given custom header group", + "produces": [ + "application/json" + ], + "tags": [ + "Custom-Headers" + ], + "summary": "List headers in a custom header group", + "parameters": [ + { + "type": "string", + "description": "Custom Headers ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "List of headers", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Header" + } + } + }, + "404": { + "description": "Custom headers not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + }, + "post": { + "description": "Creates a new header within the given custom header group", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Headers" + ], + "summary": "Create a header", + "parameters": [ + { + "type": "string", + "description": "Custom Headers ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Header to create", + "name": "header", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Header" + } + } + ], + "responses": { + "200": { + "description": "Created header", + "schema": { + "$ref": "#/definitions/models.Header" + } + }, + "400": { + "description": "Invalid request body", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "404": { + "description": "Custom headers not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + } + }, + "/deployments/{id}": { + "get": { + "description": "Get a single deployment by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Deployments" + ], + "summary": "Get deployment by ID", + "parameters": [ + { + "type": "string", + "description": "Deployment ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Deployment" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + } + }, + "/forward-rules/{id}": { + "get": { + "description": "Returns a single forward rule by ID", + "produces": [ + "application/json" + ], + "tags": [ + "Forward-Rules" + ], + "summary": "Get a forward rule", + "parameters": [ + { + "type": "string", + "description": "Forward Rule ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Forward rule", + "schema": { + "$ref": "#/definitions/models.ForwardRule" + } + }, + "404": { + "description": "Forward rule not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + }, + "put": { + "description": "Replaces an existing forward rule by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Forward-Rules" + ], + "summary": "Update a forward rule", + "parameters": [ + { + "type": "string", + "description": "Forward Rule ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated forward rule", + "name": "rule", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.ForwardRule" + } + } + ], + "responses": { + "200": { + "description": "Updated forward rule", + "schema": { + "$ref": "#/definitions/models.ForwardRule" + } + }, + "400": { + "description": "Invalid request body", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "404": { + "description": "Forward rule not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + }, + "delete": { + "description": "Deletes a forward rule by ID", + "produces": [ + "application/json" + ], + "tags": [ + "Forward-Rules" + ], + "summary": "Delete a forward rule", + "parameters": [ + { + "type": "string", + "description": "Forward Rule ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No content" + }, + "404": { + "description": "Forward rule not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + } + }, + "/headers/{id}": { + "get": { + "description": "Returns a single header by ID", + "produces": [ + "application/json" + ], + "tags": [ + "Headers" + ], + "summary": "Get a header", + "parameters": [ + { + "type": "string", + "description": "Header ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Header", + "schema": { + "$ref": "#/definitions/models.Header" + } + }, + "404": { + "description": "Header not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + }, + "put": { + "description": "Replaces an existing header by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Headers" + ], + "summary": "Update a header", + "parameters": [ + { + "type": "string", + "description": "Header ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated header", + "name": "header", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Header" + } + } + ], + "responses": { + "200": { + "description": "Updated header", + "schema": { + "$ref": "#/definitions/models.Header" + } + }, + "400": { + "description": "Invalid request body", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "404": { + "description": "Header not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + }, + "delete": { + "description": "Deletes a header by ID", + "produces": [ + "application/json" + ], + "tags": [ + "Headers" + ], + "summary": "Delete a header", + "parameters": [ + { + "type": "string", + "description": "Header ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No content" + }, + "404": { + "description": "Header not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + } + }, + "/sites": { + "get": { + "description": "Get a list of all sites", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Sites" + ], + "summary": "Get all sites", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.GetAllSitesResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + }, + "post": { + "description": "Create a new site with the provided details", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Sites" + ], + "summary": "Create a new site", + "parameters": [ + { + "description": "Site details", + "name": "site", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Site" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.CreateSiteResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + } + }, + "/sites/{id}": { + "get": { + "description": "Get a single site by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Sites" + ], + "summary": "Get site by ID", + "parameters": [ + { + "type": "string", + "description": "Site ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Site" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + }, + "put": { + "description": "Update an existing site by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Sites" + ], + "summary": "Update an existing site", + "parameters": [ + { + "type": "string", + "description": "Site ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated site details", + "name": "site", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Site" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Site" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + }, + "delete": { + "description": "Delete a site by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Sites" + ], + "summary": "Delete a site", + "parameters": [ + { + "type": "string", + "description": "Site ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + } + }, + "/sites/{id}/custom-headers": { + "get": { + "description": "Returns all custom header groups associated with the given site", + "produces": [ + "application/json" + ], + "tags": [ + "Custom-Headers" + ], + "summary": "List custom header groups for a site", + "parameters": [ + { + "type": "string", + "description": "Site ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "List of custom header groups", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.CustomHeaders" + } + } + }, + "404": { + "description": "Site not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + }, + "post": { + "description": "Creates a new custom header group for the given site", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Custom-Headers" + ], + "summary": "Create a custom header group", + "parameters": [ + { + "type": "string", + "description": "Site ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Custom header group to create", + "name": "customHeaders", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.CustomHeaders" + } + } + ], + "responses": { + "200": { + "description": "Created custom header group", + "schema": { + "$ref": "#/definitions/models.CustomHeaders" + } + }, + "400": { + "description": "Invalid request body", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "404": { + "description": "Site not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + } + }, + "/sites/{id}/enabled": { + "patch": { + "description": "Enable or disable a site by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Sites" + ], + "summary": "Toggle site enabled status", + "parameters": [ + { + "type": "string", + "description": "Site ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ToggleEnabledResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + } + }, + "/sites/{id}/forward-rules": { + "get": { + "description": "Returns all forward rules associated with the given site", + "produces": [ + "application/json" + ], + "tags": [ + "Forward-Rules" + ], + "summary": "List forward rules for a site", + "parameters": [ + { + "type": "string", + "description": "Site ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "List of forward rules", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ForwardRule" + } + } + }, + "404": { + "description": "Site not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + }, + "post": { + "description": "Creates a new forward rule for the given site", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Forward-Rules" + ], + "summary": "Create a forward rule", + "parameters": [ + { + "type": "string", + "description": "Site ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Forward rule to create", + "name": "rule", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.ForwardRule" + } + } + ], + "responses": { + "200": { + "description": "Created forward rule", + "schema": { + "$ref": "#/definitions/models.ForwardRule" + } + }, + "400": { + "description": "Invalid request body", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "404": { + "description": "Site not found", + "schema": { + "$ref": "#/definitions/models.APIError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + } + }, + "/sites/{siteId}/deployments": { + "get": { + "description": "Get a list of deployments for a specific site", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Deployments" + ], + "summary": "Get deployments for a site", + "parameters": [ + { + "type": "string", + "description": "Site ID", + "name": "siteId", + "in": "path", + "required": true + }, + { + "type": "integer", + "default": 100, + "description": "Maximum number of deployments to return", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.GetDeploymentsBySiteResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.APIError" + } + } + } + } + } + }, + "definitions": { + "models.APIError": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + }, + "models.CreateSiteResponse": { + "type": "object", + "properties": { + "raw_deploy_token": { + "type": "string" + }, + "site": { + "$ref": "#/definitions/models.Site" + } + } + }, + "models.CustomHeaders": { + "type": "object", + "properties": { + "headers": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Header" + } + }, + "id": { + "type": "string" + }, + "regex": { + "type": "boolean" + }, + "source": { + "type": "string" + } + } + }, + "models.Deployment": { + "type": "object", + "properties": { + "commit_hash": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "finish_time": { + "type": "string" + }, + "id": { + "type": "string" + }, + "message": { + "type": "string" + }, + "site_id": { + "type": "string" + }, + "start_time": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/models.DeploymentStatus" + } + } + }, + "models.DeploymentStatus": { + "type": "string", + "enum": [ + "pending", + "running", + "success", + "failed" + ], + "x-enum-varnames": [ + "DeploymentStatusPending", + "DeploymentStatusRunning", + "DeploymentStatusSuccess", + "DeploymentStatusFailed" + ] + }, + "models.ForwardRule": { + "type": "object", + "properties": { + "destination": { + "type": "string" + }, + "id": { + "type": "string" + }, + "regex": { + "type": "boolean" + }, + "source": { + "type": "string" + }, + "status_code": { + "type": "integer" + } + } + }, + "models.GetAllSitesResponse": { + "type": "object", + "properties": { + "sites": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Site" + } + }, + "total": { + "type": "integer" + } + } + }, + "models.GetDeploymentsBySiteResponse": { + "type": "object", + "properties": { + "deployments": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Deployment" + } + }, + "total": { + "type": "integer" + } + } + }, + "models.Header": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "models.Site": { + "type": "object", + "properties": { + "branch": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "custom_headers": { + "type": "array", + "items": { + "$ref": "#/definitions/models.CustomHeaders" + } + }, + "deploy_token": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "forward_rules": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ForwardRule" + } + }, + "git_server": { + "type": "string" + }, + "id": { + "type": "string" + }, + "index_file": { + "type": "string" + }, + "name": { + "type": "string" + }, + "not_found_file": { + "type": "string" + }, + "owner": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "spa": { + "type": "boolean" + }, + "trailing_slash": { + "type": "boolean" + } + } + }, + "models.ToggleEnabledResponse": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + } + } + }, + "tags": [ + { + "description": "Manage sites", + "name": "Sites" + }, + { + "description": "Manage redirect rules for a site", + "name": "Forward-Rules" + }, + { + "description": "Manage custom header groups for a site", + "name": "Custom-Headers" + }, + { + "description": "Manage individual headers within a custom header group", + "name": "Headers" + } + ] +} \ No newline at end of file diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml new file mode 100644 index 0000000..8f103a4 --- /dev/null +++ b/backend/docs/swagger.yaml @@ -0,0 +1,857 @@ +basePath: /api/v1 +definitions: + models.APIError: + properties: + message: + type: string + type: object + models.CreateSiteResponse: + properties: + raw_deploy_token: + type: string + site: + $ref: '#/definitions/models.Site' + type: object + models.CustomHeaders: + properties: + headers: + items: + $ref: '#/definitions/models.Header' + type: array + id: + type: string + regex: + type: boolean + source: + type: string + type: object + models.Deployment: + properties: + commit_hash: + type: string + created_at: + type: string + finish_time: + type: string + id: + type: string + message: + type: string + site_id: + type: string + start_time: + type: string + status: + $ref: '#/definitions/models.DeploymentStatus' + type: object + models.DeploymentStatus: + enum: + - pending + - running + - success + - failed + type: string + x-enum-varnames: + - DeploymentStatusPending + - DeploymentStatusRunning + - DeploymentStatusSuccess + - DeploymentStatusFailed + models.ForwardRule: + properties: + destination: + type: string + id: + type: string + regex: + type: boolean + source: + type: string + status_code: + type: integer + type: object + models.GetAllSitesResponse: + properties: + sites: + items: + $ref: '#/definitions/models.Site' + type: array + total: + type: integer + type: object + models.GetDeploymentsBySiteResponse: + properties: + deployments: + items: + $ref: '#/definitions/models.Deployment' + type: array + total: + type: integer + type: object + models.Header: + properties: + id: + type: string + key: + type: string + value: + type: string + type: object + models.Site: + properties: + branch: + type: string + created_at: + type: string + custom_headers: + items: + $ref: '#/definitions/models.CustomHeaders' + type: array + deploy_token: + type: string + domain: + type: string + enabled: + type: boolean + forward_rules: + items: + $ref: '#/definitions/models.ForwardRule' + type: array + git_server: + type: string + id: + type: string + index_file: + type: string + name: + type: string + not_found_file: + type: string + owner: + type: string + repository: + type: string + spa: + type: boolean + trailing_slash: + type: boolean + type: object + models.ToggleEnabledResponse: + properties: + enabled: + type: boolean + type: object +host: localhost:4321 +info: + contact: {} + description: Self-hosted static site deployment service + title: Quay API + version: "1.0" +paths: + /custom-headers/{id}: + delete: + description: Deletes a custom header group by ID + parameters: + - description: Custom Headers ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No content + "404": + description: Custom headers not found + schema: + $ref: '#/definitions/models.APIError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.APIError' + summary: Delete a custom header group + tags: + - Custom-Headers + get: + description: Returns a single custom header group by ID + parameters: + - description: Custom Headers ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Custom header group + schema: + $ref: '#/definitions/models.CustomHeaders' + "404": + description: Custom headers not found + schema: + $ref: '#/definitions/models.APIError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.APIError' + summary: Get a custom header group + tags: + - Custom-Headers + put: + consumes: + - application/json + description: Replaces an existing custom header group by ID + parameters: + - description: Custom Headers ID + in: path + name: id + required: true + type: string + - description: Updated custom header group + in: body + name: customHeaders + required: true + schema: + $ref: '#/definitions/models.CustomHeaders' + produces: + - application/json + responses: + "200": + description: Updated custom header group + schema: + $ref: '#/definitions/models.CustomHeaders' + "400": + description: Invalid request body + schema: + $ref: '#/definitions/models.APIError' + "404": + description: Custom headers not found + schema: + $ref: '#/definitions/models.APIError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.APIError' + summary: Update a custom header group + tags: + - Custom-Headers + /custom-headers/{id}/headers: + get: + description: Returns all individual headers belonging to the given custom header + group + parameters: + - description: Custom Headers ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: List of headers + schema: + items: + $ref: '#/definitions/models.Header' + type: array + "404": + description: Custom headers not found + schema: + $ref: '#/definitions/models.APIError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.APIError' + summary: List headers in a custom header group + tags: + - Custom-Headers + post: + consumes: + - application/json + description: Creates a new header within the given custom header group + parameters: + - description: Custom Headers ID + in: path + name: id + required: true + type: string + - description: Header to create + in: body + name: header + required: true + schema: + $ref: '#/definitions/models.Header' + produces: + - application/json + responses: + "200": + description: Created header + schema: + $ref: '#/definitions/models.Header' + "400": + description: Invalid request body + schema: + $ref: '#/definitions/models.APIError' + "404": + description: Custom headers not found + schema: + $ref: '#/definitions/models.APIError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.APIError' + summary: Create a header + tags: + - Headers + /deployments/{id}: + get: + consumes: + - application/json + description: Get a single deployment by its ID + parameters: + - description: Deployment ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.Deployment' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.APIError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.APIError' + summary: Get deployment by ID + tags: + - Deployments + /forward-rules/{id}: + delete: + description: Deletes a forward rule by ID + parameters: + - description: Forward Rule ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No content + "404": + description: Forward rule not found + schema: + $ref: '#/definitions/models.APIError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.APIError' + summary: Delete a forward rule + tags: + - Forward-Rules + get: + description: Returns a single forward rule by ID + parameters: + - description: Forward Rule ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Forward rule + schema: + $ref: '#/definitions/models.ForwardRule' + "404": + description: Forward rule not found + schema: + $ref: '#/definitions/models.APIError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.APIError' + summary: Get a forward rule + tags: + - Forward-Rules + put: + consumes: + - application/json + description: Replaces an existing forward rule by ID + parameters: + - description: Forward Rule ID + in: path + name: id + required: true + type: string + - description: Updated forward rule + in: body + name: rule + required: true + schema: + $ref: '#/definitions/models.ForwardRule' + produces: + - application/json + responses: + "200": + description: Updated forward rule + schema: + $ref: '#/definitions/models.ForwardRule' + "400": + description: Invalid request body + schema: + $ref: '#/definitions/models.APIError' + "404": + description: Forward rule not found + schema: + $ref: '#/definitions/models.APIError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.APIError' + summary: Update a forward rule + tags: + - Forward-Rules + /headers/{id}: + delete: + description: Deletes a header by ID + parameters: + - description: Header ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No content + "404": + description: Header not found + schema: + $ref: '#/definitions/models.APIError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.APIError' + summary: Delete a header + tags: + - Headers + get: + description: Returns a single header by ID + parameters: + - description: Header ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Header + schema: + $ref: '#/definitions/models.Header' + "404": + description: Header not found + schema: + $ref: '#/definitions/models.APIError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.APIError' + summary: Get a header + tags: + - Headers + put: + consumes: + - application/json + description: Replaces an existing header by ID + parameters: + - description: Header ID + in: path + name: id + required: true + type: string + - description: Updated header + in: body + name: header + required: true + schema: + $ref: '#/definitions/models.Header' + produces: + - application/json + responses: + "200": + description: Updated header + schema: + $ref: '#/definitions/models.Header' + "400": + description: Invalid request body + schema: + $ref: '#/definitions/models.APIError' + "404": + description: Header not found + schema: + $ref: '#/definitions/models.APIError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.APIError' + summary: Update a header + tags: + - Headers + /sites: + get: + consumes: + - application/json + description: Get a list of all sites + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.GetAllSitesResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.APIError' + summary: Get all sites + tags: + - Sites + post: + consumes: + - application/json + description: Create a new site with the provided details + parameters: + - description: Site details + in: body + name: site + required: true + schema: + $ref: '#/definitions/models.Site' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.CreateSiteResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.APIError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.APIError' + summary: Create a new site + tags: + - Sites + /sites/{id}: + delete: + consumes: + - application/json + description: Delete a site by its ID + parameters: + - description: Site ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No Content + "404": + description: Not Found + schema: + $ref: '#/definitions/models.APIError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.APIError' + summary: Delete a site + tags: + - Sites + get: + consumes: + - application/json + description: Get a single site by its ID + parameters: + - description: Site ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.Site' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.APIError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.APIError' + summary: Get site by ID + tags: + - Sites + put: + consumes: + - application/json + description: Update an existing site by its ID + parameters: + - description: Site ID + in: path + name: id + required: true + type: string + - description: Updated site details + in: body + name: site + required: true + schema: + $ref: '#/definitions/models.Site' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.Site' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.APIError' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.APIError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.APIError' + summary: Update an existing site + tags: + - Sites + /sites/{id}/custom-headers: + get: + description: Returns all custom header groups associated with the given site + parameters: + - description: Site ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: List of custom header groups + schema: + items: + $ref: '#/definitions/models.CustomHeaders' + type: array + "404": + description: Site not found + schema: + $ref: '#/definitions/models.APIError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.APIError' + summary: List custom header groups for a site + tags: + - Custom-Headers + post: + consumes: + - application/json + description: Creates a new custom header group for the given site + parameters: + - description: Site ID + in: path + name: id + required: true + type: string + - description: Custom header group to create + in: body + name: customHeaders + required: true + schema: + $ref: '#/definitions/models.CustomHeaders' + produces: + - application/json + responses: + "200": + description: Created custom header group + schema: + $ref: '#/definitions/models.CustomHeaders' + "400": + description: Invalid request body + schema: + $ref: '#/definitions/models.APIError' + "404": + description: Site not found + schema: + $ref: '#/definitions/models.APIError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.APIError' + summary: Create a custom header group + tags: + - Custom-Headers + /sites/{id}/enabled: + patch: + consumes: + - application/json + description: Enable or disable a site by its ID + parameters: + - description: Site ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.ToggleEnabledResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.APIError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.APIError' + summary: Toggle site enabled status + tags: + - Sites + /sites/{id}/forward-rules: + get: + description: Returns all forward rules associated with the given site + parameters: + - description: Site ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: List of forward rules + schema: + items: + $ref: '#/definitions/models.ForwardRule' + type: array + "404": + description: Site not found + schema: + $ref: '#/definitions/models.APIError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.APIError' + summary: List forward rules for a site + tags: + - Forward-Rules + post: + consumes: + - application/json + description: Creates a new forward rule for the given site + parameters: + - description: Site ID + in: path + name: id + required: true + type: string + - description: Forward rule to create + in: body + name: rule + required: true + schema: + $ref: '#/definitions/models.ForwardRule' + produces: + - application/json + responses: + "200": + description: Created forward rule + schema: + $ref: '#/definitions/models.ForwardRule' + "400": + description: Invalid request body + schema: + $ref: '#/definitions/models.APIError' + "404": + description: Site not found + schema: + $ref: '#/definitions/models.APIError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.APIError' + summary: Create a forward rule + tags: + - Forward-Rules + /sites/{siteId}/deployments: + get: + consumes: + - application/json + description: Get a list of deployments for a specific site + parameters: + - description: Site ID + in: path + name: siteId + required: true + type: string + - default: 100 + description: Maximum number of deployments to return + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.GetDeploymentsBySiteResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.APIError' + summary: Get deployments for a site + tags: + - Deployments +swagger: "2.0" +tags: +- description: Manage sites + name: Sites +- description: Manage redirect rules for a site + name: Forward-Rules +- description: Manage custom header groups for a site + name: Custom-Headers +- description: Manage individual headers within a custom header group + name: Headers diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..50376db --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,52 @@ +module quay + +go 1.25.0 + +require ( + github.com/Flussen/swagger-fiber-v3 v1.0.1 + github.com/gofiber/fiber/v3 v3.1.0 + github.com/golang-jwt/jwt/v5 v5.0.0 + github.com/google/uuid v1.6.0 + github.com/joho/godotenv v1.5.1 + github.com/maypok86/otter/v2 v2.3.0 + github.com/swaggo/swag v1.16.4 + golang.org/x/crypto v0.48.0 + modernc.org/sqlite v1.50.0 +) + +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/andybalholm/brotli v1.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.6 // indirect + github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/gofiber/schema v1.7.0 // indirect + github.com/gofiber/utils/v2 v2.0.2 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/klauspost/compress v1.18.4 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/philhofer/fwd v1.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/stretchr/testify v1.11.1 // indirect + github.com/tinylib/msgp v1.6.3 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.69.0 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/tools v0.42.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.72.0 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 0000000..83d81bd --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,153 @@ +github.com/Flussen/swagger-fiber-v3 v1.0.1 h1:lgR2+ADJRx7Kh4oGidsf790UVwXrgC4I7p/3SAmHimw= +github.com/Flussen/swagger-fiber-v3 v1.0.1/go.mod h1:rHViWTgpklVFVsYkWgL8zip4QHJlKwuBax8wY0G3sPw= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= +github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= +github.com/gofiber/schema v1.7.0 h1:yNM+FNRZjyYEli9Ey0AXRBrAY9jTnb+kmGs3lJGPvKg= +github.com/gofiber/schema v1.7.0/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk= +github.com/gofiber/utils/v2 v2.0.2 h1:ShRRssz0F3AhTlAQcuEj54OEDtWF7+HJDwEi/aa6QLI= +github.com/gofiber/utils/v2 v2.0.2/go.mod h1:+9Ub4NqQ+IaJoTliq5LfdmOJAA/Hzwf4pXOxOa3RrJ0= +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/maypok86/otter/v2 v2.3.0 h1:8H8AVVFUSzJwIegKwv1uF5aGitTY+AIrtktg7OcLs8w= +github.com/maypok86/otter/v2 v2.3.0/go.mod h1:XgIdlpmL6jYz882/CAx1E4C1ukfgDKSaw4mWq59+7l8= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU= +github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/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= +github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= +github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= +github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s= +github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI= +github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +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/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U= +modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8= +modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU= +modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= +modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c= +modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM= +modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/backend/internal/database/deployment_sqlite.go b/backend/internal/database/deployment_sqlite.go new file mode 100644 index 0000000..d1fea1f --- /dev/null +++ b/backend/internal/database/deployment_sqlite.go @@ -0,0 +1,149 @@ +package database + +import ( + "database/sql" + "fmt" + "quay/app/models" + "quay/app/repository" + "time" + + "github.com/google/uuid" +) + +type SQLiteDeploymentRepository struct { + db *sql.DB +} + +func NewSQLiteDeploymentRepository(db *sql.DB) *SQLiteDeploymentRepository { + return &SQLiteDeploymentRepository{db: db} +} + +var _ repository.DeploymentRepository = (*SQLiteDeploymentRepository)(nil) + +func (r *SQLiteDeploymentRepository) GetDeploymentByID(id string) (*models.Deployment, error) { + row := r.db.QueryRow(` + SELECT id, site_id, commit_hash, message, status, start_time, finish_time, created_at + FROM deployments WHERE id = ?`, id) + + d, err := scanDeployment(row) + if err != nil { + return nil, fmt.Errorf("get deployment: %w", err) + } + return d, nil +} + +func (r *SQLiteDeploymentRepository) GetDeploymentsForSite(siteId string, limit int) ([]models.Deployment, error) { + rows, err := r.db.Query(` + SELECT id, site_id, commit_hash, message, status, start_time, finish_time, created_at + FROM deployments WHERE site_id = ? + ORDER BY created_at DESC + LIMIT ?`, siteId, limit) + + if err != nil { + return nil, fmt.Errorf("get deployments: %w", err) + } + + var deployments []models.Deployment + for rows.Next() { + d, err := scanDeployment(rows) + if err != nil { + return nil, fmt.Errorf("scan deployment: %w", err) + } + deployments = append(deployments, *d) + } + rows.Close() + if err := rows.Err(); err != nil { + return nil, err + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate deployments: %w", err) + } + + return deployments, nil +} + +func (r *SQLiteDeploymentRepository) CreateDeployment(d *models.Deployment) (string, error) { + d.Id = uuid.NewString() + if d.Status == "" || !d.Status.IsValid() { + d.Status = models.DeploymentStatusPending + } + _, err := r.db.Exec(` + INSERT INTO deployments (id, site_id, commit_hash, message, status, start_time, finish_time) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + d.Id, d.SiteId, d.CommitHash, d.Message, d.Status, d.StartTime, d.FinishTime, + ) + if err != nil { + return "", fmt.Errorf("create deployment: %w", err) + } + return d.Id, nil +} + +func (r *SQLiteDeploymentRepository) UpdateDeployment(d *models.Deployment) error { + _, err := r.db.Exec(` + UPDATE deployments SET site_id=?, commit_hash=?, message=?, status=?, start_time=?, finish_time=? + WHERE id=?`, + d.SiteId, d.CommitHash, d.Message, d.Status, d.StartTime, d.FinishTime, d.Id, + ) + if err != nil { + return fmt.Errorf("update deployment: %w", err) + } + return nil +} + +func (r *SQLiteDeploymentRepository) UpdateDeploymentStatus(deploymentId string, status models.DeploymentStatus) error { + if !status.IsValid() { + return fmt.Errorf("invalid deployment status") + } + + _, err := r.db.Exec(` + UPDATE deployments SET status=? WHERE id=?`, + status, deploymentId, + ) + if err != nil { + return fmt.Errorf("update deployment status: %w", err) + } + return nil +} + +func (r *SQLiteDeploymentRepository) UpdateDeploymentFinishTime(deploymentId string, finishTime time.Time) error { + _, err := r.db.Exec(` + UPDATE deployments SET finish_time=? WHERE id=?`, + finishTime.Format(time.DateTime), deploymentId, + ) + if err != nil { + return fmt.Errorf("update deployment finish time: %w", err) + } + return nil +} + +func (r *SQLiteDeploymentRepository) UpdateDeploymentGitInfo(deploymentId, commitHash, message string) error { + _, err := r.db.Exec(` + UPDATE deployments SET commit_hash=?, message=? WHERE id=?`, + commitHash, message, deploymentId, + ) + if err != nil { + return fmt.Errorf("update deployment git info: %w", err) + } + return nil +} + +func (r *SQLiteDeploymentRepository) DeleteDeployment(id string) error { + _, err := r.db.Exec(`DELETE FROM deployments WHERE id = ?`, id) + if err != nil { + return fmt.Errorf("delete deployment: %w", err) + } + return nil +} + +func scanDeployment(s scanner) (*models.Deployment, error) { + var d models.Deployment + err := s.Scan( + &d.Id, &d.SiteId, &d.CommitHash, &d.Message, + &d.Status, &d.StartTime, &d.FinishTime, &d.CreatedAt, + ) + if err != nil { + return nil, err + } + return &d, nil +} diff --git a/backend/internal/database/gitserver_sqlite.go b/backend/internal/database/gitserver_sqlite.go new file mode 100644 index 0000000..03df7bd --- /dev/null +++ b/backend/internal/database/gitserver_sqlite.go @@ -0,0 +1,104 @@ +package database + +import ( + "database/sql" + "fmt" + "quay/app/models" + "quay/app/repository" + + "github.com/google/uuid" +) + +type SQLiteGitServerRepository struct { + db *sql.DB +} + +func NewSQLiteGitServerRepository(db *sql.DB) *SQLiteGitServerRepository { + return &SQLiteGitServerRepository{db: db} +} + +var _ repository.GitServerRepository = (*SQLiteGitServerRepository)(nil) + +func (r *SQLiteGitServerRepository) ListGitServers() ([]models.GitServer, error) { + rows, err := r.db.Query(`SELECT id, name, protocol, base_url, type, auth_token, created_at FROM gitservers`) + if err != nil { + return nil, fmt.Errorf("list gitservers: %w", err) + } + + var out []models.GitServer + for rows.Next() { + gs, err := scanGitServer(rows) + if err != nil { + rows.Close() + return nil, fmt.Errorf("list gitservers scan: %w", err) + } + out = append(out, *gs) + } + rows.Close() + if err := rows.Err(); err != nil { + return nil, err + } + return out, nil +} + +func (r *SQLiteGitServerRepository) GetGitServer(id string) (*models.GitServer, error) { + row := r.db.QueryRow(`SELECT id, name, protocol, base_url, type, auth_token, created_at FROM gitservers WHERE id = ?`, id) + return scanGitServer(row) +} + +func (r *SQLiteGitServerRepository) CreateGitServer(gs *models.GitServer) error { + tx, err := r.db.Begin() + if err != nil { + return fmt.Errorf("create gitserver begin tx: %w", err) + } + defer tx.Rollback() + + gs.ID = uuid.NewString() + + _, err = tx.Exec(`INSERT INTO gitservers (id, name, protocol, base_url, type, auth_token) VALUES (?, ?, ?, ?, ?, ?)`, + gs.ID, gs.Name, gs.Protocol, gs.BaseUrl, gs.Type, gs.AuthToken) + if err != nil { + if isSQLiteUniqueConstraintError(err) { + return fmt.Errorf("create gitserver insert: %w", repository.ErrGitServerAlreadyExists) + } + return fmt.Errorf("create gitserver insert: %w", err) + } + + return tx.Commit() +} + +func (r *SQLiteGitServerRepository) UpdateGitServer(gs *models.GitServer) error { + tx, err := r.db.Begin() + if err != nil { + return fmt.Errorf("update gitserver begin tx: %w", err) + } + defer tx.Rollback() + + _, err = tx.Exec(`UPDATE gitservers SET name = ?, protocol = ?, base_url = ?, type = ?, auth_token = ? WHERE id = ?`, + gs.Name, gs.Protocol, gs.BaseUrl, gs.Type, gs.AuthToken, gs.ID) + if err != nil { + if isSQLiteUniqueConstraintError(err) { + return fmt.Errorf("update gitserver: %w", repository.ErrGitServerAlreadyExists) + } + return fmt.Errorf("update gitserver: %w", err) + } + + return tx.Commit() +} + +func (r *SQLiteGitServerRepository) DeleteGitServer(id string) error { + _, err := r.db.Exec(`DELETE FROM gitservers WHERE id = ?`, id) + if err != nil { + return fmt.Errorf("delete gitserver: %w", err) + } + return nil +} + +func scanGitServer(s scanner) (*models.GitServer, error) { + g := new(models.GitServer) + err := s.Scan(&g.ID, &g.Name, &g.Protocol, &g.BaseUrl, &g.Type, &g.AuthToken, &g.CreatedAt) + if err != nil { + return nil, err + } + return g, nil +} diff --git a/backend/internal/database/init_sqlite.go b/backend/internal/database/init_sqlite.go new file mode 100644 index 0000000..8b79683 --- /dev/null +++ b/backend/internal/database/init_sqlite.go @@ -0,0 +1,89 @@ +package database + +import ( + "database/sql" + "log" +) + +func InitializeSQLite(db *sql.DB) error { + _, err := db.Exec(` +CREATE TABLE IF NOT EXISTS sites ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + git_server TEXT NOT NULL, + owner TEXT NOT NULL, + repository TEXT NOT NULL, + branch TEXT NOT NULL, + domain TEXT NOT NULL, + deploy_token TEXT NOT NULL, + enabled INTEGER NOT NULL DEFAULT 1, + spa INTEGER NOT NULL DEFAULT 0, + not_found_file TEXT NOT NULL DEFAULT '404.html', + index_file TEXT NOT NULL DEFAULT 'index.html', + trailing_slash INTEGER DEFAULT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS forward_rules ( + id TEXT PRIMARY KEY, + site_id TEXT NOT NULL, + source TEXT NOT NULL, + destination TEXT NOT NULL, + status_code INTEGER NOT NULL, + regex INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS custom_headers ( + id TEXT PRIMARY KEY, + site_id TEXT NOT NULL, + source TEXT NOT NULL, + regex INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS headers ( + id TEXT PRIMARY KEY, + custom_header_id TEXT NOT NULL, + key TEXT NOT NULL, + value TEXT NOT NULL, + FOREIGN KEY (custom_header_id) REFERENCES custom_headers(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS deployments ( + id TEXT PRIMARY KEY, + site_id TEXT NOT NULL, + commit_hash TEXT, + message TEXT, + status TEXT NOT NULL, + start_time TIMESTAMP NOT NULL DEFAULT 0, + finish_time TIMESTAMP NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + role TEXT NOT NULL, + hashed_password TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +) +; + +CREATE TABLE IF NOT EXISTS gitservers ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + protocol TEXT NOT NULL, + base_url TEXT NOT NULL, + type TEXT NOT NULL, + auth_token TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +`) + + if err == nil { + log.Println("Database initialized successfully") + } + + return err +} diff --git a/backend/internal/database/site_sqlite.go b/backend/internal/database/site_sqlite.go new file mode 100644 index 0000000..9ace9cd --- /dev/null +++ b/backend/internal/database/site_sqlite.go @@ -0,0 +1,505 @@ +package database + +import ( + "database/sql" + "errors" + "fmt" + "quay/app/models" + "quay/app/repository" + + "github.com/google/uuid" +) + +type SQLiteSiteRepository struct { + db *sql.DB +} + +func NewSQLiteSiteRepository(db *sql.DB) *SQLiteSiteRepository { + return &SQLiteSiteRepository{db: db} +} + +var _ repository.SiteRepository = (*SQLiteSiteRepository)(nil) + +// Sites + +func (r *SQLiteSiteRepository) GetSite(id string) (*models.Site, error) { + row := r.db.QueryRow(` + SELECT id, name, git_server, owner, repository, branch, domain, deploy_token, spa, enabled, not_found_file, index_file, trailing_slash, created_at + FROM sites WHERE id = ?`, id) + + s, err := scanSite(row) + if err != nil { + return nil, fmt.Errorf("get site: %w", err) + } + + if err := r.populateSiteRelations(s); err != nil { + return nil, err + } + return s, nil +} + +func (r *SQLiteSiteRepository) GetSiteByDomain(domain string) (*models.Site, error) { + row := r.db.QueryRow(` + SELECT id, name, git_server, owner, repository, branch, domain, deploy_token, spa, enabled, not_found_file, index_file, trailing_slash, created_at + FROM sites WHERE domain = ?`, domain) + + s, err := scanSite(row) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return nil, fmt.Errorf("get site by domain: %w", err) + } + + if err := r.populateSiteRelations(s); err != nil { + return nil, err + } + return s, nil +} + +func (r *SQLiteSiteRepository) ListSites() ([]models.Site, error) { + rows, err := r.db.Query(` + SELECT id, name, git_server, owner, repository, branch, domain, deploy_token, spa, enabled, not_found_file, index_file, trailing_slash, created_at + FROM sites`) + if err != nil { + return nil, fmt.Errorf("list sites: %w", err) + } + + var sites []models.Site + for rows.Next() { + s, err := scanSite(rows) + if err != nil { + rows.Close() + return nil, fmt.Errorf("list sites scan: %w", err) + } + sites = append(sites, *s) + } + rows.Close() + if err := rows.Err(); err != nil { + return nil, err + } + + for i := range sites { + if err := r.populateSiteRelations(&sites[i]); err != nil { + return nil, err + } + } + return sites, nil +} + +func (r *SQLiteSiteRepository) CreateSite(s *models.Site) error { + tx, err := r.db.Begin() + if err != nil { + return fmt.Errorf("create site begin tx: %w", err) + } + defer tx.Rollback() + + s.ID = uuid.NewString() + + _, err = tx.Exec(` + INSERT INTO sites (id, name, git_server, owner, repository, branch, domain, deploy_token, spa, enabled, not_found_file, index_file, trailing_slash) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + s.ID, s.Name, s.GitServer, s.Owner, s.Repository, s.Branch, + s.Domain, s.DeployToken, s.Spa, s.Enabled, s.NotFoundFile, s.IndexFile, s.TrailingSlash, + ) + if err != nil { + return fmt.Errorf("create site insert: %w", err) + } + + for i := range s.ForwardRules { + if err := insertForwardRule(tx, s.ID, &s.ForwardRules[i]); err != nil { + return err + } + } + + for i := range s.CustomHeaders { + if err := insertCustomHeaders(tx, s.ID, &s.CustomHeaders[i]); err != nil { + return err + } + } + + return tx.Commit() +} + +func (r *SQLiteSiteRepository) UpdateSite(s *models.Site) error { + tx, err := r.db.Begin() + if err != nil { + return fmt.Errorf("update site begin tx: %w", err) + } + defer tx.Rollback() + + _, err = tx.Exec(` + UPDATE sites SET name=?, git_server=?, owner=?, repository=?, branch=?, domain=?, + deploy_token=?, spa=?, enabled=?, not_found_file=?, index_file=?, trailing_slash=? WHERE id=?`, + s.Name, s.GitServer, s.Owner, s.Repository, s.Branch, s.Domain, + s.DeployToken, s.Spa, s.Enabled, s.NotFoundFile, s.IndexFile, s.TrailingSlash, + s.ID, + ) + if err != nil { + return fmt.Errorf("update site: %w", err) + } + + if _, err := tx.Exec(`DELETE FROM forward_rules WHERE site_id = ?`, s.ID); err != nil { + return fmt.Errorf("update site delete forward rules: %w", err) + } + for i := range s.ForwardRules { + if err := insertForwardRule(tx, s.ID, &s.ForwardRules[i]); err != nil { + return err + } + } + + if _, err := tx.Exec(` + DELETE FROM headers WHERE custom_header_id IN ( + SELECT id FROM custom_headers WHERE site_id = ? + )`, s.ID); err != nil { + return fmt.Errorf("update site delete headers: %w", err) + } + if _, err := tx.Exec(`DELETE FROM custom_headers WHERE site_id = ?`, s.ID); err != nil { + return fmt.Errorf("update site delete custom headers: %w", err) + } + for _, ch := range s.CustomHeaders { + if err := insertCustomHeaders(tx, s.ID, &ch); err != nil { + return err + } + } + + return tx.Commit() +} + +func (r *SQLiteSiteRepository) ToggleEnabled(id string) (enabledReturn bool, err error) { + tx, err := r.db.Begin() + if err != nil { + return false, fmt.Errorf("toggle enabled begin tx: %w", err) + } + defer tx.Rollback() + + var enabled int + err = tx.QueryRow(`SELECT enabled FROM sites WHERE id = ?`, id).Scan(&enabled) + if err != nil { + return false, fmt.Errorf("toggle enabled select: %w", err) + } + + newEnabled := 0 + if enabled == 0 { + newEnabled = 1 + } + + _, err = tx.Exec(`UPDATE sites SET enabled = ? WHERE id = ?`, newEnabled, id) + if err != nil { + return false, fmt.Errorf("toggle enabled update: %w", err) + } + + if err := tx.Commit(); err != nil { + return false, fmt.Errorf("toggle enabled commit: %w", err) + } + return newEnabled != 0, nil +} + +func (r *SQLiteSiteRepository) DeleteSite(id string) error { + _, err := r.db.Exec(`DELETE FROM sites WHERE id = ?`, id) + if err != nil { + return fmt.Errorf("delete site: %w", err) + } + return nil +} + +// Forward Rules + +func (r *SQLiteSiteRepository) GetForwardRule(id string) (*models.ForwardRule, error) { + row := r.db.QueryRow(` + SELECT id, source, destination, status_code, regex + FROM forward_rules WHERE id = ?`, id) + + var fr models.ForwardRule + var regex int + err := row.Scan(&fr.ID, &fr.Source, &fr.Destination, &fr.StatusCode, ®ex) + if err != nil { + return nil, fmt.Errorf("get forward rule: %w", err) + } + fr.Regex = regex != 0 + return &fr, nil +} + +func (r *SQLiteSiteRepository) CreateForwardRule(siteID string, fr *models.ForwardRule) error { + tx, err := r.db.Begin() + if err != nil { + return fmt.Errorf("create forward rule begin tx: %w", err) + } + defer tx.Rollback() + if err := insertForwardRule(tx, siteID, fr); err != nil { + return err + } + return tx.Commit() +} + +func (r *SQLiteSiteRepository) UpdateForwardRule(siteID string, fr *models.ForwardRule) error { + _, err := r.db.Exec(` + UPDATE forward_rules SET source=?, destination=?, status_code=?, regex=? WHERE id=?`, + fr.Source, fr.Destination, fr.StatusCode, fr.Regex, fr.ID, + ) + if err != nil { + return fmt.Errorf("update forward rule: %w", err) + } + return nil +} + +func (r *SQLiteSiteRepository) DeleteForwardRule(_ string, id string) error { + _, err := r.db.Exec(`DELETE FROM forward_rules WHERE id = ?`, id) + if err != nil { + return fmt.Errorf("delete forward rule: %w", err) + } + return nil +} + +// Custom Headers + +func (r *SQLiteSiteRepository) GetCustomHeaders(id string) (*models.CustomHeaders, error) { + row := r.db.QueryRow(`SELECT id, source, regex FROM custom_headers WHERE id = ?`, id) + + var ch models.CustomHeaders + var regex int + if err := row.Scan(&ch.ID, &ch.Source, ®ex); err != nil { + return nil, fmt.Errorf("get custom headers: %w", err) + } + + headers, err := r.listHeaders(ch.ID) + if err != nil { + return nil, err + } + ch.Headers = headers + ch.Regex = regex != 0 + return &ch, nil +} + +func (r *SQLiteSiteRepository) CreateCustomHeaders(siteID string, ch *models.CustomHeaders) error { + tx, err := r.db.Begin() + if err != nil { + return fmt.Errorf("create custom headers begin tx: %w", err) + } + defer tx.Rollback() + if err := insertCustomHeaders(tx, siteID, ch); err != nil { + return err + } + return tx.Commit() +} + +func (r *SQLiteSiteRepository) UpdateCustomHeaders(siteID string, ch *models.CustomHeaders) error { + tx, err := r.db.Begin() + if err != nil { + return fmt.Errorf("update custom headers begin tx: %w", err) + } + defer tx.Rollback() + + if _, err := tx.Exec(`UPDATE custom_headers SET source=?, regex=? WHERE id=?`, ch.Source, ch.Regex, ch.ID); err != nil { + return fmt.Errorf("update custom headers: %w", err) + } + if _, err := tx.Exec(`DELETE FROM headers WHERE custom_header_id = ?`, ch.ID); err != nil { + return fmt.Errorf("update custom headers delete headers: %w", err) + } + for _, h := range ch.Headers { + if err := insertHeader(tx, ch.ID, &h); err != nil { + return err + } + } + return tx.Commit() +} + +func (r *SQLiteSiteRepository) DeleteCustomHeaders(_ string, id string) error { + _, err := r.db.Exec(`DELETE FROM custom_headers WHERE id = ?`, id) + if err != nil { + return fmt.Errorf("delete custom headers: %w", err) + } + return nil +} + +// Headers + +func (r *SQLiteSiteRepository) GetHeader(id string) (*models.Header, error) { + row := r.db.QueryRow(`SELECT id, key, value FROM headers WHERE id = ?`, id) + + var h models.Header + if err := row.Scan(&h.ID, &h.Key, &h.Value); err != nil { + return nil, fmt.Errorf("get header: %w", err) + } + return &h, nil +} + +func (r *SQLiteSiteRepository) CreateHeader(_ string, customHeaderID string, h *models.Header) error { + h.ID = uuid.NewString() + _, err := r.db.Exec( + `INSERT INTO headers (id, custom_header_id, key, value) VALUES (?, ?, ?, ?)`, + h.ID, customHeaderID, h.Key, h.Value, + ) + if err != nil { + return fmt.Errorf("create header: %w", err) + } + return nil +} + +func (r *SQLiteSiteRepository) UpdateHeader(_ string, h *models.Header) error { + _, err := r.db.Exec(`UPDATE headers SET key=?, value=? WHERE id=?`, h.Key, h.Value, h.ID) + if err != nil { + return fmt.Errorf("update header: %w", err) + } + return nil +} + +func (r *SQLiteSiteRepository) DeleteHeader(_ string, id string) error { + _, err := r.db.Exec(`DELETE FROM headers WHERE id = ?`, id) + if err != nil { + return fmt.Errorf("delete header: %w", err) + } + return nil +} + +// Helpers + +type scanner interface { + Scan(dest ...any) error +} + +func scanSite(s scanner) (*models.Site, error) { + var site models.Site + var enabled int + err := s.Scan( + &site.ID, &site.Name, &site.GitServer, &site.Owner, &site.Repository, + &site.Branch, &site.Domain, &site.DeployToken, &site.Spa, &enabled, &site.NotFoundFile, + &site.IndexFile, &site.TrailingSlash, &site.CreatedAt, + ) + if err != nil { + return nil, err + } + site.Enabled = enabled != 0 + return &site, nil +} + +func (r *SQLiteSiteRepository) populateSiteRelations(s *models.Site) error { + rules, err := r.listForwardRules(s.ID) + if err != nil { + return err + } + s.ForwardRules = rules + + customHeaders, err := r.listCustomHeaders(s.ID) + if err != nil { + return err + } + s.CustomHeaders = customHeaders + + return nil +} + +func (r *SQLiteSiteRepository) listForwardRules(siteID string) ([]models.ForwardRule, error) { + rows, err := r.db.Query(` + SELECT id, source, destination, status_code, regex + FROM forward_rules WHERE site_id = ?`, siteID) + if err != nil { + return nil, fmt.Errorf("list forward rules: %w", err) + } + defer rows.Close() + + var rules []models.ForwardRule + for rows.Next() { + var fr models.ForwardRule + var regex int + if err := rows.Scan(&fr.ID, &fr.Source, &fr.Destination, &fr.StatusCode, ®ex); err != nil { + return nil, fmt.Errorf("list forward rules scan: %w", err) + } + fr.Regex = regex != 0 + rules = append(rules, fr) + } + return rules, rows.Err() +} + +func (r *SQLiteSiteRepository) listCustomHeaders(siteID string) ([]models.CustomHeaders, error) { + rows, err := r.db.Query(`SELECT id, source, regex FROM custom_headers WHERE site_id = ?`, siteID) + if err != nil { + return nil, fmt.Errorf("list custom headers: %w", err) + } + + var result []models.CustomHeaders + for rows.Next() { + var ch models.CustomHeaders + var regex int + if err := rows.Scan(&ch.ID, &ch.Source, ®ex); err != nil { + rows.Close() + return nil, fmt.Errorf("list custom headers scan: %w", err) + } + ch.Regex = regex != 0 + result = append(result, ch) + } + rows.Close() + if err := rows.Err(); err != nil { + return nil, err + } + + for i := range result { + headers, err := r.listHeaders(result[i].ID) + if err != nil { + return nil, err + } + result[i].Headers = headers + } + return result, nil +} + +func (r *SQLiteSiteRepository) listHeaders(customHeaderID string) ([]models.Header, error) { + rows, err := r.db.Query(` + SELECT id, key, value FROM headers WHERE custom_header_id = ?`, customHeaderID) + if err != nil { + return nil, fmt.Errorf("list headers: %w", err) + } + defer rows.Close() + + var headers []models.Header + for rows.Next() { + var h models.Header + if err := rows.Scan(&h.ID, &h.Key, &h.Value); err != nil { + return nil, fmt.Errorf("list headers scan: %w", err) + } + headers = append(headers, h) + } + return headers, rows.Err() +} + +func insertForwardRule(tx *sql.Tx, siteID string, fr *models.ForwardRule) error { + fr.ID = uuid.NewString() + _, err := tx.Exec(` + INSERT INTO forward_rules (id, site_id, source, destination, status_code, regex) + VALUES (?, ?, ?, ?, ?, ?)`, + fr.ID, siteID, fr.Source, fr.Destination, fr.StatusCode, fr.Regex, + ) + if err != nil { + return fmt.Errorf("insert forward rule: %w", err) + } + return nil +} + +func insertCustomHeaders(tx *sql.Tx, siteID string, ch *models.CustomHeaders) error { + ch.ID = uuid.NewString() + _, err := tx.Exec(` + INSERT INTO custom_headers (id, site_id, source, regex) VALUES (?, ?, ?, ?)`, + ch.ID, siteID, ch.Source, ch.Regex, + ) + if err != nil { + return fmt.Errorf("insert custom headers: %w", err) + } + for _, h := range ch.Headers { + if err := insertHeader(tx, ch.ID, &h); err != nil { + return err + } + } + return nil +} + +func insertHeader(tx *sql.Tx, customHeaderID string, h *models.Header) error { + h.ID = uuid.NewString() + _, err := tx.Exec(` + INSERT INTO headers (id, custom_header_id, key, value) VALUES (?, ?, ?, ?)`, + h.ID, customHeaderID, h.Key, h.Value, + ) + if err != nil { + return fmt.Errorf("insert header: %w", err) + } + return nil +} diff --git a/backend/internal/database/sqlite.go b/backend/internal/database/sqlite.go new file mode 100644 index 0000000..1ac981b --- /dev/null +++ b/backend/internal/database/sqlite.go @@ -0,0 +1,23 @@ +package database + +import ( + "database/sql" + + _ "modernc.org/sqlite" +) + +func ConnectSQLite(dbPath string) (*sql.DB, error) { + db, err := sql.Open("sqlite", dbPath+"?_busy_timeout=5000&_journal_mode=WAL") + if err != nil { + return nil, err + } + + // SQLite doesn't like multiple connections + db.SetMaxOpenConns(1) + + if err = db.Ping(); err != nil { + return nil, err + } + + return db, nil +} diff --git a/backend/internal/database/user_sqlite.go b/backend/internal/database/user_sqlite.go new file mode 100644 index 0000000..fdc1664 --- /dev/null +++ b/backend/internal/database/user_sqlite.go @@ -0,0 +1,158 @@ +package database + +import ( + "database/sql" + "errors" + "fmt" + "quay/app/models" + "quay/app/repository" + + "github.com/google/uuid" + "modernc.org/sqlite" + sqlite3 "modernc.org/sqlite/lib" +) + +type SQLiteUserRepository struct { + db *sql.DB +} + +func NewSQLiteUserRepository(db *sql.DB) *SQLiteUserRepository { + return &SQLiteUserRepository{db: db} +} + +var _ repository.UserRepository = (*SQLiteUserRepository)(nil) + +func (r *SQLiteUserRepository) GetAllUsers() ([]models.User, error) { + rows, err := r.db.Query(` + SELECT id, name, role, hashed_password, created_at + FROM users`) + if err != nil { + return nil, fmt.Errorf("list users: %w", err) + } + + var users []models.User + for rows.Next() { + s, err := scanUser(rows) + if err != nil { + rows.Close() + return nil, fmt.Errorf("list users scan: %w", err) + } + users = append(users, *s) + } + rows.Close() + if err := rows.Err(); err != nil { + return nil, err + } + return users, nil +} + +func (r *SQLiteUserRepository) GetUserById(id string) (*models.User, error) { + row := r.db.QueryRow(` + SELECT id, name, role, hashed_password, created_at + FROM users WHERE id = ?`, id) + + u, err := scanUser(row) + if err != nil { + return nil, fmt.Errorf("get user by id: %w", err) + } + + return u, nil +} + +func (r *SQLiteUserRepository) GetUserByName(name string) (*models.User, error) { + row := r.db.QueryRow(` + SELECT id, name, role, hashed_password, created_at + FROM users WHERE name = ?`, name) + + u, err := scanUser(row) + if err != nil { + return nil, fmt.Errorf("get user by name: %w", err) + } + + return u, nil +} + +func (r *SQLiteUserRepository) CreateUser(user *models.User) error { + tx, err := r.db.Begin() + if err != nil { + return fmt.Errorf("create user begin tx: %w", err) + } + defer tx.Rollback() + + user.ID = uuid.NewString() + + _, err = tx.Exec(` + INSERT INTO users (id, name, role, hashed_password, created_at) + VALUES (?, ?, ?, ?, ?)`, + user.ID, user.Name, user.Role, user.HashedPassword, user.CreatedAt, + ) + if err != nil { + if isSQLiteUniqueConstraintError(err) { + return fmt.Errorf("create user insert: %w", repository.ErrUserAlreadyExists) + } + return fmt.Errorf("create user insert: %w", err) + } + + return tx.Commit() +} + +func (r *SQLiteUserRepository) UpdateUser(user *models.User) error { + tx, err := r.db.Begin() + if err != nil { + return fmt.Errorf("update user begin tx: %w", err) + } + defer tx.Rollback() + + _, err = tx.Exec(` + UPDATE users SET name=?, role=? WHERE id=?`, + user.Name, user.Role, user.ID, + ) + if err != nil { + if isSQLiteUniqueConstraintError(err) { + return fmt.Errorf("update user: %w", repository.ErrUserAlreadyExists) + } + return fmt.Errorf("update user: %w", err) + } + + return tx.Commit() +} + +func (r *SQLiteUserRepository) DeleteUser(id string) error { + _, err := r.db.Exec(`DELETE FROM users WHERE id = ?`, id) + if err != nil { + return fmt.Errorf("delete user: %w", err) + } + return nil +} + +func scanUser(s scanner) (*models.User, error) { + u := new(models.User) + err := s.Scan( + &u.ID, + &u.Name, + &u.Role, + &u.HashedPassword, + &u.CreatedAt, + ) + if err != nil { + return nil, err + } + return u, nil +} + +func (r *SQLiteUserRepository) AdminUserExists() (bool, error) { + var count int + err := r.db.QueryRow(`SELECT COUNT(*) FROM users WHERE role = 'admin'`).Scan(&count) + if err != nil { + return false, fmt.Errorf("admin user exists: %w", err) + } + return count > 0, nil +} + +func isSQLiteUniqueConstraintError(err error) bool { + var sqliteErr *sqlite.Error + if !errors.As(err, &sqliteErr) { + return false + } + return sqliteErr.Code() == sqlite3.SQLITE_CONSTRAINT_UNIQUE +} diff --git a/backend/internal/envconfig/envconfig.go b/backend/internal/envconfig/envconfig.go new file mode 100644 index 0000000..efcdbae --- /dev/null +++ b/backend/internal/envconfig/envconfig.go @@ -0,0 +1,52 @@ +package envconfig + +import ( + "os" +) + +type EnvConfig struct { + Port string + ConfigDir string + GithubPat string + StoragePath string + DashboardHost string + StaticDashboardPath string +} + +func Load() EnvConfig { + port := os.Getenv("PORT") + if port == "" { + port = "4321" + } + + configDir := os.Getenv("CONFIG_DIR") + if configDir == "" { + configDir = "./" + } + + githubPat := os.Getenv("GITHUB_PAT") + if githubPat == "" { + githubPat = "" + } + + storagePath := os.Getenv("STORAGE_PATH") + if storagePath == "" { + storagePath = "./storage" + } + + dashboardHost := os.Getenv("DASHBOARD_HOST") + if dashboardHost == "" { + dashboardHost = "localhost" + } + + staticDashboardPath := os.Getenv("STATIC_DASHBOARD_PATH") + + return EnvConfig{ + Port: port, + ConfigDir: configDir, + GithubPat: githubPat, + StoragePath: storagePath, + DashboardHost: dashboardHost, + StaticDashboardPath: staticDashboardPath, + } +} diff --git a/internal/fiberconfig/fiberconfig.go b/backend/internal/fiberconfig/fiberconfig.go similarity index 73% rename from internal/fiberconfig/fiberconfig.go rename to backend/internal/fiberconfig/fiberconfig.go index beb6f20..1d3f860 100644 --- a/internal/fiberconfig/fiberconfig.go +++ b/backend/internal/fiberconfig/fiberconfig.go @@ -6,11 +6,6 @@ import ( ) func Setup(app *fiber.App) { - app.Use("/api", func(c fiber.Ctx) error { - c.Set("Content-Type", "application/json") - return c.Next() - }) - app.Use(logger.New(logger.Config{ Format: "[${time}] ${status} - ${method} ${path} (${latency}) - ${ip}\n", TimeFormat: "2006-01-02 15:04:05", diff --git a/backend/internal/security/deploy_token.go b/backend/internal/security/deploy_token.go new file mode 100644 index 0000000..16da0f2 --- /dev/null +++ b/backend/internal/security/deploy_token.go @@ -0,0 +1,40 @@ +package security + +import ( + "crypto/rand" + "crypto/sha256" + "crypto/subtle" + "encoding/hex" + "fmt" +) + +func generateToken() (rawToken string, err error) { + bytes := make([]byte, 32) + if _, err = rand.Read(bytes); err != nil { + return "", err + } + return "quay_" + hex.EncodeToString(bytes), nil +} + +func hashToken(rawToken string) string { + sum := sha256.Sum256([]byte(rawToken)) + return hex.EncodeToString(sum[:]) +} + +func CreateDeployToken() (rawToken, hashedToken string, err error) { + rawToken, err = generateToken() + if err != nil { + return "", "", fmt.Errorf("failed to generate token: %w", err) + } + hashedToken = hashToken(rawToken) + return rawToken, hashedToken, nil +} + +func CompareDeployTokens(incomingRawToken, storedHashedToken string) bool { + incomingHash := hashToken(incomingRawToken) + + return subtle.ConstantTimeCompare( + []byte(incomingHash), + []byte(storedHashedToken), + ) == 1 +} diff --git a/backend/internal/security/jwt.go b/backend/internal/security/jwt.go new file mode 100644 index 0000000..2d19515 --- /dev/null +++ b/backend/internal/security/jwt.go @@ -0,0 +1,66 @@ +package security + +import ( + "errors" + "os" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +var ErrMissingSecret = errors.New("JWT secret not set") + +type Claims struct { + UserID string `json:"sub"` + Role string `json:"role"` + jwt.RegisteredClaims +} + +func getSecret() (string, error) { + s := os.Getenv("JWT_SECRET") + if s == "" { + return "", ErrMissingSecret + } + return s, nil +} + +func GenerateToken(userID, role string, ttl time.Duration) (string, error) { + secret, err := getSecret() + if err != nil { + return "", err + } + + claims := Claims{ + UserID: userID, + Role: role, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(ttl)), + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(secret)) +} + +func ValidateToken(tokenString string) (*Claims, error) { + secret, err := getSecret() + if err != nil { + return nil, err + } + + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(t *jwt.Token) (interface{}, error) { + if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.New("unexpected signing method") + } + return []byte(secret), nil + }) + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims, nil + } + return nil, errors.New("invalid token") +} diff --git a/backend/internal/security/password.go b/backend/internal/security/password.go new file mode 100644 index 0000000..2aef301 --- /dev/null +++ b/backend/internal/security/password.go @@ -0,0 +1,13 @@ +package security + +import "golang.org/x/crypto/bcrypt" + +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) + return string(bytes), err +} + +func CheckPasswordHash(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} diff --git a/backend/main.go b/backend/main.go new file mode 100644 index 0000000..be22a6e --- /dev/null +++ b/backend/main.go @@ -0,0 +1,68 @@ +package main + +import ( + "database/sql" + "log" + "os" + "path/filepath" + "quay/app/routes" + "quay/internal/database" + "quay/internal/envconfig" + "quay/internal/fiberconfig" + + "github.com/gofiber/fiber/v3" + "github.com/joho/godotenv" +) + +// @title Quay API +// @version 1.0 +// @description Self-hosted static site deployment service +// @host localhost:4321 +// @BasePath /api/v1 + +// @tag.name Sites +// @tag.description Manage sites + +// @tag.name Forward-Rules +// @tag.description Manage redirect rules for a site + +// @tag.name Custom-Headers +// @tag.description Manage custom header groups for a site + +// @tag.name Headers +// @tag.description Manage individual headers within a custom header group + +func main() { + _ = godotenv.Load() + + envCfg := envconfig.Load() + + if os.Getenv("JWT_SECRET") == "" || os.Getenv("JWT_SECRET") == "CHANGE_ME" { + log.Fatal("JWT_SECRET environment variable is not set or is set to the default value. Please set it to a secure random string before running the application.") + } + + dbPath := filepath.Join(envCfg.ConfigDir, "db.sqlite") + db, err := database.ConnectSQLite(dbPath) + if err != nil { + log.Fatal("Failed to connect to database:", err) + } + log.Println("Connected to database") + defer func(db *sql.DB) { + err := db.Close() + if err != nil { + log.Println("Failed to close database:", err) + } + }(db) + + err = database.InitializeSQLite(db) + if err != nil { + log.Fatal("Failed to initialize database:", err) + } + + app := fiber.New() + + fiberconfig.Setup(app) + routes.Register(app, &envCfg, db) + + log.Fatal(app.Listen(":" + envCfg.Port)) +} diff --git a/config.example.yaml b/config.example.yaml deleted file mode 100644 index 03f378d..0000000 --- a/config.example.yaml +++ /dev/null @@ -1,48 +0,0 @@ -# Configuration file for Quay -# This file defines the global settings and the sites to be deployed. -# You can use ${VARIABLE_NAME} to reference environment variables for sensitive information. - -sites: - - repo: my-lib-docs - owner: yourname - branch: gh-pages - domain: docs.my-lib.com - deploy_token: "${MY_LIB_DEPLOY_TOKEN}" - enabled: true - not_found_file: 404.html - forward_rules: - - source: /npm - destination: https://www.npmjs.com/package/my-lib - status_code: 302 - - source: ^/docs/v(\d+)$ - destination: https://v$1.docs.my-lib.com/ - status_code: 301 - regex: true - - source: ^/blog/(?P[^/]+)$ - destination: /posts/${slug} - status_code: 302 - regex: true - custom_headers: - - source: /json/* # glob (default) - headers: - Content-Type: "application/json" - Cache-Control: "max-age=7200, must-revalidate" - - source: ^/api/v\d+/ # regex opt-in - regex: true - headers: - Cache-Control: "no-store" - - - repo: portfolio - owner: yourname - branch: main - domain: yourname.com - spa: true - deploy_token: "${PORTFOLIO_DEPLOY_TOKEN}" - enabled: true - - - repo: other-docs - owner: yourname - branch: gh-pages - domain: other-docs.yourdomain.com - deploy_token: "${OTHER_DOCS_DEPLOY_TOKEN}" - enabled: true \ No newline at end of file diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 0c5f7e0..18f3c1b 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -6,6 +6,9 @@ services: container_name: quay env_file: - .env + environment: + - JWT_SECRET=b9bddc359f9f1b346582e9b50ce65c3c6c6242a8b6e21421a7411b325b8682c4 + - DASHBOARD_HOST=localhost ports: - '8080:4321' volumes: diff --git a/docker-compose.yml b/docker-compose.yml index f0395cb..3ad4481 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,9 @@ services: container_name: quay env_file: - .env + environment: + - JWT_SECRET=b9bddc359f9f1b346582e9b50ce65c3c6c6242a8b6e21421a7411b325b8682c4 + - DASHBOARD_HOST=localhost ports: - '8080:4321' volumes: diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..7dbf7eb --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 0000000..5c23ec4 --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "radix-nova", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "menuColor": "default", + "menuAccent": "subtle", + "registries": {} +} diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..61cefc9 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,20 @@ + + + + + + + Quay + + + + + + + + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..42e8e3e --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,43 @@ +{ + "name": "quay-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@fontsource-variable/geist": "^5.2.8", + "@radix-ui/react-visually-hidden": "^1.2.4", + "@tailwindcss/vite": "^4.2.2", + "@tanstack/react-query": "^5.96.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^1.7.0", + "radix-ui": "^1.4.3", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-router": "^7.14.0", + "shadcn": "^4.1.2", + "tailwind-merge": "^3.5.0", + "tailwindcss": "^4.2.2", + "tw-animate-css": "^1.4.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@types/node": "^24.12.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.57.0", + "vite": "^8.0.1" + } +} diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml new file mode 100644 index 0000000..663108e --- /dev/null +++ b/frontend/pnpm-lock.yaml @@ -0,0 +1,6095 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@fontsource-variable/geist': + specifier: ^5.2.8 + version: 5.2.8 + '@radix-ui/react-visually-hidden': + specifier: ^1.2.4 + version: 1.2.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tailwindcss/vite': + specifier: ^4.2.2 + version: 4.2.2(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.1)(jiti@2.6.1)) + '@tanstack/react-query': + specifier: ^5.96.2 + version: 5.96.2(react@19.2.4) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + lucide-react: + specifier: ^1.7.0 + version: 1.7.0(react@19.2.4) + radix-ui: + specifier: ^1.4.3 + version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: + specifier: ^19.2.4 + version: 19.2.4 + react-dom: + specifier: ^19.2.4 + version: 19.2.4(react@19.2.4) + react-router: + specifier: ^7.14.0 + version: 7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + shadcn: + specifier: ^4.1.2 + version: 4.1.2(@types/node@24.12.1)(typescript@5.9.3) + tailwind-merge: + specifier: ^3.5.0 + version: 3.5.0 + tailwindcss: + specifier: ^4.2.2 + version: 4.2.2 + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 + devDependencies: + '@eslint/js': + specifier: ^9.39.4 + version: 9.39.4 + '@types/node': + specifier: ^24.12.0 + version: 24.12.1 + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.1)(jiti@2.6.1)) + eslint: + specifier: ^9.39.4 + version: 9.39.4(jiti@2.6.1) + eslint-plugin-react-hooks: + specifier: ^7.0.1 + version: 7.0.1(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-react-refresh: + specifier: ^0.5.2 + version: 0.5.2(eslint@9.39.4(jiti@2.6.1)) + globals: + specifier: ^17.4.0 + version: 17.4.0 + typescript: + specifier: ~5.9.3 + version: 5.9.3 + typescript-eslint: + specifier: ^8.57.0 + version: 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + vite: + specifier: ^8.0.1 + version: 8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.1)(jiti@2.6.1) + +packages: + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.28.6': + resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.28.6': + resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.28.6': + resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.6': + resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-typescript@7.28.5': + resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@dotenvx/dotenvx@1.59.1': + resolution: {integrity: sha512-Qg+meC+XFxliuVSDlEPkKnaUjdaJKK6FNx/Wwl2UxhQR8pyPIuLhMavsF7ePdB9qFZUWV1jEK3ckbJir/WmF4w==} + hasBin: true + + '@ecies/ciphers@0.2.6': + resolution: {integrity: sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g==} + engines: {bun: '>=1', deno: '>=2.7.10', node: '>=16'} + peerDependencies: + '@noble/ciphers': ^1.0.0 + + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@fontsource-variable/geist@5.2.8': + resolution: {integrity: sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw==} + + '@hono/node-server@1.19.12': + resolution: {integrity: sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@mswjs/interceptors@0.41.3': + resolution: {integrity: sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==} + engines: {node: '>=18'} + + '@napi-rs/wasm-runtime@1.1.2': + resolution: {integrity: sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@noble/ciphers@1.3.0': + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.7': + resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + + '@oxc-project/types@0.122.0': + resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-accessible-icon@1.1.7': + resolution: {integrity: sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-accordion@1.2.12': + resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-alert-dialog@1.1.15': + resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-aspect-ratio@1.1.7': + resolution: {integrity: sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-avatar@1.1.10': + resolution: {integrity: sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collapsible@1.1.12': + resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context-menu@2.2.16': + resolution: {integrity: sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.16': + resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-form@0.1.8': + resolution: {integrity: sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-hover-card@1.1.15': + resolution: {integrity: sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.7': + resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.1.16': + resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menubar@1.1.16': + resolution: {integrity: sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-navigation-menu@1.2.14': + resolution: {integrity: sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-one-time-password-field@0.1.8': + resolution: {integrity: sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-password-toggle-field@0.1.3': + resolution: {integrity: sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.4': + resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-progress@1.1.7': + resolution: {integrity: sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-radio-group@1.3.8': + resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.2.10': + resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.7': + resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slider@1.3.6': + resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.2.4': + resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toast@1.2.15': + resolution: {integrity: sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle-group@1.1.11': + resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle@1.1.10': + resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toolbar@1.1.11': + resolution: {integrity: sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.8': + resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-is-hydrated@0.1.0': + resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-visually-hidden@1.2.4': + resolution: {integrity: sha512-kaeiyGCe844dkb9AVF+rb4yTyb1LiLN/e3es3nLiRyN4dC8AduBYPMnnNlDjX2VDOcvDEiPnRNMJeWCfsX0txg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@rolldown/binding-android-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': + resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.12': + resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} + + '@rolldown/pluginutils@1.0.0-rc.7': + resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@tailwindcss/node@4.2.2': + resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==} + + '@tailwindcss/oxide-android-arm64@4.2.2': + resolution: {integrity: sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.2': + resolution: {integrity: sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.2': + resolution: {integrity: sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.2': + resolution: {integrity: sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': + resolution: {integrity: sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': + resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.2': + resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.2': + resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.2.2': + resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.2.2': + resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': + resolution: {integrity: sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.2': + resolution: {integrity: sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.2': + resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.2.2': + resolution: {integrity: sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@tanstack/query-core@5.96.2': + resolution: {integrity: sha512-hzI6cTVh4KNRk8UtoIBS7Lv9g6BnJPXvBKsvYH1aGWvv0347jT3BnSvztOE+kD76XGvZnRC/t6qdW1CaIfwCeA==} + + '@tanstack/react-query@5.96.2': + resolution: {integrity: sha512-sYyzzJT4G0g02azzJ8o55VFFV31XvFpdUpG+unxS0vSaYsJnSPKGoI6WdPwUucJL1wpgGfwfmntNX/Ub1uOViA==} + peerDependencies: + react: ^18 || ^19 + + '@ts-morph/common@0.27.0': + resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@24.12.1': + resolution: {integrity: sha512-v6Ct1W1Fdz7xg5jYCg4FTrbNcIqzds2jv/HL6+5Rs/Cyjf0oljAgW59zvDZXyYG7nt9MLrAFJv9erP/fLjwt+g==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@types/statuses@2.0.6': + resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + + '@types/validate-npm-package-name@4.0.2': + resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} + + '@typescript-eslint/eslint-plugin@8.58.0': + resolution: {integrity: sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.58.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.58.0': + resolution: {integrity: sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.58.0': + resolution: {integrity: sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.58.0': + resolution: {integrity: sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.58.0': + resolution: {integrity: sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.58.0': + resolution: {integrity: sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.58.0': + resolution: {integrity: sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.58.0': + resolution: {integrity: sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.58.0': + resolution: {integrity: sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.58.0': + resolution: {integrity: sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-react@6.0.1': + resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + ast-types@0.16.1: + resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + engines: {node: '>=4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.13: + resolution: {integrity: sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw==} + engines: {node: '>=6.0.0'} + hasBin: true + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + brace-expansion@1.1.13: + resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} + + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001784: + resolution: {integrity: sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + code-block-writer@13.0.3: + resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cosmiconfig@9.0.1: + resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + dedent@1.7.2: + resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + + dotenv@17.4.0: + resolution: {integrity: sha512-kCKF62fwtzwYm0IGBNjRUjtJgMfGapII+FslMHIjMR5KTnwEmBmWLDRSnc3XSNP8bNy34tekgQyDT0hr7pERRQ==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eciesjs@0.4.18: + resolution: {integrity: sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ==} + engines: {bun: '>=1', deno: '>=2', node: '>=16'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.5.331: + resolution: {integrity: sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + enhanced-resolve@5.20.1: + resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} + engines: {node: '>=10.13.0'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-react-hooks@7.0.1: + resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react-refresh@0.5.2: + resolution: {integrity: sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==} + peerDependencies: + eslint: ^9 || ^10 + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + execa@9.6.1: + resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} + engines: {node: ^18.19.0 || >=20.5.0} + + express-rate-limit@8.3.2: + resolution: {integrity: sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-extra@11.3.4: + resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} + engines: {node: '>=14.14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + fuzzysort@3.1.0: + resolution: {integrity: sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.5.0: + resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-own-enumerable-keys@1.0.0: + resolution: {integrity: sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==} + engines: {node: '>=14.16'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@17.4.0: + resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==} + engines: {node: '>=18'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphql@16.13.2: + resolution: {integrity: sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + hono@4.12.10: + resolution: {integrity: sha512-mx/p18PLy5og9ufies2GOSUqep98Td9q4i/EF6X7yJgAiIopxqdfIO3jbqsi3jRgTgw88jMDEzVKi+V2EF+27w==} + engines: {node: '>=16.9.0'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-obj@3.0.0: + resolution: {integrity: sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==} + engines: {node: '>=12'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-regexp@3.1.0: + resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==} + engines: {node: '>=12'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isexe@3.1.5: + resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} + engines: {node: '>=18'} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + jose@6.2.2: + resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + log-symbols@6.0.0: + resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} + engines: {node: '>=18'} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@1.7.0: + resolution: {integrity: sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + msw@2.12.14: + resolution: {integrity: sha512-4KXa4nVBIBjbDbd7vfQNuQ25eFxug0aropCQFoI0JdOBuJWamkT1yLVIWReFI8SiTRc+H1hKzaNk+cLk2N9rtQ==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-releases@2.0.37: + resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-treeify@1.1.33: + resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==} + engines: {node: '>= 10'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + open@11.0.0: + resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} + engines: {node: '>=20'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@8.2.0: + resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} + engines: {node: '>=18'} + + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + powershell-utils@0.1.0: + resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} + engines: {node: '>=20'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + engines: {node: '>=0.6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + radix-ui@1.4.3: + resolution: {integrity: sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + peerDependencies: + react: ^19.2.4 + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-router@7.14.0: + resolution: {integrity: sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} + engines: {node: '>=0.10.0'} + + recast@0.23.11: + resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} + engines: {node: '>= 4'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + rettime@0.10.1: + resolution: {integrity: sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rolldown@1.0.0-rc.12: + resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shadcn@4.1.2: + resolution: {integrity: sha512-qNQcCavkbYsgBj+X09tF2bTcwRd8abR880bsFkDU2kMqceMCLAm5c+cLg7kWDhfh1H9g08knpQ5ZEf6y/co16g==} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} + + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + stringify-object@5.0.0: + resolution: {integrity: sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==} + engines: {node: '>=14.16'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + + tailwind-merge@3.5.0: + resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} + + tailwindcss@4.2.2: + resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==} + + tapable@2.3.2: + resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} + engines: {node: '>=6'} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tldts-core@7.0.27: + resolution: {integrity: sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==} + + tldts@7.0.27: + resolution: {integrity: sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg==} + hasBin: true + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + engines: {node: '>=16'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-morph@26.0.0: + resolution: {integrity: sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==} + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@5.5.0: + resolution: {integrity: sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==} + engines: {node: '>=20'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typescript-eslint@8.58.0: + resolution: {integrity: sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + until-async@3.0.2: + resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + validate-npm-package-name@7.0.2: + resolution: {integrity: sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==} + engines: {node: ^20.17.0 || >=22.9.0} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vite@8.0.3: + resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.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 + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + wsl-utils@0.3.1: + resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} + engines: {node: '>=20'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + +snapshots: + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.29.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.28.5': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/preset-typescript@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@dotenvx/dotenvx@1.59.1': + dependencies: + commander: 11.1.0 + dotenv: 17.4.0 + eciesjs: 0.4.18 + execa: 5.1.1 + fdir: 6.5.0(picomatch@4.0.4) + ignore: 5.3.2 + object-treeify: 1.1.33 + picomatch: 4.0.4 + which: 4.0.0 + + '@ecies/ciphers@0.2.6(@noble/ciphers@1.3.0)': + dependencies: + '@noble/ciphers': 1.3.0 + + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))': + dependencies: + eslint: 9.39.4(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.14.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@floating-ui/utils@0.2.11': {} + + '@fontsource-variable/geist@5.2.8': {} + + '@hono/node-server@1.19.12(hono@4.12.10)': + dependencies: + hono: 4.12.10 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@inquirer/ansi@1.0.2': {} + + '@inquirer/confirm@5.1.21(@types/node@24.12.1)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.12.1) + '@inquirer/type': 3.0.10(@types/node@24.12.1) + optionalDependencies: + '@types/node': 24.12.1 + + '@inquirer/core@10.3.2(@types/node@24.12.1)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.12.1) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.12.1 + + '@inquirer/figures@1.0.15': {} + + '@inquirer/type@3.0.10(@types/node@24.12.1)': + optionalDependencies: + '@types/node': 24.12.1 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.12(hono@4.12.10) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.3.2(express@5.2.1) + hono: 4.12.10 + jose: 6.2.2 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + transitivePeerDependencies: + - supports-color + + '@mswjs/interceptors@0.41.3': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + + '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@noble/ciphers@1.3.0': {} + + '@noble/curves@1.9.7': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/hashes@1.8.0': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + + '@oxc-project/types@0.122.0': {} + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-accessible-icon@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-aspect-ratio@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-avatar@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-form@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-hover-card@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-label@2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-menubar@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-one-time-password-field@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-password-toggle-field@0.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/rect': 1.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-slot': 1.2.4(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-progress@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-separator@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-slot@1.2.4(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toast@1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toolbar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-visually-hidden@1.2.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/rect@1.1.1': {} + + '@rolldown/binding-android-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.12': {} + + '@rolldown/pluginutils@1.0.0-rc.7': {} + + '@sec-ant/readable-stream@0.4.1': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + '@tailwindcss/node@4.2.2': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.20.1 + jiti: 2.6.1 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.2 + + '@tailwindcss/oxide-android-arm64@4.2.2': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.2': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.2': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.2': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.2': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.2': + optional: true + + '@tailwindcss/oxide@4.2.2': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.2 + '@tailwindcss/oxide-darwin-arm64': 4.2.2 + '@tailwindcss/oxide-darwin-x64': 4.2.2 + '@tailwindcss/oxide-freebsd-x64': 4.2.2 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.2 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.2 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.2 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.2 + '@tailwindcss/oxide-linux-x64-musl': 4.2.2 + '@tailwindcss/oxide-wasm32-wasi': 4.2.2 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 + + '@tailwindcss/vite@4.2.2(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.1)(jiti@2.6.1))': + dependencies: + '@tailwindcss/node': 4.2.2 + '@tailwindcss/oxide': 4.2.2 + tailwindcss: 4.2.2 + vite: 8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.1)(jiti@2.6.1) + + '@tanstack/query-core@5.96.2': {} + + '@tanstack/react-query@5.96.2(react@19.2.4)': + dependencies: + '@tanstack/query-core': 5.96.2 + react: 19.2.4 + + '@ts-morph/common@0.27.0': + dependencies: + fast-glob: 3.3.3 + minimatch: 10.2.5 + path-browserify: 1.0.1 + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@24.12.1': + dependencies: + undici-types: 7.16.0 + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@types/statuses@2.0.6': {} + + '@types/validate-npm-package-name@4.0.2': {} + + '@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/type-utils': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.58.0 + eslint: 9.39.4(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.58.0 + debug: 4.4.3 + eslint: 9.39.4(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.58.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@5.9.3) + '@typescript-eslint/types': 8.58.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.58.0': + dependencies: + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/visitor-keys': 8.58.0 + + '@typescript-eslint/tsconfig-utils@8.58.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.4(jiti@2.6.1) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.58.0': {} + + '@typescript-eslint/typescript-estree@8.58.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.58.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@5.9.3) + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/visitor-keys': 8.58.0 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + eslint: 9.39.4(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.58.0': + dependencies: + '@typescript-eslint/types': 8.58.0 + eslint-visitor-keys: 5.0.1 + + '@vitejs/plugin-react@6.0.1(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.1)(jiti@2.6.1))': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.7 + vite: 8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.1)(jiti@2.6.1) + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + agent-base@7.1.4: {} + + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv@6.14.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + ast-types@0.16.1: + dependencies: + tslib: 2.8.1 + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.13: {} + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.0 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + brace-expansion@1.1.13: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.13 + caniuse-lite: 1.0.30001784 + electron-to-chromium: 1.5.331 + node-releases: 2.0.37 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001784: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-spinners@2.9.2: {} + + cli-width@4.1.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clsx@2.1.1: {} + + code-block-writer@13.0.3: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@11.1.0: {} + + commander@14.0.3: {} + + concat-map@0.0.1: {} + + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cookie@1.1.1: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cosmiconfig@9.0.1(typescript@5.9.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.9.3 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + data-uri-to-buffer@4.0.1: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + dedent@1.7.2: {} + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-lazy-prop@3.0.0: {} + + depd@2.0.0: {} + + detect-libc@2.1.2: {} + + detect-node-es@1.1.0: {} + + diff@8.0.4: {} + + dotenv@17.4.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eciesjs@0.4.18: + dependencies: + '@ecies/ciphers': 0.2.6(@noble/ciphers@1.3.0) + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + + ee-first@1.1.1: {} + + electron-to-chromium@1.5.331: {} + + emoji-regex@10.6.0: {} + + emoji-regex@8.0.0: {} + + encodeurl@2.0.0: {} + + enhanced-resolve@5.20.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.2 + + env-paths@2.2.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-react-hooks@7.0.1(eslint@9.39.4(jiti@2.6.1)): + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.2 + eslint: 9.39.4(jiti@2.6.1) + hermes-parser: 0.25.1 + zod: 4.3.6 + zod-validation-error: 4.0.2(zod@4.3.6) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-refresh@0.5.2(eslint@9.39.4(jiti@2.6.1)): + dependencies: + eslint: 9.39.4(jiti@2.6.1) + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.4(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.14.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + esprima@4.0.1: {} + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + etag@1.8.1: {} + + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + execa@9.6.1: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.2 + + express-rate-limit@8.3.2(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.1.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.1.0: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fs-extra@11.3.4: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + fuzzysort@3.1.0: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.5.0: {} + + get-intrinsic@1.3.0: + 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 + + get-nonce@1.0.1: {} + + get-own-enumerable-keys@1.0.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@6.0.1: {} + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@17.4.0: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphql@16.13.2: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + headers-polyfill@4.0.3: {} + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + hono@4.12.10: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + human-signals@2.1.0: {} + + human-signals@8.0.1: {} + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inherits@2.0.4: {} + + ip-address@10.1.0: {} + + ipaddr.js@1.9.1: {} + + is-arrayish@0.2.1: {} + + is-docker@3.0.0: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-in-ssh@1.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-interactive@2.0.0: {} + + is-node-process@1.2.0: {} + + is-number@7.0.0: {} + + is-obj@3.0.0: {} + + is-plain-obj@4.1.0: {} + + is-promise@4.0.0: {} + + is-regexp@3.1.0: {} + + is-stream@2.0.1: {} + + is-stream@4.0.1: {} + + is-unicode-supported@1.3.0: {} + + is-unicode-supported@2.1.0: {} + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + isexe@2.0.0: {} + + isexe@3.1.5: {} + + jiti@2.6.1: {} + + jose@6.2.2: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kleur@3.0.3: {} + + kleur@4.1.5: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + 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 + + lines-and-columns@1.2.4: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + log-symbols@6.0.0: + dependencies: + chalk: 5.6.2 + is-unicode-supported: 1.3.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@1.7.0(react@19.2.4): + dependencies: + react: 19.2.4 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mimic-fn@2.1.0: {} + + mimic-function@5.0.1: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.13 + + minimist@1.2.8: {} + + ms@2.1.3: {} + + msw@2.12.14(@types/node@24.12.1)(typescript@5.9.3): + dependencies: + '@inquirer/confirm': 5.1.21(@types/node@24.12.1) + '@mswjs/interceptors': 0.41.3 + '@open-draft/deferred-promise': 2.2.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.13.2 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.10.1 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.1 + type-fest: 5.5.0 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/node' + + mute-stream@2.0.0: {} + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + negotiator@1.0.0: {} + + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + node-releases@2.0.37: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-treeify@1.1.33: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + open@11.0.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.1.0 + wsl-utils: 0.3.1 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@8.2.0: + dependencies: + chalk: 5.6.2 + cli-cursor: 5.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 6.0.0 + stdin-discarder: 0.2.2 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + outvariant@1.4.3: {} + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-ms@4.0.0: {} + + parseurl@1.3.3: {} + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-to-regexp@6.3.0: {} + + path-to-regexp@8.4.2: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + pkce-challenge@5.0.1: {} + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + powershell-utils@0.1.0: {} + + prelude-ls@1.2.1: {} + + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + punycode@2.3.1: {} + + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + + queue-microtask@1.2.3: {} + + radix-ui@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-accessible-icon': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-alert-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-aspect-ratio': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-avatar': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-checkbox': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context-menu': 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-form': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-hover-card': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-menubar': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-navigation-menu': 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-one-time-password-field': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-password-toggle-field': 0.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-progress': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-radio-group': 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-scroll-area': 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-select': 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slider': 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-switch': 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toast': 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toolbar': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + react-dom@19.2.4(react@19.2.4): + dependencies: + react: 19.2.4 + scheduler: 0.27.0 + + react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.4): + dependencies: + react: 19.2.4 + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.4): + dependencies: + react: 19.2.4 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.4) + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.4) + use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + + react-router@7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + cookie: 1.1.1 + react: 19.2.4 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.4(react@19.2.4) + + react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.4): + dependencies: + get-nonce: 1.0.1 + react: 19.2.4 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + react@19.2.4: {} + + recast@0.23.11: + dependencies: + ast-types: 0.16.1 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + rettime@0.10.1: {} + + reusify@1.1.0: {} + + rolldown@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): + dependencies: + '@oxc-project/types': 0.122.0 + '@rolldown/pluginutils': 1.0.0-rc.12 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-x64': 1.0.0-rc.12 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + run-applescript@7.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safer-buffer@2.1.2: {} + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + semver@7.7.4: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + set-cookie-parser@2.7.2: {} + + setprototypeof@1.2.0: {} + + shadcn@4.1.2(@types/node@24.12.1)(typescript@5.9.3): + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) + '@dotenvx/dotenvx': 1.59.1 + '@modelcontextprotocol/sdk': 1.29.0(zod@3.25.76) + '@types/validate-npm-package-name': 4.0.2 + browserslist: 4.28.2 + commander: 14.0.3 + cosmiconfig: 9.0.1(typescript@5.9.3) + dedent: 1.7.2 + deepmerge: 4.3.1 + diff: 8.0.4 + execa: 9.6.1 + fast-glob: 3.3.3 + fs-extra: 11.3.4 + fuzzysort: 3.1.0 + https-proxy-agent: 7.0.6 + kleur: 4.1.5 + msw: 2.12.14(@types/node@24.12.1)(typescript@5.9.3) + node-fetch: 3.3.2 + open: 11.0.0 + ora: 8.2.0 + postcss: 8.5.8 + postcss-selector-parser: 7.1.1 + prompts: 2.4.2 + recast: 0.23.11 + stringify-object: 5.0.0 + tailwind-merge: 3.5.0 + ts-morph: 26.0.0 + tsconfig-paths: 4.2.0 + validate-npm-package-name: 7.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + transitivePeerDependencies: + - '@cfworker/json-schema' + - '@types/node' + - babel-plugin-macros + - supports-color + - typescript + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + sisteransi@1.0.5: {} + + source-map-js@1.2.1: {} + + source-map@0.6.1: {} + + statuses@2.0.2: {} + + stdin-discarder@0.2.2: {} + + strict-event-emitter@0.5.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.5.0 + strip-ansi: 7.2.0 + + stringify-object@5.0.0: + dependencies: + get-own-enumerable-keys: 1.0.0 + is-obj: 3.0.0 + is-regexp: 3.1.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@3.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-final-newline@4.0.0: {} + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tagged-tag@1.0.0: {} + + tailwind-merge@3.5.0: {} + + tailwindcss@4.2.2: {} + + tapable@2.3.2: {} + + tiny-invariant@1.3.3: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tldts-core@7.0.27: {} + + tldts@7.0.27: + dependencies: + tldts-core: 7.0.27 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + tough-cookie@6.0.1: + dependencies: + tldts: 7.0.27 + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-morph@26.0.0: + dependencies: + '@ts-morph/common': 0.27.0 + code-block-writer: 13.0.3 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + tw-animate-css@1.4.0: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@5.5.0: + dependencies: + tagged-tag: 1.0.0 + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typescript-eslint@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.4(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + undici-types@7.16.0: {} + + unicorn-magic@0.3.0: {} + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + until-async@3.0.2: {} + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.4): + dependencies: + react: 19.2.4 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.4): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.4 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + use-sync-external-store@1.6.0(react@19.2.4): + dependencies: + react: 19.2.4 + + util-deprecate@1.0.2: {} + + validate-npm-package-name@7.0.2: {} + + vary@1.1.2: {} + + vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.1)(jiti@2.6.1): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.8 + rolldown: 1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.12.1 + fsevents: 2.3.3 + jiti: 2.6.1 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + web-streams-polyfill@3.3.3: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + which@4.0.0: + dependencies: + isexe: 3.1.5 + + word-wrap@1.2.5: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + wsl-utils@0.3.1: + dependencies: + is-wsl: 3.1.1 + powershell-utils: 0.1.0 + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + yoctocolors-cjs@2.1.3: {} + + yoctocolors@2.1.2: {} + + zod-to-json-schema@3.25.2(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod-validation-error@4.0.2(zod@4.3.6): + dependencies: + zod: 4.3.6 + + zod@3.25.76: {} + + zod@4.3.6: {} diff --git a/frontend/public/apple-touch-icon.png b/frontend/public/apple-touch-icon.png new file mode 100644 index 0000000..f9cce87 Binary files /dev/null and b/frontend/public/apple-touch-icon.png differ diff --git a/frontend/public/favicon-96x96.png b/frontend/public/favicon-96x96.png new file mode 100644 index 0000000..b034fcf Binary files /dev/null and b/frontend/public/favicon-96x96.png differ diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000..c6943c0 Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..a4d97a8 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ +RealFaviconGeneratorhttps://realfavicongenerator.net \ No newline at end of file diff --git a/frontend/public/logo-dark.svg b/frontend/public/logo-dark.svg new file mode 100644 index 0000000..77b5c18 --- /dev/null +++ b/frontend/public/logo-dark.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/logo-light.svg b/frontend/public/logo-light.svg new file mode 100644 index 0000000..d90f8c3 --- /dev/null +++ b/frontend/public/logo-light.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/site.webmanifest b/frontend/public/site.webmanifest new file mode 100644 index 0000000..a7b5f2e --- /dev/null +++ b/frontend/public/site.webmanifest @@ -0,0 +1,21 @@ +{ + "name": "Quay", + "short_name": "Quay", + "icons": [ + { + "src": "/web-app-manifest-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/web-app-manifest-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "theme_color": "#171717", + "background_color": "#171717", + "display": "standalone" +} \ No newline at end of file diff --git a/frontend/public/web-app-manifest-192x192.png b/frontend/public/web-app-manifest-192x192.png new file mode 100644 index 0000000..df5741a Binary files /dev/null and b/frontend/public/web-app-manifest-192x192.png differ diff --git a/frontend/public/web-app-manifest-512x512.png b/frontend/public/web-app-manifest-512x512.png new file mode 100644 index 0000000..13c7328 Binary files /dev/null and b/frontend/public/web-app-manifest-512x512.png differ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..79e7f2d --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,121 @@ +import { useState } from 'react' +import reactLogo from './assets/react.svg' +import viteLogo from './assets/vite.svg' +import heroImg from './assets/hero.png' +import { Button } from "@/components/ui/button.tsx"; + +function App() { + const [count, setCount] = useState(0) + + return ( + <> +
+
+ + React logo + Vite logo +
+
+

Get started

+

+ Edit src/App.tsx and save to test HMR +

+
+ +
+ +
+ +
+
+ +

Documentation

+

Your questions, answered

+ +
+
+ +

Connect with us

+

Join the Vite community

+ +
+
+ +
+
+ + ) +} + +export default App diff --git a/frontend/src/api/auth.api.ts b/frontend/src/api/auth.api.ts new file mode 100644 index 0000000..8b5c8bb --- /dev/null +++ b/frontend/src/api/auth.api.ts @@ -0,0 +1,27 @@ +import { makeApiUrl, fetchWithAuth } from '.'; +import type { User } from './types/user'; + +export const login = async (name: string, password: string): Promise => { + const response = await fetch(makeApiUrl('/login'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ name, password }), + }); + + if (!response.ok) { + throw new Error('Failed to login'); + } + + const data = await response.json(); + return data.token; +}; + +export const getMe = async (): Promise => { + const response = await fetchWithAuth('/me', { method: 'GET' }); + if (!response.ok) { + throw new Error('Failed to fetch current user'); + } + return response.json(); +}; diff --git a/frontend/src/api/customHeaders.api.ts b/frontend/src/api/customHeaders.api.ts new file mode 100644 index 0000000..37ffdf7 --- /dev/null +++ b/frontend/src/api/customHeaders.api.ts @@ -0,0 +1,106 @@ +import { fetchWithAuth } from '.'; +import type { + CreateCustomHeadersRequest, + CreateHeaderRequest, + CustomHeaders, + Header, +} from './types/site'; + +export const getCustomHeaders = async (siteId: string): Promise => { + const response = await fetchWithAuth(`/sites/${siteId}/custom-headers`, { + method: 'GET', + }); + if (!response.ok) { + throw new Error('Failed to fetch custom headers'); + } + return response.json(); +}; + +export const createCustomHeaders = async ( + siteId: string, + data: CreateCustomHeadersRequest +): Promise => { + const response = await fetchWithAuth(`/sites/${siteId}/custom-headers`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + if (!response.ok) { + throw new Error('Failed to create custom headers'); + } + return response.json(); +}; + +export const updateCustomHeaders = async ( + siteId: string, + groupId: string, + data: CreateCustomHeadersRequest +): Promise => { + const response = await fetchWithAuth(`/sites/${siteId}/custom-headers/${groupId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + if (!response.ok) { + throw new Error('Failed to update custom headers'); + } + return response.json(); +}; + +export const deleteCustomHeaders = async (siteId: string, groupId: string): Promise => { + const response = await fetchWithAuth(`/sites/${siteId}/custom-headers/${groupId}`, { + method: 'DELETE', + }); + if (!response.ok) { + throw new Error('Failed to delete custom headers'); + } +}; + +export const createHeader = async ( + siteId: string, + groupId: string, + data: CreateHeaderRequest +): Promise
=> { + const response = await fetchWithAuth(`/sites/${siteId}/custom-headers/${groupId}/headers`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + if (!response.ok) { + throw new Error('Failed to create header'); + } + return response.json(); +}; + +export const updateHeader = async ( + siteId: string, + headerId: string, + data: CreateHeaderRequest +): Promise
=> { + const response = await fetchWithAuth(`/sites/${siteId}/headers/${headerId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + if (!response.ok) { + throw new Error('Failed to update header'); + } + return response.json(); +}; + +export const deleteHeader = async (siteId: string, headerId: string): Promise => { + const response = await fetchWithAuth(`/sites/${siteId}/headers/${headerId}`, { + method: 'DELETE', + }); + if (!response.ok) { + throw new Error('Failed to delete header'); + } +}; diff --git a/frontend/src/api/deployments.api.ts b/frontend/src/api/deployments.api.ts new file mode 100644 index 0000000..752cba2 --- /dev/null +++ b/frontend/src/api/deployments.api.ts @@ -0,0 +1,18 @@ +import { fetchWithAuth } from '.'; +import type { Deployment } from './types/deployments'; + +export const getDeploymentsForSite = async ( + siteId: string, + limit: number = 100 +): Promise => { + const response = await fetchWithAuth(`/sites/${siteId}/deployments?limit=${limit}`, { + method: 'GET', + }); + + if (!response.ok) { + throw new Error('Failed to fetch deployments'); + } + + const data = await response.json(); + return data.deployments; +}; diff --git a/frontend/src/api/forwardrules.api.ts b/frontend/src/api/forwardrules.api.ts new file mode 100644 index 0000000..2680ea1 --- /dev/null +++ b/frontend/src/api/forwardrules.api.ts @@ -0,0 +1,56 @@ +import { fetchWithAuth } from '.'; +import type { CreateForwardRuleRequest, ForwardRule } from './types/site'; + +export const getForwardRules = async (siteId: string): Promise => { + const response = await fetchWithAuth(`/sites/${siteId}/forward-rules`, { + method: 'GET', + }); + if (!response.ok) { + throw new Error('Failed to fetch forward rules'); + } + return response.json(); +}; + +export const createForwardRule = async ( + siteId: string, + data: CreateForwardRuleRequest +): Promise => { + const response = await fetchWithAuth(`/sites/${siteId}/forward-rules`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + if (!response.ok) { + throw new Error('Failed to create forward rule'); + } + return response.json(); +}; + +export const updateForwardRule = async ( + siteId: string, + ruleId: string, + data: CreateForwardRuleRequest +): Promise => { + const response = await fetchWithAuth(`/sites/${siteId}/forward-rules/${ruleId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + if (!response.ok) { + throw new Error('Failed to update forward rule'); + } + return response.json(); +}; + +export const deleteForwardRule = async (siteId: string, ruleId: string): Promise => { + const response = await fetchWithAuth(`/sites/${siteId}/forward-rules/${ruleId}`, { + method: 'DELETE', + }); + if (!response.ok) { + throw new Error('Failed to delete forward rule'); + } +}; diff --git a/frontend/src/api/gitservers.api.ts b/frontend/src/api/gitservers.api.ts new file mode 100644 index 0000000..5d655f6 --- /dev/null +++ b/frontend/src/api/gitservers.api.ts @@ -0,0 +1,65 @@ +import { fetchWithAuth } from '.'; +import type { CreateGitServerRequest, GitServer, UpdateGitServerRequest } from './types/gitserver'; + +export const getGitServers = async (): Promise => { + const response = await fetchWithAuth('/gitservers', { + method: 'GET', + }); + if (response.status === 404) { + return []; + } + if (!response.ok) { + throw new Error('Failed to fetch git servers'); + } + return response.json(); +}; + +export const getGitServerById = async (id: string): Promise => { + const response = await fetchWithAuth(`/gitservers/${id}`, { + method: 'GET', + }); + if (!response.ok) { + throw new Error('Failed to fetch git server'); + } + return response.json(); +}; + +export const createGitServer = async (data: CreateGitServerRequest): Promise => { + const response = await fetchWithAuth('/gitservers', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + if (!response.ok) { + throw new Error('Failed to create git server'); + } + return response.json(); +}; + +export const updateGitServer = async ( + id: string, + data: UpdateGitServerRequest +): Promise => { + const response = await fetchWithAuth(`/gitservers/${id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + if (!response.ok) { + throw new Error('Failed to update git server'); + } + return response.json(); +}; + +export const deleteGitServer = async (id: string): Promise => { + const response = await fetchWithAuth(`/gitservers/${id}`, { + method: 'DELETE', + }); + if (!response.ok) { + throw new Error('Failed to delete git server'); + } +}; diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts new file mode 100644 index 0000000..0548bb1 --- /dev/null +++ b/frontend/src/api/index.ts @@ -0,0 +1,24 @@ +export const API_BASE_URL = '/api/v1'; + +export const makeApiUrl = (endpoint: string) => { + const base = API_BASE_URL.endsWith('/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL; + const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`; + return `${base}${path}`; +}; + +export const authHeaders = () => { + try { + const token = localStorage.getItem('token'); + return token ? { Authorization: `Bearer ${token}` } : {}; + } catch (e) { + return {}; + } +}; + +export const fetchWithAuth = (endpoint: string, options: RequestInit = {}) => { + const headers = { + ...(options.headers || {}), + ...authHeaders(), + } as Record; + return fetch(makeApiUrl(endpoint), { ...options, headers }); +}; diff --git a/frontend/src/api/sites.api.ts b/frontend/src/api/sites.api.ts new file mode 100644 index 0000000..e80198a --- /dev/null +++ b/frontend/src/api/sites.api.ts @@ -0,0 +1,81 @@ +import { fetchWithAuth } from '.'; +import type { + CreateSiteRequest, + CreateSiteResponse, + GetAllSitesResponse, + Site, + ToggleSiteEnabledResponse, +} from './types/site'; + +export const getSites = async (): Promise => { + const response = await fetchWithAuth('/sites', { + method: 'GET', + }); + if (response.status === 404) { + return { sites: [], total: 0 }; + } + if (!response.ok) { + throw new Error('Failed to fetch sites'); + } + return response.json(); +}; + +export const getSite = async (id: string): Promise => { + const response = await fetchWithAuth(`/sites/${id}`, { + method: 'GET', + }); + if (!response.ok) { + throw new Error('Failed to fetch site'); + } + return response.json(); +}; + +export const createSite = async (data: CreateSiteRequest): Promise => { + const response = await fetchWithAuth('/sites', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + if (!response.ok) { + throw new Error('Failed to create site'); + } + return response.json(); +}; + +export const updateSite = async (id: string, data: Partial): Promise => { + const response = await fetchWithAuth(`/sites/${id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + if (!response.ok) { + throw new Error('Failed to update site'); + } + return response.json(); +}; + +export const toggleSiteEnabled = async (id: string): Promise => { + const response = await fetchWithAuth(`/sites/${id}/enabled`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + }); + if (!response.ok) { + throw new Error('Failed to toggle site enabled state'); + } + return response.json(); +}; + +export const deleteSite = async (id: string): Promise => { + const response = await fetchWithAuth(`/sites/${id}`, { + method: 'DELETE', + }); + if (!response.ok) { + throw new Error('Failed to delete site'); + } +}; diff --git a/frontend/src/api/types/deployments.ts b/frontend/src/api/types/deployments.ts new file mode 100644 index 0000000..402e86f --- /dev/null +++ b/frontend/src/api/types/deployments.ts @@ -0,0 +1,17 @@ +export type DeploymentStatus = 'pending' | 'running' | 'success' | 'failed'; + +export interface Deployment { + id: string; + site_id: string; + commit_hash: string; + message: string; + status: DeploymentStatus; + start_time: string; + finish_time: string; + created_at: string; +} + +export interface GetDeploymentsBySiteResponse { + deployments: Deployment[]; + total: number; +} diff --git a/frontend/src/api/types/gitserver.ts b/frontend/src/api/types/gitserver.ts new file mode 100644 index 0000000..6b8bbe0 --- /dev/null +++ b/frontend/src/api/types/gitserver.ts @@ -0,0 +1,28 @@ +export type GitServerProtocol = 'http' | 'https'; +export type GitServerType = 'github' | 'gitlab' | 'gitea'; + +export interface GitServer { + id: string; + name: string; + protocol: GitServerProtocol; + baseUrl: string; + type: GitServerType; + auth_token?: string; + created_at: string; +} + +export interface CreateGitServerRequest { + name: string; + protocol: GitServerProtocol; + baseUrl: string; + type: GitServerType; + auth_token?: string; +} + +export interface UpdateGitServerRequest { + name?: string; + protocol?: GitServerProtocol; + baseUrl?: string; + type?: GitServerType; + auth_token?: string; +} diff --git a/frontend/src/api/types/site.ts b/frontend/src/api/types/site.ts new file mode 100644 index 0000000..5ee8bfe --- /dev/null +++ b/frontend/src/api/types/site.ts @@ -0,0 +1,83 @@ +export interface ForwardRule { + id: string; + source: string; + destination: string; + status_code: number; + regex: boolean; +} + +export interface Header { + id: string; + key: string; + value: string; +} + +export interface CustomHeaders { + id: string; + source: string; + regex: boolean; + headers: Header[]; +} + +export interface Site { + id: string; + name: string; + git_server: string; + owner: string; + repository: string; + branch: string; + domain: string; + deploy_token: string; + enabled: boolean; + spa: boolean; + not_found_file: string; + index_file: string; + trailing_slash: boolean | null; + created_at: string; + forward_rules: ForwardRule[]; + custom_headers: CustomHeaders[]; +} + +export interface CreateSiteRequest { + name: string; + git_server: string; + owner: string; + repository: string; + branch: string; + domain: string; + enabled: boolean; + spa: boolean; + not_found_file: string; + index_file: string; +} + +export interface GetAllSitesResponse { + sites: Site[]; + total: number; +} + +export interface CreateSiteResponse { + site: Site; + raw_deploy_token: string; +} + +export interface ToggleSiteEnabledResponse { + enabled: boolean; +} + +export interface CreateForwardRuleRequest { + source: string; + destination: string; + status_code: number; + regex: boolean; +} + +export interface CreateCustomHeadersRequest { + source: string; + regex: boolean; +} + +export interface CreateHeaderRequest { + key: string; + value: string; +} diff --git a/frontend/src/api/types/user.ts b/frontend/src/api/types/user.ts new file mode 100644 index 0000000..0dc32e4 --- /dev/null +++ b/frontend/src/api/types/user.ts @@ -0,0 +1,18 @@ +export interface User { + id: string; + name: string; + role: 'admin' | 'user'; + hashed_password: string; + created_at: string; +} + +export interface CreateUserRequest { + name: string; + role: 'admin' | 'user'; + password: string; +} + +export interface UpdateUserRequest { + name?: string; + role?: 'admin' | 'user'; +} diff --git a/frontend/src/api/users.api.ts b/frontend/src/api/users.api.ts new file mode 100644 index 0000000..7164e5a --- /dev/null +++ b/frontend/src/api/users.api.ts @@ -0,0 +1,62 @@ +import { fetchWithAuth } from '.'; +import type { CreateUserRequest, UpdateUserRequest, User } from './types/user'; + +export const getUsers = async (): Promise => { + const response = await fetchWithAuth('/users', { + method: 'GET', + }); + if (response.status === 404) { + return []; + } + if (!response.ok) { + throw new Error('Failed to fetch sites'); + } + return response.json(); +}; + +export const getUserById = async (id: string): Promise => { + const response = await fetchWithAuth(`/users/${id}`, { + method: 'GET', + }); + if (!response.ok) { + throw new Error('Failed to fetch user'); + } + return response.json(); +}; + +export const createUser = async (data: CreateUserRequest): Promise => { + const response = await fetchWithAuth('/users', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + if (!response.ok) { + throw new Error('Failed to create user'); + } + return response.json(); +}; + +export const updateUser = async (id: string, data: Partial): Promise => { + const response = await fetchWithAuth(`/users/${id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + if (!response.ok) { + throw new Error('Failed to update user'); + } + return response.json(); +}; + +export const deleteUser = async (id: string): Promise => { + const response = await fetchWithAuth(`/users/${id}`, { + method: 'DELETE', + }); + if (!response.ok) { + throw new Error('Failed to delete user'); + } +}; diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png new file mode 100644 index 0000000..cc51a3d Binary files /dev/null and b/frontend/src/assets/hero.png differ diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/frontend/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/frontend/src/components/AppSidebar.tsx b/frontend/src/components/AppSidebar.tsx new file mode 100644 index 0000000..a467982 --- /dev/null +++ b/frontend/src/components/AppSidebar.tsx @@ -0,0 +1,88 @@ +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from '@/components/ui/sidebar'; +import { PanelsTopLeft, Users as UsersIcon, Code2 } from 'lucide-react'; +import { Link, useLocation } from 'react-router'; +import { NavUser } from './NavUser'; +import { Logo } from './logo'; +import { Separator } from './ui/separator'; + +interface AppSidebarProps { + userName?: string; + profilePictureUrl?: string; +} + +const AppSidebar = ({ userName, profilePictureUrl }: AppSidebarProps) => { + const location = useLocation(); + + return ( + + + + + + + + + + + + + + {/* Sites */} + + + + + + + Sites + + + + + + + + Git Servers + + + + + + + + Users + + + + + + + + + + + + ); +}; + +export default AppSidebar; diff --git a/frontend/src/components/DeploymentStatusBadge.tsx b/frontend/src/components/DeploymentStatusBadge.tsx new file mode 100644 index 0000000..f0825ff --- /dev/null +++ b/frontend/src/components/DeploymentStatusBadge.tsx @@ -0,0 +1,39 @@ +import { CheckCircle2, Clock, Loader2, XCircle } from 'lucide-react'; +import type { DeploymentStatus } from '../api/types/deployments'; +import { cn } from '../lib/utils'; + +const StatusIcon = ({ status }: { status: DeploymentStatus }) => { + switch (status) { + case 'success': + return ; + case 'failed': + return ; + case 'running': + return ; + case 'pending': + return ; + } +}; + +const DeploymentStatusBadge = ({ status }: { status: DeploymentStatus }) => { + const statusText = status.charAt(0).toUpperCase() + status.slice(1); + + return ( +
+ + +
+ ); +}; + +export default DeploymentStatusBadge; diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx new file mode 100644 index 0000000..6a00145 --- /dev/null +++ b/frontend/src/components/Footer.tsx @@ -0,0 +1,5 @@ +const Footer = () => { + return
; +}; + +export default Footer; diff --git a/frontend/src/components/GitServerTypeIcon.tsx b/frontend/src/components/GitServerTypeIcon.tsx new file mode 100644 index 0000000..05b244d --- /dev/null +++ b/frontend/src/components/GitServerTypeIcon.tsx @@ -0,0 +1,20 @@ +import { Code2 } from 'lucide-react'; +import type { GitServerType } from '../api/types/gitserver'; +import { GiteaIcon } from './icons/GiteaIcon'; +import { GitHubIcon } from './icons/GitHubIcon'; +import { GitLabIcon } from './icons/GitLabIcon'; + +const GitServerTypeIcon = ({ type, size = 32 }: { type: GitServerType; size?: number }) => { + switch (type) { + case 'github': + return ; + case 'gitlab': + return ; + case 'gitea': + return ; + default: + return ; + } +}; + +export default GitServerTypeIcon; diff --git a/frontend/src/components/NavUser.tsx b/frontend/src/components/NavUser.tsx new file mode 100644 index 0000000..2675885 --- /dev/null +++ b/frontend/src/components/NavUser.tsx @@ -0,0 +1,128 @@ +import { useTheme } from './theme-provider'; +import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from './ui/sidebar'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from './ui/dropdown-menu'; +import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'; +import { ChevronsUpDown, Laptop, LogOut, Moon, Sun } from 'lucide-react'; +import { Link } from 'react-router'; + +interface NavUserProps { + userName?: string; + profilePictureUrl?: string; +} + +export function NavUser({ userName, profilePictureUrl }: NavUserProps) { + const { isMobile } = useSidebar(); + const { theme, setTheme } = useTheme(); + + return ( + + + + + + + + + {userName + ? userName + .split(' ') + .map((n) => n[0]) + .join('') + .toUpperCase() + : 'T'} + + +
+ {userName} +
+ +
+
+ + +
+ + + + {userName + ? userName + .split(' ') + .map((n) => n[0]) + .join('') + .toUpperCase() + : 'T'} + + +
+ {userName} +
+
+
+ + + + + {theme === 'light' ? ( + + ) : theme === 'dark' ? ( + + ) : ( + + )} + Theme + + + + setTheme('light')}> + + Light + + setTheme('dark')}> + + Dark + + setTheme('system')}> + + System + + + + + + + + Logout + + +
+
+
+
+ ); +} diff --git a/frontend/src/components/TopBar.tsx b/frontend/src/components/TopBar.tsx new file mode 100644 index 0000000..e44e8e0 --- /dev/null +++ b/frontend/src/components/TopBar.tsx @@ -0,0 +1,15 @@ +interface TopBarProps { + title: string; + button?: React.ReactNode; +} + +const TopBar = ({ title, button }: TopBarProps) => { + return ( +
+

{title}

+ {button} +
+ ); +}; + +export default TopBar; diff --git a/frontend/src/components/icons/GitHubIcon.tsx b/frontend/src/components/icons/GitHubIcon.tsx new file mode 100644 index 0000000..372e3c0 --- /dev/null +++ b/frontend/src/components/icons/GitHubIcon.tsx @@ -0,0 +1,15 @@ +import type { IconProps } from './props'; + +export const GitHubIcon = ({ size = 24 }: IconProps) => ( + + GitHub + + +); diff --git a/frontend/src/components/icons/GitLabIcon.tsx b/frontend/src/components/icons/GitLabIcon.tsx new file mode 100644 index 0000000..82fd9e3 --- /dev/null +++ b/frontend/src/components/icons/GitLabIcon.tsx @@ -0,0 +1,15 @@ +import type { IconProps } from './props'; + +export const GitLabIcon = ({ size = 24 }: IconProps) => ( + + GitLab + + +); diff --git a/frontend/src/components/icons/GiteaIcon.tsx b/frontend/src/components/icons/GiteaIcon.tsx new file mode 100644 index 0000000..e415fdf --- /dev/null +++ b/frontend/src/components/icons/GiteaIcon.tsx @@ -0,0 +1,15 @@ +import type { IconProps } from './props'; + +export const GiteaIcon = ({ size = 24 }: IconProps) => ( + + Gitea + + +); diff --git a/frontend/src/components/icons/props.ts b/frontend/src/components/icons/props.ts new file mode 100644 index 0000000..0062bfa --- /dev/null +++ b/frontend/src/components/icons/props.ts @@ -0,0 +1 @@ +export type IconProps = { size?: number | string }; diff --git a/frontend/src/components/logo.tsx b/frontend/src/components/logo.tsx new file mode 100644 index 0000000..5f04a57 --- /dev/null +++ b/frontend/src/components/logo.tsx @@ -0,0 +1,19 @@ +import { useTheme } from './theme-provider'; + +export const Logo = () => { + const { theme } = useTheme(); + const effectiveTheme = + theme === 'system' + ? window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light' + : theme; + + return ( + Quay Logo + ); +}; diff --git a/frontend/src/components/navbar.tsx b/frontend/src/components/navbar.tsx new file mode 100644 index 0000000..1f23dc3 --- /dev/null +++ b/frontend/src/components/navbar.tsx @@ -0,0 +1,74 @@ +import { Logo } from '@/components/logo'; +import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'; +import { Link } from 'react-router'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { GitBranch, LogOut, Users } from 'lucide-react'; + +interface NavbarProps { + userName?: string; + profilePictureUrl?: string; +} + +const Navbar = ({ userName, profilePictureUrl }: NavbarProps) => { + return ( + + ); +}; + +export default Navbar; diff --git a/frontend/src/components/theme-provider.tsx b/frontend/src/components/theme-provider.tsx new file mode 100644 index 0000000..26a5ad8 --- /dev/null +++ b/frontend/src/components/theme-provider.tsx @@ -0,0 +1,71 @@ +import { createContext, useContext, useEffect, useState } from 'react'; + +export type Theme = 'dark' | 'light' | 'system'; + +type ThemeProviderProps = { + children: React.ReactNode; + defaultTheme?: Theme; + storageKey?: string; +}; + +type ThemeProviderState = { + theme: Theme; + setTheme: (theme: Theme) => void; +}; + +const initialState: ThemeProviderState = { + theme: 'system', + setTheme: () => null, +}; + +const ThemeProviderContext = createContext(initialState); + +export function ThemeProvider({ + children, + defaultTheme = 'system', + storageKey = 'vite-ui-theme', + ...props +}: ThemeProviderProps) { + const [theme, setTheme] = useState( + () => (localStorage.getItem(storageKey) as Theme) || defaultTheme + ); + + useEffect(() => { + const root = window.document.documentElement; + + root.classList.remove('light', 'dark'); + + if (theme === 'system') { + const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light'; + + root.classList.add(systemTheme); + return; + } + + root.classList.add(theme); + }, [theme]); + + const value = { + theme, + setTheme: (theme: Theme) => { + localStorage.setItem(storageKey, theme); + setTheme(theme); + }, + }; + + return ( + + {children} + + ); +} + +export const useTheme = () => { + const context = useContext(ThemeProviderContext); + + if (context === undefined) throw new Error('useTheme must be used within a ThemeProvider'); + + return context; +}; diff --git a/frontend/src/components/ui/accordion.tsx b/frontend/src/components/ui/accordion.tsx new file mode 100644 index 0000000..fcfee5c --- /dev/null +++ b/frontend/src/components/ui/accordion.tsx @@ -0,0 +1,79 @@ +import * as React from "react" +import { Accordion as AccordionPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" +import { ChevronDownIcon, ChevronUpIcon } from "lucide-react" + +function Accordion({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
+ {children} +
+
+ ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/frontend/src/components/ui/alert-dialog.tsx b/frontend/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..186efdb --- /dev/null +++ b/frontend/src/components/ui/alert-dialog.tsx @@ -0,0 +1,197 @@ +import * as React from "react" +import { AlertDialog as AlertDialogPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogContent({ + className, + size = "default", + ...props +}: React.ComponentProps & { + size?: "default" | "sm" +}) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogMedia({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogAction({ + className, + variant = "default", + size = "default", + ...props +}: React.ComponentProps & + Pick, "variant" | "size">) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + variant = "outline", + size = "default", + ...props +}: React.ComponentProps & + Pick, "variant" | "size">) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogMedia, + AlertDialogOverlay, + AlertDialogPortal, + AlertDialogTitle, + AlertDialogTrigger, +} diff --git a/frontend/src/components/ui/avatar.tsx b/frontend/src/components/ui/avatar.tsx new file mode 100644 index 0000000..99f3ed2 --- /dev/null +++ b/frontend/src/components/ui/avatar.tsx @@ -0,0 +1,110 @@ +import * as React from "react" +import { Avatar as AvatarPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + size = "default", + ...props +}: React.ComponentProps & { + size?: "default" | "sm" | "lg" +}) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) { + return ( + svg]:hidden", + "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", + "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", + className + )} + {...props} + /> + ) +} + +function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AvatarGroupCount({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3", + className + )} + {...props} + /> + ) +} + +export { + Avatar, + AvatarImage, + AvatarFallback, + AvatarGroup, + AvatarGroupCount, + AvatarBadge, +} diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx new file mode 100644 index 0000000..cacff11 --- /dev/null +++ b/frontend/src/components/ui/badge.tsx @@ -0,0 +1,49 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", + secondary: + "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", + destructive: + "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20", + outline: + "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground", + ghost: + "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", + link: "text-primary underline-offset-4 hover:underline", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant = "default", + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot.Root : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/frontend/src/components/ui/breadcrumb.tsx b/frontend/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..db9afc0 --- /dev/null +++ b/frontend/src/components/ui/breadcrumb.tsx @@ -0,0 +1,122 @@ +import * as React from "react" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" +import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react" + +function Breadcrumb({ className, ...props }: React.ComponentProps<"nav">) { + return ( +