diff --git a/go.mod b/go.mod index b0ad3f92..d994dfd2 100644 --- a/go.mod +++ b/go.mod @@ -40,8 +40,8 @@ require ( github.com/rogpeppe/go-internal v1.15.0 // indirect github.com/sirupsen/logrus v1.10.2 // indirect github.com/xhit/go-str2duration/v2 v2.2.0 // indirect - github.com/xo/terminfo v1.2.0 // indirect - go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect + github.com/xo/terminfo v1.3.1 // indirect + go.yaml.in/yaml/v4 v4.0.0 // indirect golang.org/x/sync v0.24.0 // indirect golang.org/x/sys v0.49.0 // indirect golang.org/x/text v0.43.0 // indirect diff --git a/go.sum b/go.sum index 09a30623..d896482b 100644 --- a/go.sum +++ b/go.sum @@ -72,13 +72,13 @@ github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWD github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/xhit/go-str2duration/v2 v2.2.0 h1:SofrD1D+8IuhhkP+OiG1ij4Zq+FiiYSEY/sORBanDG0= github.com/xhit/go-str2duration/v2 v2.2.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= -github.com/xo/terminfo v1.2.0 h1:d0ZTOCpuGE0lwSAOs0zcJjwz3jWQyqcRt9XbGJpCOl4= -github.com/xo/terminfo v1.2.0/go.mod h1:lGzkSo8Fe7IRh/w+Gqz7n5mDog4FVXCj3gy/DYTBqio= +github.com/xo/terminfo v1.3.1 h1:4ZwPCuu4PEFw0PkNp3Hv/x3gqvgLCzYhurszsj101cM= +github.com/xo/terminfo v1.3.1/go.mod h1:lGzkSo8Fe7IRh/w+Gqz7n5mDog4FVXCj3gy/DYTBqio= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= -go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4= -go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +go.yaml.in/yaml/v4 v4.0.0 h1:/LG1pNO84Z4l603j/xMn69CcS5OYhMQ/f7l2LoQUGuw= +go.yaml.in/yaml/v4 v4.0.0/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54= golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w= golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= diff --git a/internal/engine/proxystatus.go b/internal/engine/proxystatus.go index 0a7ba88d..104945ce 100644 --- a/internal/engine/proxystatus.go +++ b/internal/engine/proxystatus.go @@ -4,8 +4,10 @@ import ( "context" "fmt" "os" + "sort" "strconv" "strings" + "time" "github.com/labstack/onebox/internal/app" "github.com/labstack/onebox/internal/proxy" @@ -19,14 +21,15 @@ const renewalFloorDays = 21 // proxyRaw is the managed proxy's status reads, gathered concurrently with the // app-side reads. type proxyRaw struct { - ids []string - health string // proxy container health, parsed from docker ps .Status - discovery bool // isolated Docker discovery controller is running - applied string // config and Compose identity the host applied - owner string // sole application identity from the host owner record - ownerEnv string // environment identity from the owner record - acme []string // raw ACME stores; parsed at render, and keys never leave - localHash string // locally staged config and Compose identity (computed offline) + ids []string + health string // proxy container health, parsed from docker ps .Status + discovery bool // isolated Docker discovery controller is running + applied string // config and Compose identity the host applied + owner string // sole application identity from the host owner record + ownerEnv string // environment identity from the owner record + acme []acmeStore // raw ACME stores; parsed at render, and keys never leave + certStores map[string]string // resolver → ACME store file, from the applied static config; nil when unknown + localHash string // locally staged config and Compose identity (computed offline) // Why a read could not be trusted, when it could not. Recorded rather // than raised: gather returns on the first error and Status renders // nothing after it, so raising costs the operator every other fact about @@ -34,6 +37,9 @@ type proxyRaw struct { ownerIssue string appliedIssue string acmeIssue string + // certConfigIssue: the applied static configuration could not be read or + // parsed, so certificates are judged by expiry alone. + certConfigIssue string } func (px proxyRaw) ownershipIssue(application, environment string) string { @@ -154,7 +160,13 @@ func (e *Engine) proxyReads(ctx context.Context, px *proxyRaw) []func() error { return nil }, func() error { - for _, name := range []string{"acme.json", "acme-wildcard.json"} { + // The static configuration the host applied says which resolver + // writes which store, and so which stores to read. It is read + // first, in the same step as the stores it names. + if err := e.readCertificateConfig(ctx, hp, px); err != nil { + return err + } + for _, name := range certificateStoreFiles(px.certStores) { path := hp.Acme + "/" + name res, err := e.T.Run(ctx, readableFileProbe(path)) if err != nil { @@ -167,7 +179,7 @@ func (e *Engine) proxyReads(ctx context.Context, px *proxyRaw) []func() error { if res.ExitCode != 0 { return statusReadResult("proxy certificate store", res, nil) } - px.acme = append(px.acme, res.Stdout) + px.acme = append(px.acme, acmeStore{file: name, path: path, body: res.Stdout}) } return nil }, @@ -184,6 +196,80 @@ func (e *Engine) proxyReads(ctx context.Context, px *proxyRaw) []func() error { } } +// readCertificateConfig reads the static configuration the host applied and +// records which store each of its resolvers writes. The host's file is the +// one that matters: the running proxy renews by it, and a local change that +// was never applied renews nothing yet. A failed read or a configuration that +// does not parse is recorded, not raised, and leaves certStores nil so the +// certificates are still judged by expiry. Only a transport error is raised. No file at all is a host that +// never applied a proxy, which has no certificates to judge. +func (e *Engine) readCertificateConfig(ctx context.Context, hp proxy.Paths, px *proxyRaw) error { + for _, name := range proxy.StaticConfigNames { + path := hp.ConfigDir + "/" + name + res, err := e.T.Run(ctx, readableFileProbe(path)) + if err != nil { + return err + } + if issue, refused := statusFileIssue("the applied static configuration", path, res); refused { + px.certConfigIssue = issue + "; certificates are judged by expiry alone" + return nil + } + // Any other failure, such as cat losing a race with ob proxy apply + // swapping the directory, is recorded too: the stores this step reads + // next matter more than the configuration that refines them. + if res.ExitCode != 0 { + px.certConfigIssue = fmt.Sprintf("the applied static configuration at %s could not be read (exit %d); certificates are judged by expiry alone", path, res.ExitCode) + return nil + } + if strings.TrimSpace(res.Stdout) == "" { + continue + } + // The parse error is not quoted: it can repeat values from the file, + // and the message reaches the terminal and the JSON snapshot. + stores, err := proxy.CertificateStores([]byte(res.Stdout)) + if err != nil { + px.certConfigIssue = fmt.Sprintf("the applied static configuration at %s is not valid YAML; certificates are judged by expiry alone", path) + return nil + } + px.certStores = stores + return nil + } + return nil +} + +// resolverStore looks a resolver up without regard to case, because Traefik +// matches its configuration that way and a store key may differ in case from +// the name the configuration spells. A missed match would call a renewing +// certificate a leftover, so the lenient match is the safe one. +func resolverStore(stores map[string]string, resolver string) (string, bool) { + if file, ok := stores[resolver]; ok { + return file, true + } + for name, file := range stores { + if strings.EqualFold(name, resolver) { + return file, true + } + } + return "", false +} + +// certificateStoreFiles lists the store files to read: the two Onebox has +// always written, so that keys left in them stay visible, plus any other file +// the applied configuration points a resolver at. +func certificateStoreFiles(stores map[string]string) []string { + files := []string{"acme.json", "acme-wildcard.json"} + seen := map[string]bool{"acme.json": true, "acme-wildcard.json": true} + var extra []string + for _, file := range stores { + if file != "" && !seen[file] { + seen[file] = true + extra = append(extra, file) + } + } + sort.Strings(extra) + return append(files, extra...) +} + // readableFileProbe reads a status file that may legitimately be absent: // 0 with the contents on stdout, 2 present but unreadable, 5 absence could not // be established, 0 and empty when there is nothing there. @@ -310,36 +396,113 @@ func (e *Engine) renderProxy(px proxyRaw) (bool, error) { fmt.Fprintf(e.Opts.Out, "proxy %-12s %-10s %s owner: %s\n", proxy.ContainerName, health, state, owner) // cert runway — the renewal loop is the proxy's one silent failure mode + // Printed before the store check so the text view names the same issues + // as the JSON snapshot when both reads fail. + if px.certConfigIssue != "" { + fmt.Fprintf(e.Opts.Out, " cert resolvers unknown ⚠ (%s)\n", px.certConfigIssue) + diverged = true + } if px.acmeIssue != "" { fmt.Fprintf(e.Opts.Out, " cert store unreadable ⚠ (%s)\n", px.acmeIssue) return true, nil } - certs, err := proxyCertExpiries(px.acme) + certs, err := judgeCertificates(px.acme, px.certStores, e.Opts.Now()) if err != nil { fmt.Fprintf(e.Opts.Out, " cert store unreadable ⚠ (%v)\n", err) return true, nil } for _, c := range certs { - days := int(c.NotAfter.Sub(e.Opts.Now()).Hours() / 24) mark := "" - if days < renewalFloorDays { + switch { + case c.Orphaned: + mark = e.ui.Dim(fmt.Sprintf(" not renewed by the applied configuration (resolver %q in %s)", c.Resolver, c.Store)) + case c.Overdue: mark = e.ui.Warn(" RENEWAL OVERDUE ⚠ (lego renews at 30d — check CF_DNS_API_TOKEN / proxy logs)") diverged = true } - e.ui.Println(fmt.Sprintf(" cert %-20s expires %s %s%s", c.Domain, c.NotAfter.UTC().Format("2006-01-02"), e.ui.Dim(fmt.Sprintf("(%dd)", days)), mark)) + e.ui.Println(fmt.Sprintf(" cert %-20s expires %s %s%s", c.Domain, c.NotAfter.UTC().Format("2006-01-02"), e.ui.Dim(fmt.Sprintf("(%dd)", c.Days)), mark)) } return diverged, nil } -func proxyCertExpiries(stores []string) ([]proxy.CertExpiry, error) { - var out []proxy.CertExpiry - for _, store := range stores { - certs, err := proxy.CertExpiries([]byte(store)) +// acmeStore is one ACME store file as read from the host. The body holds +// private keys, so it is parsed in place and never rendered. +type acmeStore struct { + file string // file name, as a resolver's acme.storage names it + path string // where the file lives on the host + body string +} + +// judgedCertificate is one certificate from an ACME store with the verdict +// that both status views report. +type judgedCertificate struct { + proxy.CertExpiry + Store string // host path of the store the certificate was read from + Days int // whole days until expiry + Overdue bool // a configured resolver failed to renew it in time + Orphaned bool // no configured resolver writes it to this store +} + +// judgeCertificates parses every store and judges each certificate by the +// resolver that issued it. A certificate is renewed only when the static +// configuration defines its resolver with this very store as its storage, so +// only such a certificate can be overdue. A key that fails that test was left +// behind by an older proxy configuration (a renamed resolver, wildcard routes +// that were removed, a storage path that moved). Nothing renews it, and +// judging its expiry would report a healthy host as diverged forever, so it is +// reported as orphaned instead. Status does not advise deleting it: Traefik +// reads its static configuration only at startup, so a proxy that was not +// restarted after an apply may still renew under a key the file no longer +// names. +// +// stores maps each resolver of the applied configuration to its store file. +// A nil map means that configuration is unknown. Then no certificate is +// called orphaned, since that would invite deleting one the proxy may still +// renew, and every certificate is judged by its expiry, since hiding a real +// overdue renewal is the worse mistake. +// +// Certificates are sorted, because the store's resolver keys parse from a map +// and both views must print the same order on every run. +func judgeCertificates(raw []acmeStore, stores map[string]string, now time.Time) ([]judgedCertificate, error) { + var out []judgedCertificate + for _, store := range raw { + certs, err := proxy.CertExpiries([]byte(store.body)) if err != nil { return nil, err } - out = append(out, certs...) + for _, cert := range certs { + days := int(cert.NotAfter.Sub(now).Hours() / 24) + // Without the configuration every certificate might be renewed, + // so each is judged by its expiry, as before resolvers were known. + // A resolver whose store CertificateStores could not place ("") + // might write here, so it is given the benefit of the doubt. + renewed := true + if stores != nil { + file, configured := resolverStore(stores, cert.Resolver) + renewed = configured && (file == "" || file == store.file) + } + out = append(out, judgedCertificate{ + CertExpiry: cert, + Store: store.path, + Days: days, + Overdue: renewed && days < renewalFloorDays, + Orphaned: !renewed, + }) + } } + sort.Slice(out, func(i, j int) bool { + a, b := out[i], out[j] + if a.Domain != b.Domain { + return a.Domain < b.Domain + } + if a.Resolver != b.Resolver { + return a.Resolver < b.Resolver + } + if a.Store != b.Store { + return a.Store < b.Store + } + return a.NotAfter.Before(b.NotAfter) + }) return out, nil } diff --git a/internal/engine/proxystatus_test.go b/internal/engine/proxystatus_test.go index 69d3d1c2..5b1dd8b4 100644 --- a/internal/engine/proxystatus_test.go +++ b/internal/engine/proxystatus_test.go @@ -25,6 +25,31 @@ import ( ) func acmeFixture(t *testing.T, domain string, notAfter time.Time) string { + t.Helper() + return acmeStoreFixture(map[string][]string{ + app.ManagedCertificateResolver: {acmeCertificate(t, domain, notAfter)}, + }) +} + +// acmeStoreFixture builds a store from resolver keys and their certificate +// entries, as written by acmeCertificate. +func acmeStoreFixture(resolvers map[string][]string) string { + var body strings.Builder + body.WriteString("{") + first := true + for name, certs := range resolvers { + if !first { + body.WriteString(",") + } + first = false + fmt.Fprintf(&body, `%q:{"Certificates":[%s]}`, name, strings.Join(certs, ",")) + } + body.WriteString("}") + return body.String() +} + +// acmeCertificate is one entry of a resolver's Certificates list. +func acmeCertificate(t *testing.T, domain string, notAfter time.Time) string { t.Helper() key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { @@ -41,7 +66,7 @@ func acmeFixture(t *testing.T, domain string, notAfter time.Time) string { t.Fatal(err) } pemB := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}) - return fmt.Sprintf(`{"le":{"Certificates":[{"domain":{"main":%q},"certificate":%q}]}}`, + return fmt.Sprintf(`{"domain":{"main":%q},"certificate":%q}`, domain, base64.StdEncoding.EncodeToString(pemB)) } @@ -79,6 +104,8 @@ func statusProxyEngine(t *testing.T, appliedHash *string, acme string, proxyHeal return transport.Result{Stdout: "sample production\n"}, true case strings.Contains(cmd, "cat '/var/lib/onebox/_host/proxy/acme/acme.json'"): return transport.Result{Stdout: acme}, true + case strings.Contains(cmd, "cat '/var/lib/onebox/_host/proxy/config/traefik.yml'"): + return transport.Result{Stdout: testManagedProxyStatic}, true case strings.Contains(cmd, "--format") && strings.Contains(cmd, "onebox.app='sample'"): return transport.Result{Stdout: "S1|web|R7|Up (healthy)\n" + "W1|worker|R7|Up (healthy)\nPG1|postgres|R7|Up (healthy)\n"}, true @@ -210,6 +237,252 @@ func TestStatusManagedProxyCertRenewalOverdue(t *testing.T) { } } +// staleResolverStore is the store from issue #235: a resolver key left behind +// by an older proxy configuration still holds a certificate for a domain that +// the configured resolver also covers. Nothing renews the leftover, so it sits +// inside the renewal floor while the served certificate has plenty of runway. +func staleResolverStore(t *testing.T) string { + t.Helper() + return acmeStoreFixture(map[string][]string{ + "le": {acmeCertificate(t, "app.example.com", time.Date(2026, 7, 11, 12, 0, 0, 0, time.UTC))}, // 7d out + app.ManagedCertificateResolver: {acmeCertificate(t, "app.example.com", time.Date(2026, 9, 20, 12, 0, 0, 0, time.UTC))}, // 78d out + }) +} + +func TestStatusIgnoresCertificateFromUnconfiguredResolver(t *testing.T) { + applied := "" + e, _, out, _ := statusProxyEngine(t, &applied, staleResolverStore(t), "healthy") + if err := e.Status(context.Background()); err != nil { + t.Fatalf("an orphaned resolver's certificate must not make status diverge: %v\n%s", err, out.String()) + } + if strings.Contains(out.String(), "RENEWAL OVERDUE") { + t.Fatalf("orphaned certificate must not be flagged overdue:\n%s", out.String()) + } + if !strings.Contains(out.String(), `not renewed by the applied configuration (resolver "le" in /var/lib/onebox/_host/proxy/acme/acme.json)`) { + t.Fatalf("orphaned certificate must name its resolver and store:\n%s", out.String()) + } +} + +// A newer certificate under a leftover resolver is not served, so it must not +// hide a managed resolver whose renewal is failing. +func TestStatusFlagsManagedCertificateEvenWhenOrphanedOneIsNewer(t *testing.T) { + applied := "" + acme := acmeStoreFixture(map[string][]string{ + app.ManagedCertificateResolver: {acmeCertificate(t, "app.example.com", time.Date(2026, 7, 14, 12, 0, 0, 0, time.UTC))}, // 10d out + "le": {acmeCertificate(t, "app.example.com", time.Date(2026, 9, 2, 12, 0, 0, 0, time.UTC))}, // 60d out + }) + e, _, out, _ := statusProxyEngine(t, &applied, acme, "healthy") + if err := e.Status(context.Background()); err == nil { + t.Fatalf("the managed certificate is overdue, so status must diverge:\n%s", out.String()) + } + if n := strings.Count(out.String(), "RENEWAL OVERDUE"); n != 1 { + t.Fatalf("exactly the managed certificate must be flagged; got %d:\n%s", n, out.String()) + } +} + +// A host that once served wildcard routes keeps their certificate after the +// routes and the DNS challenge are removed. The static configuration no longer +// defines the wildcard resolver, so nothing renews that certificate. +func TestStatusIgnoresWildcardCertificateAfterResolverIsRemoved(t *testing.T) { + applied := "" + wildcard := acmeStoreFixture(map[string][]string{ + app.ManagedWildcardCertificateResolver: {acmeCertificate(t, "*.preview.example.com", time.Date(2026, 7, 11, 12, 0, 0, 0, time.UTC))}, + }) + e, f, out, _ := statusProxyEngine(t, &applied, acmeFixture(t, "app.example.com", time.Date(2026, 9, 15, 12, 0, 0, 0, time.UTC)), "healthy") + base := f.Dynamic + f.Dynamic = func(cmd string) (transport.Result, bool) { + if strings.Contains(cmd, "acme-wildcard.json") { + return transport.Result{Stdout: wildcard}, true + } + return base(cmd) + } + if err := e.Status(context.Background()); err != nil { + t.Fatalf("an unconfigured wildcard resolver's certificate must not make status diverge: %v\n%s", err, out.String()) + } + if strings.Contains(out.String(), "RENEWAL OVERDUE") { + t.Fatalf("orphaned wildcard certificate must not be flagged overdue:\n%s", out.String()) + } +} + +// A configured resolver renews only the store its acme.storage names. The +// same key in the other store file is a leftover. +func TestStatusIgnoresConfiguredResolverKeyInOtherStore(t *testing.T) { + applied := "" + misplaced := acmeFixture(t, "app.example.com", time.Date(2026, 7, 11, 12, 0, 0, 0, time.UTC)) + e, f, out, _ := statusProxyEngine(t, &applied, acmeFixture(t, "app.example.com", time.Date(2026, 9, 15, 12, 0, 0, 0, time.UTC)), "healthy") + base := f.Dynamic + f.Dynamic = func(cmd string) (transport.Result, bool) { + if strings.Contains(cmd, "acme-wildcard.json") { + return transport.Result{Stdout: misplaced}, true + } + return base(cmd) + } + if err := e.Status(context.Background()); err != nil { + t.Fatalf("a resolver key in a store it does not write must not make status diverge: %v\n%s", err, out.String()) + } + if !strings.Contains(out.String(), "/var/lib/onebox/_host/proxy/acme/acme-wildcard.json") { + t.Fatalf("orphaned certificate must name the store it sits in:\n%s", out.String()) + } +} + +// withHostCommand answers one more host command ahead of the fixture's own. +func withHostCommand(f *transport.Fake, match string, result transport.Result) { + base := f.Dynamic + f.Dynamic = func(cmd string) (transport.Result, bool) { + if strings.Contains(cmd, match) { + return result, true + } + return base(cmd) + } +} + +// The host still runs a configuration whose wildcard resolver the local +// project has since removed. That resolver still renews on the host, so its +// failing renewal must be reported, not called a leftover to delete. +func TestStatusJudgesByAppliedConfigNotLocalConfig(t *testing.T) { + applied := "" + e, f, out, _ := statusProxyEngine(t, &applied, acmeFixture(t, "app.example.com", time.Date(2026, 9, 15, 12, 0, 0, 0, time.UTC)), "healthy") + withHostCommand(f, "cat '/var/lib/onebox/_host/proxy/config/traefik.yml'", transport.Result{ + Stdout: testManagedProxyStatic + " " + app.ManagedWildcardCertificateResolver + ":\n acme:\n storage: /letsencrypt/acme-wildcard.json\n dnsChallenge:\n provider: cloudflare\n", + }) + withHostCommand(f, "acme-wildcard.json", transport.Result{Stdout: acmeStoreFixture(map[string][]string{ + app.ManagedWildcardCertificateResolver: {acmeCertificate(t, "*.preview.example.com", time.Date(2026, 7, 11, 12, 0, 0, 0, time.UTC))}, + })}) + if err := e.Status(context.Background()); err == nil { + t.Fatalf("the applied wildcard resolver's certificate is overdue, so status must diverge:\n%s", out.String()) + } + if !strings.Contains(out.String(), "RENEWAL OVERDUE") || strings.Contains(out.String(), "not renewed") { + t.Fatalf("a resolver the host applied must be judged, never offered for removal:\n%s", out.String()) + } +} + +// A resolver may write a store under any name in the store directory. Status +// reads that file too, so its certificates are judged. +func TestStatusReadsStoreNamedByAppliedConfig(t *testing.T) { + applied := "" + e, f, out, _ := statusProxyEngine(t, &applied, "", "healthy") + withHostCommand(f, "cat '/var/lib/onebox/_host/proxy/config/traefik.yml'", transport.Result{ + Stdout: "certificatesResolvers:\n letsencrypt:\n acme:\n storage: /letsencrypt/certs.json\n", + }) + withHostCommand(f, "cat '/var/lib/onebox/_host/proxy/acme/certs.json'", transport.Result{ + Stdout: acmeFixture(t, "app.example.com", time.Date(2026, 7, 11, 12, 0, 0, 0, time.UTC)), + }) + if err := e.Status(context.Background()); err == nil { + t.Fatalf("the certificate in the configured store is overdue, so status must diverge:\n%s", out.String()) + } + if !strings.Contains(out.String(), "RENEWAL OVERDUE") { + t.Fatalf("the configured store must be read and judged:\n%s", out.String()) + } +} + +// An applied configuration that cannot be parsed must not hide an overdue +// renewal, and must not mark anything for removal. +func TestStatusJudgesByExpiryWhenAppliedConfigIsUnreadable(t *testing.T) { + applied := "" + e, f, _, _ := statusProxyEngine(t, &applied, staleResolverStore(t), "healthy") + withHostCommand(f, "cat '/var/lib/onebox/_host/proxy/config/traefik.yml'", transport.Result{Stdout: "certificatesResolvers: ["}) + + snapshot, err := e.StatusSnapshot(context.Background()) + if err != nil { + t.Fatalf("snapshot: %v", err) + } + px := snapshot.Proxy + if px == nil || px.Complete || !px.Diverged { + t.Fatalf("an unparsable applied configuration must leave the proxy incomplete and diverged: %#v", px) + } + overdue := false + for _, c := range px.Certificates { + if c.Orphaned { + t.Fatalf("no certificate may be called orphaned without the configuration: %#v", px.Certificates) + } + overdue = overdue || c.RenewalOverdue + } + if !overdue { + t.Fatalf("the 7-day certificate must still be judged overdue by expiry: %#v", px.Certificates) + } + if px.LocalConfigHash == "" { + t.Fatalf("the local configuration read is independent and must survive: %#v", px) + } +} + +// A resolver whose store cannot be placed might write anywhere, so its +// certificates are judged by expiry and never offered for removal. +func TestJudgeCertificatesTrustsResolverWithUnplacedStore(t *testing.T) { + now := time.Date(2026, 7, 4, 12, 0, 0, 0, time.UTC) + raw := []acmeStore{{file: "acme.json", path: "/h/acme.json", body: acmeFixture(t, "app.example.com", time.Date(2026, 7, 11, 12, 0, 0, 0, time.UTC))}} + certs, err := judgeCertificates(raw, map[string]string{app.ManagedCertificateResolver: ""}, now) + if err != nil { + t.Fatal(err) + } + if len(certs) != 1 || certs[0].Orphaned || !certs[0].Overdue { + t.Fatalf("certificate of a resolver with an unplaced store must be judged by expiry: %#v", certs) + } +} + +// A failed read of the applied configuration must not cost the certificate +// expiries that the same step reads next. +func TestStatusKeepsCertificatesWhenAppliedConfigReadFails(t *testing.T) { + applied := "" + e, f, _, _ := statusProxyEngine(t, &applied, acmeFixture(t, "app.example.com", time.Date(2026, 7, 11, 12, 0, 0, 0, time.UTC)), "healthy") + withHostCommand(f, "cat '/var/lib/onebox/_host/proxy/config/traefik.yml'", transport.Result{ExitCode: 1}) + + snapshot, err := e.StatusSnapshot(context.Background()) + if err != nil { + t.Fatalf("snapshot: %v", err) + } + px := snapshot.Proxy + if px == nil || len(px.Certificates) != 1 || !px.Certificates[0].RenewalOverdue { + t.Fatalf("certificates must still be read and judged by expiry: %#v", px) + } +} + +func TestJudgeCertificatesMatchesResolverWithoutCase(t *testing.T) { + now := time.Date(2026, 7, 4, 12, 0, 0, 0, time.UTC) + raw := []acmeStore{{file: "acme.json", path: "/h/acme.json", body: acmeFixture(t, "app.example.com", time.Date(2026, 7, 11, 12, 0, 0, 0, time.UTC))}} + certs, err := judgeCertificates(raw, map[string]string{"LetsEncrypt": "acme.json"}, now) + if err != nil { + t.Fatal(err) + } + if len(certs) != 1 || certs[0].Orphaned || !certs[0].Overdue { + t.Fatalf("a resolver spelled in another case is still the one that renews: %#v", certs) + } +} + +func TestJudgeCertificatesUsesConfiguredStores(t *testing.T) { + now := time.Date(2026, 7, 4, 12, 0, 0, 0, time.UTC) + soon := time.Date(2026, 7, 11, 12, 0, 0, 0, time.UTC) + raw := []acmeStore{ + {file: "acme.json", path: "/h/acme.json", body: acmeFixture(t, "app.example.com", soon)}, + {file: "acme-wildcard.json", path: "/h/acme-wildcard.json", body: acmeStoreFixture(map[string][]string{ + app.ManagedWildcardCertificateResolver: {acmeCertificate(t, "*.preview.example.com", soon)}, + })}, + } + withDNS := map[string]string{ + app.ManagedCertificateResolver: "acme.json", + app.ManagedWildcardCertificateResolver: "acme-wildcard.json", + } + certs, err := judgeCertificates(raw, withDNS, now) + if err != nil { + t.Fatal(err) + } + for _, c := range certs { + if !c.Overdue || c.Orphaned { + t.Fatalf("both resolvers are configured for their stores, so both certificates are overdue: %#v", certs) + } + } + + // The configuration is unknown: judge by expiry, orphan nothing. + certs, err = judgeCertificates(raw, nil, now) + if err != nil { + t.Fatal(err) + } + for _, c := range certs { + if !c.Overdue || c.Orphaned { + t.Fatalf("an unknown configuration must fall back to expiry alone: %#v", certs) + } + } +} + // An up-but-unhealthy proxy (health parsed from ps .Status) must force // divergence — all other proxy tests only ever pass "healthy". func TestStatusManagedProxyUnhealthy(t *testing.T) { diff --git a/internal/engine/status_snapshot.go b/internal/engine/status_snapshot.go index c213239e..f7f823c6 100644 --- a/internal/engine/status_snapshot.go +++ b/internal/engine/status_snapshot.go @@ -102,11 +102,18 @@ type StatusProxy struct { } // StatusCertificate is the public, private-key-free portion of an ACME entry. +// Resolver is the store key that holds the certificate and Store is the host +// path of the store file. Orphaned marks a certificate that no resolver in the +// host's applied static configuration writes to that store: nothing renews it, so it is never +// overdue and never makes the proxy diverge. type StatusCertificate struct { Domain string `json:"domain"` + Resolver string `json:"resolver"` + Store string `json:"store"` NotAfter time.Time `json:"not_after"` DaysRemaining int `json:"days_remaining"` RenewalOverdue bool `json:"renewal_overdue"` + Orphaned bool `json:"orphaned"` } // StatusWarning describes an observation that could not be completed. @@ -475,6 +482,10 @@ func makeStatusProxy(raw proxyRaw, readComplete []bool, now time.Time, applicati status.Complete = false status.Issues = append(status.Issues, raw.acmeIssue) } + if complete(statusProxyCertificatesRead) && raw.certConfigIssue != "" { + status.Complete = false + status.Issues = append(status.Issues, raw.certConfigIssue) + } if complete(statusProxyOwnerRead) { status.Owner = raw.owner // The read returned, but it returned a record we would not act on. @@ -497,31 +508,26 @@ func makeStatusProxy(raw proxyRaw, readComplete []bool, now time.Time, applicati // input, the ungated form would answer "nothing is near expiry" about a // store it never opened. if complete(statusProxyCertificatesRead) && raw.acmeIssue == "" { - certs, err := proxyCertExpiries(raw.acme) + certs, err := judgeCertificates(raw.acme, raw.certStores, now) if err != nil { status.Complete = false status.Issues = append(status.Issues, "certificate store is unreadable") parseWarning = &StatusWarning{Component: "proxy.certificates", Message: err.Error()} } else { for _, cert := range certs { - days := int(cert.NotAfter.Sub(now).Hours() / 24) - overdue := days < renewalFloorDays status.Certificates = append(status.Certificates, StatusCertificate{ Domain: cert.Domain, + Resolver: cert.Resolver, + Store: cert.Store, NotAfter: cert.NotAfter.UTC(), - DaysRemaining: days, - RenewalOverdue: overdue, + DaysRemaining: cert.Days, + RenewalOverdue: cert.Overdue, + Orphaned: cert.Orphaned, }) - if overdue { - status.Issues = append(status.Issues, fmt.Sprintf("certificate renewal is overdue for %s", cert.Domain)) + if cert.Overdue { + status.Issues = append(status.Issues, fmt.Sprintf("certificate renewal is overdue for %s (resolver %s, %s)", cert.Domain, cert.Resolver, cert.Store)) } } - sort.Slice(status.Certificates, func(i, j int) bool { - if status.Certificates[i].Domain == status.Certificates[j].Domain { - return status.Certificates[i].NotAfter.Before(status.Certificates[j].NotAfter) - } - return status.Certificates[i].Domain < status.Certificates[j].Domain - }) } } diff --git a/internal/engine/status_snapshot_test.go b/internal/engine/status_snapshot_test.go index 248b3bea..3b2a90c6 100644 --- a/internal/engine/status_snapshot_test.go +++ b/internal/engine/status_snapshot_test.go @@ -633,6 +633,31 @@ func TestStatusSnapshotReportsRefusedProxyReadsWithoutLosingTheRest(t *testing.T } } +func TestStatusSnapshotIgnoresCertificateFromUnconfiguredResolver(t *testing.T) { + applied := "" + e, _, _, _ := statusProxyEngine(t, &applied, staleResolverStore(t), "healthy") + + snapshot, err := e.StatusSnapshot(context.Background()) + if err != nil { + t.Fatalf("snapshot: %v", err) + } + if snapshot.Diverged || snapshot.Proxy == nil || snapshot.Proxy.Diverged || len(snapshot.Proxy.Issues) != 0 { + t.Fatalf("an orphaned resolver's certificate must not make the snapshot diverge: %#v", snapshot.Proxy) + } + certs := snapshot.Proxy.Certificates + if len(certs) != 2 { + t.Fatalf("both certificates must stay listed: %#v", certs) + } + // Sorted by domain, then resolver: "le" before "letsencrypt". + orphan, served := certs[0], certs[1] + if orphan.Resolver != "le" || orphan.Store != "/var/lib/onebox/_host/proxy/acme/acme.json" || !orphan.Orphaned || orphan.RenewalOverdue || orphan.DaysRemaining != 7 { + t.Fatalf("leftover certificate must be orphaned, not overdue: %#v", orphan) + } + if served.Resolver != app.ManagedCertificateResolver || served.Orphaned || served.RenewalOverdue || served.DaysRemaining != 78 { + t.Fatalf("managed certificate must be judged normally: %#v", served) + } +} + func TestStatusSnapshotManagedProxy(t *testing.T) { applied := "" acme := acmeFixture(t, "app.example.com", time.Date(2026, 7, 14, 12, 0, 0, 0, time.UTC)) diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index 002475bd..40cea0a6 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -23,6 +23,7 @@ import ( "fmt" "hash" "os" + "path" "path/filepath" "sort" "strings" @@ -137,7 +138,7 @@ services: %s volumes: - ./config:/etc/traefik:ro - ./dynamic:/etc/traefik/dynamic:ro - - ./acme:/letsencrypt + - ./acme:`+AcmeStorageDir+` tmpfs: ["/tmp:rw,noexec,nosuid,size=16m"] healthcheck: test: ["CMD", "traefik", "healthcheck"] @@ -185,15 +186,18 @@ func sortedEntrypointNames(entrypoints map[string]app.ProxyEntrypoint) []string // CertExpiry is one issued certificate's identity and lifetime — the ONLY // data status extracts from acme.json (which also holds private keys; those -// never leave the parse). +// never leave the parse). Resolver is the store key the certificate sits +// under, which names the Traefik certificate resolver that issued it. type CertExpiry struct { + Resolver string Domain string NotAfter time.Time } // CertExpiries parses a lego/Traefik acme.json store and returns each -// certificate's main domain and expiry. An empty store (fresh acme.json is an -// empty file) returns nothing; malformed JSON errors so status can say so. +// certificate's resolver, main domain and expiry. An empty store (fresh +// acme.json is an empty file) returns nothing; malformed JSON errors so status +// can say so. func CertExpiries(acmeJSON []byte) ([]CertExpiry, error) { if len(strings.TrimSpace(string(acmeJSON))) == 0 { return nil, nil @@ -212,7 +216,7 @@ func CertExpiries(acmeJSON []byte) ([]CertExpiry, error) { return nil, fmt.Errorf("acme store: %w", err) } var out []CertExpiry - for _, resolver := range store { + for name, resolver := range store { for _, c := range resolver.Certificates { pemChain, err := base64.StdEncoding.DecodeString(c.Certificate) if err != nil { @@ -226,12 +230,66 @@ func CertExpiries(acmeJSON []byte) ([]CertExpiry, error) { if err != nil { continue } - out = append(out, CertExpiry{Domain: c.Domain.Main, NotAfter: cert.NotAfter}) + out = append(out, CertExpiry{Resolver: name, Domain: c.Domain.Main, NotAfter: cert.NotAfter}) } } return out, nil } +// AcmeStorageDir is where the proxy container mounts the host's ACME store +// directory. Only a store under it is one the host can read back. +const AcmeStorageDir = "/letsencrypt" + +// StaticConfigNames are the file names a static configuration may have, in +// the order Traefik searches /etc/traefik for them. Traefik also looks for +// traefik.toml first, but Onebox refuses to stage one. +var StaticConfigNames = []string{"traefik.yaml", "traefik.yml"} + +// CertificateStores returns every certificate resolver that a static +// configuration defines, mapped to the file name of the ACME store it writes +// inside the host's store directory. A resolver whose store cannot be placed +// there maps to "": its storage is unset, of an unexpected type, or outside +// the directory. Status must not call such a resolver's certificates leftovers, +// because it cannot tell where that resolver writes. +// +// Keys match without regard to case, as Traefik's own loader matches them. A +// relative storage path is resolved from /, the proxy's working directory. +func CertificateStores(staticConfig []byte) (map[string]string, error) { + var document map[string]any + if err := yaml.Unmarshal(staticConfig, &document); err != nil { + return nil, errors.New("the static configuration is not valid YAML") + } + stores := map[string]string{} + resolvers, _ := foldKey(document, "certificatesResolvers").(map[string]any) + for name, value := range resolvers { + stores[name] = "" + resolver, _ := value.(map[string]any) + acme, _ := foldKey(resolver, "acme").(map[string]any) + storage, _ := foldKey(acme, "storage").(string) + if storage == "" { + continue + } + if !path.IsAbs(storage) { + storage = "/" + storage + } + storage = path.Clean(storage) + if path.Dir(storage) == AcmeStorageDir { + stores[name] = path.Base(storage) + } + } + return stores, nil +} + +// foldKey returns the value under key, matched without regard to case. +func foldKey(document map[string]any, key string) any { + for k, v := range document { + if strings.EqualFold(k, key) { + return v + } + } + return nil +} + // Stage builds the local staging payload for the host proxy: compose.yaml + // config/, and returns the payload hash — the // identity used for cross-app conflict detection and change detection. @@ -276,7 +334,7 @@ entryPoints: const defaultStaticConfigFooter = `certificatesResolvers: ` + app.ManagedCertificateResolver + `: acme: - storage: /letsencrypt/acme.json + storage: ` + AcmeStorageDir + `/acme.json httpChallenge: entryPoint: web ` @@ -317,7 +375,7 @@ func renderStaticConfigWithDNS(entrypoints map[string]app.ProxyEntrypoint, dns * } out.WriteString(defaultStaticConfigFooter) if dns != nil { - fmt.Fprintf(&out, " %s:\n acme:\n storage: /letsencrypt/acme-wildcard.json\n dnsChallenge:\n provider: %s\n", app.ManagedWildcardCertificateResolver, dns.Provider) + fmt.Fprintf(&out, " %s:\n acme:\n storage: "+AcmeStorageDir+"/acme-wildcard.json\n dnsChallenge:\n provider: %s\n", app.ManagedWildcardCertificateResolver, dns.Provider) if len(dns.Resolvers) > 0 { out.WriteString(" resolvers:\n") for _, resolver := range dns.Resolvers { diff --git a/internal/proxy/proxy_test.go b/internal/proxy/proxy_test.go index 381401e5..bfda262b 100644 --- a/internal/proxy/proxy_test.go +++ b/internal/proxy/proxy_test.go @@ -641,7 +641,7 @@ func TestCertExpiries(t *testing.T) { if err != nil { t.Fatal(err) } - if len(certs) != 1 || certs[0].Domain != "app.example.com" || !certs[0].NotAfter.Equal(exp) { + if len(certs) != 1 || certs[0].Resolver != "le" || certs[0].Domain != "app.example.com" || !certs[0].NotAfter.Equal(exp) { t.Fatalf("got %+v", certs) } @@ -659,6 +659,47 @@ func TestCertExpiries(t *testing.T) { } } +func TestCertificateStores(t *testing.T) { + for _, tc := range []struct { + name string + body string + want map[string]string + }{ + {"default", string(renderStaticConfig(nil)), + map[string]string{app.ManagedCertificateResolver: "acme.json"}}, + {"dns challenge", string(renderStaticConfigWithDNS(nil, &app.ProxyDNSChallenge{Provider: "cloudflare"})), + map[string]string{app.ManagedCertificateResolver: "acme.json", app.ManagedWildcardCertificateResolver: "acme-wildcard.json"}}, + {"custom, storage outside the store directory", + "certificatesResolvers:\n letsencrypt:\n acme:\n storage: /letsencrypt/certs.json\n other:\n acme:\n storage: /data/acme.json\n unset:\n acme: {}\n escape:\n acme:\n storage: /letsencrypt/../etc/passwd\n typed:\n acme:\n storage: [x]\n", + map[string]string{app.ManagedCertificateResolver: "certs.json", "other": "", "unset": "", "escape": "", "typed": ""}}, + {"relative storage resolves from /", + "certificatesResolvers:\n letsencrypt:\n acme:\n storage: letsencrypt/acme.json\n", + map[string]string{app.ManagedCertificateResolver: "acme.json"}}, + {"keys match without regard to case", + "certificatesresolvers:\n letsencrypt:\n ACME:\n Storage: /letsencrypt/acme.json\n", + map[string]string{app.ManagedCertificateResolver: "acme.json"}}, + {"no resolvers", "ping: {}\n", map[string]string{}}, + } { + t.Run(tc.name, func(t *testing.T) { + got, err := CertificateStores([]byte(tc.body)) + if err != nil { + t.Fatal(err) + } + if len(got) != len(tc.want) { + t.Fatalf("got %v, want %v", got, tc.want) + } + for k, v := range tc.want { + if file, ok := got[k]; !ok || file != v { + t.Fatalf("got %v, want %v", got, tc.want) + } + } + }) + } + if _, err := CertificateStores([]byte("certificatesResolvers: [")); err == nil { + t.Fatal("malformed configuration must error") + } +} + // A project that declares a domain and nothing else must be able to bootstrap. // Onebox owns the proxy; requiring the author to write Traefik's static // configuration first contradicts that, and the file they would write is the diff --git a/site/package-lock.json b/site/package-lock.json index 7e1299ba..8d610bf1 100644 --- a/site/package-lock.json +++ b/site/package-lock.json @@ -9,10 +9,10 @@ "version": "0.0.0", "dependencies": { "@astrojs/markdown-remark": "^7.3.4", - "@astrojs/starlight": "0.42.7", + "@astrojs/starlight": "0.42.10", "@fontsource/ibm-plex-mono": "^5.3.0", "@fontsource/ibm-plex-sans": "^5.3.0", - "astro": "7.4.2", + "astro": "7.4.7", "sharp": "^0.35.5" }, "devDependencies": { @@ -220,9 +220,9 @@ } }, "node_modules/@astrojs/compiler-binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.5.tgz", - "integrity": "sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.7.tgz", + "integrity": "sha512-zJccwplOAbgfZju95IUHazLnx9RAPtPN2YGJXI6UbrgeaR9QfPjGrQL6ERkSAtnVCA4Q+83ApGyaX5zdpCaKkQ==", "license": "MIT", "optional": true, "dependencies": { @@ -236,8 +236,8 @@ "url": "https://github.com/sponsors/Brooooooklyn" }, "peerDependencies": { - "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", - "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" + "@emnapi/core": "^1.7.1 || 2.0.0-beta.2 || 2.0.0-beta.3", + "@emnapi/runtime": "^1.7.1 || 2.0.0-beta.2 || 2.0.0-beta.3" } }, "node_modules/@astrojs/compiler-binding-win32-arm64-msvc": { @@ -342,20 +342,20 @@ } }, "node_modules/@astrojs/markdown-remark": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.3.4.tgz", - "integrity": "sha512-AB8S4EQKSryzH624iMDtvC+1AD5nSPzYpCXxc7e1H5ydXhv9B5Td2QfCPA0JyEwqsJpsIsPlW0iOqiy1imdqYw==", + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.3.6.tgz", + "integrity": "sha512-L90VeM6yRCjTiqJvHmIZo2GwSYcWlvX/30T8S03NH2LUShiKIfCQusvQWFUU6sjmI07WouDPv2kz30SEaVCcdQ==", "license": "MIT", "dependencies": { - "@astrojs/internal-helpers": "0.13.0", - "@astrojs/prism": "4.1.1", + "@astrojs/internal-helpers": "0.13.1", + "@astrojs/prism": "4.1.2", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.19.0", "estree-util-visit": "^2.0.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", - "hast-util-to-html": "^9.0.5", - "hast-util-to-text": "^4.0.2", + "hast-util-to-html": "^9.0.6", + "hast-util-to-text": "^4.0.3", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", @@ -366,11 +366,39 @@ "source-map": "^0.8.0", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", - "unist-util-visit": "^5.1.0", - "unist-util-visit-parents": "^6.0.2", + "unist-util-visit": "^5.2.0", + "unist-util-visit-parents": "^6.1.0", "vfile": "^6.0.3" } }, + "node_modules/@astrojs/markdown-remark/node_modules/@astrojs/internal-helpers": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.13.1.tgz", + "integrity": "sha512-LgGLGzoC0wCT3mjDYrxz+hct/tws0enpWsLJW3bswHR3PKgusp/H9xRIT8rTdsyvLM0TcRhhvRL30J2oaZNcqA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.5", + "@types/mdast": "^4.0.4", + "picomatch": "^4.0.7", + "retext-smartypants": "^6.2.0", + "shiki": "^4.5.0", + "smol-toml": "^1.9.1", + "unified": "^11.0.5", + "yaml": "^2.9.1" + } + }, + "node_modules/@astrojs/markdown-remark/node_modules/@astrojs/prism": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.1.2.tgz", + "integrity": "sha512-jMrUEpFDarDO1KWPvVc9eSa8DSfwBlwEOHwX+8zvZQpiqXRGDxFnOXHsYqV6DP7w+0qIgTr98gS1GdQlm8ypWg==", + "license": "MIT", + "dependencies": { + "prismjs": "^1.30.0" + }, + "engines": { + "node": ">=22.23.4" + } + }, "node_modules/@astrojs/markdown-satteri": { "version": "0.4.5", "resolved": "https://registry.npmjs.org/@astrojs/markdown-satteri/-/markdown-satteri-0.4.5.tgz", @@ -420,28 +448,26 @@ } }, "node_modules/@astrojs/sitemap": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.7.3.tgz", - "integrity": "sha512-f8euLVsyeAmAkSm/1M2Kb8sL8byQmfgbvBNaHFItCheTj/IpiJYSEWVcqDHZ/yEHxiS7+w87mQkzwZaPHmk5GA==", + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.7.4.tgz", + "integrity": "sha512-LbKNC24bdUWcQf/pThB6qLlSqHojxGjZDURIzFocY8rlWnAn2t74nnhnK6S5x0NHriHoAduLEpVjRykmeGiVvA==", "license": "MIT", "dependencies": { "sitemap": "^9.0.0", - "stream-replace-string": "^2.0.0", "zod": "^4.3.6" } }, "node_modules/@astrojs/starlight": { - "version": "0.42.7", - "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.42.7.tgz", - "integrity": "sha512-mlbit5iRjLx5eIcLf84Q2CwkrrsEgZrQwKKBK+vRf+ZGiSrwy1UGdT1hUfAIaHrMfcvkDpL+gw3aua7HobiKFg==", + "version": "0.42.10", + "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.42.10.tgz", + "integrity": "sha512-gdy/kWrcEKv4fJLTdO+zwPf4auBsDZNb7diklRT9LCR4MsTaMHRa15V3o6YACnOoZMflqxY9/3BLmExb/cne9g==", "license": "MIT", "dependencies": { - "@astrojs/markdown-satteri": "^0.4.0", - "@astrojs/mdx": "^8.0.0", - "@astrojs/sitemap": "^3.7.3", + "@astrojs/markdown-satteri": "^0.4.3", + "@astrojs/mdx": "^8.0.3", + "@astrojs/sitemap": "^3.7.4", "@pagefind/default-ui": "^1.3.0", "@types/hast": "^3.0.5", - "@types/js-yaml": "^4.0.9", "@types/mdast": "^4.0.4", "astro-expressive-code": "^0.44.0", "bcp-47": "^2.1.0", @@ -451,7 +477,6 @@ "hast-util-to-string": "^3.0.1", "hastscript": "^9.0.1", "i18next": "^26.0.7", - "js-yaml": "^4.1.1", "klona": "^2.0.6", "magic-string": "^1.2.3", "mdast-util-directive": "^3.1.0", @@ -463,7 +488,8 @@ "ultrahtml": "^1.6.0", "unified": "^11.0.5", "unist-util-visit": "^5.1.0", - "vfile": "^6.0.3" + "vfile": "^6.0.3", + "yaml": "^2.9.1" }, "peerDependencies": { "@astrojs/markdown-remark": "^7.3.0", @@ -476,9 +502,9 @@ } }, "node_modules/@astrojs/telemetry": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.4.1.tgz", - "integrity": "sha512-SMkvMsGOpM0FMAoT5pzhgI3LZC0JSPWf7Evwop+fZ3N6UkusPM7lI+sOJjMNI5OJrVoiei1/AGo1cY7sikTc5A==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.4.2.tgz", + "integrity": "sha512-2JOMRUxJrBSfiVDfeKLv1wBmvSIx1Y6u2YEO8awZ9qSq3u/rNAiUyFdOYi8dHv91kHrmYEQX7pRDw0J4xyj3UA==", "license": "MIT", "dependencies": { "ci-info": "^4.4.0", @@ -546,6 +572,19 @@ "node": ">=6.9.0" } }, + "node_modules/@bruits/satteri-android-arm64": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/@bruits/satteri-android-arm64/-/satteri-android-arm64-0.11.7.tgz", + "integrity": "sha512-ej+1rIKs+yb6/OtcIe1SO22OXjJV0qqUopl5VNZF8Sqc/Pj4m1DXoqBQsPxW5OUYYiDUymIy6hnnn8M78VYk2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, "node_modules/@bruits/satteri-darwin-arm64": { "version": "0.10.5", "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-arm64/-/satteri-darwin-arm64-0.10.5.tgz", @@ -572,6 +611,19 @@ "darwin" ] }, + "node_modules/@bruits/satteri-freebsd-x64": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/@bruits/satteri-freebsd-x64/-/satteri-freebsd-x64-0.11.7.tgz", + "integrity": "sha512-EFlqw+RDRo3wYdy1ZSR/dNYhfWjuZL86PYwRcpLWWEZskjfpNnhIdxS13Xw2QJE1hSjTGVq4aZaVvlUjoj8p5A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@bruits/satteri-linux-arm64-gnu": { "version": "0.10.5", "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-gnu/-/satteri-linux-arm64-gnu-0.10.5.tgz", @@ -1544,9 +1596,9 @@ "license": "MIT" }, "node_modules/@oxc-project/types": { - "version": "0.155.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.155.0.tgz", - "integrity": "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==", + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.156.0.tgz", + "integrity": "sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/oxc-project" @@ -1650,9 +1702,9 @@ ] }, "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz", - "integrity": "sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.16.tgz", + "integrity": "sha512-ia3DwAs0OsMGS7+IConptSiyk9pEXkOx/AbJ2n/lAGxH3QkWQus1t8d7gnAT38FCU2sQu8lUbeR6AiZFv4JxaA==", "cpu": [ "arm" ], @@ -1666,9 +1718,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.15.tgz", - "integrity": "sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.16.tgz", + "integrity": "sha512-kx5wBiMB6E7TEvpl2B9gjLU004BkvEm2Dr941BFjx/SontPwSzNuQUIoVKxHHoSjpFIfHMmcguyLOtyn0hObRg==", "cpu": [ "arm64" ], @@ -1682,9 +1734,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.15.tgz", - "integrity": "sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.16.tgz", + "integrity": "sha512-qslZrXnKJU4cUpjoAmtAo/KQM9e2IRMlmpig6Zpwj+8KWxW8v+nkAf9216Bsek9lOC1jvg2yCjygCFo2f83msQ==", "cpu": [ "arm64" ], @@ -1698,9 +1750,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.15.tgz", - "integrity": "sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.16.tgz", + "integrity": "sha512-FdF+y48ND13eAyM2mQn+85kvAqNlvRWROy2rhgA463X/0Gv4vGtazZtfaF1mRBLscYvtOlh4QnLjOWiFzX69OQ==", "cpu": [ "x64" ], @@ -1714,9 +1766,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.15.tgz", - "integrity": "sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.16.tgz", + "integrity": "sha512-ddjqFM5e95AgLbUPtKIS7V1KTVlA0RUgCVr5mn+/IEhsdy3pr+Vuj/HZ7nlMN2FyxIwUktOH1LEPIQXO1v9y1Q==", "cpu": [ "x64" ], @@ -1730,9 +1782,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.15.tgz", - "integrity": "sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.16.tgz", + "integrity": "sha512-tHdKWbFkFBI687iim3LIyIvyL7j66VvQYkDPss5Stn/qyE2tWkR1YD0dXmXPFKKqT/o5ZEJVsaRsWS2meQwGOA==", "cpu": [ "arm" ], @@ -1746,9 +1798,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.15.tgz", - "integrity": "sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.16.tgz", + "integrity": "sha512-FJiLGAblPBMlBEdbOqw6kW6QcXDWAf/oJu7gYs9ZJZwmg+K+/zjFLxPTIY9izJUzxzc24FHvmseyA0IvJSl17w==", "cpu": [ "arm64" ], @@ -1765,9 +1817,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.15.tgz", - "integrity": "sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.16.tgz", + "integrity": "sha512-hrJpblMsrQ58U9veWpbXzkhV5zStunds6HWLprqq1qauvgBr1E6P+24kLj6lSLI9vjRn/RYpjprhkHQPKGWpNg==", "cpu": [ "arm64" ], @@ -1784,9 +1836,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.15.tgz", - "integrity": "sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.16.tgz", + "integrity": "sha512-qJd/dC6j6CwLG9/s/RP2nb1RZ0GIgj7Ioj7x/SYa3RzjVrlzrmH0fhIOpfvqQDeIsdP0loi+kHaMhJzY1PBIaA==", "cpu": [ "ppc64" ], @@ -1803,9 +1855,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.15.tgz", - "integrity": "sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.16.tgz", + "integrity": "sha512-Vx7Xg5uUJuNcujJjI70OICaH0EYj80BtOLZTSN7uOYg8QOLlyKnaG0IW7Fn/J0IpJsaKzmDKMhOQv1JBM9m9vg==", "cpu": [ "s390x" ], @@ -1822,9 +1874,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.15.tgz", - "integrity": "sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.16.tgz", + "integrity": "sha512-TNKhgE3uJTsTzatUSSYt0Kgo+Oa2uSlSjz4r1aEIea7HX6ULBym4fAlYUhG1i6pjNf2njfCUAfKwr6C3E9Xg1w==", "cpu": [ "x64" ], @@ -1841,9 +1893,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.15.tgz", - "integrity": "sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.16.tgz", + "integrity": "sha512-7chXdpjXZ8qG0+mIxECN3cT9kvhZ3/oZhMX6Pp876P7CDrIy8nJswva6Mq357JR7eYXfuskwZxMeV1SzoZdHrA==", "cpu": [ "x64" ], @@ -1860,9 +1912,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.15.tgz", - "integrity": "sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.16.tgz", + "integrity": "sha512-Kwjt+7uNTJK5/MDkGWGVO2KQPnj8yAV75dMpfwyHZrJSXWHrniOB0aUEWlVYf3H1hOqFLIStLdM5QXq6bnKmOQ==", "cpu": [ "arm64" ], @@ -1876,9 +1928,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.15.tgz", - "integrity": "sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.16.tgz", + "integrity": "sha512-/LH6WfjDRXXJz/p5xl4TmU+jpcnwcxbKEQBW/GZE0JHfN0lGIyTtsUEGaQ/q/GikkGJ2yLKe6pwm7AnEWO8pdw==", "cpu": [ "arm64" ], @@ -1892,9 +1944,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.15.tgz", - "integrity": "sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.16.tgz", + "integrity": "sha512-1lv1KvX0Z871zfk7vw+dnXMMnHNyDwMzWG8Kv3HxDXMOhzCX4SHc7HK319fInP0i+0p9D8brkfzL7pJOaGl+tw==", "cpu": [ "x64" ], @@ -2056,12 +2108,6 @@ "@types/unist": "*" } }, - "node_modules/@types/js-yaml": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", - "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", - "license": "MIT" - }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -2384,12 +2430,6 @@ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", "license": "MIT" }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -2419,15 +2459,15 @@ } }, "node_modules/astro": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/astro/-/astro-7.4.2.tgz", - "integrity": "sha512-FAH4cdDlFaVnQGjazLTEIC5vJqGlo5XANc1wIbzQHIqAjJTll/RWTUs+SPNW1JdezG4YGxMZllaE5A41T2kzhA==", + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/astro/-/astro-7.4.7.tgz", + "integrity": "sha512-fH54IOaCkZY1paEjygKn8CPfuJ3w8ns7PourcPML5AouJkQuTBqBQ2RbbIbBhutZbg2qn5se6Yl9DYybzqjC0Q==", "license": "MIT", "dependencies": { - "@astrojs/compiler-rs": "^0.5.1", - "@astrojs/internal-helpers": "0.13.0", - "@astrojs/markdown-satteri": "0.4.5", - "@astrojs/telemetry": "3.4.1", + "@astrojs/compiler-rs": "^0.5.2", + "@astrojs/internal-helpers": "0.13.1", + "@astrojs/markdown-satteri": "0.4.7", + "@astrojs/telemetry": "3.4.2", "@capsizecss/unpack": "^4.0.1", "@clack/prompts": "^1.8.1", "@oslojs/encoding": "^1.1.0", @@ -2438,10 +2478,10 @@ "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^2.0.1", - "devalue": "^6.0.2", + "devalue": "^6.0.3", "diff": "^9.0.0", "dset": "^3.1.4", - "es-module-lexer": "^3.0.3", + "es-module-lexer": "^3.0.4", "flattie": "^1.1.1", "fontace": "~0.4.1", "get-tsconfig": "5.0.0-beta.6", @@ -2449,33 +2489,33 @@ "html-escaper": "3.0.3", "http-cache-semantics": "^4.3.0", "jsonc-parser": "^3.3.1", - "magic-string": "^1.4.3", + "magic-string": "^1.4.5", "magicast": "^0.5.5", "mrmime": "^2.0.1", "neotraverse": "^1.0.1", "obug": "^3.0.0", "p-limit": "^7.3.3", - "p-queue": "^9.3.5", + "p-queue": "^10.0.0", "package-manager-detector": "^1.9.0", "piccolore": "^0.1.3", "picomatch": "^4.0.7", - "rolldown": "^1.2.13", + "rolldown": "^1.2.16", "shiki": "^4.5.0", "smol-toml": "^1.9.1", "svgo": "^4.1.0", "tinyclip": "^1.0.3", "tinyexec": "^1.3.1", "tinyglobby": "^0.2.17", - "tinyps": "^0.5.0", + "tinyps": "^0.5.1", "ultrahtml": "^1.7.0", "unifont": "^1.0.3", - "unstorage": "^1.17.5", + "unstorage": "^1.17.6", "verkit": "^0.5.0", - "vite": "^8.3.4", + "vite": "^8.3.5", "vitefu": "^1.1.3", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", - "zod": "^4.6.6" + "zod": "^4.6.7" }, "bin": { "astro": "bin/astro.mjs" @@ -2514,6 +2554,257 @@ "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta || ^7.0.0" } }, + "node_modules/astro/node_modules/@astrojs/internal-helpers": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.13.1.tgz", + "integrity": "sha512-LgGLGzoC0wCT3mjDYrxz+hct/tws0enpWsLJW3bswHR3PKgusp/H9xRIT8rTdsyvLM0TcRhhvRL30J2oaZNcqA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.5", + "@types/mdast": "^4.0.4", + "picomatch": "^4.0.7", + "retext-smartypants": "^6.2.0", + "shiki": "^4.5.0", + "smol-toml": "^1.9.1", + "unified": "^11.0.5", + "yaml": "^2.9.1" + } + }, + "node_modules/astro/node_modules/@astrojs/markdown-satteri": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-satteri/-/markdown-satteri-0.4.7.tgz", + "integrity": "sha512-MZvnec/898GGGHQZfkArVbkdZmL1imjIM8RWkmzbAmse737zr8D9inUjFQ5eLBXV3vPobOUs/ZRV/D6Po/bVDg==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.13.1", + "@astrojs/prism": "4.1.2", + "github-slugger": "^2.0.0", + "satteri": "^0.11.6" + } + }, + "node_modules/astro/node_modules/@astrojs/prism": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.1.2.tgz", + "integrity": "sha512-jMrUEpFDarDO1KWPvVc9eSa8DSfwBlwEOHwX+8zvZQpiqXRGDxFnOXHsYqV6DP7w+0qIgTr98gS1GdQlm8ypWg==", + "license": "MIT", + "dependencies": { + "prismjs": "^1.30.0" + }, + "engines": { + "node": ">=22.23.4" + } + }, + "node_modules/astro/node_modules/@bruits/satteri-darwin-arm64": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-arm64/-/satteri-darwin-arm64-0.11.7.tgz", + "integrity": "sha512-yllQOX1kVWrwqL7tamQlp4NP0loIb4KuadjuNrAe4JEOerHpgYISPRDb8DqLipgvP7Ks4e87LydZpdakMR1rhQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/astro/node_modules/@bruits/satteri-darwin-x64": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-x64/-/satteri-darwin-x64-0.11.7.tgz", + "integrity": "sha512-RMQrKzEZRZSiYyLFifiSuk0nBH6uAY1TVMNR/h/aE4rr2OS6l82e4MJTpC6H/tqSMoE+3CwbkUKBQGLHIweYlg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/astro/node_modules/@bruits/satteri-linux-arm64-gnu": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-gnu/-/satteri-linux-arm64-gnu-0.11.7.tgz", + "integrity": "sha512-aeH365GHaqv01wRSiHaGUlpL28U8e9UnV9LxjIF6BOyTj71/M5bop4HKomcis4VxFZjd68fDqNyU22yFkF/h7A==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/astro/node_modules/@bruits/satteri-linux-arm64-musl": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-musl/-/satteri-linux-arm64-musl-0.11.7.tgz", + "integrity": "sha512-72WlBhuwOHm+2Nv4Qf1WzfS75PHOLz81sYkDJnmj0Rm2C4Xax5faSiTDt7vWo1IcK6rDlkuRDNnWIw8uIKWuVQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/astro/node_modules/@bruits/satteri-linux-x64-gnu": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-gnu/-/satteri-linux-x64-gnu-0.11.7.tgz", + "integrity": "sha512-gDp4v1jjckeNCetLtrMUfDV4CKGYL6fayDPdoaQq09nkDCSrclxEFK7kAmRuHhoalgsao4bZMFqQr3HQtN2NEQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/astro/node_modules/@bruits/satteri-linux-x64-musl": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-musl/-/satteri-linux-x64-musl-0.11.7.tgz", + "integrity": "sha512-zqzSpQLA6V3J/TO2Vjuc1VRQPIQQdoSYmzKKFL+3GwGlbXiFQP2ZUDSLw9xlm0tTKXkCM9ANr4jMiDASZ+JGUw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/astro/node_modules/@bruits/satteri-wasm32-wasi": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/@bruits/satteri-wasm32-wasi/-/satteri-wasm32-wasi-0.11.7.tgz", + "integrity": "sha512-L46f6rtlaBR4fsTWGCTFcXtbtka2GyM3l9TsmE0FgTxjXGq+sndYg5X3hzlhWDkNvJpRtCHCmgTRJ5+cIi4o3A==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.2.7" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/astro/node_modules/@bruits/satteri-win32-arm64-msvc": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-arm64-msvc/-/satteri-win32-arm64-msvc-0.11.7.tgz", + "integrity": "sha512-R2+o6EbJK4pqbd086H9BGUVYAyZ5F6FnShfoyRIu0Zu9DTE3H6EU/Z+ua7p9dmYf6JQQbE9DCwwavi4xiaGAhQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/astro/node_modules/@bruits/satteri-win32-x64-msvc": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-x64-msvc/-/satteri-win32-x64-msvc-0.11.7.tgz", + "integrity": "sha512-OIyBFJ35uMu5CH5RWnIR3bKsZwScKZ7vXmhCohZ+qonT1SkxmHCEf4H8YRWThRL2EXdVDjkjhfOsaiA4O24Vvw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/astro/node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/astro/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/astro/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/astro/node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.7.tgz", + "integrity": "sha512-zJccwplOAbgfZju95IUHazLnx9RAPtPN2YGJXI6UbrgeaR9QfPjGrQL6ERkSAtnVCA4Q+83ApGyaX5zdpCaKkQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || 2.0.0-beta.2 || 2.0.0-beta.3", + "@emnapi/runtime": "^1.7.1 || 2.0.0-beta.2 || 2.0.0-beta.3" + } + }, + "node_modules/astro/node_modules/satteri": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/satteri/-/satteri-0.11.7.tgz", + "integrity": "sha512-xDOSxkc1ew3z3IovIjmsqJaVHRXTGYJkPvx0LL/U6o90km8kSU+pmrZVvnWd5creJeE0UnvMEienlkPuYQdJaA==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.5", + "@types/hast": "^3.0.5", + "@types/mdast": "^4.0.4", + "@types/unist": "^3.0.3" + }, + "optionalDependencies": { + "@bruits/satteri-android-arm64": "0.11.7", + "@bruits/satteri-darwin-arm64": "0.11.7", + "@bruits/satteri-darwin-x64": "0.11.7", + "@bruits/satteri-freebsd-x64": "0.11.7", + "@bruits/satteri-linux-arm64-gnu": "0.11.7", + "@bruits/satteri-linux-arm64-musl": "0.11.7", + "@bruits/satteri-linux-x64-gnu": "0.11.7", + "@bruits/satteri-linux-x64-musl": "0.11.7", + "@bruits/satteri-wasm32-wasi": "0.11.7", + "@bruits/satteri-win32-arm64-msvc": "0.11.7", + "@bruits/satteri-win32-x64-msvc": "0.11.7" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -3221,12 +3512,6 @@ "@types/estree": "^1.0.0" } }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "license": "MIT" - }, "node_modules/expressive-code": { "version": "0.44.1", "resolved": "https://registry.npmjs.org/expressive-code/-/expressive-code-0.44.1.tgz", @@ -3659,9 +3944,9 @@ } }, "node_modules/hast-util-to-html": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", - "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.6.tgz", + "integrity": "sha512-1SZUxCN3TtJAE7e2BQN2MBHsoZ1gnmi9GoCP5Di59CSWptIezSAW09yDZyVbanb78V9eCwrKqdQsjtfVTxYHWA==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -3741,15 +4026,12 @@ } }, "node_modules/hast-util-to-text": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", - "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.3.tgz", + "integrity": "sha512-5yrlWOvGFs6CqHriUpSsNrjzT/Tdo+IOQudbznxOn8K5qblp18MJyzOg8IgDOKQ8MYgQcJA7SusuCjSpDr8mSA==", "license": "MIT", "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "hast-util-is-element": "^3.0.0", - "unist-util-find-after": "^5.0.0" + "@types/hast": "^3.0.0" }, "funding": { "type": "opencollective", @@ -3932,28 +4214,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/js-yaml": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", - "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -4258,18 +4518,18 @@ } }, "node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, "node_modules/magic-string": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", - "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.5.tgz", + "integrity": "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" @@ -5525,28 +5785,12 @@ } }, "node_modules/p-queue": { - "version": "9.3.5", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.5.tgz", - "integrity": "sha512-Lki/zhXFVmDhesBUn/yHRLW9Bte7mE7xoFHzTR8sIomV7a2z+LxYAPkGvdfidNiu4fKSfjtawixxSHcZci5QcQ==", - "license": "MIT", - "dependencies": { - "eventemitter3": "^5.0.4", - "p-timeout": "^7.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-timeout": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.2.tgz", - "integrity": "sha512-prbX4Z3YszrFNgH+MW5Zoeq3baXrMtP/MQnFeET90UB/GtGcGDQ5Usg9OCy6ETjTTntOw1SL2z9fMPUppN3Guw==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-10.0.0.tgz", + "integrity": "sha512-P2wrCri3bhGLPo3kX7qYVXbo/5GvaVATU0DpO24CbeT53Y4z35t8JUWku04ERpXHUYB5rPMi3TkWE3fmJgx5rA==", "license": "MIT", "engines": { - "node": ">=20" + "node": ">=22" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -6135,12 +6379,12 @@ } }, "node_modules/rolldown": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.15.tgz", - "integrity": "sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.16.tgz", + "integrity": "sha512-bWvlMpFKiTfDKfnK7FC2MPgPeWCf/Kpwsq49ApdW+FQaBZb6O6dZ4MAPbPUjp7hYQrw/RobqCzn64BRrQecGHw==", "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.155.0", + "@oxc-project/types": "=0.156.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -6150,21 +6394,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.15", - "@rolldown/binding-android-arm64": "1.2.15", - "@rolldown/binding-darwin-arm64": "1.2.15", - "@rolldown/binding-darwin-x64": "1.2.15", - "@rolldown/binding-freebsd-x64": "1.2.15", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.15", - "@rolldown/binding-linux-arm64-gnu": "1.2.15", - "@rolldown/binding-linux-arm64-musl": "1.2.15", - "@rolldown/binding-linux-ppc64-gnu": "1.2.15", - "@rolldown/binding-linux-s390x-gnu": "1.2.15", - "@rolldown/binding-linux-x64-gnu": "1.2.15", - "@rolldown/binding-linux-x64-musl": "1.2.15", - "@rolldown/binding-openharmony-arm64": "1.2.15", - "@rolldown/binding-win32-arm64-msvc": "1.2.15", - "@rolldown/binding-win32-x64-msvc": "1.2.15" + "@rolldown/binding-android-arm-eabi": "1.2.16", + "@rolldown/binding-android-arm64": "1.2.16", + "@rolldown/binding-darwin-arm64": "1.2.16", + "@rolldown/binding-darwin-x64": "1.2.16", + "@rolldown/binding-freebsd-x64": "1.2.16", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.16", + "@rolldown/binding-linux-arm64-gnu": "1.2.16", + "@rolldown/binding-linux-arm64-musl": "1.2.16", + "@rolldown/binding-linux-ppc64-gnu": "1.2.16", + "@rolldown/binding-linux-s390x-gnu": "1.2.16", + "@rolldown/binding-linux-x64-gnu": "1.2.16", + "@rolldown/binding-linux-x64-musl": "1.2.16", + "@rolldown/binding-openharmony-arm64": "1.2.16", + "@rolldown/binding-win32-arm64-msvc": "1.2.16", + "@rolldown/binding-win32-x64-msvc": "1.2.16" } }, "node_modules/satteri": { @@ -6350,12 +6594,6 @@ "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", "license": "MIT" }, - "node_modules/stream-replace-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/stream-replace-string/-/stream-replace-string-2.0.0.tgz", - "integrity": "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==", - "license": "MIT" - }, "node_modules/string-width": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", @@ -6623,20 +6861,6 @@ "undici": "^8.0.0" } }, - "node_modules/unist-util-find-after": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", - "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/unist-util-is": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", @@ -6718,14 +6942,14 @@ } }, "node_modules/unist-util-visit": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", - "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.2.0.tgz", + "integrity": "sha512-fpsYs/VVoZxfGY9/j3FQpZmN8wdLJaV1Pt38AaJNZu9uKIHcFU3halCdBveEq1HGKO1OEfJY65w1zvoFV1b3qQ==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" + "unist-util-visit-parents": "^6.1.0" }, "funding": { "type": "opencollective", @@ -6760,19 +6984,19 @@ } }, "node_modules/unstorage": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", - "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.6.tgz", + "integrity": "sha512-gjKtcmn/KwZuJZO9o2sc0esAQnhmTw0LviDHwevTXbJMYs0dRTp8ZEEWXHU0vTpzbN3cA0yxlTeyXssMp3zDqw==", "license": "MIT", "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", - "h3": "^1.15.10", - "lru-cache": "^11.2.7", + "h3": "^1.15.11", + "lru-cache": "^11.5.3", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", - "ufo": "^1.6.3" + "ufo": "^1.6.4" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", @@ -6783,7 +7007,7 @@ "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", - "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", @@ -7179,13 +7403,6 @@ "vscode-uri": "^3.1.0" } }, - "node_modules/vscode-css-languageservice/node_modules/vscode-languageserver-types": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", - "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", - "dev": true, - "license": "MIT" - }, "node_modules/vscode-html-languageservice": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-5.6.2.tgz", @@ -7217,13 +7434,13 @@ } }, "node_modules/vscode-jsonrpc": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-10.0.0.tgz", - "integrity": "sha512-USSfxg0vDIpTQqlQBGxF3mBARyoS8cU7LHLpbYNh6dOJt32dlskxPWB0Irrh5TllTmyf7oNLAvqDqzklbwwnpA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", "dev": true, "license": "MIT", "engines": { - "node": ">=14.17.0" + "node": ">=14.0.0" } }, "node_modules/vscode-languageserver": { @@ -7240,14 +7457,14 @@ } }, "node_modules/vscode-languageserver-protocol": { - "version": "3.18.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.18.5.tgz", - "integrity": "sha512-FW4fBMzZ99N8xeob4py0l050wgCoRgTpEq5Xcsb9lM1bc29UKGR/wSV+4knaqFxssFHajy7OzNiKP/m3U1ZEuA==", + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", "dev": true, "license": "MIT", "dependencies": { - "vscode-jsonrpc": "10.0.0", - "vscode-languageserver-types": "3.18.5" + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" } }, "node_modules/vscode-languageserver-textdocument": { @@ -7258,34 +7475,6 @@ "license": "MIT" }, "node_modules/vscode-languageserver-types": { - "version": "3.18.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.18.5.tgz", - "integrity": "sha512-kPd6+JQNz3HMJvjKlqUxERSlZHAntl/aGpsk7ImwIh0ewDKDcjgAEyVSYciPiOJD3ftbK9IrfiqUeTZLSbCizw==", - "dev": true, - "license": "MIT" - }, - "node_modules/vscode-languageserver/node_modules/vscode-jsonrpc": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", - "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/vscode-languageserver/node_modules/vscode-languageserver-protocol": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", - "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", - "dev": true, - "license": "MIT", - "dependencies": { - "vscode-jsonrpc": "8.2.0", - "vscode-languageserver-types": "3.17.5" - } - }, - "node_modules/vscode-languageserver/node_modules/vscode-languageserver-types": { "version": "3.17.5", "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", diff --git a/site/package.json b/site/package.json index 637ebe6f..c204a5a4 100644 --- a/site/package.json +++ b/site/package.json @@ -15,10 +15,10 @@ }, "dependencies": { "@astrojs/markdown-remark": "^7.3.4", - "@astrojs/starlight": "0.42.7", + "@astrojs/starlight": "0.42.10", "@fontsource/ibm-plex-mono": "^5.3.0", "@fontsource/ibm-plex-sans": "^5.3.0", - "astro": "7.4.2", + "astro": "7.4.7", "sharp": "^0.35.5" }, "devDependencies": {