diff --git a/api/schemas.yaml b/api/schemas.yaml index df0b416..53aa297 100644 --- a/api/schemas.yaml +++ b/api/schemas.yaml @@ -873,6 +873,24 @@ components: type: string description: DKIM record content example: "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA..." + key_type: + type: string + description: "Key type from k= tag (e.g. rsa, ed25519); defaults to rsa if absent" + example: "rsa" + hash_algorithms: + type: array + items: + type: string + description: "Acceptable hash algorithms from h= tag; empty means all accepted (RFC 6376 default: sha256)" + example: ["sha256"] + signing_algorithm: + type: string + description: "Algorithm used in DKIM-Signature a= tag (e.g. rsa-sha256, ed25519-sha256)" + example: "rsa-sha256" + key_size: + type: integer + description: "Public key size in bits (RSA: 1024/2048/4096; Ed25519: always 256)" + example: 2048 valid: type: boolean description: Whether the DKIM record is valid @@ -891,6 +909,10 @@ components: type: string description: DMARC record content example: "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com" + domain: + type: string + description: Domain at which the DMARC record was found (may differ from From domain when organizational domain fallback was used) + example: "example.com" policy: type: string enum: [none, quarantine, reject, unknown] @@ -901,12 +923,38 @@ components: enum: [none, quarantine, reject, unknown] description: DMARC subdomain policy (sp tag) - policy for subdomains if different from main policy example: "quarantine" + nonexistent_subdomain_policy: + type: string + enum: [none, quarantine, reject, unknown] + description: DMARC non-existent subdomain policy (np tag) - policy for non-existent subdomains (NXDOMAIN); defaults to sp= or p= if absent (DMARCbis) + example: "reject" percentage: type: integer minimum: 0 maximum: 100 - description: Percentage of messages subjected to filtering (pct tag, default 100) + description: "Percentage of messages subjected to filtering (pct tag, default 100). DEPRECATED in DMARCbis: use test_mode (t=y) instead." example: 100 + test_mode: + type: boolean + description: "DMARCbis t= tag: when true (t=y), receivers downgrade effective policy one level (reject→quarantine, quarantine→none). Replaces the deprecated pct= tag for testing." + example: false + psd: + type: string + enum: [y, n, u] + description: "DMARCbis psd= tag: y=this is a Public Suffix Domain, n=this is an Organizational Domain boundary, u=unknown (default, use DNS Tree Walk to determine)" + example: "u" + deprecated_pct: + type: boolean + description: "Whether the deprecated pct= tag was found in the record (pct is removed in DMARCbis; migrate to t=y for testing mode)" + example: false + deprecated_rf: + type: boolean + description: "Whether the deprecated rf= tag was found in the record (rf is removed in DMARCbis; failure report formats are now defined separately)" + example: false + deprecated_ri: + type: boolean + description: "Whether the deprecated ri= tag was found in the record (ri is removed in DMARCbis; aggregate reporting interval is now fixed at ≥24 hours)" + example: false spf_alignment: type: string enum: [relaxed, strict] diff --git a/go.mod b/go.mod index bbdc2ce..a975215 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/getkin/kin-openapi v0.138.0 github.com/gin-gonic/gin v1.12.0 github.com/google/uuid v1.6.0 - github.com/oapi-codegen/runtime v1.3.0 + github.com/oapi-codegen/runtime v1.4.0 golang.org/x/net v0.54.0 gorm.io/driver/postgres v1.6.0 gorm.io/driver/sqlite v1.6.0 diff --git a/go.sum b/go.sum index d5a7083..f4c8d28 100644 --- a/go.sum +++ b/go.sum @@ -135,8 +135,8 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oapi-codegen/oapi-codegen/v2 v2.7.0 h1:/8daqIYZfwnsHEAZdHUu9m0D5LA+5DoJCP7zLlT5Cs0= github.com/oapi-codegen/oapi-codegen/v2 v2.7.0/go.mod h1:qzFy6iuobJw/hD1aRILee4G87/ShmhR0xYCwcUtZMCw= -github.com/oapi-codegen/runtime v1.3.0 h1:vyK1zc0gDWWXgk2xoQa4+X4RNNc5SL2RbTpJS/4vMYA= -github.com/oapi-codegen/runtime v1.3.0/go.mod h1:kOdeacKy7t40Rclb1je37ZLFboFxh+YLy0zaPCMibPY= +github.com/oapi-codegen/runtime v1.4.0 h1:KLOSFOp7UzkbS7Cs1ms6NBEKYr0WmH2wZG0KKbd2er4= +github.com/oapi-codegen/runtime v1.4.0/go.mod h1:5sw5fxCDmnOzKNYmkVNF8d34kyUeejJEY8HNT2WaPec= github.com/oasdiff/yaml v0.0.9 h1:zQOvd2UKoozsSsAknnWoDJlSK4lC0mpmjfDsfqNwX48= github.com/oasdiff/yaml v0.0.9/go.mod h1:8lvhgJG4xiKPj3HN5lDow4jZHPlx1i7dIwzkdAo6oAM= github.com/oasdiff/yaml3 v0.0.12 h1:75urAtPeDg2/iDEWwzNrLOWxI9N/dCh81nTTJtokt2M= diff --git a/internal/app/cli_analyzer.go b/internal/app/cli_analyzer.go index d8336a5..c704c56 100644 --- a/internal/app/cli_analyzer.go +++ b/internal/app/cli_analyzer.go @@ -202,6 +202,9 @@ func outputHumanReadable(result *analyzer.AnalysisResult, emailAnalyzer *analyze if dns.DmarcRecord.SubdomainPolicy != nil { fmt.Fprintf(writer, ", Subdomain Policy: %s", *dns.DmarcRecord.SubdomainPolicy) } + if dns.DmarcRecord.NonexistentSubdomainPolicy != nil { + fmt.Fprintf(writer, ", Non-Existent Subdomain Policy: %s", *dns.DmarcRecord.NonexistentSubdomainPolicy) + } fmt.Fprintln(writer) if dns.DmarcRecord.Record != nil { fmt.Fprintf(writer, " %s\n", *dns.DmarcRecord.Record) diff --git a/pkg/analyzer/authentication.go b/pkg/analyzer/authentication.go index da31b1c..bd8880d 100644 --- a/pkg/analyzer/authentication.go +++ b/pkg/analyzer/authentication.go @@ -174,9 +174,7 @@ func (a *AuthenticationAnalyzer) CalculateAuthenticationScore(results *model.Aut score += 12 * a.calculateXGoogleDKIMScore(results) / 100 // Penalty-only: X-Aligned-From (up to -5 points on failure) - if xAlignedScore := a.calculateXAlignedFromScore(results); xAlignedScore < 100 { - score += 5 * (xAlignedScore - 100) / 100 - } + score += 5 * a.calculateXAlignedFromScore(results) / 100 // Ensure score doesn't exceed 100 if score > 100 { diff --git a/pkg/analyzer/authentication_test.go b/pkg/analyzer/authentication_test.go index 44c1abb..0b17bf0 100644 --- a/pkg/analyzer/authentication_test.go +++ b/pkg/analyzer/authentication_test.go @@ -47,7 +47,7 @@ func TestGetAuthenticationScore(t *testing.T) { Result: model.AuthResultResultPass, }, }, - expectedScore: 73, // SPF=25 + DKIM=23 + DMARC=25 + expectedScore: 90, // SPF=30 + DKIM=30 + DMARC=30 }, { name: "SPF and DKIM only", @@ -59,7 +59,7 @@ func TestGetAuthenticationScore(t *testing.T) { {Result: model.AuthResultResultPass}, }, }, - expectedScore: 48, // SPF=25 + DKIM=23 + expectedScore: 60, // SPF=30 + DKIM=30 }, { name: "SPF fail, DKIM pass", @@ -71,7 +71,7 @@ func TestGetAuthenticationScore(t *testing.T) { {Result: model.AuthResultResultPass}, }, }, - expectedScore: 23, // SPF=0 + DKIM=23 + expectedScore: 30, // SPF=0 + DKIM=30 }, { name: "SPF softfail", @@ -80,7 +80,7 @@ func TestGetAuthenticationScore(t *testing.T) { Result: model.AuthResultResultSoftfail, }, }, - expectedScore: 4, + expectedScore: 5, // 30 * 17 / 100 = 5 }, { name: "No authentication", @@ -97,7 +97,7 @@ func TestGetAuthenticationScore(t *testing.T) { Result: model.AuthResultResultPass, }, }, - expectedScore: 35, // SPF (25) + BIMI (10) + expectedScore: 40, // SPF (30) + BIMI (10) }, } diff --git a/pkg/analyzer/authentication_x_aligned_from.go b/pkg/analyzer/authentication_x_aligned_from.go index ec1571c..45c2e2e 100644 --- a/pkg/analyzer/authentication_x_aligned_from.go +++ b/pkg/analyzer/authentication_x_aligned_from.go @@ -51,16 +51,16 @@ func (a *AuthenticationAnalyzer) calculateXAlignedFromScore(results *model.Authe if results.XAlignedFrom != nil { switch results.XAlignedFrom.Result { case model.AuthResultResultPass: - // pass: positive contribution - return 100 + // pass: no impact + return 0 case model.AuthResultResultFail: // fail: negative contribution - return 0 + return -100 default: // neutral, none, etc.: no impact return 0 } } - return 100 + return 0 } diff --git a/pkg/analyzer/authentication_x_aligned_from_test.go b/pkg/analyzer/authentication_x_aligned_from_test.go index 1ea6d1c..ee90c0d 100644 --- a/pkg/analyzer/authentication_x_aligned_from_test.go +++ b/pkg/analyzer/authentication_x_aligned_from_test.go @@ -92,18 +92,18 @@ func TestCalculateXAlignedFromScore(t *testing.T) { expectedScore int }{ { - name: "pass result gives positive score", + name: "pass result gives no penalty", result: &model.AuthResult{ Result: model.AuthResultResultPass, }, - expectedScore: 100, + expectedScore: 0, }, { - name: "fail result gives zero score", + name: "fail result gives full penalty", result: &model.AuthResult{ Result: model.AuthResultResultFail, }, - expectedScore: 0, + expectedScore: -100, }, { name: "neutral result gives zero score", diff --git a/pkg/analyzer/dns.go b/pkg/analyzer/dns.go index 3210dd1..6bc7c39 100644 --- a/pkg/analyzer/dns.go +++ b/pkg/analyzer/dns.go @@ -106,7 +106,7 @@ func (d *DNSAnalyzer) AnalyzeDNS(email *EmailMessage, headersResults *model.Head // Check DKIM records by parsing DKIM-Signature headers directly for _, sig := range parseDKIMSignatures(email.Header["Dkim-Signature"]) { - dkimRecord := d.checkDKIMRecord(sig.Domain, sig.Selector) + dkimRecord := d.checkDKIMRecord(sig) if dkimRecord != nil { if results.DkimRecords == nil { results.DkimRecords = new([]model.DKIMRecord) diff --git a/pkg/analyzer/dns_dkim.go b/pkg/analyzer/dns_dkim.go index 2ae03cb..115e347 100644 --- a/pkg/analyzer/dns_dkim.go +++ b/pkg/analyzer/dns_dkim.go @@ -23,6 +23,8 @@ package analyzer import ( "context" + "crypto/x509" + "encoding/base64" "fmt" "strings" @@ -30,17 +32,18 @@ import ( "git.happydns.org/happyDeliver/internal/utils" ) -// DKIMHeader holds the domain and selector extracted from a DKIM-Signature header. +// DKIMHeader holds the domain, selector and signing algorithm from a DKIM-Signature header. type DKIMHeader struct { - Domain string - Selector string + Domain string + Selector string + Algorithm string // from a= tag (e.g. rsa-sha256, ed25519-sha256) } -// parseDKIMSignatures extracts domain and selector from DKIM-Signature header values. +// parseDKIMSignatures extracts domain, selector and algorithm from DKIM-Signature header values. func parseDKIMSignatures(signatures []string) []DKIMHeader { var results []DKIMHeader for _, sig := range signatures { - var domain, selector string + var domain, selector, algorithm string for _, part := range strings.Split(sig, ";") { kv := strings.SplitN(strings.TrimSpace(part), "=", 2) if len(kv) != 2 { @@ -53,19 +56,61 @@ func parseDKIMSignatures(signatures []string) []DKIMHeader { domain = val case "s": selector = val + case "a": + algorithm = val } } if domain != "" && selector != "" { - results = append(results, DKIMHeader{Domain: domain, Selector: selector}) + results = append(results, DKIMHeader{Domain: domain, Selector: selector, Algorithm: algorithm}) } } return results } -// checkmodel.DKIMRecord looks up and validates DKIM record for a domain and selector -func (d *DNSAnalyzer) checkDKIMRecord(domain, selector string) *model.DKIMRecord { - // DKIM records are at: selector._domainkey.domain - dkimDomain := fmt.Sprintf("%s._domainkey.%s", selector, domain) +// parseDKIMTags splits a DKIM DNS record into a tag→value map. +func parseDKIMTags(record string) map[string]string { + tags := make(map[string]string) + for _, part := range strings.Split(record, ";") { + kv := strings.SplitN(strings.TrimSpace(part), "=", 2) + if len(kv) != 2 { + continue + } + tags[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1]) + } + return tags +} + +// parseKeySize derives the public key bit length from a base64-encoded DER public key. +// For RSA keys it parses the PKIX structure; for Ed25519 it always returns 256. +func parseKeySize(keyType, p string) *int { + switch strings.ToLower(keyType) { + case "ed25519": + return utils.PtrTo(256) + case "rsa", "": + der, err := base64.StdEncoding.DecodeString(p) + if err != nil { + // Try without padding + der, err = base64.RawStdEncoding.DecodeString(p) + if err != nil { + return nil + } + } + pub, err := x509.ParsePKIXPublicKey(der) + if err != nil { + return nil + } + if rsaPub, ok := pub.(interface{ Size() int }); ok { + bits := rsaPub.Size() * 8 + return &bits + } + return nil + } + return nil +} + +// checkDKIMRecord looks up and validates DKIM record for a domain and selector. +func (d *DNSAnalyzer) checkDKIMRecord(h DKIMHeader) *model.DKIMRecord { + dkimDomain := fmt.Sprintf("%s._domainkey.%s", h.Selector, h.Domain) ctx, cancel := context.WithTimeout(context.Background(), d.Timeout) defer cancel() @@ -73,53 +118,83 @@ func (d *DNSAnalyzer) checkDKIMRecord(domain, selector string) *model.DKIMRecord txtRecords, err := d.resolver.LookupTXT(ctx, dkimDomain) if err != nil { return &model.DKIMRecord{ - Selector: selector, - Domain: domain, - Valid: false, - Error: utils.PtrTo(fmt.Sprintf("Failed to lookup DKIM record: %v", err)), + Selector: h.Selector, + Domain: h.Domain, + SigningAlgorithm: signingAlgorithmPtr(h.Algorithm), + Valid: false, + Error: utils.PtrTo(fmt.Sprintf("Failed to lookup DKIM record: %v", err)), } } if len(txtRecords) == 0 { return &model.DKIMRecord{ - Selector: selector, - Domain: domain, - Valid: false, - Error: utils.PtrTo("No DKIM record found"), + Selector: h.Selector, + Domain: h.Domain, + SigningAlgorithm: signingAlgorithmPtr(h.Algorithm), + Valid: false, + Error: utils.PtrTo("No DKIM record found"), } } // Concatenate all TXT record parts (DKIM can be split) dkimRecord := strings.Join(txtRecords, "") - // Basic validation - should contain "v=DKIM1" and "p=" (public key) if !d.validateDKIM(dkimRecord) { return &model.DKIMRecord{ - Selector: selector, - Domain: domain, - Record: utils.PtrTo(dkimRecord), - Valid: false, - Error: utils.PtrTo("DKIM record appears malformed"), + Selector: h.Selector, + Domain: h.Domain, + Record: utils.PtrTo(dkimRecord), + SigningAlgorithm: signingAlgorithmPtr(h.Algorithm), + Valid: false, + Error: utils.PtrTo("DKIM record appears malformed"), } } + tags := parseDKIMTags(dkimRecord) + + keyType := tags["k"] + if keyType == "" { + keyType = "rsa" // RFC 6376 default + } + + var hashAlgorithms []string + if h, ok := tags["h"]; ok && h != "" { + for _, alg := range strings.Split(h, ":") { + if a := strings.TrimSpace(alg); a != "" { + hashAlgorithms = append(hashAlgorithms, a) + } + } + } + if hashAlgorithms == nil { + hashAlgorithms = []string{} + } + return &model.DKIMRecord{ - Selector: selector, - Domain: domain, - Record: &dkimRecord, - Valid: true, + Selector: h.Selector, + Domain: h.Domain, + Record: &dkimRecord, + KeyType: utils.PtrTo(keyType), + HashAlgorithms: &hashAlgorithms, + SigningAlgorithm: signingAlgorithmPtr(h.Algorithm), + KeySize: parseKeySize(keyType, tags["p"]), + Valid: true, } } -// validateDKIM performs basic DKIM record validation +func signingAlgorithmPtr(a string) *string { + if a == "" { + return nil + } + return &a +} + +// validateDKIM performs basic DKIM record validation. func (d *DNSAnalyzer) validateDKIM(record string) bool { - // Should contain p= tag (public key) if !strings.Contains(record, "p=") { return false } - // Often contains v=DKIM1 but not required - // If v= is present, it should be DKIM1 + // If v= is present, it must be DKIM1 if strings.Contains(record, "v=") && !strings.Contains(record, "v=DKIM1") { return false } @@ -128,21 +203,57 @@ func (d *DNSAnalyzer) validateDKIM(record string) bool { } func (d *DNSAnalyzer) calculateDKIMScore(results *model.DNSResults) (score int) { - // DKIM provides strong email authentication - if results.DkimRecords != nil && len(*results.DkimRecords) > 0 { - hasValidDKIM := false - for _, dkim := range *results.DkimRecords { - if dkim.Valid { - hasValidDKIM = true - break + if results.DkimRecords == nil || len(*results.DkimRecords) == 0 { + return 0 + } + + hasValid := false + for _, dkim := range *results.DkimRecords { + if dkim.Valid { + hasValid = true + break + } + } + + if !hasValid { + return 25 + } + + score = 100 + + // Apply security penalties on the best valid record + for _, dkim := range *results.DkimRecords { + if !dkim.Valid { + continue + } + + // SHA-1 signing is deprecated (RFC 8301) + if dkim.SigningAlgorithm != nil && strings.HasSuffix(*dkim.SigningAlgorithm, "-sha1") { + if score > 60 { + score = 60 } } - if hasValidDKIM { - score += 100 - } else { - // Partial credit if DKIM record exists but has issues - score += 25 + + // Key size penalties apply only to RSA + keyType := "" + if dkim.KeyType != nil { + keyType = strings.ToLower(*dkim.KeyType) } + if keyType == "rsa" || keyType == "" { + if dkim.KeySize != nil { + switch { + case *dkim.KeySize < 1024: + if score > 25 { + score = 25 + } + case *dkim.KeySize < 2048: + if score > 75 { + score = 75 + } + } + } + } + // Ed25519 keys (256-bit curve, ~3000-bit RSA equivalent) need no penalty. } return diff --git a/pkg/analyzer/dns_dkim_test.go b/pkg/analyzer/dns_dkim_test.go index 45da53c..40e28a5 100644 --- a/pkg/analyzer/dns_dkim_test.go +++ b/pkg/analyzer/dns_dkim_test.go @@ -22,6 +22,10 @@ package analyzer import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" "testing" "time" ) @@ -47,56 +51,56 @@ func TestParseDKIMSignatures(t *testing.T) { signatures: []string{ `v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=from:to:subject:date:message-id; bh=abcdef1234567890=; b=SIGNATURE_DATA_HERE==`, }, - expected: []DKIMHeader{{Domain: "gmail.com", Selector: "20210112"}}, + expected: []DKIMHeader{{Domain: "gmail.com", Selector: "20210112", Algorithm: "rsa-sha256"}}, }, { name: "Microsoft 365 style", signatures: []string{ `v=1; a=rsa-sha256; c=relaxed/relaxed; d=contoso.com; s=selector1; h=From:Date:Subject:Message-ID; bh=UErATeHehIIPIXPeUA==; b=SIGNATURE_DATA==`, }, - expected: []DKIMHeader{{Domain: "contoso.com", Selector: "selector1"}}, + expected: []DKIMHeader{{Domain: "contoso.com", Selector: "selector1", Algorithm: "rsa-sha256"}}, }, { name: "Tab-folded multiline (Postfix-style)", signatures: []string{ "v=1; a=rsa-sha256; c=relaxed/simple; d=nemunai.re; s=thot;\r\n\tt=1760866834; bh=YNB7c8Qgm8YGn9X1FAXTcdpO7t4YSZFiMrmpCfD/3zw=;\r\n\th=From:To:Subject;\r\n\tb=T4TFaypMpsHGYCl3PGLwmzOYRF11rYjC7lF8V5VFU+ldvG8WBpFn==", }, - expected: []DKIMHeader{{Domain: "nemunai.re", Selector: "thot"}}, + expected: []DKIMHeader{{Domain: "nemunai.re", Selector: "thot", Algorithm: "rsa-sha256"}}, }, { name: "Space-folded multiline (RFC-style)", signatures: []string{ "v=1; a=rsa-sha256; c=relaxed/relaxed;\r\n d=football.example.com; i=@football.example.com;\r\n q=dns/txt; s=test; t=1528637909; h=from:to:subject;\r\n bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;\r\n b=F45dVWDfMbQDGHJFlXUNB2HKfbCeLRyhDXgFpEL8Gwps==", }, - expected: []DKIMHeader{{Domain: "football.example.com", Selector: "test"}}, + expected: []DKIMHeader{{Domain: "football.example.com", Selector: "test", Algorithm: "rsa-sha256"}}, }, { name: "d= and s= on separate continuation lines", signatures: []string{ "v=1; a=rsa-sha256;\r\n\tc=relaxed/relaxed;\r\n\td=mycompany.com;\r\n\ts=selector1;\r\n\tbh=hash=;\r\n\tb=sig==", }, - expected: []DKIMHeader{{Domain: "mycompany.com", Selector: "selector1"}}, + expected: []DKIMHeader{{Domain: "mycompany.com", Selector: "selector1", Algorithm: "rsa-sha256"}}, }, { name: "No space after semicolons", signatures: []string{ `v=1;a=rsa-sha256;c=relaxed/relaxed;d=example.net;s=mail;h=from:to:subject;bh=abc=;b=xyz==`, }, - expected: []DKIMHeader{{Domain: "example.net", Selector: "mail"}}, + expected: []DKIMHeader{{Domain: "example.net", Selector: "mail", Algorithm: "rsa-sha256"}}, }, { name: "Multiple spaces after semicolons", signatures: []string{ `v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=myselector; bh=hash=; b=sig==`, }, - expected: []DKIMHeader{{Domain: "example.com", Selector: "myselector"}}, + expected: []DKIMHeader{{Domain: "example.com", Selector: "myselector", Algorithm: "rsa-sha256"}}, }, { name: "Ed25519 signature (RFC 8463)", signatures: []string{ "v=1; a=ed25519-sha256; c=relaxed/relaxed;\r\n d=football.example.com; i=@football.example.com;\r\n q=dns/txt; s=brisbane; t=1528637909; h=from:to:subject;\r\n bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;\r\n b=/gCrinpcQOoIfuHNQIbq4pgh9kyIK3AQ==", }, - expected: []DKIMHeader{{Domain: "football.example.com", Selector: "brisbane"}}, + expected: []DKIMHeader{{Domain: "football.example.com", Selector: "brisbane", Algorithm: "ed25519-sha256"}}, }, { name: "Multiple signatures (ESP double-signing)", @@ -105,8 +109,8 @@ func TestParseDKIMSignatures(t *testing.T) { `v=1; a=rsa-sha256; c=relaxed/relaxed; d=sendib.com; s=mail; h=from:to:subject; bh=hash1=; b=sig2==`, }, expected: []DKIMHeader{ - {Domain: "mydomain.com", Selector: "mail"}, - {Domain: "sendib.com", Selector: "mail"}, + {Domain: "mydomain.com", Selector: "mail", Algorithm: "rsa-sha256"}, + {Domain: "sendib.com", Selector: "mail", Algorithm: "rsa-sha256"}, }, }, { @@ -116,8 +120,8 @@ func TestParseDKIMSignatures(t *testing.T) { `v=1; a=rsa-sha256; c=relaxed/relaxed; d=football.example.com; s=test; h=from:to:subject; bh=hash=; b=rsaSig==`, }, expected: []DKIMHeader{ - {Domain: "football.example.com", Selector: "brisbane"}, - {Domain: "football.example.com", Selector: "test"}, + {Domain: "football.example.com", Selector: "brisbane", Algorithm: "ed25519-sha256"}, + {Domain: "football.example.com", Selector: "test", Algorithm: "rsa-sha256"}, }, }, { @@ -127,8 +131,8 @@ func TestParseDKIMSignatures(t *testing.T) { `v=1; a=rsa-sha256; c=relaxed/simple; d=customerdomain.io; s=ug7nbtf4gccmlpwj322ax3p6ow6fovbt; h=from:to:subject; bh=sesHash=; b=customSig==`, }, expected: []DKIMHeader{ - {Domain: "amazonses.com", Selector: "224i4yxa5dv7c2xz3womw6peuabd"}, - {Domain: "customerdomain.io", Selector: "ug7nbtf4gccmlpwj322ax3p6ow6fovbt"}, + {Domain: "amazonses.com", Selector: "224i4yxa5dv7c2xz3womw6peuabd", Algorithm: "rsa-sha256"}, + {Domain: "customerdomain.io", Selector: "ug7nbtf4gccmlpwj322ax3p6ow6fovbt", Algorithm: "rsa-sha256"}, }, }, { @@ -136,56 +140,56 @@ func TestParseDKIMSignatures(t *testing.T) { signatures: []string{ `v=1; a=rsa-sha256; c=relaxed/relaxed; d=mail.example.co.uk; s=dkim2025; h=from:to:subject; bh=hash=; b=sig==`, }, - expected: []DKIMHeader{{Domain: "mail.example.co.uk", Selector: "dkim2025"}}, + expected: []DKIMHeader{{Domain: "mail.example.co.uk", Selector: "dkim2025", Algorithm: "rsa-sha256"}}, }, { name: "Deeply nested subdomain", signatures: []string{ `v=1; a=rsa-sha256; c=relaxed/relaxed; d=bounce.transactional.mail.example.com; s=s2048; h=from:to:subject; bh=hash=; b=sig==`, }, - expected: []DKIMHeader{{Domain: "bounce.transactional.mail.example.com", Selector: "s2048"}}, + expected: []DKIMHeader{{Domain: "bounce.transactional.mail.example.com", Selector: "s2048", Algorithm: "rsa-sha256"}}, }, { name: "Selector with hyphens (Microsoft 365 custom domain style)", signatures: []string{ `v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=selector1-contoso-com; h=from:to:subject; bh=hash=; b=sig==`, }, - expected: []DKIMHeader{{Domain: "example.com", Selector: "selector1-contoso-com"}}, + expected: []DKIMHeader{{Domain: "example.com", Selector: "selector1-contoso-com", Algorithm: "rsa-sha256"}}, }, { name: "Selector with dots", signatures: []string{ `v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=smtp.mail; h=from:to:subject; bh=hash=; b=sig==`, }, - expected: []DKIMHeader{{Domain: "example.com", Selector: "smtp.mail"}}, + expected: []DKIMHeader{{Domain: "example.com", Selector: "smtp.mail", Algorithm: "rsa-sha256"}}, }, { name: "Single-character selector", signatures: []string{ `v=1; a=rsa-sha256; c=relaxed/relaxed; d=tiny.io; s=x; h=from:to:subject; bh=hash=; b=sig==`, }, - expected: []DKIMHeader{{Domain: "tiny.io", Selector: "x"}}, + expected: []DKIMHeader{{Domain: "tiny.io", Selector: "x", Algorithm: "rsa-sha256"}}, }, { name: "Postmark-style timestamp selector, s= before d=", signatures: []string{ `v=1; a=rsa-sha1; c=relaxed/relaxed; s=20130519032151pm; d=postmarkapp.com; h=From:Date:Subject; bh=vYFvy46eesUDGJ45hyBTH30JfN4=; b=iHeFQ+7rCiSQs3DPjR2eUSZSv4i==`, }, - expected: []DKIMHeader{{Domain: "postmarkapp.com", Selector: "20130519032151pm"}}, + expected: []DKIMHeader{{Domain: "postmarkapp.com", Selector: "20130519032151pm", Algorithm: "rsa-sha1"}}, }, { name: "d= and s= at the very end", signatures: []string{ `v=1; a=rsa-sha256; c=relaxed/relaxed; h=from:to:subject; bh=hash=; b=sig==; d=example.net; s=trailing`, }, - expected: []DKIMHeader{{Domain: "example.net", Selector: "trailing"}}, + expected: []DKIMHeader{{Domain: "example.net", Selector: "trailing", Algorithm: "rsa-sha256"}}, }, { name: "Full tag set", signatures: []string{ `v=1; a=rsa-sha256; d=example.com; s=selector1; c=relaxed/simple; q=dns/txt; i=user@example.com; t=1255993973; x=1256598773; h=From:Sender:Reply-To:Subject:Date:Message-Id:To:Cc; bh=+7qxGePcmmrtZAIVQAtkSSGHfQ/ftNuvUTWJ3vXC9Zc=; b=dB85+qM+If1KGQmqMLNpqLgNtUaG5dhGjYjQD6/QXtXmViJx8tf9gLEjcHr+musLCAvr0Fsn1DA3ZLLlUxpf4AR==`, }, - expected: []DKIMHeader{{Domain: "example.com", Selector: "selector1"}}, + expected: []DKIMHeader{{Domain: "example.com", Selector: "selector1", Algorithm: "rsa-sha256"}}, }, { name: "Missing d= tag", @@ -216,8 +220,8 @@ func TestParseDKIMSignatures(t *testing.T) { `v=1; a=rsa-sha256; c=relaxed/relaxed; d=also-good.com; s=sel2; h=from:to; bh=hash=; b=sig==`, }, expected: []DKIMHeader{ - {Domain: "good.com", Selector: "sel1"}, - {Domain: "also-good.com", Selector: "sel2"}, + {Domain: "good.com", Selector: "sel1", Algorithm: "rsa-sha256"}, + {Domain: "also-good.com", Selector: "sel2", Algorithm: "rsa-sha256"}, }, }, } @@ -235,6 +239,9 @@ func TestParseDKIMSignatures(t *testing.T) { if result[i].Selector != tt.expected[i].Selector { t.Errorf("result[%d].Selector = %q, want %q", i, result[i].Selector, tt.expected[i].Selector) } + if result[i].Algorithm != tt.expected[i].Algorithm { + t.Errorf("result[%d].Algorithm = %q, want %q", i, result[i].Algorithm, tt.expected[i].Algorithm) + } } }) } @@ -284,3 +291,119 @@ func TestValidateDKIM(t *testing.T) { }) } } + +func TestParseDKIMTags(t *testing.T) { + tests := []struct { + name string + record string + wantTags map[string]string + }{ + { + name: "standard RSA record", + record: "v=DKIM1; k=rsa; p=MIIBI; h=sha256", + wantTags: map[string]string{"v": "DKIM1", "k": "rsa", "p": "MIIBI", "h": "sha256"}, + }, + { + name: "ed25519 record", + record: "v=DKIM1; k=ed25519; p=11qYAYKxCrfVS", + wantTags: map[string]string{"v": "DKIM1", "k": "ed25519", "p": "11qYAYKxCrfVS"}, + }, + { + name: "missing k= defaults", + record: "v=DKIM1; p=MIIBI", + wantTags: map[string]string{"v": "DKIM1", "p": "MIIBI"}, + }, + { + name: "empty record", + record: "", + wantTags: map[string]string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := parseDKIMTags(tt.record) + for key, want := range tt.wantTags { + if got[key] != want { + t.Errorf("tag %q = %q, want %q", key, got[key], want) + } + } + }) + } +} + +func TestParseKeySize(t *testing.T) { + // Generate a real RSA key for testing + rsaKey1024, _ := rsa.GenerateKey(rand.Reader, 1024) + rsaKey2048, _ := rsa.GenerateKey(rand.Reader, 2048) + + der1024, _ := x509.MarshalPKIXPublicKey(&rsaKey1024.PublicKey) + der2048, _ := x509.MarshalPKIXPublicKey(&rsaKey2048.PublicKey) + + p1024 := base64.StdEncoding.EncodeToString(der1024) + p2048 := base64.StdEncoding.EncodeToString(der2048) + + tests := []struct { + name string + keyType string + p string + want *int + }{ + { + name: "RSA 1024", + keyType: "rsa", + p: p1024, + want: intPtr(1024), + }, + { + name: "RSA 2048", + keyType: "rsa", + p: p2048, + want: intPtr(2048), + }, + { + name: "Ed25519 always 256", + keyType: "ed25519", + p: "11qYAYKxCrfVS", + want: intPtr(256), + }, + { + name: "Unknown key type", + keyType: "unknown", + p: "somedata", + want: nil, + }, + { + name: "Invalid RSA base64", + keyType: "rsa", + p: "!!!not-base64!!!", + want: nil, + }, + { + name: "Empty k= defaults to RSA", + keyType: "", + p: p2048, + want: intPtr(2048), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := parseKeySize(tt.keyType, tt.p) + if tt.want == nil { + if got != nil { + t.Errorf("parseKeySize(%q, ...) = %d, want nil", tt.keyType, *got) + } + return + } + if got == nil { + t.Fatalf("parseKeySize(%q, ...) = nil, want %d", tt.keyType, *tt.want) + } + if *got != *tt.want { + t.Errorf("parseKeySize(%q, ...) = %d, want %d", tt.keyType, *got, *tt.want) + } + }) + } +} + +func intPtr(v int) *int { return &v } diff --git a/pkg/analyzer/dns_dmarc.go b/pkg/analyzer/dns_dmarc.go index 686fb0b..b89500b 100644 --- a/pkg/analyzer/dns_dmarc.go +++ b/pkg/analyzer/dns_dmarc.go @@ -24,233 +24,290 @@ package analyzer import ( "context" "fmt" - "regexp" + "net" + "strconv" "strings" "git.happydns.org/happyDeliver/internal/model" "git.happydns.org/happyDeliver/internal/utils" ) -// checkmodel.DMARCRecord looks up and validates DMARC record for a domain -func (d *DNSAnalyzer) checkDMARCRecord(domain string) *model.DMARCRecord { - // DMARC records are at: _dmarc.domain - dmarcDomain := fmt.Sprintf("_dmarc.%s", domain) +var dmarcPolicyStrength = map[string]int{"none": 0, "quarantine": 1, "reject": 2} +// lookupDMARCAt queries _dmarc. and returns the raw DMARC1 TXT record. +// notFound=true means no record exists (NXDOMAIN or empty); false means a real DNS error occurred. +func (d *DNSAnalyzer) lookupDMARCAt(domain string) (record string, notFound bool, err error) { ctx, cancel := context.WithTimeout(context.Background(), d.Timeout) defer cancel() - txtRecords, err := d.resolver.LookupTXT(ctx, dmarcDomain) + txtRecords, lookupErr := d.resolver.LookupTXT(ctx, fmt.Sprintf("_dmarc.%s", domain)) + if lookupErr != nil { + if dnsErr, ok := lookupErr.(*net.DNSError); ok && dnsErr.IsNotFound { + return "", true, nil + } + return "", false, lookupErr + } + + for _, txt := range txtRecords { + if strings.HasPrefix(txt, "v=DMARC1") { + return txt, false, nil + } + } + return "", true, nil +} + +// parseDMARCRecord parses a raw DMARC TXT record into a DMARCRecord model. +func (d *DNSAnalyzer) parseDMARCRecord(foundDomain, rawRecord string) *model.DMARCRecord { + tags := parseDKIMTags(rawRecord) + + // Policy + policy := "unknown" + switch tags["p"] { + case "none", "quarantine", "reject": + policy = tags["p"] + } + + // SPF alignment (default: relaxed) + spfAlignment := utils.PtrTo(model.DMARCRecordSpfAlignmentRelaxed) + if tags["aspf"] == "s" { + spfAlignment = utils.PtrTo(model.DMARCRecordSpfAlignmentStrict) + } + + // DKIM alignment (default: relaxed) + dkimAlignment := utils.PtrTo(model.DMARCRecordDkimAlignmentRelaxed) + if tags["adkim"] == "s" { + dkimAlignment = utils.PtrTo(model.DMARCRecordDkimAlignmentStrict) + } + + // Subdomain policy + var subdomainPolicy *model.DMARCRecordSubdomainPolicy + switch tags["sp"] { + case "none", "quarantine", "reject": + subdomainPolicy = utils.PtrTo(model.DMARCRecordSubdomainPolicy(tags["sp"])) + } + + // Non-existent subdomain policy (DMARCbis np=) + var nonexistentSubdomainPolicy *model.DMARCRecordNonexistentSubdomainPolicy + switch tags["np"] { + case "none", "quarantine", "reject": + nonexistentSubdomainPolicy = utils.PtrTo(model.DMARCRecordNonexistentSubdomainPolicy(tags["np"])) + } + + // Percentage (pct=, deprecated in DMARCbis) + var percentage *int + if pctStr, ok := tags["pct"]; ok { + if pct, err := strconv.Atoi(pctStr); err == nil && pct >= 0 && pct <= 100 { + percentage = &pct + } + } + + // Test mode (DMARCbis t=) + var testMode *bool + if t, ok := tags["t"]; ok { + v := t == "y" + testMode = &v + } + + // PSD (DMARCbis psd=) + var psd *model.DMARCRecordPsd + switch tags["psd"] { + case "y", "n", "u": + psd = utils.PtrTo(model.DMARCRecordPsd(tags["psd"])) + } + + rec := &model.DMARCRecord{ + Domain: &foundDomain, + Record: &rawRecord, + Policy: utils.PtrTo(model.DMARCRecordPolicy(policy)), + SubdomainPolicy: subdomainPolicy, + NonexistentSubdomainPolicy: nonexistentSubdomainPolicy, + Percentage: percentage, + TestMode: testMode, + Psd: psd, + SpfAlignment: spfAlignment, + DkimAlignment: dkimAlignment, + } + if percentage != nil { + rec.DeprecatedPct = utils.PtrTo(true) + } + if _, ok := tags["rf"]; ok { + rec.DeprecatedRf = utils.PtrTo(true) + } + if _, ok := tags["ri"]; ok { + rec.DeprecatedRi = utils.PtrTo(true) + } + + if !d.validateDMARC(rawRecord) { + rec.Valid = false + rec.Error = utils.PtrTo("DMARC record appears malformed") + return rec + } + + rec.Valid = true + return rec +} + +// walkDNSForDMARC implements the DMARCbis DNS Tree Walk algorithm (Section 4.10). +// It queries _dmarc. and walks up the label hierarchy until a valid DMARC +// record is found or all labels are exhausted. Maximum 8 DNS queries per message. +// For domains with ≥8 labels, after the initial miss the walk jumps to the 7-label +// suffix before resuming normally (to stay within the 8-query budget). +// Single-label (TLD) records are only accepted when they carry psd=y. +func (d *DNSAnalyzer) walkDNSForDMARC(domain string) (record, foundDomain string, err error) { + labels := strings.Split(strings.ToLower(strings.TrimSuffix(domain, ".")), ".") + n := len(labels) + + for i, queries := 0, 0; i < n && queries < 8; i, queries = i+1, queries+1 { + current := strings.Join(labels[i:], ".") + + raw, notFound, lookupErr := d.lookupDMARCAt(current) + if lookupErr != nil { + return "", "", lookupErr + } + if !notFound { + // Single-label (TLD) records are only used when the record explicitly opts in. + if !strings.Contains(current, ".") { + if d.extractDMARCPSDValue(raw) != "y" { + break + } + } + return raw, current, nil + } + + // DMARCbis §4.10: after missing on a ≥8-label domain, shortcut to the + // 7-label suffix for the next query rather than stepping one label at a time. + if i == 0 && n >= 8 { + i = n - 8 // the outer i++ will land at n-7 (7 labels from the right) + } + } + + return "", "", nil +} + +// checkDMARCRecord looks up and validates the DMARC record for a domain using +// the DMARCbis DNS Tree Walk algorithm (Section 4.10), which supersedes the +// RFC 7489 PSL-based organizational domain lookup and the RFC 9091 PSD DMARC +// experimental fallback. +func (d *DNSAnalyzer) checkDMARCRecord(domain string) *model.DMARCRecord { + raw, foundDomain, err := d.walkDNSForDMARC(domain) if err != nil { return &model.DMARCRecord{ Valid: false, Error: utils.PtrTo(fmt.Sprintf("Failed to lookup DMARC record: %v", err)), } } - - // Find DMARC record (starts with "v=DMARC1") - var dmarcRecord string - for _, txt := range txtRecords { - if strings.HasPrefix(txt, "v=DMARC1") { - dmarcRecord = txt - break - } - } - - if dmarcRecord == "" { + if foundDomain == "" { return &model.DMARCRecord{ Valid: false, Error: utils.PtrTo("No DMARC record found"), } } - - // Extract policy - policy := d.extractDMARCPolicy(dmarcRecord) - - // Extract subdomain policy - subdomainPolicy := d.extractDMARCSubdomainPolicy(dmarcRecord) - - // Extract percentage - percentage := d.extractDMARCPercentage(dmarcRecord) - - // Extract alignment modes - spfAlignment := d.extractDMARCSPFAlignment(dmarcRecord) - dkimAlignment := d.extractDMARCDKIMAlignment(dmarcRecord) - - // Basic validation - if !d.validateDMARC(dmarcRecord) { - return &model.DMARCRecord{ - Record: &dmarcRecord, - Policy: utils.PtrTo(model.DMARCRecordPolicy(policy)), - SubdomainPolicy: subdomainPolicy, - Percentage: percentage, - SpfAlignment: spfAlignment, - DkimAlignment: dkimAlignment, - Valid: false, - Error: utils.PtrTo("DMARC record appears malformed"), - } - } - - return &model.DMARCRecord{ - Record: &dmarcRecord, - Policy: utils.PtrTo(model.DMARCRecordPolicy(policy)), - SubdomainPolicy: subdomainPolicy, - Percentage: percentage, - SpfAlignment: spfAlignment, - DkimAlignment: dkimAlignment, - Valid: true, - } + return d.parseDMARCRecord(foundDomain, raw) } -// extractDMARCPolicy extracts the policy from a DMARC record -func (d *DNSAnalyzer) extractDMARCPolicy(record string) string { - // Look for p=none, p=quarantine, or p=reject - re := regexp.MustCompile(`p=(none|quarantine|reject)`) - matches := re.FindStringSubmatch(record) - if len(matches) > 1 { - return matches[1] +// extractDMARCPSDValue returns the raw psd= value ("y", "n", "u") or "" if absent. +// Used during DNS Tree Walk before full record parsing. +func (d *DNSAnalyzer) extractDMARCPSDValue(record string) string { + v := parseDKIMTags(record)["psd"] + switch v { + case "y", "n", "u": + return v } - return "unknown" + return "" } -// extractDMARCSPFAlignment extracts SPF alignment mode from a DMARC record -// Returns "relaxed" (default) or "strict" -func (d *DNSAnalyzer) extractDMARCSPFAlignment(record string) *model.DMARCRecordSpfAlignment { - // Look for aspf=s (strict) or aspf=r (relaxed) - re := regexp.MustCompile(`aspf=(r|s)`) - matches := re.FindStringSubmatch(record) - if len(matches) > 1 { - if matches[1] == "s" { - return utils.PtrTo(model.DMARCRecordSpfAlignmentStrict) - } - return utils.PtrTo(model.DMARCRecordSpfAlignmentRelaxed) - } - // Default is relaxed if not specified - return utils.PtrTo(model.DMARCRecordSpfAlignmentRelaxed) -} - -// extractDMARCDKIMAlignment extracts DKIM alignment mode from a DMARC record -// Returns "relaxed" (default) or "strict" -func (d *DNSAnalyzer) extractDMARCDKIMAlignment(record string) *model.DMARCRecordDkimAlignment { - // Look for adkim=s (strict) or adkim=r (relaxed) - re := regexp.MustCompile(`adkim=(r|s)`) - matches := re.FindStringSubmatch(record) - if len(matches) > 1 { - if matches[1] == "s" { - return utils.PtrTo(model.DMARCRecordDkimAlignmentStrict) - } - return utils.PtrTo(model.DMARCRecordDkimAlignmentRelaxed) - } - // Default is relaxed if not specified - return utils.PtrTo(model.DMARCRecordDkimAlignmentRelaxed) -} - -// extractDMARCSubdomainPolicy extracts subdomain policy from a DMARC record -// Returns the sp tag value or nil if not specified (defaults to main policy) -func (d *DNSAnalyzer) extractDMARCSubdomainPolicy(record string) *model.DMARCRecordSubdomainPolicy { - // Look for sp=none, sp=quarantine, or sp=reject - re := regexp.MustCompile(`sp=(none|quarantine|reject)`) - matches := re.FindStringSubmatch(record) - if len(matches) > 1 { - return utils.PtrTo(model.DMARCRecordSubdomainPolicy(matches[1])) - } - // If sp is not specified, it defaults to the main policy (p tag) - // Return nil to indicate it's using the default - return nil -} - -// extractDMARCPercentage extracts the percentage from a DMARC record -// Returns the pct tag value or nil if not specified (defaults to 100) -func (d *DNSAnalyzer) extractDMARCPercentage(record string) *int { - // Look for pct= - re := regexp.MustCompile(`pct=(\d+)`) - matches := re.FindStringSubmatch(record) - if len(matches) > 1 { - // Convert string to int - var pct int - fmt.Sscanf(matches[1], "%d", &pct) - // Validate range (0-100) - if pct >= 0 && pct <= 100 { - return &pct - } - } - // Default is 100 if not specified - return nil -} - -// validateDMARC performs basic DMARC record validation +// validateDMARC performs basic DMARC record validation. +// Per DMARCbis, p= is now RECOMMENDED (not required): a record with a valid +// rua= but no p= is treated as p=none and considered valid. func (d *DNSAnalyzer) validateDMARC(record string) bool { - // Must start with v=DMARC1 if !strings.HasPrefix(record, "v=DMARC1") { return false } - // Must have a policy tag + // p= absent is allowed in DMARCbis when rua= is present (treated as p=none). if !strings.Contains(record, "p=") { - return false + return strings.Contains(record, "rua=") } return true } func (d *DNSAnalyzer) calculateDMARCScore(results *model.DNSResults) (score int) { - // DMARC ties SPF and DKIM together and provides policy - if results.DmarcRecord != nil { - if results.DmarcRecord.Valid { - score += 50 - // Bonus points for stricter policies - if results.DmarcRecord.Policy != nil { - switch *results.DmarcRecord.Policy { - case "reject": - // Strictest policy - full points already awarded - score += 25 - case "quarantine": - // Good policy - no deduction - case "none": - // Weakest policy - deduct 5 points - score -= 25 - } - } - // Bonus points for strict alignment modes (2 points each) - if results.DmarcRecord.SpfAlignment != nil && *results.DmarcRecord.SpfAlignment == model.DMARCRecordSpfAlignmentStrict { - score += 5 - } - if results.DmarcRecord.DkimAlignment != nil && *results.DmarcRecord.DkimAlignment == model.DMARCRecordDkimAlignmentStrict { - score += 5 - } - // Subdomain policy scoring (sp tag) - // +3 for stricter or equal subdomain policy, -3 for weaker - if results.DmarcRecord.SubdomainPolicy != nil { - mainPolicy := string(*results.DmarcRecord.Policy) - subPolicy := string(*results.DmarcRecord.SubdomainPolicy) + if results.DmarcRecord == nil { + return + } - // Policy strength: none < quarantine < reject - policyStrength := map[string]int{"none": 0, "quarantine": 1, "reject": 2} - - mainStrength := policyStrength[mainPolicy] - subStrength := policyStrength[subPolicy] - - if subStrength >= mainStrength { - // Subdomain policy is equal or stricter - score += 15 - } else { - // Subdomain policy is weaker - score -= 15 - } - } else { - // No sp tag means subdomains inherit main policy (good default) - score += 15 - } - // Percentage scoring (pct tag) - // Apply the percentage on the current score - if results.DmarcRecord.Percentage != nil { - pct := *results.DmarcRecord.Percentage - - score = score * pct / 100 - } - } else if results.DmarcRecord.Record != nil { - // Partial credit if DMARC record exists but has issues + if !results.DmarcRecord.Valid { + if results.DmarcRecord.Record != nil { + // Partial credit if a DMARC record exists but has issues score += 20 } + return + } + + score += 50 + + // Determine effective policy: DMARCbis t=y downgrades policy one level. + effectivePolicy := "none" + if results.DmarcRecord.Policy != nil { + effectivePolicy = string(*results.DmarcRecord.Policy) + } + testMode := results.DmarcRecord.TestMode != nil && *results.DmarcRecord.TestMode + if testMode { + switch effectivePolicy { + case "reject": + effectivePolicy = "quarantine" + case "quarantine": + effectivePolicy = "none" + } + } + + // Bonus/penalty for policy strength + switch effectivePolicy { + case "reject": + score += 25 + case "none": + score -= 25 + } + + // Bonus points for strict alignment modes + if results.DmarcRecord.SpfAlignment != nil && *results.DmarcRecord.SpfAlignment == model.DMARCRecordSpfAlignmentStrict { + score += 5 + } + if results.DmarcRecord.DkimAlignment != nil && *results.DmarcRecord.DkimAlignment == model.DMARCRecordDkimAlignmentStrict { + score += 5 + } + + // Subdomain policy scoring (sp tag): +15 for equal-or-stricter, -15 for weaker + if results.DmarcRecord.SubdomainPolicy != nil { + subPolicy := string(*results.DmarcRecord.SubdomainPolicy) + if dmarcPolicyStrength[subPolicy] >= dmarcPolicyStrength[effectivePolicy] { + score += 15 + } else { + score -= 15 + } + } else { + score += 15 // inherits main policy — good default + } + + // Non-existent subdomain policy scoring (np tag, DMARCbis): +15 for equal-or-stricter, -15 for weaker + effectiveSubPolicy := effectivePolicy + if results.DmarcRecord.SubdomainPolicy != nil { + effectiveSubPolicy = string(*results.DmarcRecord.SubdomainPolicy) + } + if results.DmarcRecord.NonexistentSubdomainPolicy == nil { + score += 15 // inherits subdomain/main policy — good default + } else if dmarcPolicyStrength[string(*results.DmarcRecord.NonexistentSubdomainPolicy)] >= dmarcPolicyStrength[effectiveSubPolicy] { + score += 15 + } else { + score -= 15 + } + + // pct= scaling (deprecated in DMARCbis, kept for backward compatibility). + // pct=0 is an anti-pattern: score it as zero enforcement. + if results.DmarcRecord.Percentage != nil { + pct := *results.DmarcRecord.Percentage + score = score * pct / 100 } return diff --git a/pkg/analyzer/dns_dmarc_test.go b/pkg/analyzer/dns_dmarc_test.go index 93f4511..5c34a32 100644 --- a/pkg/analyzer/dns_dmarc_test.go +++ b/pkg/analyzer/dns_dmarc_test.go @@ -22,14 +22,206 @@ package analyzer import ( + "context" + "fmt" + "net" "testing" "time" - "git.happydns.org/happyDeliver/internal/model" "git.happydns.org/happyDeliver/internal/utils" ) -func TestExtractDMARCPolicy(t *testing.T) { +// mockDNSResolver maps domain names to TXT records for testing. +// An entry with value nil means NXDOMAIN; an error value triggers a DNS error. +type mockDNSResolver struct { + txt map[string][]string + err map[string]error +} + +func (m *mockDNSResolver) LookupTXT(_ context.Context, name string) ([]string, error) { + if err, ok := m.err[name]; ok { + return nil, err + } + if records, ok := m.txt[name]; ok { + return records, nil + } + return nil, &net.DNSError{Err: "no such host", Name: name, IsNotFound: true} +} + +func (m *mockDNSResolver) LookupMX(_ context.Context, _ string) ([]*net.MX, error) { + return nil, nil +} +func (m *mockDNSResolver) LookupAddr(_ context.Context, _ string) ([]string, error) { + return nil, nil +} +func (m *mockDNSResolver) LookupHost(_ context.Context, _ string) ([]string, error) { + return nil, nil +} + +func newMockAnalyzer(txt map[string][]string, errMap map[string]error) *DNSAnalyzer { + if errMap == nil { + errMap = map[string]error{} + } + return NewDNSAnalyzerWithResolver(5*time.Second, &mockDNSResolver{txt: txt, err: errMap}) +} + +func TestCheckDMARCRecordFallback(t *testing.T) { + const orgRecord = "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com" + const subRecord = "v=DMARC1; p=reject" + const psdRecord = "v=DMARC1; p=none; psd=y" + + tests := []struct { + name string + domain string + txt map[string][]string + errMap map[string]error + wantValid bool + wantDomain *string + wantErrSubst string + }{ + { + name: "exact domain has DMARC record — no fallback", + domain: "mail.example.com", + txt: map[string][]string{ + "_dmarc.mail.example.com": {subRecord}, + "_dmarc.example.com": {orgRecord}, + }, + wantValid: true, + wantDomain: utils.PtrTo("mail.example.com"), + }, + { + name: "exact domain NXDOMAIN — tree walk reaches org domain", + domain: "mail.example.com", + txt: map[string][]string{ + "_dmarc.example.com": {orgRecord}, + }, + wantValid: true, + wantDomain: utils.PtrTo("example.com"), + }, + { + name: "exact domain has no v=DMARC1 TXT — tree walk reaches org domain", + domain: "mail.example.com", + txt: map[string][]string{ + "_dmarc.mail.example.com": {"some-other-txt"}, + "_dmarc.example.com": {orgRecord}, + }, + wantValid: true, + wantDomain: utils.PtrTo("example.com"), + }, + { + name: "both exact and org NXDOMAIN but PSD (TLD) has psd=y — DMARCbis Tree Walk", + domain: "mail.example.com", + txt: map[string][]string{ + "_dmarc.com": {psdRecord}, + }, + wantValid: true, + wantDomain: utils.PtrTo("com"), + }, + { + name: "PSD record exists but no psd=y — TLD record ignored by Tree Walk", + domain: "mail.example.com", + txt: map[string][]string{ + "_dmarc.com": {"v=DMARC1; p=none"}, + }, + wantValid: false, + wantErrSubst: "No DMARC record found", + }, + { + name: "no record at any level", + domain: "mail.example.com", + txt: map[string][]string{}, + wantValid: false, + wantErrSubst: "No DMARC record found", + }, + { + name: "DNS error on exact domain — error returned", + domain: "mail.example.com", + errMap: map[string]error{ + "_dmarc.mail.example.com": fmt.Errorf("SERVFAIL"), + }, + wantValid: false, + wantErrSubst: "SERVFAIL", + }, + { + name: "domain already at org level — found immediately", + domain: "example.com", + txt: map[string][]string{ + "_dmarc.example.com": {orgRecord}, + }, + wantValid: true, + wantDomain: utils.PtrTo("example.com"), + }, + { + name: "deep subdomain — tree walk finds record two levels up", + domain: "a.b.example.com", + txt: map[string][]string{ + "_dmarc.example.com": {orgRecord}, + }, + wantValid: true, + wantDomain: utils.PtrTo("example.com"), + }, + { + name: "8-label domain — shortcut to 7-label suffix on miss", + domain: "a.b.c.d.e.f.example.com", + txt: map[string][]string{ + "_dmarc.b.c.d.e.f.example.com": {orgRecord}, + }, + wantValid: true, + wantDomain: utils.PtrTo("b.c.d.e.f.example.com"), + }, + { + name: "psd=n record stops tree walk at that level", + domain: "mail.sub.example.com", + txt: map[string][]string{ + "_dmarc.sub.example.com": {"v=DMARC1; p=reject; psd=n"}, + }, + wantValid: true, + wantDomain: utils.PtrTo("sub.example.com"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + analyzer := newMockAnalyzer(tt.txt, tt.errMap) + result := analyzer.checkDMARCRecord(tt.domain) + + if result.Valid != tt.wantValid { + t.Errorf("Valid = %v, want %v", result.Valid, tt.wantValid) + } + if tt.wantDomain != nil { + if result.Domain == nil { + t.Fatalf("Domain = nil, want %q", *tt.wantDomain) + } + if *result.Domain != *tt.wantDomain { + t.Errorf("Domain = %q, want %q", *result.Domain, *tt.wantDomain) + } + } + if tt.wantErrSubst != "" { + if result.Error == nil { + t.Fatalf("Error = nil, want substring %q", tt.wantErrSubst) + } + if !contains(*result.Error, tt.wantErrSubst) { + t.Errorf("Error = %q, want substring %q", *result.Error, tt.wantErrSubst) + } + } + }) + } +} + +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsStr(s, substr)) +} + +func containsStr(s, sub string) bool { + for i := 0; i <= len(s)-len(sub); i++ { + if s[i:i+len(sub)] == sub { + return true + } + } + return false +} + +func TestParseDMARCRecordPolicy(t *testing.T) { tests := []struct { name string record string @@ -61,9 +253,135 @@ func TestExtractDMARCPolicy(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := analyzer.extractDMARCPolicy(tt.record) - if result != tt.expectedPolicy { - t.Errorf("extractDMARCPolicy(%q) = %q, want %q", tt.record, result, tt.expectedPolicy) + rec := analyzer.parseDMARCRecord("example.com", tt.record) + if rec.Policy == nil { + t.Fatalf("parseDMARCRecord(%q).Policy = nil", tt.record) + } + if string(*rec.Policy) != tt.expectedPolicy { + t.Errorf("parseDMARCRecord(%q).Policy = %q, want %q", tt.record, string(*rec.Policy), tt.expectedPolicy) + } + }) + } +} + +func TestParseDMARCRecordTestMode(t *testing.T) { + tests := []struct { + name string + record string + wantMode *bool + }{ + { + name: "t=y sets test mode", + record: "v=DMARC1; p=reject; t=y", + wantMode: utils.PtrTo(true), + }, + { + name: "t=n explicitly disables test mode", + record: "v=DMARC1; p=reject; t=n", + wantMode: utils.PtrTo(false), + }, + { + name: "absent t tag returns nil", + record: "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com", + wantMode: nil, + }, + } + + analyzer := NewDNSAnalyzer(5 * time.Second) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := analyzer.parseDMARCRecord("example.com", tt.record).TestMode + if tt.wantMode == nil { + if result != nil { + t.Errorf("parseDMARCRecord(%q).TestMode = %v, want nil", tt.record, *result) + } + } else { + if result == nil { + t.Fatalf("parseDMARCRecord(%q).TestMode = nil, want %v", tt.record, *tt.wantMode) + } + if *result != *tt.wantMode { + t.Errorf("parseDMARCRecord(%q).TestMode = %v, want %v", tt.record, *result, *tt.wantMode) + } + } + }) + } +} + +func TestParseDMARCRecordPSD(t *testing.T) { + tests := []struct { + name string + record string + wantPSD *string + }{ + { + name: "psd=y marks Public Suffix Domain", + record: "v=DMARC1; p=none; psd=y", + wantPSD: utils.PtrTo("y"), + }, + { + name: "psd=n marks Org Domain boundary", + record: "v=DMARC1; p=reject; psd=n", + wantPSD: utils.PtrTo("n"), + }, + { + name: "psd=u is explicit unknown", + record: "v=DMARC1; p=quarantine; psd=u", + wantPSD: utils.PtrTo("u"), + }, + { + name: "absent psd tag returns nil", + record: "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com", + wantPSD: nil, + }, + } + + analyzer := NewDNSAnalyzer(5 * time.Second) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := analyzer.parseDMARCRecord("example.com", tt.record).Psd + if tt.wantPSD == nil { + if result != nil { + t.Errorf("parseDMARCRecord(%q).Psd = %v, want nil", tt.record, *result) + } + } else { + if result == nil { + t.Fatalf("parseDMARCRecord(%q).Psd = nil, want %q", tt.record, *tt.wantPSD) + } + if string(*result) != *tt.wantPSD { + t.Errorf("parseDMARCRecord(%q).Psd = %q, want %q", tt.record, string(*result), *tt.wantPSD) + } + } + }) + } +} + +func TestParseDMARCRecordDeprecatedTags(t *testing.T) { + tests := []struct { + name string + record string + wantRf bool + wantRi bool + }{ + {name: "rf tag present", record: "v=DMARC1; p=none; rf=afrf", wantRf: true, wantRi: false}, + {name: "ri tag present", record: "v=DMARC1; p=none; ri=86400", wantRf: false, wantRi: true}, + {name: "rf tag absent", record: "v=DMARC1; p=quarantine; rua=mailto:x@example.com", wantRf: false, wantRi: false}, + {name: "ri tag absent", record: "v=DMARC1; p=quarantine", wantRf: false, wantRi: false}, + } + + analyzer := NewDNSAnalyzer(5 * time.Second) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rec := analyzer.parseDMARCRecord("example.com", tt.record) + gotRf := rec.DeprecatedRf != nil && *rec.DeprecatedRf + gotRi := rec.DeprecatedRi != nil && *rec.DeprecatedRi + if gotRf != tt.wantRf { + t.Errorf("parseDMARCRecord(%q).DeprecatedRf = %v, want %v", tt.record, gotRf, tt.wantRf) + } + if gotRi != tt.wantRi { + t.Errorf("parseDMARCRecord(%q).DeprecatedRi = %v, want %v", tt.record, gotRi, tt.wantRi) } }) } @@ -85,13 +403,18 @@ func TestValidateDMARC(t *testing.T) { record: "v=DMARC1; p=none", expected: true, }, + { + name: "DMARCbis: p= absent but rua= present is valid (treated as p=none)", + record: "v=DMARC1; rua=mailto:dmarc@example.com", + expected: true, + }, { name: "Invalid DMARC - no version", record: "p=quarantine", expected: false, }, { - name: "Invalid DMARC - no policy", + name: "Invalid DMARC - no policy and no rua", record: "v=DMARC1", expected: false, }, @@ -114,142 +437,36 @@ func TestValidateDMARC(t *testing.T) { } } -func TestExtractDMARCSPFAlignment(t *testing.T) { - tests := []struct { - name string - record string - expectedAlignment string - }{ - { - name: "SPF alignment - strict", - record: "v=DMARC1; p=quarantine; aspf=s", - expectedAlignment: "strict", - }, - { - name: "SPF alignment - relaxed (explicit)", - record: "v=DMARC1; p=quarantine; aspf=r", - expectedAlignment: "relaxed", - }, - { - name: "SPF alignment - relaxed (default, not specified)", - record: "v=DMARC1; p=quarantine", - expectedAlignment: "relaxed", - }, - { - name: "Both alignments specified - check SPF strict", - record: "v=DMARC1; p=quarantine; aspf=s; adkim=r", - expectedAlignment: "strict", - }, - { - name: "Both alignments specified - check SPF relaxed", - record: "v=DMARC1; p=quarantine; aspf=r; adkim=s", - expectedAlignment: "relaxed", - }, - { - name: "Complex record with SPF strict", - record: "v=DMARC1; p=reject; rua=mailto:dmarc@example.com; aspf=s; adkim=s; pct=100", - expectedAlignment: "strict", - }, - } - - analyzer := NewDNSAnalyzer(5 * time.Second) - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := analyzer.extractDMARCSPFAlignment(tt.record) - if result == nil { - t.Fatalf("extractDMARCSPFAlignment(%q) returned nil, expected non-nil", tt.record) - } - if string(*result) != tt.expectedAlignment { - t.Errorf("extractDMARCSPFAlignment(%q) = %q, want %q", tt.record, string(*result), tt.expectedAlignment) - } - }) - } -} - -func TestExtractDMARCDKIMAlignment(t *testing.T) { - tests := []struct { - name string - record string - expectedAlignment string - }{ - { - name: "DKIM alignment - strict", - record: "v=DMARC1; p=reject; adkim=s", - expectedAlignment: "strict", - }, - { - name: "DKIM alignment - relaxed (explicit)", - record: "v=DMARC1; p=reject; adkim=r", - expectedAlignment: "relaxed", - }, - { - name: "DKIM alignment - relaxed (default, not specified)", - record: "v=DMARC1; p=none", - expectedAlignment: "relaxed", - }, - { - name: "Both alignments specified - check DKIM strict", - record: "v=DMARC1; p=quarantine; aspf=r; adkim=s", - expectedAlignment: "strict", - }, - { - name: "Both alignments specified - check DKIM relaxed", - record: "v=DMARC1; p=quarantine; aspf=s; adkim=r", - expectedAlignment: "relaxed", - }, - { - name: "Complex record with DKIM strict", - record: "v=DMARC1; p=reject; rua=mailto:dmarc@example.com; aspf=r; adkim=s; pct=100", - expectedAlignment: "strict", - }, - } - - analyzer := NewDNSAnalyzer(5 * time.Second) - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := analyzer.extractDMARCDKIMAlignment(tt.record) - if result == nil { - t.Fatalf("extractDMARCDKIMAlignment(%q) returned nil, expected non-nil", tt.record) - } - if string(*result) != tt.expectedAlignment { - t.Errorf("extractDMARCDKIMAlignment(%q) = %q, want %q", tt.record, string(*result), tt.expectedAlignment) - } - }) - } -} - -func TestExtractDMARCSubdomainPolicy(t *testing.T) { +func TestParseDMARCRecordAlignment(t *testing.T) { tests := []struct { name string record string - expectedPolicy *string + expectedSPF string + expectedDKIM string }{ { - name: "Subdomain policy - none", - record: "v=DMARC1; p=quarantine; sp=none", - expectedPolicy: utils.PtrTo("none"), + name: "SPF strict, DKIM relaxed", + record: "v=DMARC1; p=quarantine; aspf=s; adkim=r", + expectedSPF: "strict", + expectedDKIM: "relaxed", }, { - name: "Subdomain policy - quarantine", - record: "v=DMARC1; p=reject; sp=quarantine", - expectedPolicy: utils.PtrTo("quarantine"), + name: "SPF relaxed explicit, DKIM strict", + record: "v=DMARC1; p=quarantine; aspf=r; adkim=s", + expectedSPF: "relaxed", + expectedDKIM: "strict", }, { - name: "Subdomain policy - reject", - record: "v=DMARC1; p=quarantine; sp=reject", - expectedPolicy: utils.PtrTo("reject"), + name: "Defaults when neither specified", + record: "v=DMARC1; p=quarantine", + expectedSPF: "relaxed", + expectedDKIM: "relaxed", }, { - name: "No subdomain policy specified (defaults to main policy)", - record: "v=DMARC1; p=quarantine", - expectedPolicy: nil, - }, - { - name: "Complex record with subdomain policy", - record: "v=DMARC1; p=reject; sp=quarantine; rua=mailto:dmarc@example.com; pct=100", - expectedPolicy: utils.PtrTo("quarantine"), + name: "Both strict in complex record", + record: "v=DMARC1; p=reject; rua=mailto:dmarc@example.com; aspf=s; adkim=s; pct=100", + expectedSPF: "strict", + expectedDKIM: "strict", }, } @@ -257,86 +474,117 @@ func TestExtractDMARCSubdomainPolicy(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := analyzer.extractDMARCSubdomainPolicy(tt.record) - if tt.expectedPolicy == nil { - if result != nil { - t.Errorf("extractDMARCSubdomainPolicy(%q) = %v, want nil", tt.record, result) + rec := analyzer.parseDMARCRecord("example.com", tt.record) + if rec.SpfAlignment == nil { + t.Fatalf("parseDMARCRecord(%q).SpfAlignment = nil", tt.record) + } + if string(*rec.SpfAlignment) != tt.expectedSPF { + t.Errorf("SpfAlignment = %q, want %q", string(*rec.SpfAlignment), tt.expectedSPF) + } + if rec.DkimAlignment == nil { + t.Fatalf("parseDMARCRecord(%q).DkimAlignment = nil", tt.record) + } + if string(*rec.DkimAlignment) != tt.expectedDKIM { + t.Errorf("DkimAlignment = %q, want %q", string(*rec.DkimAlignment), tt.expectedDKIM) + } + }) + } +} + +func TestParseDMARCRecordSubdomainPolicy(t *testing.T) { + tests := []struct { + name string + record string + expectedSP *string + expectedNP *string + }{ + { + name: "sp=none, no np", + record: "v=DMARC1; p=quarantine; sp=none", + expectedSP: utils.PtrTo("none"), + expectedNP: nil, + }, + { + name: "sp=reject, np=reject", + record: "v=DMARC1; p=reject; sp=quarantine; np=reject; rua=mailto:dmarc@example.com; pct=100", + expectedSP: utils.PtrTo("quarantine"), + expectedNP: utils.PtrTo("reject"), + }, + { + name: "No sp or np (both default)", + record: "v=DMARC1; p=quarantine", + expectedSP: nil, + expectedNP: nil, + }, + { + name: "np=quarantine, no sp", + record: "v=DMARC1; p=reject; np=quarantine", + expectedSP: nil, + expectedNP: utils.PtrTo("quarantine"), + }, + } + + analyzer := NewDNSAnalyzer(5 * time.Second) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rec := analyzer.parseDMARCRecord("example.com", tt.record) + if tt.expectedSP == nil { + if rec.SubdomainPolicy != nil { + t.Errorf("parseDMARCRecord(%q).SubdomainPolicy = %v, want nil", tt.record, *rec.SubdomainPolicy) } } else { - if result == nil { - t.Fatalf("extractDMARCSubdomainPolicy(%q) returned nil, expected %q", tt.record, *tt.expectedPolicy) + if rec.SubdomainPolicy == nil { + t.Fatalf("parseDMARCRecord(%q).SubdomainPolicy = nil, want %q", tt.record, *tt.expectedSP) } - if string(*result) != *tt.expectedPolicy { - t.Errorf("extractDMARCSubdomainPolicy(%q) = %q, want %q", tt.record, string(*result), *tt.expectedPolicy) + if string(*rec.SubdomainPolicy) != *tt.expectedSP { + t.Errorf("SubdomainPolicy = %q, want %q", string(*rec.SubdomainPolicy), *tt.expectedSP) + } + } + if tt.expectedNP == nil { + if rec.NonexistentSubdomainPolicy != nil { + t.Errorf("parseDMARCRecord(%q).NonexistentSubdomainPolicy = %v, want nil", tt.record, *rec.NonexistentSubdomainPolicy) + } + } else { + if rec.NonexistentSubdomainPolicy == nil { + t.Fatalf("parseDMARCRecord(%q).NonexistentSubdomainPolicy = nil, want %q", tt.record, *tt.expectedNP) + } + if string(*rec.NonexistentSubdomainPolicy) != *tt.expectedNP { + t.Errorf("NonexistentSubdomainPolicy = %q, want %q", string(*rec.NonexistentSubdomainPolicy), *tt.expectedNP) } } }) } } -func TestExtractDMARCPercentage(t *testing.T) { +func TestParseDMARCRecordPercentage(t *testing.T) { tests := []struct { name string record string expectedPercentage *int }{ - { - name: "Percentage - 100", - record: "v=DMARC1; p=quarantine; pct=100", - expectedPercentage: utils.PtrTo(100), - }, - { - name: "Percentage - 50", - record: "v=DMARC1; p=quarantine; pct=50", - expectedPercentage: utils.PtrTo(50), - }, - { - name: "Percentage - 25", - record: "v=DMARC1; p=reject; pct=25", - expectedPercentage: utils.PtrTo(25), - }, - { - name: "Percentage - 0", - record: "v=DMARC1; p=none; pct=0", - expectedPercentage: utils.PtrTo(0), - }, - { - name: "No percentage specified (defaults to 100)", - record: "v=DMARC1; p=quarantine", - expectedPercentage: nil, - }, - { - name: "Complex record with percentage", - record: "v=DMARC1; p=reject; sp=quarantine; rua=mailto:dmarc@example.com; pct=75", - expectedPercentage: utils.PtrTo(75), - }, - { - name: "Invalid percentage > 100 (ignored)", - record: "v=DMARC1; p=quarantine; pct=150", - expectedPercentage: nil, - }, - { - name: "Invalid percentage < 0 (ignored)", - record: "v=DMARC1; p=quarantine; pct=-10", - expectedPercentage: nil, - }, + {name: "pct=100", record: "v=DMARC1; p=quarantine; pct=100", expectedPercentage: utils.PtrTo(100)}, + {name: "pct=50", record: "v=DMARC1; p=quarantine; pct=50", expectedPercentage: utils.PtrTo(50)}, + {name: "pct=0", record: "v=DMARC1; p=none; pct=0", expectedPercentage: utils.PtrTo(0)}, + {name: "no pct", record: "v=DMARC1; p=quarantine", expectedPercentage: nil}, + {name: "pct=150 ignored", record: "v=DMARC1; p=quarantine; pct=150", expectedPercentage: nil}, } analyzer := NewDNSAnalyzer(5 * time.Second) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := analyzer.extractDMARCPercentage(tt.record) + result := analyzer.parseDMARCRecord("example.com", tt.record).Percentage if tt.expectedPercentage == nil { if result != nil { - t.Errorf("extractDMARCPercentage(%q) = %v, want nil", tt.record, *result) + t.Errorf("parseDMARCRecord(%q).Percentage = %d, want nil", tt.record, *result) } } else { if result == nil { - t.Fatalf("extractDMARCPercentage(%q) returned nil, expected %d", tt.record, *tt.expectedPercentage) + t.Fatalf("parseDMARCRecord(%q).Percentage = nil, want %d", tt.record, *tt.expectedPercentage) } if *result != *tt.expectedPercentage { - t.Errorf("extractDMARCPercentage(%q) = %d, want %d", tt.record, *result, *tt.expectedPercentage) + t.Errorf("parseDMARCRecord(%q).Percentage = %d, want %d", tt.record, *result, *tt.expectedPercentage) } } }) diff --git a/pkg/analyzer/headers.go b/pkg/analyzer/headers.go index f750742..6d7b547 100644 --- a/pkg/analyzer/headers.go +++ b/pkg/analyzer/headers.go @@ -388,7 +388,7 @@ func (h *HeaderAnalyzer) analyzeDomainAlignment(email *EmailMessage, authResults if domain != "" { alignment.FromDomain = &domain // Extract organizational domain - orgDomain := h.getOrganizationalDomain(domain) + orgDomain := getOrganizationalDomain(domain) alignment.FromOrgDomain = &orgDomain } } @@ -400,7 +400,7 @@ func (h *HeaderAnalyzer) analyzeDomainAlignment(email *EmailMessage, authResults if domain != "" { alignment.ReturnPathDomain = &domain // Extract organizational domain - orgDomain := h.getOrganizationalDomain(domain) + orgDomain := getOrganizationalDomain(domain) alignment.ReturnPathOrgDomain = &orgDomain } } @@ -411,7 +411,7 @@ func (h *HeaderAnalyzer) analyzeDomainAlignment(email *EmailMessage, authResults for _, dkim := range *authResults.Dkim { if dkim.Domain != nil && *dkim.Domain != "" { domain := *dkim.Domain - orgDomain := h.getOrganizationalDomain(domain) + orgDomain := getOrganizationalDomain(domain) dkimDomains = append(dkimDomains, model.DKIMDomainInfo{ Domain: domain, OrgDomain: orgDomain, @@ -542,7 +542,7 @@ func (h *HeaderAnalyzer) extractDomain(emailAddr string) string { // getOrganizationalDomain extracts the organizational domain from a fully qualified domain name // using the Public Suffix List (PSL) to correctly handle multi-level TLDs. // For example: mail.example.com -> example.com, mail.example.co.uk -> example.co.uk -func (h *HeaderAnalyzer) getOrganizationalDomain(domain string) string { +func getOrganizationalDomain(domain string) string { domain = strings.ToLower(strings.TrimSpace(domain)) // Use golang.org/x/net/publicsuffix to get the eTLD+1 (organizational domain) diff --git a/pkg/analyzer/rbl.go b/pkg/analyzer/rbl.go index 7dea559..31cccab 100644 --- a/pkg/analyzer/rbl.go +++ b/pkg/analyzer/rbl.go @@ -318,7 +318,7 @@ func (r *DNSListChecker) CalculateScore(results *DNSListResults, forWhitelist bo return 100, "" } - if results.ListedCount <= 0 { + if results.ListedCount <= 0 || scoringListCount <= 0 { return 100, "A+" } diff --git a/pkg/analyzer/rbl_test.go b/pkg/analyzer/rbl_test.go index 8620038..f86f17b 100644 --- a/pkg/analyzer/rbl_test.go +++ b/pkg/analyzer/rbl_test.go @@ -291,34 +291,38 @@ func TestGetBlacklistScore(t *testing.T) { { name: "Listed on 1 RBL", results: &DNSListResults{ - IPsChecked: []string{"198.51.100.1"}, - ListedCount: 1, + IPsChecked: []string{"198.51.100.1"}, + ListedCount: 1, + RelevantListedCount: 1, }, - expectedScore: 84, // 100 - 1*100/6 = 84 (integer division: 100/6=16) + expectedScore: 92, // 100 - 1*100/12 = 92 (12 scoring lists = 14 default - 2 informational) }, { name: "Listed on 2 RBLs", results: &DNSListResults{ - IPsChecked: []string{"198.51.100.1"}, - ListedCount: 2, + IPsChecked: []string{"198.51.100.1"}, + ListedCount: 2, + RelevantListedCount: 2, }, - expectedScore: 67, // 100 - 2*100/6 = 67 (integer division: 200/6=33) + expectedScore: 84, // 100 - 2*100/12 = 84 }, { name: "Listed on 3 RBLs", results: &DNSListResults{ - IPsChecked: []string{"198.51.100.1"}, - ListedCount: 3, + IPsChecked: []string{"198.51.100.1"}, + ListedCount: 3, + RelevantListedCount: 3, }, - expectedScore: 50, // 100 - 3*100/6 = 50 (integer division: 300/6=50) + expectedScore: 75, // 100 - 3*100/12 = 75 }, { name: "Listed on 4+ RBLs", results: &DNSListResults{ - IPsChecked: []string{"198.51.100.1"}, - ListedCount: 4, + IPsChecked: []string{"198.51.100.1"}, + ListedCount: 4, + RelevantListedCount: 4, }, - expectedScore: 34, // 100 - 4*100/6 = 34 (integer division: 400/6=66) + expectedScore: 67, // 100 - 4*100/12 = 67 }, } @@ -326,7 +330,7 @@ func TestGetBlacklistScore(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - score, _ := checker.CalculateScore(tt.results) + score, _ := checker.CalculateScore(tt.results, false) if score != tt.expectedScore { t.Errorf("GetBlacklistScore() = %v, want %v", score, tt.expectedScore) } diff --git a/web/package-lock.json b/web/package-lock.json index 6951a71..27e6fc1 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -28,28 +28,28 @@ "prettier-plugin-svelte": "^3.4.0", "svelte": "^5.39.5", "svelte-check": "^4.3.2", - "typescript": "^5.9.2", + "typescript": "^6.0.0", "typescript-eslint": "^8.44.1", "vite": "^8.0.0", "vitest": "^3.2.4" } }, "node_modules/@emnapi/core": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", - "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.0", + "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", - "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "dev": true, "license": "MIT", "optional": true, @@ -58,9 +58,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", - "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "dev": true, "license": "MIT", "optional": true, @@ -69,9 +69,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", - "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", "cpu": [ "ppc64" ], @@ -86,9 +86,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", - "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", "cpu": [ "arm" ], @@ -103,9 +103,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", - "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", "cpu": [ "arm64" ], @@ -120,9 +120,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", - "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", "cpu": [ "x64" ], @@ -137,9 +137,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", - "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", "cpu": [ "arm64" ], @@ -154,9 +154,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", - "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", "cpu": [ "x64" ], @@ -171,9 +171,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", - "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", "cpu": [ "arm64" ], @@ -188,9 +188,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", - "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", "cpu": [ "x64" ], @@ -205,9 +205,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", - "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", "cpu": [ "arm" ], @@ -222,9 +222,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", - "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", "cpu": [ "arm64" ], @@ -239,9 +239,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", - "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", "cpu": [ "ia32" ], @@ -256,9 +256,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", - "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", "cpu": [ "loong64" ], @@ -273,9 +273,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", - "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", "cpu": [ "mips64el" ], @@ -290,9 +290,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", - "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", "cpu": [ "ppc64" ], @@ -307,9 +307,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", - "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", "cpu": [ "riscv64" ], @@ -324,9 +324,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", - "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", "cpu": [ "s390x" ], @@ -341,9 +341,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", - "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", "cpu": [ "x64" ], @@ -358,9 +358,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", - "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", "cpu": [ "arm64" ], @@ -375,9 +375,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", - "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", "cpu": [ "x64" ], @@ -392,9 +392,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", - "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", "cpu": [ "arm64" ], @@ -409,9 +409,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", - "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", "cpu": [ "x64" ], @@ -426,9 +426,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", - "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", "cpu": [ "arm64" ], @@ -443,9 +443,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", - "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", "cpu": [ "x64" ], @@ -460,9 +460,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", - "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", "cpu": [ "arm64" ], @@ -477,9 +477,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", - "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", "cpu": [ "ia32" ], @@ -494,9 +494,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", - "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", "cpu": [ "x64" ], @@ -553,13 +553,13 @@ } }, "node_modules/@eslint/compat": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-2.0.3.tgz", - "integrity": "sha512-SjIJhGigp8hmd1YGIBwh7Ovri7Kisl42GYFjrOyHhtfYGGoLW6teYi/5p8W50KSsawUPpuLOSmsq1bD0NGQLBw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-2.1.0.tgz", + "integrity": "sha512-LgaSCymEpw7tF53xvDw9SNsraPb1IBHxpdABIOM0hW8UAlP8znrjYtuxfR58FSJ3L9BhwD+FaPRFQpZq84Nh6g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.1" + "@eslint/core": "^1.2.1" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" @@ -574,13 +574,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.23.3", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", - "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^3.0.3", + "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" }, @@ -589,22 +589,22 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", - "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.1" + "@eslint/core": "^1.2.1" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/core": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", - "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -636,9 +636,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", - "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -646,13 +646,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", - "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.1", + "@eslint/core": "^1.2.1", "levn": "^0.4.1" }, "engines": { @@ -725,29 +725,43 @@ } }, "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.1", + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -834,26 +848,28 @@ "license": "MIT" }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", - "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "funding": { "type": "github", "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, "node_modules/@oxc-project/types": { - "version": "0.120.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.120.0.tgz", - "integrity": "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==", + "version": "0.130.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz", + "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==", "dev": true, "license": "MIT", "funding": { @@ -879,9 +895,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.10.tgz", - "integrity": "sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz", + "integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==", "cpu": [ "arm64" ], @@ -896,9 +912,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.10.tgz", - "integrity": "sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==", "cpu": [ "arm64" ], @@ -913,9 +929,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.10.tgz", - "integrity": "sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz", + "integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==", "cpu": [ "x64" ], @@ -930,9 +946,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.10.tgz", - "integrity": "sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz", + "integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==", "cpu": [ "x64" ], @@ -947,9 +963,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.10.tgz", - "integrity": "sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz", + "integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==", "cpu": [ "arm" ], @@ -964,13 +980,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz", + "integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -981,13 +1000,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.10.tgz", - "integrity": "sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz", + "integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -998,13 +1020,16 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz", + "integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1015,13 +1040,16 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz", + "integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1032,13 +1060,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz", + "integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1049,13 +1080,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.10.tgz", - "integrity": "sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz", + "integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1066,9 +1100,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.10.tgz", - "integrity": "sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz", + "integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==", "cpu": [ "arm64" ], @@ -1083,9 +1117,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.10.tgz", - "integrity": "sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz", + "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==", "cpu": [ "wasm32" ], @@ -1093,16 +1127,18 @@ "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" }, "engines": { - "node": ">=14.0.0" + "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.10.tgz", - "integrity": "sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz", + "integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==", "cpu": [ "arm64" ], @@ -1117,9 +1153,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.10.tgz", - "integrity": "sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz", + "integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==", "cpu": [ "x64" ], @@ -1134,16 +1170,16 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.10.tgz", - "integrity": "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", - "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", "cpu": [ "arm" ], @@ -1155,9 +1191,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", - "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", "cpu": [ "arm64" ], @@ -1169,9 +1205,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", - "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", "cpu": [ "arm64" ], @@ -1183,9 +1219,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", - "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", "cpu": [ "x64" ], @@ -1197,9 +1233,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", - "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", "cpu": [ "arm64" ], @@ -1211,9 +1247,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", - "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", "cpu": [ "x64" ], @@ -1225,13 +1261,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", - "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1239,13 +1278,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", - "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1253,13 +1295,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", - "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1267,13 +1312,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", - "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1281,13 +1329,16 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", - "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", "cpu": [ "loong64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1295,13 +1346,16 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", - "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", "cpu": [ "loong64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1309,13 +1363,16 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", - "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1323,13 +1380,16 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", - "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1337,13 +1397,16 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", - "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1351,13 +1414,16 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", - "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1365,13 +1431,16 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", - "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1379,13 +1448,16 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", - "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1393,13 +1465,16 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", - "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1407,9 +1482,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", - "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", "cpu": [ "x64" ], @@ -1421,9 +1496,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", - "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", "cpu": [ "arm64" ], @@ -1435,9 +1510,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", - "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", "cpu": [ "arm64" ], @@ -1449,9 +1524,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", - "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", "cpu": [ "ia32" ], @@ -1463,9 +1538,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", - "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", "cpu": [ "x64" ], @@ -1477,9 +1552,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", - "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", "cpu": [ "x64" ], @@ -1518,9 +1593,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.55.0", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.55.0.tgz", - "integrity": "sha512-MdFRjevVxmAknf2NbaUkDF16jSIzXMWd4Nfah0Qp8TtQVoSp3bV4jKt8mX7z7qTUTWvgSaxtR0EG5WJf53gcuA==", + "version": "2.60.1", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.60.1.tgz", + "integrity": "sha512-mQjlkNo+rJvpln7V2IGY2j99BqhcFbS4UN0AQNKNYfhBAFZTuCDAdW3a1sgf330mvtNvsBXn3HpAhcmvdJTcIQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1529,7 +1604,7 @@ "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", - "devalue": "^5.6.4", + "devalue": "^5.8.1", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", @@ -1547,7 +1622,7 @@ "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", - "typescript": "^5.3.3", + "typescript": "^5.3.3 || ^6.0.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "peerDependenciesMeta": { @@ -1560,9 +1635,9 @@ } }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.0.0.tgz", - "integrity": "sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.1.2.tgz", + "integrity": "sha512-DrUBA2UXRfDmUX/ZTiEopd3X40yavsJF1FX2RygcuIScHL7o5YX1fMvoYnDhjeJQC4weCOklirpNWlcb2NiSeA==", "dev": true, "license": "MIT", "dependencies": { @@ -1580,9 +1655,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", "dev": true, "license": "MIT", "optional": true, @@ -1623,9 +1698,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -1637,9 +1712,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.12.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", - "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "version": "24.12.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", + "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", "dev": true, "license": "MIT", "dependencies": { @@ -1654,20 +1729,20 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz", - "integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz", + "integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/type-utils": "8.57.1", - "@typescript-eslint/utils": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/type-utils": "8.59.3", + "@typescript-eslint/utils": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1677,9 +1752,9 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.57.1", + "@typescript-eslint/parser": "^8.59.3", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { @@ -1693,16 +1768,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.1.tgz", - "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz", + "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", "debug": "^4.4.3" }, "engines": { @@ -1714,18 +1789,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz", - "integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz", + "integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.1", - "@typescript-eslint/types": "^8.57.1", + "@typescript-eslint/tsconfig-utils": "^8.59.3", + "@typescript-eslint/types": "^8.59.3", "debug": "^4.4.3" }, "engines": { @@ -1736,18 +1811,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz", - "integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz", + "integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1" + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1758,9 +1833,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz", - "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz", + "integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==", "dev": true, "license": "MIT", "engines": { @@ -1771,21 +1846,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz", - "integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz", + "integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/utils": "8.57.1", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3", "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1796,13 +1871,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz", - "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz", + "integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==", "dev": true, "license": "MIT", "engines": { @@ -1814,21 +1889,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz", - "integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz", + "integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.57.1", - "@typescript-eslint/tsconfig-utils": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", + "@typescript-eslint/project-service": "8.59.3", + "@typescript-eslint/tsconfig-utils": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1838,13 +1913,13 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "dev": true, "license": "ISC", "bin": { @@ -1855,16 +1930,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz", - "integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz", + "integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1" + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1875,17 +1950,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz", - "integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz", + "integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/types": "8.59.3", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -2008,9 +2083,9 @@ } }, "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -2117,9 +2192,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -2411,9 +2486,9 @@ } }, "node_modules/defu": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", "dev": true, "license": "MIT" }, @@ -2435,16 +2510,16 @@ } }, "node_modules/devalue": { - "version": "5.6.4", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", - "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==", + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", + "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", "dev": true, "license": "MIT" }, "node_modules/dotenv": { - "version": "17.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", - "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -2462,9 +2537,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", - "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2475,32 +2550,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.4", - "@esbuild/android-arm": "0.27.4", - "@esbuild/android-arm64": "0.27.4", - "@esbuild/android-x64": "0.27.4", - "@esbuild/darwin-arm64": "0.27.4", - "@esbuild/darwin-x64": "0.27.4", - "@esbuild/freebsd-arm64": "0.27.4", - "@esbuild/freebsd-x64": "0.27.4", - "@esbuild/linux-arm": "0.27.4", - "@esbuild/linux-arm64": "0.27.4", - "@esbuild/linux-ia32": "0.27.4", - "@esbuild/linux-loong64": "0.27.4", - "@esbuild/linux-mips64el": "0.27.4", - "@esbuild/linux-ppc64": "0.27.4", - "@esbuild/linux-riscv64": "0.27.4", - "@esbuild/linux-s390x": "0.27.4", - "@esbuild/linux-x64": "0.27.4", - "@esbuild/netbsd-arm64": "0.27.4", - "@esbuild/netbsd-x64": "0.27.4", - "@esbuild/openbsd-arm64": "0.27.4", - "@esbuild/openbsd-x64": "0.27.4", - "@esbuild/openharmony-arm64": "0.27.4", - "@esbuild/sunos-x64": "0.27.4", - "@esbuild/win32-arm64": "0.27.4", - "@esbuild/win32-ia32": "0.27.4", - "@esbuild/win32-x64": "0.27.4" + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" } }, "node_modules/escape-string-regexp": { @@ -2517,18 +2592,18 @@ } }, "node_modules/eslint": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.1.0.tgz", - "integrity": "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz", + "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.3", - "@eslint/config-helpers": "^0.5.3", - "@eslint/core": "^1.1.1", - "@eslint/plugin-kit": "^0.6.1", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -2589,9 +2664,9 @@ } }, "node_modules/eslint-plugin-svelte": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.16.0.tgz", - "integrity": "sha512-DJXxqpYZUxcE0SfYo8EJzV2ZC+zAD7fJp1n1HwcEMRR1cOEUYvjT9GuzJeNghMjgb7uxuK3IJAzI+x6zzUxO5A==", + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.17.1.tgz", + "integrity": "sha512-NyiXHtS3Ni7e532RBwS9OXlMKDIrENg3gY+/+ODjZzQx2xhU3NlJ+nIl1a93iUUQeiJL3lS8KLmY+W8hklzweQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2706,14 +2781,21 @@ } }, "node_modules/esrap": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.4.tgz", - "integrity": "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==", + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.9.tgz", + "integrity": "sha512-4KijP+NxCWthMCUC3qHbE6n4vCjqgJS1uAYKhuT/GWfFTf1Qyive2TgOjep+gzbSzRfnNyaN/UU9YmdOt8Eg0A==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15", + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } } }, "node_modules/esrecurse": { @@ -2913,9 +2995,9 @@ } }, "node_modules/globals": { - "version": "17.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", - "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", "dev": true, "license": "MIT", "engines": { @@ -3059,9 +3141,9 @@ "license": "ISC" }, "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "dev": true, "license": "MIT", "bin": { @@ -3293,6 +3375,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3314,6 +3399,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3335,6 +3423,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3356,6 +3447,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3445,9 +3539,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "dev": true, "license": "MIT" }, @@ -3469,13 +3563,13 @@ } }, "node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -3522,9 +3616,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -3562,15 +3656,15 @@ "license": "MIT" }, "node_modules/nypm": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", - "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.6.tgz", + "integrity": "sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q==", "dev": true, "license": "MIT", "dependencies": { - "citty": "^0.2.0", + "citty": "^0.2.2", "pathe": "^2.0.3", - "tinyexec": "^1.0.2" + "tinyexec": "^1.1.1" }, "bin": { "nypm": "dist/cli.mjs" @@ -3580,9 +3674,9 @@ } }, "node_modules/nypm/node_modules/citty": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", - "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", + "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", "dev": true, "license": "MIT" }, @@ -3725,9 +3819,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -3738,21 +3832,21 @@ } }, "node_modules/pkg-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", - "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", "dev": true, "license": "MIT", "dependencies": { - "confbox": "^0.2.2", - "exsolve": "^1.0.7", + "confbox": "^0.2.4", + "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "dev": true, "funding": [ { @@ -3897,9 +3991,9 @@ } }, "node_modules/prettier": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, "license": "MIT", "bin": { @@ -3913,9 +4007,9 @@ } }, "node_modules/prettier-plugin-svelte": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.1.tgz", - "integrity": "sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.2.tgz", + "integrity": "sha512-ItFouLvzSFE3ulNl4DKoWM3BGcbDCNVpIyy/Y3F2gC3aNiGLxtFUdffVqO5Z5hhYG+DFT5KULWaxmeFFpdbvaQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -3959,14 +4053,14 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.10.tgz", - "integrity": "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz", + "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.120.0", - "@rolldown/pluginutils": "1.0.0-rc.10" + "@oxc-project/types": "=0.130.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" @@ -3975,27 +4069,27 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.10", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.10", - "@rolldown/binding-darwin-x64": "1.0.0-rc.10", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.10", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.10", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.10", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.10", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10" + "@rolldown/binding-android-arm64": "1.0.1", + "@rolldown/binding-darwin-arm64": "1.0.1", + "@rolldown/binding-darwin-x64": "1.0.1", + "@rolldown/binding-freebsd-x64": "1.0.1", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.1", + "@rolldown/binding-linux-arm64-gnu": "1.0.1", + "@rolldown/binding-linux-arm64-musl": "1.0.1", + "@rolldown/binding-linux-ppc64-gnu": "1.0.1", + "@rolldown/binding-linux-s390x-gnu": "1.0.1", + "@rolldown/binding-linux-x64-gnu": "1.0.1", + "@rolldown/binding-linux-x64-musl": "1.0.1", + "@rolldown/binding-openharmony-arm64": "1.0.1", + "@rolldown/binding-wasm32-wasi": "1.0.1", + "@rolldown/binding-win32-arm64-msvc": "1.0.1", + "@rolldown/binding-win32-x64-msvc": "1.0.1" } }, "node_modules/rollup": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", - "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", "dev": true, "license": "MIT", "dependencies": { @@ -4009,34 +4103,41 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.0", - "@rollup/rollup-android-arm64": "4.60.0", - "@rollup/rollup-darwin-arm64": "4.60.0", - "@rollup/rollup-darwin-x64": "4.60.0", - "@rollup/rollup-freebsd-arm64": "4.60.0", - "@rollup/rollup-freebsd-x64": "4.60.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", - "@rollup/rollup-linux-arm-musleabihf": "4.60.0", - "@rollup/rollup-linux-arm64-gnu": "4.60.0", - "@rollup/rollup-linux-arm64-musl": "4.60.0", - "@rollup/rollup-linux-loong64-gnu": "4.60.0", - "@rollup/rollup-linux-loong64-musl": "4.60.0", - "@rollup/rollup-linux-ppc64-gnu": "4.60.0", - "@rollup/rollup-linux-ppc64-musl": "4.60.0", - "@rollup/rollup-linux-riscv64-gnu": "4.60.0", - "@rollup/rollup-linux-riscv64-musl": "4.60.0", - "@rollup/rollup-linux-s390x-gnu": "4.60.0", - "@rollup/rollup-linux-x64-gnu": "4.60.0", - "@rollup/rollup-linux-x64-musl": "4.60.0", - "@rollup/rollup-openbsd-x64": "4.60.0", - "@rollup/rollup-openharmony-arm64": "4.60.0", - "@rollup/rollup-win32-arm64-msvc": "4.60.0", - "@rollup/rollup-win32-ia32-msvc": "4.60.0", - "@rollup/rollup-win32-x64-gnu": "4.60.0", - "@rollup/rollup-win32-x64-msvc": "4.60.0", + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" } }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -4176,9 +4277,9 @@ } }, "node_modules/svelte": { - "version": "5.54.1", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.54.1.tgz", - "integrity": "sha512-ow8tncN097Ty8U1H+C3bM1xNlsCbnO2UZeN0lWBnv8f3jKho7QTTQ2LWbMXrPQDodLjH91n4kpNnLolyRhVE6A==", + "version": "5.55.7", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.7.tgz", + "integrity": "sha512-ymI5ykLPwIHW839E053FQbI1G+jnRFJEw3Kv5Y4njixVWywQBx+NUFpkkKyk5LIb36Fg9DVXSYpqiGekLD0hyw==", "dev": true, "license": "MIT", "dependencies": { @@ -4191,9 +4292,9 @@ "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", - "devalue": "^5.6.4", + "devalue": "^5.8.1", "esm-env": "^1.2.1", - "esrap": "^2.2.2", + "esrap": "^2.2.4", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", @@ -4204,9 +4305,9 @@ } }, "node_modules/svelte-check": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.5.tgz", - "integrity": "sha512-1bSwIRCvvmSHrlK52fOlZmVtUZgil43jNL/2H18pRpa+eQjzGt6e3zayxhp1S7GajPFKNM/2PMCG+DZFHlG9fw==", + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.8.tgz", + "integrity": "sha512-67adfgBox5eNSNIvIIwgFizKGdcRrGpiMoNO2obHcYuLz7iTa8Xgm/NGU3ntMFnNm8K1grFOIG6HhMLX/vcN8w==", "dev": true, "license": "MIT", "dependencies": { @@ -4228,9 +4329,9 @@ } }, "node_modules/svelte-eslint-parser": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.6.0.tgz", - "integrity": "sha512-qoB1ehychT6OxEtQAqc/guSqLS20SlA53Uijl7x375s8nlUT0lb9ol/gzraEEatQwsyPTJo87s2CmKL9Xab+Uw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.6.1.tgz", + "integrity": "sha512-hhvSH6kRj46UzrBVO5TaotD+Iuvruj5ccKBcO4wAhVcPTLmIc/c32D8UllBTYO0on4LzYuM0rNzf1lM/gBlkSQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4244,7 +4345,7 @@ }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0", - "pnpm": "10.30.3" + "pnpm": "10.33.0" }, "funding": { "url": "https://github.com/sponsors/ota-meshi" @@ -4314,9 +4415,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", - "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", "dev": true, "license": "MIT", "engines": { @@ -4324,14 +4425,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -4415,9 +4516,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4429,16 +4530,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.1.tgz", - "integrity": "sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.3.tgz", + "integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.57.1", - "@typescript-eslint/parser": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/utils": "8.57.1" + "@typescript-eslint/eslint-plugin": "8.59.3", + "@typescript-eslint/parser": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4449,7 +4550,7 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/uglify-js": { @@ -4491,17 +4592,17 @@ "license": "MIT" }, "node_modules/vite": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz", - "integrity": "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==", + "version": "8.0.13", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz", + "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.10", - "tinyglobby": "^0.2.15" + "picomatch": "^4.0.4", + "postcss": "^8.5.14", + "rolldown": "1.0.1", + "tinyglobby": "^0.2.16" }, "bin": { "vite": "bin/vite.js" @@ -4517,8 +4618,8 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.0", - "esbuild": "^0.27.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", @@ -4592,9 +4693,9 @@ } }, "node_modules/vite-node/node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz", + "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", "dev": true, "license": "MIT", "dependencies": { @@ -4667,9 +4768,9 @@ } }, "node_modules/vitefu": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.2.tgz", - "integrity": "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", "dev": true, "license": "MIT", "workspaces": [ @@ -4678,7 +4779,7 @@ "tests/projects/workspace/packages/*" ], "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "vite": { @@ -4794,9 +4895,9 @@ "license": "MIT" }, "node_modules/vitest/node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz", + "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", "dev": true, "license": "MIT", "dependencies": { @@ -4935,9 +5036,9 @@ } }, "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "extraneous": true, "license": "ISC", "bin": { diff --git a/web/src/lib/components/DmarcRecordDisplay.svelte b/web/src/lib/components/DmarcRecordDisplay.svelte index b7a3e7b..e2b83f0 100644 --- a/web/src/lib/components/DmarcRecordDisplay.svelte +++ b/web/src/lib/components/DmarcRecordDisplay.svelte @@ -3,15 +3,31 @@ interface Props { dmarcRecord?: DmarcRecord; + fromDomain?: string; } - let { dmarcRecord }: Props = $props(); + let { dmarcRecord, fromDomain }: Props = $props(); + + const isFallback = $derived( + !!dmarcRecord?.domain && !!fromDomain && dmarcRecord.domain !== fromDomain, + ); + // A single-label domain (no dot) is a TLD/PSD level fallback + const isPsdFallback = $derived(isFallback && !dmarcRecord?.domain?.includes(".")); // Helper function to determine policy strength const policyStrength = (policy: string | undefined): number => { const strength: Record = { none: 0, quarantine: 1, reject: 2 }; return strength[policy || "none"] || 0; }; + + // Effective policy after applying DMARCbis t=y downgrade + const effectivePolicy = $derived((): string => { + const p = dmarcRecord?.policy ?? "none"; + if (!dmarcRecord?.test_mode) return p; + if (p === "reject") return "quarantine"; + if (p === "quarantine") return "none"; + return p; + }); {#if dmarcRecord} @@ -52,6 +68,27 @@ {/if} + + {#if isFallback} +
+ Record found at: + {dmarcRecord.domain} +
+ + No DMARC record exists for {fromDomain}. The record above was + inherited from + {#if isPsdFallback} + the Public Suffix Domain {dmarcRecord.domain} via the DMARCbis + DNS Tree Walk (which obsoletes the RFC 9091 PSD DMARC experiment). + {:else} + the organizational domain {dmarcRecord.domain} via the + DMARCbis DNS Tree Walk (compatible with RFC 7489 organizational domain + fallback). + {/if} +
+
+ {/if} + {#if dmarcRecord.policy}
@@ -99,6 +136,53 @@
{/if} + + {#if dmarcRecord.test_mode} +
+ Test Mode: + t=y (active) +
+ + Test mode active — DMARCbis-compliant receivers will + downgrade the effective policy one level: + {#if dmarcRecord.policy === "reject"} + p=reject is applied as p=quarantine. + {:else if dmarcRecord.policy === "quarantine"} + p=quarantine is applied as p=none (no action taken). + {:else} + p=none is unaffected by test mode. + {/if} + Aggregate reports are still generated normally. + This tag replaces the deprecated pct= for gradual rollout. +
+
+ {/if} + + + {#if dmarcRecord.psd === "y"} +
+ Public Suffix Domain: + psd=y +
+ + PSD declared — this domain is declared as a Public Suffix + Domain. DMARCbis-compliant receivers will apply this policy to subdomains + that have no DMARC record of their own when using the DNS Tree Walk algorithm. +
+
+ {:else if dmarcRecord.psd === "n"} +
+ Organizational Domain Boundary: + psd=n +
+ + Org Domain declaredpsd=n explicitly declares + this as an Organizational Domain boundary. Subdomains with separate DNS + delegation will use their own independent DMARCbis Tree Walk. +
+
+ {/if} + {#if dmarcRecord.subdomain_policy} {@const mainStrength = policyStrength(dmarcRecord.policy)} @@ -142,7 +226,43 @@ {/if} - + + {#if dmarcRecord.nonexistent_subdomain_policy} + {@const effectiveSubStrength = policyStrength(dmarcRecord.subdomain_policy ?? dmarcRecord.policy)} + {@const npStrength = policyStrength(dmarcRecord.nonexistent_subdomain_policy)} +
+ Non-Existent Subdomain Policy: + + {dmarcRecord.nonexistent_subdomain_policy} + + {#if npStrength >= effectiveSubStrength} +
+ + Good configuration — non-existent subdomain policy is equal to or stricter + than the effective subdomain policy. +
+ {:else} +
+ + Weaker protection for non-existent subdomains — consider setting + np={dmarcRecord.subdomain_policy ?? dmarcRecord.policy} to match your subdomain policy. +
+ {/if} +
+ + The np= tag is introduced by DMARCbis (draft-ietf-dmarc-dmarcbis), + a draft RFC updating RFC 7489. Support may vary across mail receivers. +
+
+ {/if} + + {#if dmarcRecord.percentage !== undefined}
Enforcement Percentage: @@ -155,25 +275,35 @@ > {dmarcRecord.percentage}% +
+ + Deprecated tag — the pct= tag is removed in + DMARCbis. Many receivers already ignore it. For gradual rollout, replace it + with t=y (test mode); for full enforcement, simply remove + pct= from your record. + {#if dmarcRecord.percentage === 0} +
pct=0 is an anti-pattern — it was widely misused + as a signal to bypass DMARC entirely, which is one reason the tag was + removed. Use t=y instead. + {/if} +
{#if dmarcRecord.percentage === 100}
Full enforcement — all messages are subject to DMARC policy. - This provides maximum protection.
- {:else if dmarcRecord.percentage >= 50} + {:else if dmarcRecord.percentage > 0 && dmarcRecord.percentage >= 50}
Partial enforcement — only {dmarcRecord.percentage}% of - messages are subject to DMARC policy. Consider increasing to - pct=100 once you've validated your configuration. + messages are subject to DMARC policy. Receivers ignoring pct= will apply + the full policy regardless.
- {:else} + {:else if dmarcRecord.percentage > 0}
Low enforcement — only {dmarcRecord.percentage}% of - messages are protected. Gradually increase to pct=100 for full - protection. + messages are protected. Receivers ignoring pct= will apply full policy.
{/if}
@@ -259,6 +389,30 @@ {/if} + + {#if dmarcRecord.deprecated_rf || dmarcRecord.deprecated_ri} +
+ + Deprecated tags detected — your record contains + {#if dmarcRecord.deprecated_rf && dmarcRecord.deprecated_ri} + rf= and ri= tags that are + {:else if dmarcRecord.deprecated_rf} + the rf= tag that is + {:else} + the ri= tag that is + {/if} + removed in DMARCbis. Modern receivers will ignore + {dmarcRecord.deprecated_rf && dmarcRecord.deprecated_ri ? "them" : "it"}. + {#if dmarcRecord.deprecated_ri} + Aggregate reporting interval is now fixed at ≥ 24 hours regardless of + ri=. + {/if} + You can safely remove + {dmarcRecord.deprecated_rf && dmarcRecord.deprecated_ri ? "these tags" : "this tag"} + from your DMARC record. +
+ {/if} + {#if dmarcRecord.error}
diff --git a/web/src/lib/components/DnsRecordsCard.svelte b/web/src/lib/components/DnsRecordsCard.svelte index b7997b0..6dabe0b 100644 --- a/web/src/lib/components/DnsRecordsCard.svelte +++ b/web/src/lib/components/DnsRecordsCard.svelte @@ -165,7 +165,10 @@ {/if} - +