From 56f4ef049884df6dd83a3bcd8eae378ce3e7ac1b Mon Sep 17 00:00:00 2001 From: pzzzy <119996856+pzzzy@users.noreply.github.com> Date: Wed, 15 Jul 2026 23:40:10 -0400 Subject: [PATCH 1/2] Reduce monitor noise and classify canceled downloads --- docs/large-transfers.md | 4 +++ internal/activity/activity.go | 5 +++ internal/ftpserver/server.go | 50 +++++++++++++++++++++++++- internal/ftpserver/server_test.go | 59 +++++++++++++++++++++++++++++++ internal/httpapi/server.go | 36 ++++++++++++++++++- internal/httpapi/server_test.go | 31 ++++++++++++++++ scripts/monitor.sh | 1 + scripts/protocol-lab.sh | 2 +- scripts/weekly-report.sh | 11 +++++- 9 files changed, 195 insertions(+), 4 deletions(-) diff --git a/docs/large-transfers.md b/docs/large-transfers.md index 7f8bf6f..ed5a6e4 100644 --- a/docs/large-transfers.md +++ b/docs/large-transfers.md @@ -10,6 +10,8 @@ The request history contained several failures at roughly 60 seconds followed by Media clients commonly request a small tail range to inspect container metadata. macftpd records a partial response as a completed download only when it reaches EOF and transfers a meaningful amount: one percent of the object, capped at 8 MiB. A tiny tail probe therefore does not inflate download counts, while a substantial resume that completes the object does. +If a browser, media player, or tunnel closes a response before it finishes, macftpd records the transfer as `canceled`. The byte count and range remain in the event for diagnosis, but the weekly report keeps these client-side interruptions separate from server failures. + ## Integrity checks For an end-to-end transfer test, compare the byte count and SHA-256 digest at the source and destination: @@ -46,4 +48,6 @@ Stale chunk parts older than 24 hours are cleaned opportunistically. The upload, - Confirm sufficient free space for the incoming staged file and, on overwrite, one retained copy of the previous destination. - Compare the final byte count and digest for release or incident validation. - Treat tiny EOF range requests as media probes; use the activity detail, status, bytes, and range fields to distinguish them from completed transfers. +- Review `Client Cancellations` separately from `Failures` in the weekly report. Repeated cancellations at a consistent duration can still reveal a proxy or timeout problem, but isolated broken pipes and resets normally mean the client stopped reading. +- The loopback FTP monitor identifies itself with `CLNT macftpd-monitor`. Intermediate successful probe actions are suppressed, one completed cycle is retained per hour, and every failed action is retained. - When Cloudflare caching is enabled, configure a cache tag and public base URL. Public mutations purge the tag; HTTP mutations fall back to exact object and parent-listing URLs. diff --git a/internal/activity/activity.go b/internal/activity/activity.go index c5b7a59..9f6d73f 100644 --- a/internal/activity/activity.go +++ b/internal/activity/activity.go @@ -225,6 +225,11 @@ func (e Event) humanMessage() string { return fmt.Sprintf("%s %s failed for %s", actor, action, subject) } return fmt.Sprintf("%s %s failed", actor, action) + case "canceled", "cancelled": + if subject != "" { + return fmt.Sprintf("%s %s canceled for %s", actor, action, subject) + } + return fmt.Sprintf("%s %s canceled", actor, action) } if e.DestPath != "" && subject != "" { return fmt.Sprintf("%s %s %s to %s", actor, action, subject, e.DestPath) diff --git a/internal/ftpserver/server.go b/internal/ftpserver/server.go index 08875e3..9e30e49 100644 --- a/internal/ftpserver/server.go +++ b/internal/ftpserver/server.go @@ -49,6 +49,8 @@ type Server struct { publicHook func(string) readNoiseMu sync.Mutex readNoise map[string]readNoiseEvent + monitorMu sync.Mutex + monitorLast time.Time } type readNoiseEvent struct { @@ -59,6 +61,11 @@ type readNoiseEvent struct { const readNoiseReportInterval = 10 * time.Minute +const ( + monitorClientName = "macftpd-monitor" + monitorSuccessInterval = time.Hour +) + type session struct { server *Server conn net.Conn @@ -78,6 +85,7 @@ type session struct { restartSet bool secure bool protPrivate bool + monitor bool statusID int64 } @@ -336,11 +344,16 @@ func (s *session) dispatch(cmd, arg string) bool { case "SYST": s.reply(215, "UNIX Type: L8") case "FEAT": - features := []string{"UTF8", "EPSV", "PASV", "REST STREAM", "SIZE", "MDTM", "MLST type*;size*;modify*;perm*;", "MLSD"} + features := []string{"UTF8", "CLNT", "EPSV", "PASV", "REST STREAM", "SIZE", "MDTM", "MLST type*;size*;modify*;perm*;", "MLSD"} if s.server.tlsConfig != nil { features = append(features, "AUTH TLS", "PBSZ", "PROT") } s.multiline(211, features, "End") + case "CLNT": + if monitorClientAllowed(s.conn.RemoteAddr(), arg) { + s.monitor = true + } + s.reply(200, "Client name noted") case "OPTS": s.reply(200, "OK") case "PWD", "XPWD": @@ -1467,6 +1480,15 @@ func (s *session) loginLimitKey() string { } func (s *session) logActivity(action, outcome, pathValue, destPath string, bytes int64, detail string) { + if s.monitor && outcome == "ok" { + // A successful cleanup is the end-to-end monitor signal. Keep one per + // hour and discard intermediate login/mkdir/upload/download successes. + // Failures are never coalesced. + if action != "delete" || !s.server.monitorSuccessDue() { + return + } + detail = "FTP monitor cycle completed" + } actor := s.username if s.user.Username != "" { actor = s.user.Username @@ -1488,6 +1510,32 @@ func (s *session) logActivity(action, outcome, pathValue, destPath string, bytes }) } +func monitorClientAllowed(remote net.Addr, name string) bool { + if !strings.EqualFold(strings.TrimSpace(name), monitorClientName) || remote == nil { + return false + } + if tcp, ok := remote.(*net.TCPAddr); ok { + return tcp.IP.IsLoopback() + } + host, _, err := net.SplitHostPort(remote.String()) + if err != nil { + host = remote.String() + } + ip := net.ParseIP(strings.Trim(host, "[]")) + return ip != nil && ip.IsLoopback() +} + +func (s *Server) monitorSuccessDue() bool { + s.monitorMu.Lock() + defer s.monitorMu.Unlock() + now := time.Now() + if !s.monitorLast.IsZero() && now.Sub(s.monitorLast) < monitorSuccessInterval { + return false + } + s.monitorLast = now + return true +} + func (s *session) updateStatus(mutate func(*status.Session)) { if s.server.tracker == nil || s.statusID == 0 { return diff --git a/internal/ftpserver/server_test.go b/internal/ftpserver/server_test.go index 5befb20..a9c1027 100644 --- a/internal/ftpserver/server_test.go +++ b/internal/ftpserver/server_test.go @@ -1,6 +1,7 @@ package ftpserver import ( + "bufio" "bytes" "context" "crypto/ecdsa" @@ -50,6 +51,64 @@ func TestPassiveDataPeerMustMatchControlPeer(t *testing.T) { } } +func TestFTPMonitorMarkerRequiresLoopback(t *testing.T) { + loopback := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 50000} + external := &net.TCPAddr{IP: net.ParseIP("203.0.113.10"), Port: 50000} + if !monitorClientAllowed(loopback, "macftpd-monitor") { + t.Fatal("loopback monitor marker was denied") + } + if monitorClientAllowed(external, "macftpd-monitor") { + t.Fatal("external client was allowed to suppress monitor activity") + } + if monitorClientAllowed(loopback, "ordinary-client") { + t.Fatal("ordinary client was treated as the monitor") + } + + left, right := net.Pipe() + defer left.Close() + defer right.Close() + conn := &testRemoteConn{Conn: left, remote: loopback} + ss := &session{server: &Server{}, conn: conn, writer: bufio.NewWriter(io.Discard)} + ss.dispatch("CLNT", "macftpd-monitor") + if !ss.monitor { + t.Fatal("CLNT did not mark the loopback monitor session") + } +} + +func TestFTPMonitorSuccessesAreCoalescedAfterCompletedCycle(t *testing.T) { + activityLog := activity.New(100) + server := &Server{activity: activityLog} + left, right := net.Pipe() + defer left.Close() + defer right.Close() + ss := &session{server: server, conn: left, username: "admin", monitor: true} + + for _, action := range []string{"login", "mkdir", "upload", "download"} { + ss.logActivity(action, "ok", "/_monitor/probe.txt", "", 10, "probe step") + } + if events := activityLog.Recent(10, 0); len(events) != 0 { + t.Fatalf("intermediate monitor successes were logged: %#v", events) + } + ss.logActivity("delete", "ok", "/_monitor/probe.txt", "", 0, "cleanup") + ss.logActivity("delete", "ok", "/_monitor/probe-2.txt", "", 0, "cleanup") + events := activityLog.Recent(10, 0) + if len(events) != 1 || events[0].Outcome != "ok" || events[0].Detail != "FTP monitor cycle completed" { + t.Fatalf("completed monitor cycle was not coalesced: %#v", events) + } + ss.logActivity("download", "failed", "/_monitor/probe.txt", "", 3, "connection reset") + events = activityLog.Recent(10, 0) + if len(events) != 2 || events[0].Outcome != "failed" { + t.Fatalf("monitor failure was suppressed: %#v", events) + } +} + +type testRemoteConn struct { + net.Conn + remote net.Addr +} + +func (c *testRemoteConn) RemoteAddr() net.Addr { return c.remote } + func TestFTPActiveSessionRevalidatesDisabledUser(t *testing.T) { dir := t.TempDir() store, err := auth.Open(dir + "/users.json") diff --git a/internal/httpapi/server.go b/internal/httpapi/server.go index 787616c..1a69bea 100644 --- a/internal/httpapi/server.go +++ b/internal/httpapi/server.go @@ -24,6 +24,7 @@ import ( "strconv" "strings" "sync" + "syscall" "time" ftpclient "github.com/jlaffaye/ftp" @@ -2593,7 +2594,15 @@ func (w *countingResponseWriter) Unwrap() http.ResponseWriter { func (s *Server) logDownloadActivity(event activity.Event, result fileServeResult) { event.Bytes = result.Bytes - if result.Err != nil || result.Status >= 400 { + if result.Err != nil && isClientDisconnect(result.Err) { + event.Outcome = "canceled" + detail := fmt.Sprintf("%s canceled status=%d bytes=%d", event.Detail, result.Status, result.Bytes) + if result.Range != "" { + detail += " range=" + result.Range + } + detail += " error=" + result.Err.Error() + event.Detail = detail + } else if result.Err != nil || result.Status >= 400 { event.Outcome = "failed" detail := fmt.Sprintf("%s failed status=%d bytes=%d", event.Detail, result.Status, result.Bytes) if result.Range != "" { @@ -2616,6 +2625,31 @@ func (s *Server) logDownloadActivity(event activity.Event, result fileServeResul s.logActivity(event) } +func isClientDisconnect(err error) bool { + if err == nil { + return false + } + if errors.Is(err, context.Canceled) || errors.Is(err, net.ErrClosed) || + errors.Is(err, io.ErrClosedPipe) || errors.Is(err, syscall.EPIPE) || + errors.Is(err, syscall.ECONNRESET) { + return true + } + message := strings.ToLower(err.Error()) + for _, signal := range []string{ + "broken pipe", + "connection reset by peer", + "client disconnected", + "stream canceled", + "stream cancelled", + "; cancel", + } { + if strings.Contains(message, signal) { + return true + } + } + return false +} + func rangeRunsToEOF(header string, size int64) bool { if size <= 0 { return false diff --git a/internal/httpapi/server_test.go b/internal/httpapi/server_test.go index d85aeed..29da8b8 100644 --- a/internal/httpapi/server_test.go +++ b/internal/httpapi/server_test.go @@ -3,6 +3,7 @@ package httpapi import ( "bytes" "encoding/json" + "errors" "html/template" "mime/multipart" "net/http" @@ -11,6 +12,7 @@ import ( "os" "strconv" "strings" + "syscall" "testing" "time" @@ -54,6 +56,35 @@ func TestTinyTailProbeDoesNotCountAsCompletedLargeDownload(t *testing.T) { } } +func TestClientDisconnectsAreCanceledRatherThanFailed(t *testing.T) { + for _, tc := range []struct { + name string + err error + want bool + }{ + {name: "broken pipe", err: syscall.EPIPE, want: true}, + {name: "reset", err: syscall.ECONNRESET, want: true}, + {name: "HTTP/2 cancel", err: errors.New("stream error: stream ID 7; CANCEL"), want: true}, + {name: "storage failure", err: errors.New("storage read failed"), want: false}, + } { + t.Run(tc.name, func(t *testing.T) { + if got := isClientDisconnect(tc.err); got != tc.want { + t.Fatalf("isClientDisconnect(%v) = %v, want %v", tc.err, got, tc.want) + } + }) + } + + srv := testServer(t) + srv.logDownloadActivity(activity.Event{ + Type: "share_download", Protocol: "http", Actor: "share-link", + Action: "download", Path: "/public/movie.mkv", Detail: "public share download", + }, fileServeResult{Status: http.StatusOK, Bytes: 4 << 20, Method: http.MethodGet, Err: syscall.EPIPE}) + events := srv.activity.Recent(10, 0) + if len(events) != 1 || events[0].Outcome != "canceled" || !strings.Contains(events[0].Message, "canceled") || isSecurityActivity(events[0]) { + t.Fatalf("client disconnect was not recorded as a non-failure cancellation: %#v", events) + } +} + func TestAdminFilesURLRemainsSafeInTemplateURLContext(t *testing.T) { tmpl := template.Must(template.New("url").Funcs(templateFuncs()).Parse(`open`)) var rendered strings.Builder diff --git a/scripts/monitor.sh b/scripts/monitor.sh index 0b0e7cf..23f2fcb 100755 --- a/scripts/monitor.sh +++ b/scripts/monitor.sh @@ -52,6 +52,7 @@ remote_file = f"{remote_dir}/{stamp}.txt" ftp = ftplib.FTP() ftp.connect(host, port, timeout=20) +ftp.sendcmd("CLNT macftpd-monitor") ftp.login(user, password) try: ftp.mkd(remote_dir) diff --git a/scripts/protocol-lab.sh b/scripts/protocol-lab.sh index 878301c..697d08d 100755 --- a/scripts/protocol-lab.sh +++ b/scripts/protocol-lab.sh @@ -45,7 +45,7 @@ ensure_dir(ftp, "_protocol_lab") ensure_dir(ftp, base) ftp.voidcmd("TYPE I") features = "\n".join(ftp.sendcmd("FEAT").splitlines()) -required = ["UTF8", "PASV", "EPSV", "REST STREAM", "SIZE", "MDTM", "MLSD"] +required = ["UTF8", "CLNT", "PASV", "EPSV", "REST STREAM", "SIZE", "MDTM", "MLSD"] missing = [x for x in required if x not in features] if missing: raise SystemExit(f"missing FEAT entries: {missing}") diff --git a/scripts/weekly-report.sh b/scripts/weekly-report.sh index 31810ba..22d7484 100755 --- a/scripts/weekly-report.sh +++ b/scripts/weekly-report.sh @@ -74,6 +74,7 @@ paths.extend(sorted(var_dir.glob("activity.jsonl.*.gz"))) counts = collections.Counter() monitor_counts = collections.Counter() failures = collections.Counter() +cancellations = collections.Counter() monitor_failures = collections.Counter() bytes_by_action = collections.Counter() paths_by_action = collections.Counter() @@ -175,7 +176,10 @@ for path in paths: bytes_by_action[action] += int(size or 0) except (TypeError, ValueError): pass - if outcome not in ("ok", "success"): + if outcome in ("canceled", "cancelled"): + if not monitor and not maintenance: + cancellations[(action, event.get("detail") or "canceled")] += 1 + elif outcome not in ("ok", "success"): if monitor: monitor_failures[(action, event.get("detail") or "failed")] += 1 elif not maintenance: @@ -203,6 +207,11 @@ if failures: for (action, detail), count in failures.most_common(10): detail = str(detail).replace("`", "'") print(f"- {action}: `{count}` `{detail[:160]}`") +if cancellations: + print("\n### Client Cancellations\n") + for (action, detail), count in cancellations.most_common(10): + detail = str(detail).replace("`", "'") + print(f"- {action}: `{count}` `{detail[:160]}`") if monitor_counts: print("\n### Monitor Summary\n") for (action, status), count in monitor_counts.most_common(): From 972e57236a3966e65293f2535cd826b574836161 Mon Sep 17 00:00:00 2001 From: pzzzy <119996856+pzzzy@users.noreply.github.com> Date: Tue, 21 Jul 2026 19:42:29 -0400 Subject: [PATCH 2/2] Improve operational diagnostics and dependency gate --- README.md | 2 + internal/activity/activity.go | 30 +- internal/activity/activity_test.go | 20 + internal/httpapi/server.go | 106 ++++- internal/httpapi/server_test.go | 63 +++ .../httpapi/templates/partial_status.html | 11 +- package-lock.json | 368 ++++++++++-------- package.json | 5 +- scripts/check.sh | 1 + 9 files changed, 421 insertions(+), 185 deletions(-) diff --git a/README.md b/README.md index 7620f8c..98ba87a 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,8 @@ curl "${auth[@]}" 'https://ftp.example.com/api/stats?path=/public/example.mp4' Other admin endpoints include `/api/users`, `/api/groups`, `/api/files`, `/api/files/action`, `/api/upload/chunk`, `/api/download`, `/api/fxp`, `/api/activity`, `/api/status`, `/api/doctor`, `/api/retention`, `/api/retention/restore`, and `/api/cloudflare/purge`. +The authenticated `/api/doctor` response includes build provenance and uptime, effective HTTP timeouts, activity-buffer capacity and age, and individual storage/integration checks. Optional integrations that are disabled are reported as informational rather than failed. The activity dashboard scans the full in-memory history before filtering monitor traffic, so a busy probe loop cannot hide human or security events. + ## Release Gate Before a release candidate, run: diff --git a/internal/activity/activity.go b/internal/activity/activity.go index 9f6d73f..b2752bb 100644 --- a/internal/activity/activity.go +++ b/internal/activity/activity.go @@ -36,6 +36,15 @@ type PathStats struct { Recent []Event `json:"recent,omitempty"` } +type State struct { + Count int `json:"count"` + Capacity int `json:"capacity"` + OldestID int64 `json:"oldest_id,omitempty"` + NewestID int64 `json:"newest_id,omitempty"` + OldestTime time.Time `json:"oldest_time,omitempty"` + NewestTime time.Time `json:"newest_time,omitempty"` +} + type Store struct { mu sync.RWMutex nextID int64 @@ -107,7 +116,7 @@ func (s *Store) Recent(limit int, afterID int64) []Event { if s == nil { return nil } - if limit <= 0 || limit > 500 { + if limit <= 0 { limit = 100 } s.mu.RLock() @@ -123,6 +132,25 @@ func (s *Store) Recent(limit int, afterID int64) []Event { return out } +func (s *Store) State() State { + if s == nil { + return State{} + } + s.mu.RLock() + defer s.mu.RUnlock() + state := State{Count: len(s.events), Capacity: s.limit} + if len(s.events) == 0 { + return state + } + oldest := s.events[0] + newest := s.events[len(s.events)-1] + state.OldestID = oldest.ID + state.NewestID = newest.ID + state.OldestTime = oldest.Time + state.NewestTime = newest.Time + return state +} + func (s *Store) StatsForPath(path string, limit int) PathStats { stats := PathStats{Path: path, Referrers: map[string]int{}} if s == nil { diff --git a/internal/activity/activity_test.go b/internal/activity/activity_test.go index c0c451d..04d3c9a 100644 --- a/internal/activity/activity_test.go +++ b/internal/activity/activity_test.go @@ -3,6 +3,7 @@ package activity import ( "path/filepath" "testing" + "time" ) func TestFileStoreReloadsRecentEvents(t *testing.T) { @@ -30,3 +31,22 @@ func TestFileStoreReloadsRecentEvents(t *testing.T) { t.Fatalf("next ID = %d, want greater than %d", next.ID, events[0].ID) } } + +func TestStateTracksBoundedHistory(t *testing.T) { + store := New(2) + firstTime := time.Date(2026, 7, 20, 12, 0, 0, 0, time.UTC) + store.Add(Event{Time: firstTime, Action: "first"}) + second := store.Add(Event{Time: firstTime.Add(time.Minute), Action: "second"}) + third := store.Add(Event{Time: firstTime.Add(2 * time.Minute), Action: "third"}) + + state := store.State() + if state.Count != 2 || state.Capacity != 2 { + t.Fatalf("state size = %#v, want 2/2", state) + } + if state.OldestID != second.ID || state.NewestID != third.ID { + t.Fatalf("state IDs = %#v, want oldest=%d newest=%d", state, second.ID, third.ID) + } + if !state.OldestTime.Equal(firstTime.Add(time.Minute)) || !state.NewestTime.Equal(firstTime.Add(2*time.Minute)) { + t.Fatalf("state times = %#v", state) + } +} diff --git a/internal/httpapi/server.go b/internal/httpapi/server.go index 1a69bea..69d9473 100644 --- a/internal/httpapi/server.go +++ b/internal/httpapi/server.go @@ -20,6 +20,8 @@ import ( "os" "path" "path/filepath" + "runtime" + "runtime/debug" "sort" "strconv" "strings" @@ -53,6 +55,7 @@ type Server struct { tracker *status.Tracker uploadMu sync.Mutex uploads map[string]*uploadLock + startedAt time.Time } type uploadLock struct { @@ -109,7 +112,7 @@ func (r userRequest) user() auth.User { } func New(cfg config.HTTPConfig, store *auth.Store, root *storage.Root, cf *cloudflare.Client, activityLog *activity.Store, links *share.Store, tracker *status.Tracker) *Server { - return &Server{cfg: cfg, store: store, root: root, cloudflare: cf, sessionKey: []byte(cfg.SessionKey), limiter: ratelimit.New(5, 10*time.Minute, 5*time.Minute), shareLimit: ratelimit.New(5, 10*time.Minute, 5*time.Minute), activity: activityLog, links: links, tracker: tracker, uploads: map[string]*uploadLock{}} + return &Server{cfg: cfg, store: store, root: root, cloudflare: cf, sessionKey: []byte(cfg.SessionKey), limiter: ratelimit.New(5, 10*time.Minute, 5*time.Minute), shareLimit: ratelimit.New(5, 10*time.Minute, 5*time.Minute), activity: activityLog, links: links, tracker: tracker, uploads: map[string]*uploadLock{}, startedAt: time.Now().UTC()} } func (s *Server) ListenAndServe(ctx context.Context) error { @@ -1878,13 +1881,10 @@ func (s *Server) activityDashboard(limit int, after int64) activityDashboard { if limit <= 0 || limit > 200 { limit = 80 } - scanLimit := limit * 6 - if scanLimit < 200 { - scanLimit = 200 - } - if scanLimit > 500 { - scanLimit = 500 - } + // The production activity buffer holds 2,000 events. Scan it in full so a + // burst of monitor or maintenance traffic cannot hide human activity and + // security events from the filtered dashboard. + const scanLimit = 2000 dashboard := activityDashboard{ Events: []activity.Event{}, Security: []activity.Event{}, @@ -2011,34 +2011,102 @@ func (s *Server) doctorAPI(w http.ResponseWriter, r *http.Request, _ principal) writeJSON(w, http.StatusMethodNotAllowed, errorBody("method not allowed")) return } - writeJSON(w, http.StatusOK, map[string]any{"checks": s.doctorChecks(), "time": time.Now().UTC()}) + payload := map[string]any{ + "checks": s.doctorChecks(), + "http": s.doctorHTTPStatus(), + "runtime": s.doctorRuntimeStatus(), + "time": time.Now().UTC(), + } + if s.activity != nil { + payload["activity"] = s.activity.State() + } + writeJSON(w, http.StatusOK, payload) } func (s *Server) doctorChecks() []map[string]any { checks := []map[string]any{} - add := func(name string, ok bool, detail string) { - checks = append(checks, map[string]any{"name": name, "ok": ok, "detail": detail}) + add := func(name, level, detail string) { + checks = append(checks, map[string]any{"name": name, "ok": level != "fail", "level": level, "detail": detail}) } if info, err := os.Stat(s.root.Base); err == nil && info.IsDir() { - add("storage root", true, s.root.Base) + add("storage root", "ok", s.root.Base) } else { - add("storage root", false, fmt.Sprint(err)) + add("storage root", "fail", fmt.Sprint(err)) } for _, dir := range []string{s.root.PublicDir, s.root.DropboxDir, "._macftpd_trash", "._macftpd_versions"} { real := filepath.Join(s.root.Base, dir) if err := os.MkdirAll(real, 0o750); err != nil { - add("storage "+dir, false, err.Error()) + add("storage "+dir, "fail", err.Error()) } else { - add("storage "+dir, true, real) + add("storage "+dir, "ok", real) } } - add("cloudflare client", s.cloudflare.Enabled(), "configured="+strconv.FormatBool(s.cloudflare.Enabled())) - add("share store", s.links != nil, strconv.Itoa(len(s.links.List()))+" links") - add("activity store", s.activity != nil, "ready") - add("turnstile", s.cfg.TurnstileSecret != "", "configured="+strconv.FormatBool(s.cfg.TurnstileSecret != "")) + cloudflareLevel := "info" + if s.cloudflare.Enabled() { + cloudflareLevel = "ok" + } + add("cloudflare client", cloudflareLevel, "optional; configured="+strconv.FormatBool(s.cloudflare.Enabled())) + if s.links == nil { + add("share store", "fail", "unavailable") + } else { + add("share store", "ok", strconv.Itoa(len(s.links.List()))+" links") + } + if s.activity == nil { + add("activity store", "fail", "unavailable") + } else { + state := s.activity.State() + detail := fmt.Sprintf("%d/%d events", state.Count, state.Capacity) + if !state.OldestTime.IsZero() { + detail += "; oldest=" + state.OldestTime.UTC().Format(time.RFC3339) + } + add("activity store", "ok", detail) + } + streamLevel := "ok" + if s.cfg.ReadTimeout != 0 || s.cfg.WriteTimeout != 0 { + streamLevel = "warning" + } + add("HTTP streaming", streamLevel, fmt.Sprintf("read_timeout=%s write_timeout=%s", time.Duration(s.cfg.ReadTimeout), time.Duration(s.cfg.WriteTimeout))) + turnstileLevel := "info" + if s.cfg.TurnstileSecret != "" { + turnstileLevel = "ok" + } + add("turnstile", turnstileLevel, "optional; configured="+strconv.FormatBool(s.cfg.TurnstileSecret != "")) return checks } +func (s *Server) doctorHTTPStatus() map[string]string { + return map[string]string{ + "read_header_timeout": s.cfg.ReadHeaderTimeout.Std(10 * time.Second).String(), + "read_timeout": time.Duration(s.cfg.ReadTimeout).String(), + "write_timeout": time.Duration(s.cfg.WriteTimeout).String(), + "idle_timeout": s.cfg.IdleTimeout.Std(60 * time.Second).String(), + } +} + +func (s *Server) doctorRuntimeStatus() map[string]any { + status := map[string]any{ + "go_version": runtime.Version(), + "started_at": s.startedAt, + } + if !s.startedAt.IsZero() { + status["uptime_seconds"] = int64(time.Since(s.startedAt).Seconds()) + } + if info, ok := debug.ReadBuildInfo(); ok { + status["module_version"] = info.Main.Version + for _, setting := range info.Settings { + switch setting.Key { + case "vcs.revision": + status["vcs_revision"] = setting.Value + case "vcs.time": + status["vcs_time"] = setting.Value + case "vcs.modified": + status["vcs_modified"] = setting.Value == "true" + } + } + } + return status +} + func (s *Server) sharesAPI(w http.ResponseWriter, r *http.Request, p principal) { switch r.Method { case http.MethodGet: diff --git a/internal/httpapi/server_test.go b/internal/httpapi/server_test.go index 29da8b8..02a0aba 100644 --- a/internal/httpapi/server_test.go +++ b/internal/httpapi/server_test.go @@ -303,6 +303,69 @@ func TestActivityDashboardSuppressesMonitorAndSeparatesSecurity(t *testing.T) { } } +func TestActivityDashboardScansPastMonitorFlood(t *testing.T) { + srv := testServer(t) + srv.activity = activity.New(2000) + human := srv.activity.Add(activity.Event{Type: "admin_file", Protocol: "http", Actor: "admin", Action: "copy", Path: "/public/important.txt"}) + for i := 0; i < 700; i++ { + srv.activity.Add(activity.Event{Type: "ftp_delete", Protocol: "ftp", Actor: "admin", Remote: "127.0.0.1:50000", Action: "delete", Path: "_monitor/probe.txt", Detail: "FTP monitor cleanup"}) + } + + dashboard := srv.activityDashboard(20, 0) + if dashboard.Monitor.Count != 700 { + t.Fatalf("monitor count = %d, want 700", dashboard.Monitor.Count) + } + if len(dashboard.Events) != 1 || dashboard.Events[0].ID != human.ID { + t.Fatalf("human event was hidden by monitor traffic: %#v", dashboard.Events) + } +} + +func TestDoctorReportsOperationalMetadataAndOptionalChecks(t *testing.T) { + srv := testServer(t) + srv.cfg.WriteTimeout = config.Duration(time.Minute) + srv.activity.Add(activity.Event{Action: "test"}) + + req := httptest.NewRequest(http.MethodGet, "/api/doctor", nil) + req.SetBasicAuth("admin", "secret") + rr := httptest.NewRecorder() + srv.requireAdmin(srv.doctorAPI)(rr, req) + if rr.Code != http.StatusOK { + t.Fatalf("doctor status = %d body=%s", rr.Code, rr.Body.String()) + } + var body struct { + Checks []struct { + Name string `json:"name"` + OK bool `json:"ok"` + Level string `json:"level"` + } `json:"checks"` + HTTP map[string]string `json:"http"` + Runtime map[string]any `json:"runtime"` + Activity activity.State `json:"activity"` + } + if err := json.Unmarshal(rr.Body.Bytes(), &body); err != nil { + t.Fatalf("decode doctor response: %v", err) + } + if body.HTTP["write_timeout"] != "1m0s" || body.HTTP["read_timeout"] != "0s" { + t.Fatalf("unexpected HTTP timeout metadata: %#v", body.HTTP) + } + if body.Runtime["go_version"] == "" || body.Runtime["started_at"] == nil { + t.Fatalf("missing runtime metadata: %#v", body.Runtime) + } + if body.Activity.Count != 1 || body.Activity.Capacity != 200 { + t.Fatalf("unexpected activity state: %#v", body.Activity) + } + levels := map[string]string{} + for _, check := range body.Checks { + if !check.OK { + t.Fatalf("non-failing doctor check reported ok=false: %#v", check) + } + levels[check.Name] = check.Level + } + if levels["HTTP streaming"] != "warning" || levels["cloudflare client"] != "info" || levels["turnstile"] != "info" { + t.Fatalf("unexpected doctor check levels: %#v", levels) + } +} + func TestUploadRejectsIgnoredDestination(t *testing.T) { srv := testServer(t) var body bytes.Buffer diff --git a/internal/httpapi/templates/partial_status.html b/internal/httpapi/templates/partial_status.html index 1a8cb47..1c1ebd4 100644 --- a/internal/httpapi/templates/partial_status.html +++ b/internal/httpapi/templates/partial_status.html @@ -13,7 +13,16 @@

Status

{{index . "name"}}
{{index . "detail"}}
- {{if index . "ok"}}ok{{else}}fail{{end}} + {{$level := index . "level"}} + {{if eq $level "fail"}} + fail + {{else if eq $level "warning"}} + warn + {{else if eq $level "info"}} + optional + {{else}} + ok + {{end}} {{end}} diff --git a/package-lock.json b/package-lock.json index 63a2d8d..769c23e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "daisyui": "^4.12.24", "htmx.org": "^2.0.10", "tailwindcss": "^3.4.19", - "wrangler": "4.110.0" + "wrangler": "4.113.0" } }, "node_modules/@alloc/quick-lru": { @@ -55,9 +55,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20260708.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260708.1.tgz", - "integrity": "sha512-HXFCvhS1wpg3uXO0CLUwmwC41i2loM5FSK69EUchOBpmYBAXxT1oHLm6EOA5lqhTk5Mu9kjRiQYxa1GwKPwfJg==", + "version": "1.20260721.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260721.1.tgz", + "integrity": "sha512-VivNMhiEdZIB4JBWxf1RMJGROErv53qmQ+dvhjA1evrCouvqRYW718VqDideU3PSV7Ythl5Df48NqZYWoaEHpQ==", "cpu": [ "x64" ], @@ -72,9 +72,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20260708.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260708.1.tgz", - "integrity": "sha512-JVlJaKDoRTVKSroHIlf8g3UCPjKj4iDbMZE2CNYht5qQ+2rL0FAUiVlV82G3BqKnnw9kHYnnsMzC08b9zVtdzA==", + "version": "1.20260721.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260721.1.tgz", + "integrity": "sha512-k7oye1ZiuwnnBBA2eTMduconr/ud5ZxFtRNTsYwMdmJeeeislw2+M72otrHxxvybCP7JWPPlJ38uhfajpcyhOA==", "cpu": [ "arm64" ], @@ -89,9 +89,9 @@ } }, "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20260708.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260708.1.tgz", - "integrity": "sha512-3daE60YdD7YX0Jtuzc9DE/r/qMkmx8ZvHTkF8Mzmp3F5tbzlV0DAzmu5PFUPF2WuvtKbAhZKbvC2cHmWpQYxnA==", + "version": "1.20260721.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260721.1.tgz", + "integrity": "sha512-hon0lW4ZQ4boAVgaw+0ZFTNS8v5MWPWvK0HZnt4tDpKYnDUviLZawtUW3KqvFmCQTipVHl1S34j3J8Eqb93hGQ==", "cpu": [ "x64" ], @@ -106,9 +106,9 @@ } }, "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20260708.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260708.1.tgz", - "integrity": "sha512-VLdNYOx5Hj+9C6isy0ACWZsbMtSxex2DIJWEe7cZxUdlphZ58ZT8zxNXK8yunFiowd34hn3VwGMopdvdj8lvmA==", + "version": "1.20260721.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260721.1.tgz", + "integrity": "sha512-nAl+HRQqpX5b7xVwWcvLPZmCk8NQ2yjI0yvJTWcHiRswbMEg1ZZckVmjJUAn0PHzZARbCSyIV7v3UjM+SPRmIQ==", "cpu": [ "arm64" ], @@ -123,9 +123,9 @@ } }, "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20260708.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260708.1.tgz", - "integrity": "sha512-bC/aSAwLy16Vjo24i9XU3aWH+eRgz7NeR5xPKavGbembO18ZywYTQbXh14eXtY6fAqN3RzRG8psijTdhX4xydA==", + "version": "1.20260721.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260721.1.tgz", + "integrity": "sha512-9paFG5cMTKz/CRixnEEnZbe5uvFPBFSDthxJHANfCWhUtBj49GSL1FPIokIg+Q+H8DGJEExU0lL92LtxD0lTxQ==", "cpu": [ "x64" ], @@ -627,9 +627,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", + "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", "cpu": [ "arm64" ], @@ -640,19 +640,19 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" + "@img/sharp-libvips-darwin-arm64": "1.3.2" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", + "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", "cpu": [ "x64" ], @@ -663,19 +663,39 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "@img/sharp-libvips-darwin-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", + "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", + "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", "cpu": [ "arm64" ], @@ -690,9 +710,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", + "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", "cpu": [ "x64" ], @@ -707,9 +727,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", + "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", "cpu": [ "arm" ], @@ -727,9 +747,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", + "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", "cpu": [ "arm64" ], @@ -747,9 +767,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", + "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", "cpu": [ "ppc64" ], @@ -767,9 +787,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", + "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", "cpu": [ "riscv64" ], @@ -787,9 +807,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", + "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", "cpu": [ "s390x" ], @@ -807,9 +827,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", + "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", "cpu": [ "x64" ], @@ -827,9 +847,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", + "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", "cpu": [ "arm64" ], @@ -847,9 +867,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", + "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", "cpu": [ "x64" ], @@ -867,9 +887,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", + "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", "cpu": [ "arm" ], @@ -883,19 +903,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "@img/sharp-libvips-linux-arm": "1.3.2" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", + "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", "cpu": [ "arm64" ], @@ -909,19 +929,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" + "@img/sharp-libvips-linux-arm64": "1.3.2" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", + "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", "cpu": [ "ppc64" ], @@ -935,19 +955,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" + "@img/sharp-libvips-linux-ppc64": "1.3.2" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", + "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", "cpu": [ "riscv64" ], @@ -961,19 +981,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" + "@img/sharp-libvips-linux-riscv64": "1.3.2" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", + "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", "cpu": [ "s390x" ], @@ -987,19 +1007,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "@img/sharp-libvips-linux-s390x": "1.3.2" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", + "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", "cpu": [ "x64" ], @@ -1013,19 +1033,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "@img/sharp-libvips-linux-x64": "1.3.2" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", + "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", "cpu": [ "arm64" ], @@ -1039,19 +1059,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", + "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", "cpu": [ "x64" ], @@ -1065,39 +1085,56 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + "@img/sharp-libvips-linuxmusl-x64": "1.3.2" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", + "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", + "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", "cpu": [ "wasm32" ], "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.7.0" + "@img/sharp-wasm32": "0.35.3" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", + "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", "cpu": [ "arm64" ], @@ -1108,16 +1145,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", + "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", "cpu": [ "ia32" ], @@ -1128,16 +1165,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", + "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", "cpu": [ "x64" ], @@ -1148,7 +1185,7 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -1798,16 +1835,16 @@ } }, "node_modules/miniflare": { - "version": "4.20260708.1", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260708.1.tgz", - "integrity": "sha512-c94O9zRDISdqO18EHt6l0iF/fWgWt8p18PJvRsA/L/NJZ9Cfke3s/F5Blg1XXF7WDutVRzWVWy8Vy4LaT5ifsA==", + "version": "4.20260721.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260721.0.tgz", + "integrity": "sha512-fBLaCxZ2i/nPH8iyLzvza0C8/sSF4sjD1ma1Skf+pkZVK0TlaW5ujHJlUHwcwR66v2JZt+Q28d4DCX/oaLG0cA==", "dev": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.34.5", "undici": "7.28.0", - "workerd": "1.20260708.1", + "workerd": "1.20260721.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, @@ -2218,48 +2255,53 @@ } }, "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", + "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", "dev": true, - "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@img/colour": "^1.0.0", + "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", - "semver": "^7.7.3" + "semver": "^7.8.5" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" + "@img/sharp-darwin-arm64": "0.35.3", + "@img/sharp-darwin-x64": "0.35.3", + "@img/sharp-freebsd-wasm32": "0.35.3", + "@img/sharp-libvips-darwin-arm64": "1.3.2", + "@img/sharp-libvips-darwin-x64": "1.3.2", + "@img/sharp-libvips-linux-arm": "1.3.2", + "@img/sharp-libvips-linux-arm64": "1.3.2", + "@img/sharp-libvips-linux-ppc64": "1.3.2", + "@img/sharp-libvips-linux-riscv64": "1.3.2", + "@img/sharp-libvips-linux-s390x": "1.3.2", + "@img/sharp-libvips-linux-x64": "1.3.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", + "@img/sharp-libvips-linuxmusl-x64": "1.3.2", + "@img/sharp-linux-arm": "0.35.3", + "@img/sharp-linux-arm64": "0.35.3", + "@img/sharp-linux-ppc64": "0.35.3", + "@img/sharp-linux-riscv64": "0.35.3", + "@img/sharp-linux-s390x": "0.35.3", + "@img/sharp-linux-x64": "0.35.3", + "@img/sharp-linuxmusl-arm64": "0.35.3", + "@img/sharp-linuxmusl-x64": "0.35.3", + "@img/sharp-webcontainers-wasm32": "0.35.3", + "@img/sharp-win32-arm64": "0.35.3", + "@img/sharp-win32-ia32": "0.35.3", + "@img/sharp-win32-x64": "0.35.3" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/source-map-js": { @@ -2486,9 +2528,9 @@ "license": "MIT" }, "node_modules/workerd": { - "version": "1.20260708.1", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260708.1.tgz", - "integrity": "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w==", + "version": "1.20260721.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260721.1.tgz", + "integrity": "sha512-b/DWhpV0jTudzQpLhDovcOgBz233386q+3Hbari7CLCNT9UXxjQziSTZ9yCoKdT2K3TSx5jrwlOisq8hlLWXYg==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -2499,17 +2541,17 @@ "node": ">=16" }, "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20260708.1", - "@cloudflare/workerd-darwin-arm64": "1.20260708.1", - "@cloudflare/workerd-linux-64": "1.20260708.1", - "@cloudflare/workerd-linux-arm64": "1.20260708.1", - "@cloudflare/workerd-windows-64": "1.20260708.1" + "@cloudflare/workerd-darwin-64": "1.20260721.1", + "@cloudflare/workerd-darwin-arm64": "1.20260721.1", + "@cloudflare/workerd-linux-64": "1.20260721.1", + "@cloudflare/workerd-linux-arm64": "1.20260721.1", + "@cloudflare/workerd-windows-64": "1.20260721.1" } }, "node_modules/wrangler": { - "version": "4.110.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.110.0.tgz", - "integrity": "sha512-xZeXKYi7hxQRF5anL+v77RkufJNpF9f3Eqeyqq2QBsETpLZgh0Agj0jJ6JPtkbgn6ukZdh8OK5egsGPWIditgg==", + "version": "4.113.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.113.0.tgz", + "integrity": "sha512-ROGzSloJv0y21It6Oc9LaruNcu1tdiQ/XzL3Jc3YkFjzXEMXzTqVhA8vQaGMTdZHTjFP0PVcwAHNgaw3gXu4wA==", "dev": true, "license": "MIT OR Apache-2.0", "dependencies": { @@ -2517,10 +2559,10 @@ "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", - "miniflare": "4.20260708.1", + "miniflare": "4.20260721.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", - "workerd": "1.20260708.1" + "workerd": "1.20260721.1" }, "bin": { "cf-wrangler": "bin/cf-wrangler.js", @@ -2534,7 +2576,7 @@ "fsevents": "2.3.3" }, "peerDependencies": { - "@cloudflare/workers-types": "^5.20260708.1" + "@cloudflare/workers-types": "^5.20260721.1" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { diff --git a/package.json b/package.json index 4379ec5..a55ef6a 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,10 @@ "daisyui": "^4.12.24", "htmx.org": "^2.0.10", "tailwindcss": "^3.4.19", - "wrangler": "4.110.0" + "wrangler": "4.113.0" + }, + "overrides": { + "sharp": "0.35.3" }, "scripts": { "build:css": "tailwindcss -i ./internal/httpapi/assets/app.css -o ./internal/httpapi/static/macftpd.css --minify", diff --git a/scripts/check.sh b/scripts/check.sh index 11542b9..12bebc9 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -31,6 +31,7 @@ go build -o "${build_dir}/macftpd" ./cmd/macftpd GOOS=darwin GOARCH=arm64 go build -o "${build_dir}/macftpd-darwin-arm64" ./cmd/macftpd npm ci +npm audit --audit-level=high cp internal/httpapi/static/macftpd.css internal/httpapi/static/htmx.min.js "${asset_snapshot}/" npm run build diff -u "${asset_snapshot}/macftpd.css" internal/httpapi/static/macftpd.css