diff --git a/Dockerfile b/Dockerfile
index c2cb370..488f323 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,22 +1,31 @@
-# Stage 1: Build frontend
-FROM node:20-alpine AS frontend
+# Stage 1: Build frontend (architecture-independent static assets)
+# $BUILDPLATFORM ensures this stage only ever runs on the native builder,
+# since the same dist/ works for both amd64 and arm64 images.
+FROM --platform=$BUILDPLATFORM node:20-alpine AS frontend
RUN corepack enable && corepack prepare pnpm@10 --activate
WORKDIR /app/web
COPY web/pnpm-lock.yaml web/package.json ./
-RUN pnpm install --frozen-lockfile
+RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
+ pnpm install --frozen-lockfile
COPY web/ .
-RUN pnpm run build
+RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
+ pnpm run build
-# Stage 2: Build Go binary (CGO_ENABLED=0 cross-compile to Linux)
-FROM golang:1.25-alpine AS backend
+# Stage 2: Cross-compile Go binary.
+# CGO_ENABLED=0 lets us compile for the target arch on the native builder,
+# avoiding slow QEMU emulation entirely.
+FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS backend
WORKDIR /app
+ARG TARGETOS TARGETARCH
COPY go.mod go.sum ./
-RUN go mod download
+RUN --mount=type=cache,target=/go/pkg/mod \
+ go mod download
COPY . .
-RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o devbox ./cmd/devbox/
+RUN --mount=type=cache,target=/root/.cache/go-build \
+ CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags="-s -w" -o devbox ./cmd/devbox/
-# Stage 3: Minimal runtime image
-FROM alpine:3.21
+# Stage 3: Minimal runtime image (per-target-architecture base)
+FROM --platform=$TARGETPLATFORM alpine:3.21
RUN apk add --no-cache ca-certificates git curl
COPY --from=backend /app/devbox /usr/local/bin/devbox
COPY --from=frontend /app/web/dist /usr/share/devbox/frontend
diff --git a/README.md b/README.md
index 29e333b..615a545 100644
--- a/README.md
+++ b/README.md
@@ -85,7 +85,7 @@ docker run -d \
-p 127.0.0.1:8080:8080 \
-v devbox-data:/data \
-e DEVBOX_PUBLIC_URL=https://dev.example.com \
- ghcr.io/ksbbs/devbox:latest
+ ghcr.io/wha7ev9r/devbox:latest
```
## 配置
@@ -198,6 +198,10 @@ logging:
retention_days: 30 # 流量日志保留天数
```
+> 限流仅信任来自 `rate_limit.trusted_proxies` 内代理网段的 `X-Real-IP` / `X-Forwarded-For`
+> (默认仅本机回环);若端口直接暴露公网,伪造这两个头不会生效,将按真实连接 IP 限流。
+> Docker 部署 + 宿主机 nginx 反代时,需把 docker 网桥网段(如 `172.16.0.0/12`)加入 `trusted_proxies`。
+
### 环境变量覆盖
所有配置项都可通过环境变量覆盖,格式 `DEVBOX_<层级>_<键>`:
@@ -280,7 +284,7 @@ Docker Hub 使用 `registry-mirrors` 配置(Docker 原生支持):
docker pull dev.example.com/ghcr/owner/image:tag
# 例如拉取 DevBox 自身
-docker pull dev.example.com/ghcr/ksbbs/devbox:latest
+docker pull dev.example.com/ghcr/wha7ev9r/devbox:latest
```
> 不需要加 `https://`,Docker 客户端会自动走 HTTPS。如果未配置 SSL,需加 `http://` 前缀并设置 Docker insecure registry。
diff --git a/cmd/devbox/main.go b/cmd/devbox/main.go
index be86db2..620d8c5 100644
--- a/cmd/devbox/main.go
+++ b/cmd/devbox/main.go
@@ -13,8 +13,6 @@ import (
"devbox/internal/config"
"devbox/internal/mirror"
"devbox/internal/server"
-
- _ "devbox/internal/mirror"
)
func main() {
diff --git a/configs/devbox.yaml b/configs/devbox.yaml
index ca44e87..a54bd8e 100644
--- a/configs/devbox.yaml
+++ b/configs/devbox.yaml
@@ -94,3 +94,4 @@ rate_limit:
interval: "3h" # 滚动时间窗口长度
whitelist: [] # 白名单 IP/CIDR(免限速)
blacklist: [] # 黑名单 IP/CIDR(永远禁止)
+ trusted_proxies: ["127.0.0.1/8", "::1/128"] # 信任转发头(如 X-Real-IP)的代理网段,反代场景按需加 docker 网桥等
diff --git a/docker-compose.yml b/docker-compose.yml
index 3a075ee..7c6ff50 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,6 +1,6 @@
services:
devbox:
- image: ghcr.io/ksbbs/devbox:latest
+ image: ghcr.io/wha7ev9r/devbox:latest
container_name: devbox
restart: always
ports:
diff --git a/internal/config/config.go b/internal/config/config.go
index 9470164..9ac36a7 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -64,12 +64,13 @@ type LoggingConfig struct {
}
type RateLimitConfig struct {
- Enabled bool `yaml:"enabled"`
- Rate int `yaml:"rate"` // max requests per rolling window
- Interval string `yaml:"interval"` // rolling window length, e.g. "3h"
- IntervalDur time.Duration `yaml:"-"` // parsed
- Whitelist []string `yaml:"whitelist"` // IPs exempt from rate limiting
- Blacklist []string `yaml:"blacklist"` // IPs always blocked
+ Enabled bool `yaml:"enabled"`
+ Rate int `yaml:"rate"` // max requests per rolling window
+ Interval string `yaml:"interval"` // rolling window length, e.g. "3h"
+ IntervalDur time.Duration `yaml:"-"` // parsed
+ Whitelist []string `yaml:"whitelist"` // IPs exempt from rate limiting
+ Blacklist []string `yaml:"blacklist"` // IPs always blocked
+ TrustedProxies []string `yaml:"trusted_proxies"` // CIDRs whose X-Real-IP / X-Forwarded-For are trusted
}
func (cfg *Config) Save(path string) error {
@@ -133,6 +134,11 @@ func applyDefaults(cfg *Config) {
if cfg.RateLimit.Rate == 0 {
cfg.RateLimit.Rate = 500
}
+ if len(cfg.RateLimit.TrustedProxies) == 0 {
+ // Loopback only by default: proxy headers are trusted solely from a
+ // reverse proxy running on the same host (e.g. nginx + 127.0.0.1 bind).
+ cfg.RateLimit.TrustedProxies = []string{"127.0.0.1/8", "::1/128"}
+ }
defaultMirrors := map[string]MirrorConfig{
"npm": {Enabled: true, Upstream: "https://registry.npmjs.org", CacheTTL: "7d"},
@@ -205,6 +211,9 @@ func applyEnvOverrides(cfg *Config) {
if v := os.Getenv("DEVBOX_RATE_LIMIT_INTERVAL"); v != "" {
cfg.RateLimit.Interval = v
}
+ if v := os.Getenv("DEVBOX_RATE_LIMIT_TRUSTED_PROXIES"); v != "" {
+ cfg.RateLimit.TrustedProxies = strings.Split(v, ",")
+ }
for name := range cfg.Mirrors {
upstream := os.Getenv(fmt.Sprintf("DEVBOX_MIRROR_%s_UPSTREAM", strings.ToUpper(name)))
diff --git a/internal/dashboard/dashboard.go b/internal/dashboard/dashboard.go
index 8702ad4..9496036 100644
--- a/internal/dashboard/dashboard.go
+++ b/internal/dashboard/dashboard.go
@@ -376,6 +376,10 @@ func (d *Dashboard) MirrorConfigHandler(w http.ResponseWriter, r *http.Request)
}
func (d *Dashboard) PublicConfigHandler(w http.ResponseWriter, r *http.Request) {
+ if !d.checkAuth(r) {
+ http.Error(w, "unauthorized", http.StatusUnauthorized)
+ return
+ }
writeJSON(w, map[string]string{"publicUrl": d.publicURL})
}
@@ -403,13 +407,19 @@ func (d *Dashboard) LoginHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
- if req.Token != d.authToken {
+ if subtle.ConstantTimeCompare([]byte(req.Token), []byte(d.authToken)) != 1 {
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
writeJSON(w, map[string]string{"status": "ok", "token": req.Token})
}
+// CheckAuth exposes the token check to other packages (e.g. server routes
+// that need the same auth policy as dashboard endpoints).
+func (d *Dashboard) CheckAuth(r *http.Request) bool {
+ return d.checkAuth(r)
+}
+
func (d *Dashboard) RateLimitConfigHandler(w http.ResponseWriter, r *http.Request) {
if !d.checkAuth(r) {
http.Error(w, "unauthorized", http.StatusUnauthorized)
diff --git a/internal/dashboard/json.go b/internal/dashboard/json.go
index 3cde27a..fb645c9 100644
--- a/internal/dashboard/json.go
+++ b/internal/dashboard/json.go
@@ -17,4 +17,4 @@ func readJSON(r *http.Request, v interface{}) bool {
return false
}
return json.Unmarshal(body, v) == nil
-}
\ No newline at end of file
+}
diff --git a/internal/dashboard/search.go b/internal/dashboard/search.go
index c83e282..72574ad 100644
--- a/internal/dashboard/search.go
+++ b/internal/dashboard/search.go
@@ -6,8 +6,13 @@ import (
"net/http"
"net/url"
"strconv"
+ "time"
)
+// searchClient bounds upstream registry API calls so a hung upstream cannot
+// stall the request (or leak goroutines) forever.
+var searchClient = &http.Client{Timeout: 10 * time.Second}
+
type SearchHandler struct{}
func NewSearchHandler() *SearchHandler {
@@ -98,7 +103,7 @@ func (sh *SearchHandler) Search(w http.ResponseWriter, r *http.Request) {
}
func searchNpm(query string, page, perPage int) ([]SearchResult, bool) {
- resp, err := http.Get(fmt.Sprintf("https://registry.npmjs.org/-/v1/search?text=%s&size=%d&from=%d", url.QueryEscape(query), perPage, (page-1)*perPage))
+ resp, err := searchClient.Get(fmt.Sprintf("https://registry.npmjs.org/-/v1/search?text=%s&size=%d&from=%d", url.QueryEscape(query), perPage, (page-1)*perPage))
if err != nil {
return nil, false
}
@@ -131,7 +136,7 @@ func searchNpm(query string, page, perPage int) ([]SearchResult, bool) {
}
func searchDocker(query string, page, perPage int) ([]SearchResult, bool) {
- resp, err := http.Get(fmt.Sprintf("https://registry.hub.docker.com/v2/search/repositories/?query=%s&page_size=%d&page=%d", url.QueryEscape(query), perPage, page))
+ resp, err := searchClient.Get(fmt.Sprintf("https://registry.hub.docker.com/v2/search/repositories/?query=%s&page_size=%d&page=%d", url.QueryEscape(query), perPage, page))
if err != nil {
return nil, false
}
@@ -164,7 +169,7 @@ func searchDocker(query string, page, perPage int) ([]SearchResult, bool) {
}
func searchPyPI(query string) []SearchResult {
- resp, err := http.Get(fmt.Sprintf("https://pypi.org/pypi/%s/json", url.QueryEscape(query)))
+ resp, err := searchClient.Get(fmt.Sprintf("https://pypi.org/pypi/%s/json", url.QueryEscape(query)))
if err != nil {
return nil
}
@@ -191,7 +196,7 @@ func searchPyPI(query string) []SearchResult {
}
func searchConda(query string) []SearchResult {
- resp, err := http.Get(fmt.Sprintf("https://api.anaconda.org/package/%s", url.QueryEscape(query)))
+ resp, err := searchClient.Get(fmt.Sprintf("https://api.anaconda.org/package/%s", url.QueryEscape(query)))
if err != nil {
return nil
}
@@ -221,7 +226,7 @@ func searchConda(query string) []SearchResult {
}
func searchRubyGems(query string, page, perPage int) ([]SearchResult, bool) {
- resp, err := http.Get(fmt.Sprintf("https://rubygems.org/api/v1/search.json?query=%s&page=%d", url.QueryEscape(query), page))
+ resp, err := searchClient.Get(fmt.Sprintf("https://rubygems.org/api/v1/search.json?query=%s&page=%d", url.QueryEscape(query), page))
if err != nil {
return nil, false
}
@@ -255,7 +260,7 @@ func searchRubyGems(query string, page, perPage int) ([]SearchResult, bool) {
func searchCargo(query string, page, perPage int) ([]SearchResult, bool) {
req, _ := http.NewRequest("GET", fmt.Sprintf("https://crates.io/api/v1/crates?q=%s&page=%d&per_page=%d", url.QueryEscape(query), page, perPage), nil)
req.Header.Set("User-Agent", "devbox/1.0")
- resp, err := http.DefaultClient.Do(req)
+ resp, err := searchClient.Do(req)
if err != nil {
return nil, false
}
@@ -288,7 +293,7 @@ func searchCargo(query string, page, perPage int) ([]SearchResult, bool) {
func searchNuGet(query string, page, perPage int) ([]SearchResult, bool) {
skip := (page - 1) * perPage
- resp, err := http.Get(fmt.Sprintf("https://azuresearch-usnc.nuget.org/query?q=%s&skip=%d&take=%d", url.QueryEscape(query), skip, perPage))
+ resp, err := searchClient.Get(fmt.Sprintf("https://azuresearch-usnc.nuget.org/query?q=%s&skip=%d&take=%d", url.QueryEscape(query), skip, perPage))
if err != nil {
return nil, false
}
diff --git a/internal/gitproxy/gitproxy.go b/internal/gitproxy/gitproxy.go
index 196466f..af3dbc3 100644
--- a/internal/gitproxy/gitproxy.go
+++ b/internal/gitproxy/gitproxy.go
@@ -53,13 +53,16 @@ func (gp *GitProxy) Handler(w http.ResponseWriter, r *http.Request) {
func (gp *GitProxy) proxyGitHub(w http.ResponseWriter, r *http.Request, path string) {
if isBlobRequest(path) {
- path = strings.Replace(path, "/blob/", "/raw/", 1)
+ // /user/repo/blob/branch/file → raw.githubusercontent.com/user/repo/branch/file
+ path = strings.Replace(path, "/blob/", "/", 1)
gp.proxyRaw(w, r, path)
return
}
if isArchiveRequest(path) {
gp.proxyArchive(w, r, gp.githubUpstream, path)
} else if isRawRequest(path) {
+ // /user/repo/raw/branch/file → raw.githubusercontent.com/user/repo/branch/file
+ path = strings.Replace(path, "/raw/", "/", 1)
gp.proxyRaw(w, r, path)
} else {
gp.proxySmartHTTP(w, r, gp.githubUpstream, path)
@@ -193,12 +196,24 @@ func copyResponseHeaders(w http.ResponseWriter, resp *http.Response) {
func copyRequestHeaders(newReq *http.Request, orig *http.Request) {
for k, vv := range orig.Header {
+ if isHopByHopHeader(k) {
+ continue
+ }
for _, v := range vv {
newReq.Header.Add(k, v)
}
}
}
+func isHopByHopHeader(k string) bool {
+ switch http.CanonicalHeaderKey(k) {
+ case "Host", "Connection", "Keep-Alive", "Proxy-Connection",
+ "Proxy-Authorization", "Transfer-Encoding", "Upgrade", "TE", "Trailer":
+ return true
+ }
+ return false
+}
+
func isHTMLResponse(resp *http.Response) bool {
ct := resp.Header.Get("Content-Type")
return strings.HasPrefix(ct, "text/html")
diff --git a/internal/gitproxy/gitproxy_test.go b/internal/gitproxy/gitproxy_test.go
index 87e3eea..5a21fff 100644
--- a/internal/gitproxy/gitproxy_test.go
+++ b/internal/gitproxy/gitproxy_test.go
@@ -31,6 +31,9 @@ func TestGitProxyGitHubClone(t *testing.T) {
func TestGitProxyGitHubRaw(t *testing.T) {
raw := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/user/repo/branch/file.go" {
+ t.Fatalf("expected path /user/repo/branch/file.go, got %s", r.URL.Path)
+ }
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
w.Write([]byte("raw-file-content"))
@@ -53,6 +56,9 @@ func TestGitProxyGitHubRaw(t *testing.T) {
func TestGitProxyGitHubBlobRedirect(t *testing.T) {
raw := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/user/repo/branch/file.go" {
+ t.Fatalf("expected path /user/repo/branch/file.go, got %s", r.URL.Path)
+ }
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
w.Write([]byte("blob-content"))
diff --git a/internal/mirror/alpine.go b/internal/mirror/alpine.go
index b92116a..bdb336c 100644
--- a/internal/mirror/alpine.go
+++ b/internal/mirror/alpine.go
@@ -69,10 +69,10 @@ func (a *AlpineMirror) ApplyConfig(cfg config.MirrorConfig) {
}
func (a *AlpineMirror) ProxyHandler(cache *Cache) http.HandlerFunc {
- a.mu.RLock()
- upstream := a.upstream
- a.mu.RUnlock()
return func(w http.ResponseWriter, r *http.Request) {
+ a.mu.RLock()
+ upstream := a.upstream
+ a.mu.RUnlock()
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/alpine")
cache.ProxyStream(w, r, upstream)
}
diff --git a/internal/mirror/apt.go b/internal/mirror/apt.go
index 112d6f4..e9ecccd 100644
--- a/internal/mirror/apt.go
+++ b/internal/mirror/apt.go
@@ -69,10 +69,10 @@ func (a *AptMirror) ApplyConfig(cfg config.MirrorConfig) {
}
func (a *AptMirror) ProxyHandler(cache *Cache) http.HandlerFunc {
- a.mu.RLock()
- upstream := a.upstream
- a.mu.RUnlock()
return func(w http.ResponseWriter, r *http.Request) {
+ a.mu.RLock()
+ upstream := a.upstream
+ a.mu.RUnlock()
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/apt")
cache.ProxyStream(w, r, upstream)
}
diff --git a/internal/mirror/cache.go b/internal/mirror/cache.go
index e7f6891..daa3ddc 100644
--- a/internal/mirror/cache.go
+++ b/internal/mirror/cache.go
@@ -75,6 +75,7 @@ func (c *Cache) Get(key string) ([]byte, http.Header, bool) {
os.Remove(path)
os.Remove(hdrPath)
os.Remove(path + ".exp")
+ c.usedBytes -= info.Size()
c.misses.Add(1)
return nil, nil, false
}
@@ -166,7 +167,7 @@ func (c *Cache) evictLRU() {
return filepath.SkipDir
}
name := d.Name()
- if filepath.Ext(name) == ".exp" || filepath.Ext(name) == ".hdr" {
+ if filepath.Ext(name) == ".exp" || filepath.Ext(name) == ".hdr" || strings.Contains(name, ".tmp") {
return nil
}
info, err := d.Info()
@@ -212,7 +213,12 @@ func (c *Cache) IsExpired(key string) bool {
func (c *Cache) ProxyHTTP(w http.ResponseWriter, r *http.Request, upstream string, ttl time.Duration) {
key := upstream + r.URL.Path + "?" + r.URL.RawQuery
- if !c.IsExpired(key) {
+ // Authenticated requests must not read or write the shared cache:
+ // the key has no identity component, so cached responses fetched with
+ // one client's credentials would leak to everyone else.
+ authenticated := r.Header.Get("Authorization") != ""
+
+ if !authenticated && !c.IsExpired(key) {
data, hdr, ok := c.Get(key)
if ok {
for k, vv := range hdr {
@@ -230,23 +236,35 @@ func (c *Cache) ProxyHTTP(w http.ResponseWriter, r *http.Request, upstream strin
if r.URL.RawQuery != "" {
target += "?" + r.URL.RawQuery
}
- resp, err := proxyClient.Get(target)
+
+ newReq, err := http.NewRequest(r.Method, target, r.Body)
if err != nil {
- http.Error(w, "upstream error: "+err.Error(), http.StatusBadGateway)
+ http.Error(w, "request error", http.StatusInternalServerError)
return
}
- defer resp.Body.Close()
+ copyRequestHeaders(newReq, r)
- body, err := io.ReadAll(resp.Body)
+ resp, err := proxyClient.Do(newReq)
if err != nil {
- http.Error(w, "read error", http.StatusInternalServerError)
+ http.Error(w, "upstream error: "+err.Error(), http.StatusBadGateway)
return
}
+ defer resp.Body.Close()
respHdr := resp.Header.Clone()
- if resp.StatusCode >= 200 && resp.StatusCode < 300 {
- c.Set(key, body, respHdr, ttl)
+ // Stream to client while spooling cacheable (200 only, so partial
+ // 206 responses never poison the cache) responses to a temp file.
+ cacheable := resp.StatusCode == http.StatusOK && ttl >= 0 && !authenticated
+ path := c.keyPath(key)
+ var tmp *os.File
+ if cacheable {
+ // Unique temp name so concurrent misses on the same key can never
+ // interleave writes into one file; only the rename publishes data.
+ tmp, _ = os.CreateTemp(c.dir, filepath.Base(path)+".tmp-")
+ if tmp != nil {
+ defer os.Remove(tmp.Name())
+ }
}
for k, vv := range respHdr {
@@ -255,7 +273,33 @@ func (c *Cache) ProxyHTTP(w http.ResponseWriter, r *http.Request, upstream strin
}
}
w.WriteHeader(resp.StatusCode)
- _, _ = w.Write(body)
+
+ var src io.Reader = resp.Body
+ if tmp != nil {
+ src = io.TeeReader(resp.Body, tmp)
+ }
+ written, copyErr := io.Copy(w, src)
+ if tmp == nil {
+ return
+ }
+ if err := tmp.Close(); err != nil || copyErr != nil || written <= 0 {
+ return
+ }
+
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ if info, err := os.Stat(path); err == nil {
+ c.usedBytes -= info.Size()
+ }
+ if err := os.Rename(tmp.Name(), path); err != nil {
+ slog.Warn("cache rename error", "path", path, "error", err)
+ return
+ }
+ c.writeCacheMeta(path, respHdr, ttl)
+ c.usedBytes += written
+ if c.maxBytes > 0 && c.usedBytes > c.maxBytes {
+ c.evictLRU()
+ }
}
func (c *Cache) ProxyStream(w http.ResponseWriter, r *http.Request, upstream string) {
@@ -263,7 +307,15 @@ func (c *Cache) ProxyStream(w http.ResponseWriter, r *http.Request, upstream str
if r.URL.RawQuery != "" {
target += "?" + r.URL.RawQuery
}
- resp, err := proxyClient.Get(target)
+
+ newReq, err := http.NewRequest(r.Method, target, r.Body)
+ if err != nil {
+ http.Error(w, "request error", http.StatusInternalServerError)
+ return
+ }
+ copyRequestHeaders(newReq, r)
+
+ resp, err := proxyClient.Do(newReq)
if err != nil {
http.Error(w, "upstream error", http.StatusBadGateway)
return
@@ -279,20 +331,68 @@ func (c *Cache) ProxyStream(w http.ResponseWriter, r *http.Request, upstream str
_, _ = io.Copy(w, resp.Body)
}
+// copyRequestHeaders copies client headers onto the upstream request,
+// skipping hop-by-hop headers (RFC 7230 §6.1) like Host, Connection and
+// Proxy-Authorization.
+func copyRequestHeaders(dst *http.Request, src *http.Request) {
+ for k, vv := range src.Header {
+ if isHopByHopHeader(k) {
+ continue
+ }
+ for _, v := range vv {
+ dst.Header.Add(k, v)
+ }
+ }
+}
+
+func isHopByHopHeader(k string) bool {
+ switch http.CanonicalHeaderKey(k) {
+ case "Host", "Connection", "Keep-Alive", "Proxy-Connection",
+ "Proxy-Authorization", "Transfer-Encoding", "Upgrade", "TE", "Trailer":
+ return true
+ }
+ return false
+}
+
+// writeCacheMeta persists response headers and (optionally) an expiry file
+// for a cached entry. Callers must hold c.mu.
+func (c *Cache) writeCacheMeta(path string, hdr http.Header, ttl time.Duration) {
+ hdrPath := path + ".hdr"
+ hf, err := os.Create(hdrPath)
+ if err != nil {
+ slog.Warn("cache header write error", "path", hdrPath, "error", err)
+ return
+ }
+ for k, vv := range hdr {
+ for _, v := range vv {
+ fmt.Fprintf(hf, "%s:%s\n", k, v)
+ }
+ }
+ hf.Close()
+
+ if ttl > 0 {
+ expPath := path + ".exp"
+ ef, err := os.Create(expPath)
+ if err != nil {
+ slog.Warn("cache expiry write error", "path", expPath, "error", err)
+ return
+ }
+ fmt.Fprintf(ef, "%d", time.Now().Add(ttl).Unix())
+ ef.Close()
+ }
+}
+
func (c *Cache) Hits() int64 { return c.hits.Load() }
func (c *Cache) Misses() int64 { return c.misses.Load() }
func (c *Cache) CleanExpired() {
c.mu.Lock()
defer c.mu.Unlock()
- cleanDirExpired(c.dir, time.Now().Unix())
-}
-
-func cleanDirExpired(dir string, now int64) {
- files, _ := os.ReadDir(dir)
+ now := time.Now().Unix()
+ files, _ := os.ReadDir(c.dir)
for _, f := range files {
if !f.IsDir() && filepath.Ext(f.Name()) == ".exp" {
- data, err := os.ReadFile(filepath.Join(dir, f.Name()))
+ data, err := os.ReadFile(filepath.Join(c.dir, f.Name()))
if err != nil {
continue
}
@@ -300,16 +400,44 @@ func cleanDirExpired(dir string, now int64) {
if err != nil {
continue
}
- if now > expiry {
- base := filepath.Join(dir, strings.TrimSuffix(f.Name(), ".exp"))
- os.Remove(base)
- os.Remove(base + ".hdr")
- os.Remove(base + ".exp")
+ if now <= expiry {
+ continue
+ }
+ base := filepath.Join(c.dir, strings.TrimSuffix(f.Name(), ".exp"))
+ if info, err := os.Stat(base); err == nil {
+ c.usedBytes -= info.Size()
}
+ os.Remove(base)
+ os.Remove(base + ".hdr")
+ os.Remove(base + ".exp")
}
}
}
+// ScanSize walks the cache dir and initializes usedBytes from the files on
+// disk, so size accounting survives process restarts.
+func (c *Cache) ScanSize() {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ var total int64
+ _ = filepath.WalkDir(c.dir, func(path string, d fs.DirEntry, err error) error {
+ if err != nil || d.IsDir() {
+ return nil
+ }
+ name := d.Name()
+ if filepath.Ext(name) == ".exp" || filepath.Ext(name) == ".hdr" || strings.Contains(name, ".tmp") {
+ return nil
+ }
+ info, err := d.Info()
+ if err != nil {
+ return nil
+ }
+ total += info.Size()
+ return nil
+ })
+ c.usedBytes = total
+}
+
func (c *Cache) keyPath(key string) string {
hash := sha256.Sum256([]byte(key))
return filepath.Join(c.dir, hex.EncodeToString(hash[:]))
diff --git a/internal/mirror/cargo.go b/internal/mirror/cargo.go
index 49300cb..fb120a4 100644
--- a/internal/mirror/cargo.go
+++ b/internal/mirror/cargo.go
@@ -69,11 +69,11 @@ func (c *CargoMirror) ApplyConfig(cfg config.MirrorConfig) {
}
func (c *CargoMirror) ProxyHandler(cache *Cache) http.HandlerFunc {
- c.mu.RLock()
- upstream := c.upstream
- cacheTTL := c.cacheTTL
- c.mu.RUnlock()
return func(w http.ResponseWriter, r *http.Request) {
+ c.mu.RLock()
+ upstream := c.upstream
+ cacheTTL := c.cacheTTL
+ c.mu.RUnlock()
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/cargo")
cache.ProxyHTTP(w, r, upstream, cacheTTL)
}
diff --git a/internal/mirror/conda.go b/internal/mirror/conda.go
index c84ebaf..64d3851 100644
--- a/internal/mirror/conda.go
+++ b/internal/mirror/conda.go
@@ -69,11 +69,11 @@ func (c *CondaMirror) ApplyConfig(cfg config.MirrorConfig) {
}
func (c *CondaMirror) ProxyHandler(cache *Cache) http.HandlerFunc {
- c.mu.RLock()
- upstream := c.upstream
- cacheTTL := c.cacheTTL
- c.mu.RUnlock()
return func(w http.ResponseWriter, r *http.Request) {
+ c.mu.RLock()
+ upstream := c.upstream
+ cacheTTL := c.cacheTTL
+ c.mu.RUnlock()
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/conda")
cache.ProxyHTTP(w, r, upstream, cacheTTL)
}
diff --git a/internal/mirror/cran.go b/internal/mirror/cran.go
index 201d34f..6f115e3 100644
--- a/internal/mirror/cran.go
+++ b/internal/mirror/cran.go
@@ -69,11 +69,11 @@ func (c *CranMirror) ApplyConfig(cfg config.MirrorConfig) {
}
func (c *CranMirror) ProxyHandler(cache *Cache) http.HandlerFunc {
- c.mu.RLock()
- upstream := c.upstream
- cacheTTL := c.cacheTTL
- c.mu.RUnlock()
return func(w http.ResponseWriter, r *http.Request) {
+ c.mu.RLock()
+ upstream := c.upstream
+ cacheTTL := c.cacheTTL
+ c.mu.RUnlock()
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/cran")
cache.ProxyHTTP(w, r, upstream, cacheTTL)
}
diff --git a/internal/mirror/ghcr.go b/internal/mirror/ghcr.go
index a2c985e..d4af93f 100644
--- a/internal/mirror/ghcr.go
+++ b/internal/mirror/ghcr.go
@@ -69,10 +69,10 @@ func (g *GhcrMirror) ApplyConfig(cfg config.MirrorConfig) {
}
func (g *GhcrMirror) ProxyHandler(cache *Cache) http.HandlerFunc {
- g.mu.RLock()
- upstream := g.upstream
- g.mu.RUnlock()
return func(w http.ResponseWriter, r *http.Request) {
+ g.mu.RLock()
+ upstream := g.upstream
+ g.mu.RUnlock()
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/ghcr")
cache.ProxyStream(w, r, upstream)
}
diff --git a/internal/mirror/github_api.go b/internal/mirror/github_api.go
index 3726eb0..2d3eb2a 100644
--- a/internal/mirror/github_api.go
+++ b/internal/mirror/github_api.go
@@ -69,11 +69,11 @@ func (g *GithubAPIMirror) ApplyConfig(cfg config.MirrorConfig) {
}
func (g *GithubAPIMirror) ProxyHandler(cache *Cache) http.HandlerFunc {
- g.mu.RLock()
- upstream := g.upstream
- cacheTTL := g.cacheTTL
- g.mu.RUnlock()
return func(w http.ResponseWriter, r *http.Request) {
+ g.mu.RLock()
+ upstream := g.upstream
+ cacheTTL := g.cacheTTL
+ g.mu.RUnlock()
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/ghapi")
cache.ProxyHTTP(w, r, upstream, cacheTTL)
}
diff --git a/internal/mirror/golang.go b/internal/mirror/golang.go
index fef895a..c7d3fdb 100644
--- a/internal/mirror/golang.go
+++ b/internal/mirror/golang.go
@@ -69,11 +69,11 @@ func (g *GolangMirror) ApplyConfig(cfg config.MirrorConfig) {
}
func (g *GolangMirror) ProxyHandler(cache *Cache) http.HandlerFunc {
- g.mu.RLock()
- upstream := g.upstream
- cacheTTL := g.cacheTTL
- g.mu.RUnlock()
return func(w http.ResponseWriter, r *http.Request) {
+ g.mu.RLock()
+ upstream := g.upstream
+ cacheTTL := g.cacheTTL
+ g.mu.RUnlock()
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/golang")
cache.ProxyHTTP(w, r, upstream, cacheTTL)
}
diff --git a/internal/mirror/homebrew.go b/internal/mirror/homebrew.go
index 3db5e95..bafa235 100644
--- a/internal/mirror/homebrew.go
+++ b/internal/mirror/homebrew.go
@@ -69,10 +69,10 @@ func (h *HomebrewMirror) ApplyConfig(cfg config.MirrorConfig) {
}
func (h *HomebrewMirror) ProxyHandler(cache *Cache) http.HandlerFunc {
- h.mu.RLock()
- upstream := h.upstream
- h.mu.RUnlock()
return func(w http.ResponseWriter, r *http.Request) {
+ h.mu.RLock()
+ upstream := h.upstream
+ h.mu.RUnlock()
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/homebrew")
cache.ProxyStream(w, r, upstream)
}
diff --git a/internal/mirror/huggingface.go b/internal/mirror/huggingface.go
index e82b658..1e03b97 100644
--- a/internal/mirror/huggingface.go
+++ b/internal/mirror/huggingface.go
@@ -69,10 +69,10 @@ func (h *HfMirror) ApplyConfig(cfg config.MirrorConfig) {
}
func (h *HfMirror) ProxyHandler(cache *Cache) http.HandlerFunc {
- h.mu.RLock()
- upstream := h.upstream
- h.mu.RUnlock()
return func(w http.ResponseWriter, r *http.Request) {
+ h.mu.RLock()
+ upstream := h.upstream
+ h.mu.RUnlock()
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/hf")
// HuggingFace files can be large (model weights), use streaming proxy
cache.ProxyStream(w, r, upstream)
diff --git a/internal/mirror/mcr.go b/internal/mirror/mcr.go
index 1d23131..d07463f 100644
--- a/internal/mirror/mcr.go
+++ b/internal/mirror/mcr.go
@@ -69,10 +69,10 @@ func (m *McrMirror) ApplyConfig(cfg config.MirrorConfig) {
}
func (m *McrMirror) ProxyHandler(cache *Cache) http.HandlerFunc {
- m.mu.RLock()
- upstream := m.upstream
- m.mu.RUnlock()
return func(w http.ResponseWriter, r *http.Request) {
+ m.mu.RLock()
+ upstream := m.upstream
+ m.mu.RUnlock()
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/mcr")
cache.ProxyStream(w, r, upstream)
}
diff --git a/internal/mirror/nuget.go b/internal/mirror/nuget.go
index e6bf8c7..2a0b3ae 100644
--- a/internal/mirror/nuget.go
+++ b/internal/mirror/nuget.go
@@ -69,11 +69,11 @@ func (n *NuGetMirror) ApplyConfig(cfg config.MirrorConfig) {
}
func (n *NuGetMirror) ProxyHandler(cache *Cache) http.HandlerFunc {
- n.mu.RLock()
- upstream := n.upstream
- cacheTTL := n.cacheTTL
- n.mu.RUnlock()
return func(w http.ResponseWriter, r *http.Request) {
+ n.mu.RLock()
+ upstream := n.upstream
+ cacheTTL := n.cacheTTL
+ n.mu.RUnlock()
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/nuget")
cache.ProxyHTTP(w, r, upstream, cacheTTL)
}
diff --git a/internal/mirror/pypi.go b/internal/mirror/pypi.go
index 1a021f6..6e9fd30 100644
--- a/internal/mirror/pypi.go
+++ b/internal/mirror/pypi.go
@@ -69,11 +69,11 @@ func (p *PypiMirror) ApplyConfig(cfg config.MirrorConfig) {
}
func (p *PypiMirror) ProxyHandler(cache *Cache) http.HandlerFunc {
- p.mu.RLock()
- upstream := p.upstream
- cacheTTL := p.cacheTTL
- p.mu.RUnlock()
return func(w http.ResponseWriter, r *http.Request) {
+ p.mu.RLock()
+ upstream := p.upstream
+ cacheTTL := p.cacheTTL
+ p.mu.RUnlock()
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/pypi")
cache.ProxyHTTP(w, r, upstream, cacheTTL)
}
diff --git a/internal/mirror/quay.go b/internal/mirror/quay.go
index f2cbde6..1d9dd6d 100644
--- a/internal/mirror/quay.go
+++ b/internal/mirror/quay.go
@@ -69,10 +69,10 @@ func (q *QuayMirror) ApplyConfig(cfg config.MirrorConfig) {
}
func (q *QuayMirror) ProxyHandler(cache *Cache) http.HandlerFunc {
- q.mu.RLock()
- upstream := q.upstream
- q.mu.RUnlock()
return func(w http.ResponseWriter, r *http.Request) {
+ q.mu.RLock()
+ upstream := q.upstream
+ q.mu.RUnlock()
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/quay")
cache.ProxyStream(w, r, upstream)
}
diff --git a/internal/mirror/rubygems.go b/internal/mirror/rubygems.go
index 02d03f4..4ffb595 100644
--- a/internal/mirror/rubygems.go
+++ b/internal/mirror/rubygems.go
@@ -69,11 +69,11 @@ func (rg *RubyGemsMirror) ApplyConfig(cfg config.MirrorConfig) {
}
func (rg *RubyGemsMirror) ProxyHandler(cache *Cache) http.HandlerFunc {
- rg.mu.RLock()
- upstream := rg.upstream
- cacheTTL := rg.cacheTTL
- rg.mu.RUnlock()
return func(w http.ResponseWriter, r *http.Request) {
+ rg.mu.RLock()
+ upstream := rg.upstream
+ cacheTTL := rg.cacheTTL
+ rg.mu.RUnlock()
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/rubygems")
cache.ProxyHTTP(w, r, upstream, cacheTTL)
}
diff --git a/internal/ratelimit/ratelimit.go b/internal/ratelimit/ratelimit.go
index dae1212..1e7e472 100644
--- a/internal/ratelimit/ratelimit.go
+++ b/internal/ratelimit/ratelimit.go
@@ -9,12 +9,13 @@ import (
)
type Limiter struct {
- mu sync.Mutex
- visitors map[string]*visitorWindow
- limit int
- window time.Duration
- whitelist []*net.IPNet
- blacklist []*net.IPNet
+ mu sync.Mutex
+ visitors map[string]*visitorWindow
+ limit int
+ window time.Duration
+ whitelist []*net.IPNet
+ blacklist []*net.IPNet
+ trustedProxies []*net.IPNet
}
type visitorWindow struct {
@@ -22,21 +23,27 @@ type visitorWindow struct {
lastSeen time.Time
}
-func New(limit int, window time.Duration, whitelist []string, blacklist []string) *Limiter {
+func New(limit int, window time.Duration, whitelist []string, blacklist []string, trustedProxies []string) *Limiter {
wlNets := parseCIDRList(whitelist)
blNets := parseCIDRList(blacklist)
+ if len(trustedProxies) == 0 {
+ // Default: trust proxy headers only from a loopback reverse proxy.
+ trustedProxies = []string{"127.0.0.1/8", "::1/128"}
+ }
+ tpNets := parseCIDRList(trustedProxies)
return &Limiter{
- visitors: make(map[string]*visitorWindow),
- limit: limit,
- window: window,
- whitelist: wlNets,
- blacklist: blNets,
+ visitors: make(map[string]*visitorWindow),
+ limit: limit,
+ window: window,
+ whitelist: wlNets,
+ blacklist: blNets,
+ trustedProxies: tpNets,
}
}
func (l *Limiter) Allow(r *http.Request) bool {
- ip := extractIP(r)
+ ip := l.extractIP(r)
ipNet := parseIP(ip)
for _, cidr := range l.blacklist {
@@ -95,7 +102,11 @@ func parseCIDRList(list []string) []*net.IPNet {
var nets []*net.IPNet
for _, entry := range list {
if !strings.Contains(entry, "/") {
- entry += "/32"
+ if strings.Contains(entry, ":") {
+ entry += "/128"
+ } else {
+ entry += "/32"
+ }
}
_, ipNet, err := net.ParseCIDR(entry)
if err == nil {
@@ -113,25 +124,48 @@ func parseIP(ipStr string) net.IP {
return ip
}
-func extractIP(r *http.Request) string {
- ip := r.Header.Get("X-Real-IP")
- if ip != "" {
- return ip
- }
- ip = r.Header.Get("X-Forwarded-For")
- if ip != "" {
- for i := 0; i < len(ip); i++ {
- if ip[i] == ',' {
- return ip[:i]
+// extractIP resolves the client IP for rate limiting. Proxy headers
+// (X-Real-IP / X-Forwarded-For) are only honored when the direct peer is
+// inside the configured trusted proxy CIDRs (loopback by default); directly
+// exposed clients can otherwise forge these headers to bypass or deflect
+// rate limiting.
+func (l *Limiter) extractIP(r *http.Request) string {
+ remoteIP := peerIP(r.RemoteAddr)
+ if remoteIP != nil && l.isTrustedProxy(remoteIP) {
+ if ip := r.Header.Get("X-Real-IP"); ip != "" {
+ return strings.TrimSpace(ip)
+ }
+ if ip := r.Header.Get("X-Forwarded-For"); ip != "" {
+ if idx := strings.IndexByte(ip, ','); idx > 0 {
+ ip = ip[:idx]
}
+ return strings.TrimSpace(ip)
}
- return ip
}
- host := r.RemoteAddr
- for i := len(host) - 1; i >= 0; i-- {
- if host[i] == ':' {
- return host[:i]
+ if remoteIP != nil {
+ return remoteIP.String()
+ }
+ return r.RemoteAddr
+}
+
+func (l *Limiter) isTrustedProxy(ip net.IP) bool {
+ for _, cidr := range l.trustedProxies {
+ if cidr.Contains(ip) {
+ return true
}
}
- return host
+ return false
+}
+
+// peerIP extracts the IP from a RemoteAddr string ("1.2.3.4:5678", "[::1]:80").
+func peerIP(remoteAddr string) net.IP {
+ host, _, err := net.SplitHostPort(remoteAddr)
+ if err != nil {
+ host = remoteAddr
+ }
+ ip := net.ParseIP(host)
+ if ip == nil {
+ return nil
+ }
+ return ip
}
diff --git a/internal/ratelimit/ratelimit_test.go b/internal/ratelimit/ratelimit_test.go
new file mode 100644
index 0000000..91d356f
--- /dev/null
+++ b/internal/ratelimit/ratelimit_test.go
@@ -0,0 +1,68 @@
+package ratelimit
+
+import (
+ "net/http/httptest"
+ "testing"
+)
+
+func TestExtractIPIgnoresForgedProxyHeaders(t *testing.T) {
+ // Directly exposed clients must not be able to forge proxy headers.
+ l := New(10, 0, nil, nil, nil)
+ req := httptest.NewRequest("GET", "/", nil)
+ req.RemoteAddr = "203.0.113.7:5678"
+ req.Header.Set("X-Real-IP", "10.0.0.1")
+ req.Header.Set("X-Forwarded-For", "10.0.0.2, 10.0.0.3")
+
+ if got := l.extractIP(req); got != "203.0.113.7" {
+ t.Fatalf("expected real peer IP 203.0.113.7, got %s", got)
+ }
+}
+
+func TestExtractIPTrustsProxyBehindLoopback(t *testing.T) {
+ // Loopback peers are trusted by default (local reverse proxy).
+ l := New(10, 0, nil, nil, nil)
+ req := httptest.NewRequest("GET", "/", nil)
+ req.RemoteAddr = "127.0.0.1:8080"
+ req.Header.Set("X-Real-IP", "203.0.113.9")
+
+ if got := l.extractIP(req); got != "203.0.113.9" {
+ t.Fatalf("expected forwarded IP 203.0.113.9, got %s", got)
+ }
+
+ req2 := httptest.NewRequest("GET", "/", nil)
+ req2.RemoteAddr = "[::1]:8080"
+ req2.Header.Set("X-Forwarded-For", "203.0.113.10, 203.0.113.11")
+
+ if got := l.extractIP(req2); got != "203.0.113.10" {
+ t.Fatalf("expected first forwarded IP 203.0.113.10, got %s", got)
+ }
+}
+
+func TestExtractIPTrustsConfiguredProxyCIDR(t *testing.T) {
+ // Docker deployment: nginx on the host, container peers are the bridge gateway.
+ l := New(10, 0, nil, nil, []string{"172.16.0.1/12", "127.0.0.1/8"})
+ req := httptest.NewRequest("GET", "/", nil)
+ req.RemoteAddr = "172.16.0.1:5678"
+ req.Header.Set("X-Real-IP", "__VG_IPV4_1baa9e3bb4e1__")
+
+ if got := l.extractIP(req); got != "__VG_IPV4_1baa9e3bb4e1__" {
+ t.Fatalf("expected forwarded IP __VG_IPV4_1baa9e3bb4e1__, got %s", got)
+ }
+
+ // Same bridge CIDR, but header absent → peer IP used.
+ req2 := httptest.NewRequest("GET", "/", nil)
+ req2.RemoteAddr = "172.16.0.1:5678"
+ if got := l.extractIP(req2); got != "172.16.0.1" {
+ t.Fatalf("expected peer IP 172.16.0.1, got %s", got)
+ }
+}
+
+func TestExtractIPIPv6Peer(t *testing.T) {
+ l := New(10, 0, nil, nil, nil)
+ req := httptest.NewRequest("GET", "/", nil)
+ req.RemoteAddr = "[2001:db8::1]:443"
+
+ if got := l.extractIP(req); got != "2001:db8::1" {
+ t.Fatalf("expected IPv6 peer, got %s", got)
+ }
+}
diff --git a/internal/server/server.go b/internal/server/server.go
index 480dfdd..034e5a1 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -83,6 +83,7 @@ func New(cfg *config.Config, configPath string, frontDir string) (*Server, error
}
cache := mirror.NewCache(cfg.Cache.Dir, cfg.Cache.MaxSizeBytes)
+ cache.ScanSize()
gp := gitproxy.New(
cfg.GitProxy.GithubUpstream,
@@ -118,7 +119,7 @@ func New(cfg *config.Config, configPath string, frontDir string) (*Server, error
var limiter *ratelimit.Limiter
if cfg.RateLimit.Enabled {
- limiter = ratelimit.New(cfg.RateLimit.Rate, cfg.RateLimit.IntervalDur, cfg.RateLimit.Whitelist, cfg.RateLimit.Blacklist)
+ limiter = ratelimit.New(cfg.RateLimit.Rate, cfg.RateLimit.IntervalDur, cfg.RateLimit.Whitelist, cfg.RateLimit.Blacklist, cfg.RateLimit.TrustedProxies)
}
s := &Server{
@@ -153,8 +154,10 @@ func (s *Server) Start() error {
}
// Git proxy routes
- mux.HandleFunc("/gh/", s.wrapWithStats("gitproxy", s.gitProxyHandler))
- mux.HandleFunc("/gl/", s.wrapWithStats("gitproxy", s.gitProxyHandler))
+ if s.cfg.GitProxy.Enabled {
+ mux.HandleFunc("/gh/", s.wrapWithStats("gitproxy", s.gitProxyHandler))
+ mux.HandleFunc("/gl/", s.wrapWithStats("gitproxy", s.gitProxyHandler))
+ }
// Dashboard API routes
mux.HandleFunc("/api/status", s.dash.StatusHandler)
@@ -165,7 +168,7 @@ func (s *Server) Start() error {
mux.HandleFunc("/api/config/public", s.dash.PublicConfigHandler)
mux.HandleFunc("/api/auth/login", s.dash.LoginHandler)
mux.HandleFunc("/api/auth/check", s.dash.AuthCheckHandler)
- mux.HandleFunc("/api/search", s.search.Search)
+ mux.HandleFunc("/api/search", s.authWrapped(s.search.Search))
mux.HandleFunc("/api/release-sources", s.dash.ReleaseSourcesHandler)
mux.HandleFunc("/api/release-sources/", s.dash.ReleaseSourceHandler)
mux.HandleFunc("/api/release-download", s.dash.ReleaseDownloadHandler)
@@ -299,12 +302,12 @@ func (s *Server) registryV2Handler(w http.ResponseWriter, r *http.Request) {
if err != nil {
slog.Error("registry token error", "registry", registryName, "error", err)
// Try without token (some repos are public)
- s.proxyRegistryRequest(w, r, target, "")
+ s.proxyRegistryRequest(w, r, target, "", 0)
return
}
slog.Info("registry request", "method", r.Method, "path", path, "target", target, "token_prefix", token[:min(10, len(token))])
- s.proxyRegistryRequest(w, r, target, token)
+ s.proxyRegistryRequest(w, r, target, token, 0)
}
func newRegistryClient() *http.Client {
@@ -323,7 +326,15 @@ func newRegistryClient() *http.Client {
}
}
-func (s *Server) proxyRegistryRequest(w http.ResponseWriter, r *http.Request, target string, token string) {
+func (s *Server) proxyRegistryRequest(w http.ResponseWriter, r *http.Request, target string, token string, depth int) {
+ // Guard against unbounded 401 retry recursion (e.g. private repos with
+ // wrong scopes) which would otherwise loop forever against the upstream.
+ if depth > 2 {
+ slog.Warn("registry auth retry loop detected, aborting", "path", r.URL.Path)
+ http.Error(w, "upstream auth loop", http.StatusUnauthorized)
+ return
+ }
+
upstreamReq, err := http.NewRequest(r.Method, target, r.Body)
if err != nil {
http.Error(w, "request error", http.StatusInternalServerError)
@@ -372,7 +383,7 @@ func (s *Server) proxyRegistryRequest(w http.ResponseWriter, r *http.Request, ta
resp.Body.Close()
// Retry without token — let upstream give fresh 401
slog.Warn("registry token rejected, retrying without token")
- s.proxyRegistryRequest(w, r, target, "")
+ s.proxyRegistryRequest(w, r, target, "", depth+1)
return
}
@@ -392,7 +403,7 @@ func (s *Server) proxyRegistryRequest(w http.ResponseWriter, r *http.Request, ta
if err == nil && newToken != "" {
resp.Body.Close()
slog.Info("registry got new token, retrying", "scope", scope)
- s.proxyRegistryRequest(w, r, target, newToken)
+ s.proxyRegistryRequest(w, r, target, newToken, depth+1)
return
}
}
@@ -606,6 +617,17 @@ func (s *Server) mirrorEnabledWrapper(m mirror.Mirror, next http.HandlerFunc) ht
}
}
+// authWrapped applies the same token policy as dashboard endpoints.
+func (s *Server) authWrapped(next http.HandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if !s.dash.CheckAuth(r) {
+ http.Error(w, "unauthorized", http.StatusUnauthorized)
+ return
+ }
+ next(w, r)
+ }
+}
+
func (s *Server) configWatcher() {
if s.configPath == "" {
return
@@ -653,7 +675,7 @@ func (s *Server) applyRuntimeConfig(cfg *config.Config) {
// Rebuild rate limiter
s.limiterMu.Lock()
if cfg.RateLimit.Enabled {
- s.limiter = ratelimit.New(cfg.RateLimit.Rate, cfg.RateLimit.IntervalDur, cfg.RateLimit.Whitelist, cfg.RateLimit.Blacklist)
+ s.limiter = ratelimit.New(cfg.RateLimit.Rate, cfg.RateLimit.IntervalDur, cfg.RateLimit.Whitelist, cfg.RateLimit.Blacklist, cfg.RateLimit.TrustedProxies)
} else {
s.limiter = nil
}
@@ -765,7 +787,7 @@ func (s *Server) rebuildLimiter() {
s.limiter = nil
return
}
- s.limiter = ratelimit.New(rl.Rate, rl.IntervalDur, rl.Whitelist, rl.Blacklist)
+ s.limiter = ratelimit.New(rl.Rate, rl.IntervalDur, rl.Whitelist, rl.Blacklist, rl.TrustedProxies)
}
func (s *Server) serverConfigSnapshot() (int, bool) {
@@ -893,7 +915,11 @@ func logMiddleware(next http.Handler, accessLog bool) http.Handler {
func (s *Server) rateLimitMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
- if strings.HasPrefix(path, "/api/") || strings.HasPrefix(path, "/v2/") ||
+ // Login is exempted from the blanket /api/ pass-through so brute
+ // force attempts are throttled like everything else.
+ if path == "/api/auth/login" {
+ // no-op: fall through to rate limiting
+ } else if strings.HasPrefix(path, "/api/") || strings.HasPrefix(path, "/v2/") ||
strings.HasPrefix(path, "/token") || path == "/" || path == "/health" ||
strings.HasSuffix(path, ".html") || strings.HasSuffix(path, ".js") ||
strings.HasSuffix(path, ".css") || strings.HasSuffix(path, ".ico") {
diff --git a/web/index.html b/web/index.html
index 06b9c09..9bf0631 100644
--- a/web/index.html
+++ b/web/index.html
@@ -2,7 +2,7 @@
-
+
DevBox
diff --git a/web/package-lock.json b/web/package-lock.json
deleted file mode 100644
index 4a62242..0000000
--- a/web/package-lock.json
+++ /dev/null
@@ -1,1945 +0,0 @@
-{
- "name": "web",
- "version": "0.0.0",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "web",
- "version": "0.0.0",
- "dependencies": {
- "vue": "^3.5.32"
- },
- "devDependencies": {
- "@tailwindcss/vite": "^4.2.4",
- "@types/node": "^24.12.2",
- "@vitejs/plugin-vue": "^6.0.6",
- "@vue/tsconfig": "^0.9.1",
- "axios": "^1.15.2",
- "tailwindcss": "^4.2.4",
- "typescript": "~6.0.2",
- "vite": "^8.0.10",
- "vue-router": "^4.6.4",
- "vue-tsc": "^3.2.7"
- }
- },
- "node_modules/@babel/helper-string-parser": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
- "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-identifier": {
- "version": "7.28.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
- "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/parser": {
- "version": "7.29.3",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
- "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.29.0"
- },
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@babel/types": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
- "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-string-parser": "^7.27.1",
- "@babel/helper-validator-identifier": "^7.28.5"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@emnapi/core": {
- "version": "1.10.0",
- "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
- "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/wasi-threads": "1.2.1",
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@emnapi/runtime": {
- "version": "1.10.0",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
- "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@emnapi/wasi-threads": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
- "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.13",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
- "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.0",
- "@jridgewell/trace-mapping": "^0.3.24"
- }
- },
- "node_modules/@jridgewell/remapping": {
- "version": "2.3.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
- "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.24"
- }
- },
- "node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
- "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
- "license": "MIT"
- },
- "node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.31",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
- "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/resolve-uri": "^3.1.0",
- "@jridgewell/sourcemap-codec": "^1.4.14"
- }
- },
- "node_modules/@napi-rs/wasm-runtime": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
- "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@tybys/wasm-util": "^0.10.1"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/Brooooooklyn"
- },
- "peerDependencies": {
- "@emnapi/core": "^1.7.1",
- "@emnapi/runtime": "^1.7.1"
- }
- },
- "node_modules/@oxc-project/types": {
- "version": "0.127.0",
- "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz",
- "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/Boshen"
- }
- },
- "node_modules/@rolldown/binding-android-arm64": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz",
- "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-darwin-arm64": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz",
- "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-darwin-x64": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz",
- "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-freebsd-x64": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz",
- "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz",
- "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm64-gnu": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz",
- "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm64-musl": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz",
- "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-ppc64-gnu": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz",
- "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-s390x-gnu": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz",
- "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-x64-gnu": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz",
- "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-x64-musl": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz",
- "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-openharmony-arm64": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz",
- "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-wasm32-wasi": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz",
- "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==",
- "cpu": [
- "wasm32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/core": "1.10.0",
- "@emnapi/runtime": "1.10.0",
- "@napi-rs/wasm-runtime": "^1.1.4"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-win32-arm64-msvc": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz",
- "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-win32-x64-msvc": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz",
- "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/pluginutils": {
- "version": "1.0.0-rc.13",
- "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.13.tgz",
- "integrity": "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@tailwindcss/node": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz",
- "integrity": "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/remapping": "^2.3.5",
- "enhanced-resolve": "^5.19.0",
- "jiti": "^2.6.1",
- "lightningcss": "1.32.0",
- "magic-string": "^0.30.21",
- "source-map-js": "^1.2.1",
- "tailwindcss": "4.2.4"
- }
- },
- "node_modules/@tailwindcss/oxide": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.4.tgz",
- "integrity": "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 20"
- },
- "optionalDependencies": {
- "@tailwindcss/oxide-android-arm64": "4.2.4",
- "@tailwindcss/oxide-darwin-arm64": "4.2.4",
- "@tailwindcss/oxide-darwin-x64": "4.2.4",
- "@tailwindcss/oxide-freebsd-x64": "4.2.4",
- "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4",
- "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4",
- "@tailwindcss/oxide-linux-arm64-musl": "4.2.4",
- "@tailwindcss/oxide-linux-x64-gnu": "4.2.4",
- "@tailwindcss/oxide-linux-x64-musl": "4.2.4",
- "@tailwindcss/oxide-wasm32-wasi": "4.2.4",
- "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4",
- "@tailwindcss/oxide-win32-x64-msvc": "4.2.4"
- }
- },
- "node_modules/@tailwindcss/oxide-android-arm64": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz",
- "integrity": "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-darwin-arm64": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz",
- "integrity": "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-darwin-x64": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz",
- "integrity": "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-freebsd-x64": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz",
- "integrity": "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz",
- "integrity": "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz",
- "integrity": "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz",
- "integrity": "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz",
- "integrity": "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-x64-musl": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz",
- "integrity": "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-wasm32-wasi": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz",
- "integrity": "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==",
- "bundleDependencies": [
- "@napi-rs/wasm-runtime",
- "@emnapi/core",
- "@emnapi/runtime",
- "@tybys/wasm-util",
- "@emnapi/wasi-threads",
- "tslib"
- ],
- "cpu": [
- "wasm32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/core": "^1.8.1",
- "@emnapi/runtime": "^1.8.1",
- "@emnapi/wasi-threads": "^1.1.0",
- "@napi-rs/wasm-runtime": "^1.1.1",
- "@tybys/wasm-util": "^0.10.1",
- "tslib": "^2.8.1"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz",
- "integrity": "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz",
- "integrity": "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/vite": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.4.tgz",
- "integrity": "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@tailwindcss/node": "4.2.4",
- "@tailwindcss/oxide": "4.2.4",
- "tailwindcss": "4.2.4"
- },
- "peerDependencies": {
- "vite": "^5.2.0 || ^6 || ^7 || ^8"
- }
- },
- "node_modules/@tybys/wasm-util": {
- "version": "0.10.1",
- "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
- "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@types/node": {
- "version": "24.12.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz",
- "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "undici-types": "~7.16.0"
- }
- },
- "node_modules/@vitejs/plugin-vue": {
- "version": "6.0.6",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.6.tgz",
- "integrity": "sha512-u9HHgfrq3AjXlysn0eINFnWQOJQLO9WN6VprZ8FXl7A2bYisv3Hui9Ij+7QZ41F/WYWarHjwBbXtD7dKg3uxbg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@rolldown/pluginutils": "1.0.0-rc.13"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- },
- "peerDependencies": {
- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
- "vue": "^3.2.25"
- }
- },
- "node_modules/@volar/language-core": {
- "version": "2.4.28",
- "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz",
- "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@volar/source-map": "2.4.28"
- }
- },
- "node_modules/@volar/source-map": {
- "version": "2.4.28",
- "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz",
- "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@volar/typescript": {
- "version": "2.4.28",
- "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz",
- "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@volar/language-core": "2.4.28",
- "path-browserify": "^1.0.1",
- "vscode-uri": "^3.0.8"
- }
- },
- "node_modules/@vue/compiler-core": {
- "version": "3.5.33",
- "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.33.tgz",
- "integrity": "sha512-3PZLQwFw4Za3TC8t0FvTy3wI16Kt+pmwcgNZca4Pj9iWL2E72a/gZlpBtAJvEdDMdCxdG/qq0C7PN0bsJuv0Rw==",
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.29.2",
- "@vue/shared": "3.5.33",
- "entities": "^7.0.1",
- "estree-walker": "^2.0.2",
- "source-map-js": "^1.2.1"
- }
- },
- "node_modules/@vue/compiler-dom": {
- "version": "3.5.33",
- "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.33.tgz",
- "integrity": "sha512-PXq0yrfCLzzL07rbXO4awtXY1Z06LG2eu6Adg3RJFa/j3Cii217XxxLXG22N330gw7GmALCY0Z8RgXEviwgpjA==",
- "license": "MIT",
- "dependencies": {
- "@vue/compiler-core": "3.5.33",
- "@vue/shared": "3.5.33"
- }
- },
- "node_modules/@vue/compiler-sfc": {
- "version": "3.5.33",
- "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.33.tgz",
- "integrity": "sha512-UTUvRO9cY+rROrx/pvN9P5Z7FgA6QGfokUCfhQE4EnmUj3rVnK+CHI0LsEO1pg+I7//iRYMUfcNcCPe7tg0CoA==",
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.29.2",
- "@vue/compiler-core": "3.5.33",
- "@vue/compiler-dom": "3.5.33",
- "@vue/compiler-ssr": "3.5.33",
- "@vue/shared": "3.5.33",
- "estree-walker": "^2.0.2",
- "magic-string": "^0.30.21",
- "postcss": "^8.5.10",
- "source-map-js": "^1.2.1"
- }
- },
- "node_modules/@vue/compiler-ssr": {
- "version": "3.5.33",
- "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.33.tgz",
- "integrity": "sha512-IErjYdnj1qIupG5xxiVIYiiRvDhGWV4zuh/RCrwfYpuL+HWQzeU6lCk/nF9r7olWMnjKxCAkOctT2qFWFkzb1A==",
- "license": "MIT",
- "dependencies": {
- "@vue/compiler-dom": "3.5.33",
- "@vue/shared": "3.5.33"
- }
- },
- "node_modules/@vue/devtools-api": {
- "version": "6.6.4",
- "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
- "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@vue/language-core": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.7.tgz",
- "integrity": "sha512-Gn4q/tRxbpVGLEuARQ43p3YELlNAFgRUVCgW9U5Cr+5q4vfD2bWDWpl3ABbJMXUt5xlE1dF8dkigg2aUq7JYYw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@volar/language-core": "2.4.28",
- "@vue/compiler-dom": "^3.5.0",
- "@vue/shared": "^3.5.0",
- "alien-signals": "^3.1.2",
- "muggle-string": "^0.4.1",
- "path-browserify": "^1.0.1",
- "picomatch": "^4.0.4"
- }
- },
- "node_modules/@vue/reactivity": {
- "version": "3.5.33",
- "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.33.tgz",
- "integrity": "sha512-p8UfIqyIhb0rYGlSgSBV+lPhF2iUSBcRy7enhTmPqKWadHy9kcOFYF1AejYBP9P+avnd3OBbD49DU4pLWX/94A==",
- "license": "MIT",
- "dependencies": {
- "@vue/shared": "3.5.33"
- }
- },
- "node_modules/@vue/runtime-core": {
- "version": "3.5.33",
- "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.33.tgz",
- "integrity": "sha512-UpFF45RI9//a7rvq7RdOQblb4tup7hHG9QsmIrxkFQLzQ7R8/iNQ5LE15NhLZ1/WcHMU2b47u6P33CPUelHyIQ==",
- "license": "MIT",
- "dependencies": {
- "@vue/reactivity": "3.5.33",
- "@vue/shared": "3.5.33"
- }
- },
- "node_modules/@vue/runtime-dom": {
- "version": "3.5.33",
- "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.33.tgz",
- "integrity": "sha512-IOxMsAOwquhfITgmOgaPYl7/j8gKUxUFoflRc+u4LxyD3+783xne8vNta1PONVCvCV9A0w7hkyEepINDqfO0tw==",
- "license": "MIT",
- "dependencies": {
- "@vue/reactivity": "3.5.33",
- "@vue/runtime-core": "3.5.33",
- "@vue/shared": "3.5.33",
- "csstype": "^3.2.3"
- }
- },
- "node_modules/@vue/server-renderer": {
- "version": "3.5.33",
- "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.33.tgz",
- "integrity": "sha512-0xylq/8/h44lVG0pZFknv1XIdEgymq2E9n59uTWJBG+dIgiT0TMCSsxrN7nO16Z0MU0MPjFcguBbZV8Itk52Hw==",
- "license": "MIT",
- "dependencies": {
- "@vue/compiler-ssr": "3.5.33",
- "@vue/shared": "3.5.33"
- },
- "peerDependencies": {
- "vue": "3.5.33"
- }
- },
- "node_modules/@vue/shared": {
- "version": "3.5.33",
- "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.33.tgz",
- "integrity": "sha512-5vR2QIlmaLG77Ygd4pMP6+SGQ5yox9VhtnbDWTy9DzMzdmeLxZ1QqxrywEZ9sa1AVubfIJyaCG3ytyWU81ufcQ==",
- "license": "MIT"
- },
- "node_modules/@vue/tsconfig": {
- "version": "0.9.1",
- "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.9.1.tgz",
- "integrity": "sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "typescript": ">= 5.8",
- "vue": "^3.4.0"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- },
- "vue": {
- "optional": true
- }
- }
- },
- "node_modules/alien-signals": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz",
- "integrity": "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/asynckit": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/axios": {
- "version": "1.15.2",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz",
- "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "follow-redirects": "^1.15.11",
- "form-data": "^4.0.5",
- "proxy-from-env": "^2.1.0"
- }
- },
- "node_modules/call-bind-apply-helpers": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
- "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/combined-stream": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
- "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "delayed-stream": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/csstype": {
- "version": "3.2.3",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
- "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "license": "MIT"
- },
- "node_modules/delayed-stream": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
- "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/detect-libc": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
- "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/dunder-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
- "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.1",
- "es-errors": "^1.3.0",
- "gopd": "^1.2.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/enhanced-resolve": {
- "version": "5.21.0",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz",
- "integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "graceful-fs": "^4.2.4",
- "tapable": "^2.3.3"
- },
- "engines": {
- "node": ">=10.13.0"
- }
- },
- "node_modules/entities": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
- "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=0.12"
- },
- "funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
- }
- },
- "node_modules/es-define-property": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
- "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-errors": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
- "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-object-atoms": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
- "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-set-tostringtag": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
- "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.6",
- "has-tostringtag": "^1.0.2",
- "hasown": "^2.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/estree-walker": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
- "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
- "license": "MIT"
- },
- "node_modules/fdir": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "picomatch": "^3 || ^4"
- },
- "peerDependenciesMeta": {
- "picomatch": {
- "optional": true
- }
- }
- },
- "node_modules/follow-redirects": {
- "version": "1.16.0",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
- "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
- "dev": true,
- "funding": [
- {
- "type": "individual",
- "url": "https://github.com/sponsors/RubenVerborgh"
- }
- ],
- "license": "MIT",
- "engines": {
- "node": ">=4.0"
- },
- "peerDependenciesMeta": {
- "debug": {
- "optional": true
- }
- }
- },
- "node_modules/form-data": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
- "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "asynckit": "^0.4.0",
- "combined-stream": "^1.0.8",
- "es-set-tostringtag": "^2.1.0",
- "hasown": "^2.0.2",
- "mime-types": "^2.1.12"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
- "node_modules/function-bind": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/get-intrinsic": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
- "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "es-define-property": "^1.0.1",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.1.1",
- "function-bind": "^1.1.2",
- "get-proto": "^1.0.1",
- "gopd": "^1.2.0",
- "has-symbols": "^1.1.0",
- "hasown": "^2.0.2",
- "math-intrinsics": "^1.1.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/get-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
- "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "dunder-proto": "^1.0.1",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/gopd": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
- "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/graceful-fs": {
- "version": "4.2.11",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
- "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/has-symbols": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
- "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-tostringtag": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
- "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-symbols": "^1.0.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/hasown": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
- "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/jiti": {
- "version": "2.6.1",
- "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
- "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "jiti": "lib/jiti-cli.mjs"
- }
- },
- "node_modules/lightningcss": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
- "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
- "dev": true,
- "license": "MPL-2.0",
- "dependencies": {
- "detect-libc": "^2.0.3"
- },
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- },
- "optionalDependencies": {
- "lightningcss-android-arm64": "1.32.0",
- "lightningcss-darwin-arm64": "1.32.0",
- "lightningcss-darwin-x64": "1.32.0",
- "lightningcss-freebsd-x64": "1.32.0",
- "lightningcss-linux-arm-gnueabihf": "1.32.0",
- "lightningcss-linux-arm64-gnu": "1.32.0",
- "lightningcss-linux-arm64-musl": "1.32.0",
- "lightningcss-linux-x64-gnu": "1.32.0",
- "lightningcss-linux-x64-musl": "1.32.0",
- "lightningcss-win32-arm64-msvc": "1.32.0",
- "lightningcss-win32-x64-msvc": "1.32.0"
- }
- },
- "node_modules/lightningcss-android-arm64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
- "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-darwin-arm64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
- "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-darwin-x64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
- "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-freebsd-x64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
- "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm-gnueabihf": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
- "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm64-gnu": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
- "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm64-musl": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
- "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-x64-gnu": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
- "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-x64-musl": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
- "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-win32-arm64-msvc": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
- "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-win32-x64-msvc": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
- "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/magic-string": {
- "version": "0.30.21",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
- "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.5"
- }
- },
- "node_modules/math-intrinsics": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
- "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "mime-db": "1.52.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/muggle-string": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
- "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/nanoid": {
- "version": "3.3.12",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
- "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
- }
- },
- "node_modules/path-browserify": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
- "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/picocolors": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "license": "ISC"
- },
- "node_modules/picomatch": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
- "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/postcss": {
- "version": "8.5.13",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz",
- "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "nanoid": "^3.3.11",
- "picocolors": "^1.1.1",
- "source-map-js": "^1.2.1"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
- "node_modules/proxy-from-env": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
- "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/rolldown": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
- "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@oxc-project/types": "=0.127.0",
- "@rolldown/pluginutils": "1.0.0-rc.17"
- },
- "bin": {
- "rolldown": "bin/cli.mjs"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- },
- "optionalDependencies": {
- "@rolldown/binding-android-arm64": "1.0.0-rc.17",
- "@rolldown/binding-darwin-arm64": "1.0.0-rc.17",
- "@rolldown/binding-darwin-x64": "1.0.0-rc.17",
- "@rolldown/binding-freebsd-x64": "1.0.0-rc.17",
- "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17",
- "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17",
- "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17",
- "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17",
- "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17",
- "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17",
- "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17",
- "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17",
- "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17",
- "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17",
- "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17"
- }
- },
- "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
- "version": "1.0.0-rc.17",
- "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz",
- "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/source-map-js": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
- "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/tailwindcss": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz",
- "integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/tapable": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
- "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- }
- },
- "node_modules/tinyglobby": {
- "version": "0.2.16",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
- "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fdir": "^6.5.0",
- "picomatch": "^4.0.4"
- },
- "engines": {
- "node": ">=12.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/SuperchupuDev"
- }
- },
- "node_modules/tslib": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
- "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "dev": true,
- "license": "0BSD",
- "optional": true
- },
- "node_modules/typescript": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
- "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
- "devOptional": true,
- "license": "Apache-2.0",
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
- },
- "engines": {
- "node": ">=14.17"
- }
- },
- "node_modules/undici-types": {
- "version": "7.16.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
- "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/vite": {
- "version": "8.0.10",
- "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
- "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "lightningcss": "^1.32.0",
- "picomatch": "^4.0.4",
- "postcss": "^8.5.10",
- "rolldown": "1.0.0-rc.17",
- "tinyglobby": "^0.2.16"
- },
- "bin": {
- "vite": "bin/vite.js"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- },
- "funding": {
- "url": "https://github.com/vitejs/vite?sponsor=1"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- },
- "peerDependencies": {
- "@types/node": "^20.19.0 || >=22.12.0",
- "@vitejs/devtools": "^0.1.0",
- "esbuild": "^0.27.0 || ^0.28.0",
- "jiti": ">=1.21.0",
- "less": "^4.0.0",
- "sass": "^1.70.0",
- "sass-embedded": "^1.70.0",
- "stylus": ">=0.54.8",
- "sugarss": "^5.0.0",
- "terser": "^5.16.0",
- "tsx": "^4.8.1",
- "yaml": "^2.4.2"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- },
- "@vitejs/devtools": {
- "optional": true
- },
- "esbuild": {
- "optional": true
- },
- "jiti": {
- "optional": true
- },
- "less": {
- "optional": true
- },
- "sass": {
- "optional": true
- },
- "sass-embedded": {
- "optional": true
- },
- "stylus": {
- "optional": true
- },
- "sugarss": {
- "optional": true
- },
- "terser": {
- "optional": true
- },
- "tsx": {
- "optional": true
- },
- "yaml": {
- "optional": true
- }
- }
- },
- "node_modules/vscode-uri": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
- "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/vue": {
- "version": "3.5.33",
- "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.33.tgz",
- "integrity": "sha512-1AgChhx5w3ALgT4oK3acm2Es/7jyZhWSVUfs3rOBlGQC0rjEDkS7G4lWlJJGGNQD+BV3reCwbQrOe1mPNwKHBQ==",
- "license": "MIT",
- "dependencies": {
- "@vue/compiler-dom": "3.5.33",
- "@vue/compiler-sfc": "3.5.33",
- "@vue/runtime-dom": "3.5.33",
- "@vue/server-renderer": "3.5.33",
- "@vue/shared": "3.5.33"
- },
- "peerDependencies": {
- "typescript": "*"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
- }
- },
- "node_modules/vue-router": {
- "version": "4.6.4",
- "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
- "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vue/devtools-api": "^6.6.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/posva"
- },
- "peerDependencies": {
- "vue": "^3.5.0"
- }
- },
- "node_modules/vue-tsc": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.7.tgz",
- "integrity": "sha512-zc1tL3HoQni1zGTGrwBVRQb7rGP5SWdu/m4rGB6JcnAC5MT5LFZIxF7Y+EJEnt4hGF23d60rXH7gRjHGb5KQQQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@volar/typescript": "2.4.28",
- "@vue/language-core": "3.2.7"
- },
- "bin": {
- "vue-tsc": "bin/vue-tsc.js"
- },
- "peerDependencies": {
- "typescript": ">=5.0.0"
- }
- }
- }
-}
diff --git a/web/package.json b/web/package.json
index b1e30f5..6910c2b 100644
--- a/web/package.json
+++ b/web/package.json
@@ -10,7 +10,8 @@
},
"dependencies": {
"axios": "^1.15.2",
- "vue": "^3.5.32"
+ "vue": "^3.5.32",
+ "vue-router": "^4.6.4"
},
"devDependencies": {
"@tailwindcss/vite": "^4.2.4",
@@ -20,7 +21,6 @@
"tailwindcss": "^4.2.4",
"typescript": "~6.0.2",
"vite": "^8.0.10",
- "vue-router": "^4.6.4",
"vue-tsc": "^3.2.7"
}
}
diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml
index 2180f43..3eef6aa 100644
--- a/web/pnpm-lock.yaml
+++ b/web/pnpm-lock.yaml
@@ -14,6 +14,9 @@ importers:
vue:
specifier: ^3.5.32
version: 3.5.35(typescript@6.0.3)
+ vue-router:
+ specifier: ^4.6.4
+ version: 4.6.4(vue@3.5.35(typescript@6.0.3))
devDependencies:
'@tailwindcss/vite':
specifier: ^4.2.4
@@ -36,9 +39,6 @@ importers:
vite:
specifier: ^8.0.10
version: 8.0.16(@types/node@24.13.1)(jiti@2.7.0)
- vue-router:
- specifier: ^4.6.4
- version: 4.6.4(vue@3.5.35(typescript@6.0.3))
vue-tsc:
specifier: ^3.2.7
version: 3.3.3(typescript@6.0.3)
diff --git a/web/public/favicon.svg b/web/public/favicon.svg
index d01a53e..de0cdca 100644
--- a/web/public/favicon.svg
+++ b/web/public/favicon.svg
@@ -1,29 +1,39 @@
diff --git a/web/public/icons.svg b/web/public/icons.svg
deleted file mode 100644
index e952219..0000000
--- a/web/public/icons.svg
+++ /dev/null
@@ -1,24 +0,0 @@
-
diff --git a/web/src/App.vue b/web/src/App.vue
index 12bc265..650129a 100644
--- a/web/src/App.vue
+++ b/web/src/App.vue
@@ -1,6 +1,14 @@