From b01ca9b38c441444761dbd0cccaa250290a4378e Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 30 Oct 2025 12:59:37 +0700 Subject: [PATCH 001/102] Report invalid records in summary --- web/src/lib/components/SummaryCard.svelte | 45 ++++++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/web/src/lib/components/SummaryCard.svelte b/web/src/lib/components/SummaryCard.svelte index cf08c2c..cfac2a7 100644 --- a/web/src/lib/components/SummaryCard.svelte +++ b/web/src/lib/components/SummaryCard.svelte @@ -130,6 +130,25 @@ } } + // SPF DNS record check + const spfRecords = report.dns_results?.spf_records; + if (spfRecords && spfRecords.length > 0) { + const invalidSpfRecords = spfRecords.filter((r) => !r.valid && r.record); + if (invalidSpfRecords.length > 0) { + segments.push({ text: ". Your SPF record" }); + if (invalidSpfRecords.length > 1) { + segments.push({ text: "s are " }); + } else { + segments.push({ text: " is " }); + } + segments.push({ + text: "invalid", + highlight: { color: "danger", bold: true }, + link: "#dns-spf", + }); + } + } + // IP Reverse DNS (iprev) check const iprevResult = report.authentication?.iprev; if (iprevResult) { @@ -217,6 +236,28 @@ } } + // DKIM DNS record check + const dkimRecords = report.dns_results?.dkim_records; + if (dkimRecords && Object.keys(dkimRecords).length > 0) { + const invalidDkimKeys = Object.entries(dkimRecords) + .filter(([_, record]) => !record.valid && record.record) + .map(([key, _]) => key); + + if (invalidDkimKeys.length > 0) { + segments.push({ text: ". Your DKIM record" }); + if (invalidDkimKeys.length > 1) { + segments.push({ text: "s are " }); + } else { + segments.push({ text: " is " }); + } + segments.push({ + text: "invalid", + highlight: { color: "danger", bold: true }, + link: "#dns-dkim", + }); + } + } + // DMARC policy check const dmarcRecord = report.dns_results?.dmarc_record; if (dmarcRecord) { @@ -235,9 +276,9 @@ segments.push({ text: "none", highlight: { monospace: true, bold: true } }); segments.push({ text: "' policy", highlight: { bold: true } }); } else if (!dmarcRecord.valid) { - segments.push({ text: ". Your DMARC record has " }); + segments.push({ text: ". Your DMARC record is " }); segments.push({ - text: "issues", + text: "invalid", highlight: { color: "danger", bold: true }, link: "#dns-dmarc", }); From 3a8a25ddeb53e64b7fad1cda430201e6f46eb3ad Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 30 Oct 2025 13:11:58 +0700 Subject: [PATCH 002/102] Add info title on non-standard authentication tests --- web/src/lib/components/AuthenticationCard.svelte | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/src/lib/components/AuthenticationCard.svelte b/web/src/lib/components/AuthenticationCard.svelte index 285b045..0b36dd0 100644 --- a/web/src/lib/components/AuthenticationCard.svelte +++ b/web/src/lib/components/AuthenticationCard.svelte @@ -197,6 +197,7 @@
X-Google-DKIM + {authentication.x_google_dkim.result} @@ -227,6 +228,7 @@
X-Aligned-From + {authentication.x_aligned_from.result} From 90dda126ad74b894badc469d571ed14c309aeef6 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 30 Oct 2025 13:45:29 +0700 Subject: [PATCH 003/102] Don't consider mailto as suspiscious, search domain alignment --- pkg/analyzer/content.go | 135 +++++++++++++++++ pkg/analyzer/content_test.go | 283 +++++++++++++++++++++++++++++++++++ 2 files changed, 418 insertions(+) diff --git a/pkg/analyzer/content.go b/pkg/analyzer/content.go index 3150d50..87c423f 100644 --- a/pkg/analyzer/content.go +++ b/pkg/analyzer/content.go @@ -220,6 +220,18 @@ func (c *ContentAnalyzer) traverseHTML(n *html.Node, results *ContentResults) { // Validate link linkCheck := c.validateLink(href) + + // Check for domain misalignment (phishing detection) + linkText := c.getNodeText(n) + if c.hasDomainMisalignment(href, linkText) { + linkCheck.IsSafe = false + if linkCheck.Warning == "" { + linkCheck.Warning = "Link text domain does not match actual URL domain (possible phishing)" + } else { + linkCheck.Warning += "; Link text domain does not match actual URL domain (possible phishing)" + } + } + results.Links = append(results.Links, linkCheck) // Check for suspicious URLs @@ -415,8 +427,131 @@ func (c *ContentAnalyzer) validateLink(urlStr string) LinkCheck { return check } +// hasDomainMisalignment checks if the link text contains a different domain than the actual URL +// This is a common phishing technique (e.g., text shows "paypal.com" but links to "evil.com") +func (c *ContentAnalyzer) hasDomainMisalignment(href, linkText string) bool { + // Parse the actual URL + parsedURL, err := url.Parse(href) + if err != nil { + return false + } + + // Extract the actual destination domain/email based on scheme + var actualDomain string + + if parsedURL.Scheme == "mailto" { + // Extract email address from mailto: URL + // Format can be: mailto:user@domain.com or mailto:user@domain.com?subject=... + mailtoAddr := parsedURL.Opaque + + // Remove query parameters if present + if idx := strings.Index(mailtoAddr, "?"); idx != -1 { + mailtoAddr = mailtoAddr[:idx] + } + + mailtoAddr = strings.TrimSpace(strings.ToLower(mailtoAddr)) + + // Extract domain from email address + if idx := strings.Index(mailtoAddr, "@"); idx != -1 { + actualDomain = mailtoAddr[idx+1:] + } else { + return false // Invalid mailto + } + } else if parsedURL.Scheme == "http" || parsedURL.Scheme == "https" { + // Check if URL has a host + if parsedURL.Host == "" { + return false + } + + // Extract the actual URL's domain (remove port if present) + actualDomain = parsedURL.Host + if idx := strings.LastIndex(actualDomain, ":"); idx != -1 { + actualDomain = actualDomain[:idx] + } + actualDomain = strings.ToLower(actualDomain) + } else { + // Skip checks for other URL schemes (tel, etc.) + return false + } + + // Normalize link text + linkText = strings.TrimSpace(linkText) + linkText = strings.ToLower(linkText) + + // Skip if link text is empty, too short, or just generic text like "click here" + if linkText == "" || len(linkText) < 4 { + return false + } + + // Common generic link texts that shouldn't trigger warnings + genericTexts := []string{ + "click here", "read more", "learn more", "download", "subscribe", + "unsubscribe", "view online", "view in browser", "click", "here", + "update", "verify", "confirm", "continue", "get started", + // mailto-specific generic texts + "email us", "contact us", "send email", "get in touch", "reach out", + "contact", "email", "write to us", + } + for _, generic := range genericTexts { + if linkText == generic { + return false + } + } + + // Extract domain-like patterns from link text using regex + // Matches patterns like "example.com", "www.example.com", "http://example.com" + domainRegex := regexp.MustCompile(`(?i)(?:https?://)?(?:www\.)?([a-z0-9][-a-z0-9]*\.)+[a-z]{2,}`) + matches := domainRegex.FindAllString(linkText, -1) + + if len(matches) == 0 { + return false + } + + // Check each domain-like pattern found in the text + for _, textDomain := range matches { + // Normalize the text domain + textDomain = strings.ToLower(textDomain) + textDomain = strings.TrimPrefix(textDomain, "http://") + textDomain = strings.TrimPrefix(textDomain, "https://") + textDomain = strings.TrimPrefix(textDomain, "www.") + + // Remove trailing slashes and paths + if idx := strings.Index(textDomain, "/"); idx != -1 { + textDomain = textDomain[:idx] + } + + // Compare domains - they should match or the actual URL should be a subdomain of the text domain + if textDomain != actualDomain { + // Check if actual domain is a subdomain of text domain + if !strings.HasSuffix(actualDomain, "."+textDomain) && !strings.HasSuffix(actualDomain, textDomain) { + // Check if they share the same base domain (last 2 parts) + textParts := strings.Split(textDomain, ".") + actualParts := strings.Split(actualDomain, ".") + + if len(textParts) >= 2 && len(actualParts) >= 2 { + textBase := strings.Join(textParts[len(textParts)-2:], ".") + actualBase := strings.Join(actualParts[len(actualParts)-2:], ".") + + if textBase != actualBase { + return true // Domain mismatch detected! + } + } else { + return true // Domain mismatch detected! + } + } + } + } + + return false +} + // isSuspiciousURL checks if a URL looks suspicious func (c *ContentAnalyzer) isSuspiciousURL(urlStr string, parsedURL *url.URL) bool { + // Skip checks for mailto: URLs + if parsedURL.Scheme == "mailto" { + return false + } + // Check for IP address instead of domain if c.isIPAddress(parsedURL.Host) { return true diff --git a/pkg/analyzer/content_test.go b/pkg/analyzer/content_test.go index 78a27e9..0aa7ff9 100644 --- a/pkg/analyzer/content_test.go +++ b/pkg/analyzer/content_test.go @@ -213,6 +213,16 @@ func TestIsSuspiciousURL(t *testing.T) { url: "https://mail.example.com/page", expected: false, }, + { + name: "Mailto with @ symbol", + url: "mailto:support@example.com", + expected: false, + }, + { + name: "Mailto with multiple @ symbols", + url: "mailto:user@subdomain@example.com", + expected: false, + }, } analyzer := NewContentAnalyzer(5 * time.Second) @@ -628,3 +638,276 @@ func findFirstLink(n *html.Node) *html.Node { func parseURL(urlStr string) (*url.URL, error) { return url.Parse(urlStr) } + +func TestHasDomainMisalignment(t *testing.T) { + tests := []struct { + name string + href string + linkText string + expected bool + reason string + }{ + // Phishing cases - should return true + { + name: "Obvious phishing - different domains", + href: "https://evil.com/page", + linkText: "Click here to verify your paypal.com account", + expected: true, + reason: "Link text shows 'paypal.com' but URL points to 'evil.com'", + }, + { + name: "Domain in link text differs from URL", + href: "http://attacker.net", + linkText: "Visit google.com for more info", + expected: true, + reason: "Link text shows 'google.com' but URL points to 'attacker.net'", + }, + { + name: "URL shown in text differs from actual URL", + href: "https://phishing-site.xyz/login", + linkText: "https://www.bank.example.com/secure", + expected: true, + reason: "Full URL in text doesn't match actual destination", + }, + { + name: "Similar but different domain", + href: "https://paypa1.com/login", + linkText: "Login to your paypal.com account", + expected: true, + reason: "Typosquatting: 'paypa1.com' vs 'paypal.com'", + }, + { + name: "Subdomain spoofing", + href: "https://paypal.com.evil.com/login", + linkText: "Verify your paypal.com account", + expected: true, + reason: "Domain is 'evil.com', not 'paypal.com'", + }, + { + name: "Multiple domains in text, none match", + href: "https://badsite.com", + linkText: "Transfer from bank.com to paypal.com", + expected: true, + reason: "Neither 'bank.com' nor 'paypal.com' matches 'badsite.com'", + }, + + // Legitimate cases - should return false + { + name: "Exact domain match", + href: "https://example.com/page", + linkText: "Visit example.com for more information", + expected: false, + reason: "Domains match exactly", + }, + { + name: "Legitimate subdomain", + href: "https://mail.google.com/inbox", + linkText: "Check your google.com email", + expected: false, + reason: "Subdomain of the mentioned domain", + }, + { + name: "www prefix variation", + href: "https://www.example.com/page", + linkText: "Visit example.com", + expected: false, + reason: "www prefix is acceptable variation", + }, + { + name: "Generic link text - click here", + href: "https://anywhere.com", + linkText: "click here", + expected: false, + reason: "Generic text doesn't contain a domain", + }, + { + name: "Generic link text - read more", + href: "https://example.com/article", + linkText: "Read more", + expected: false, + reason: "Generic text doesn't contain a domain", + }, + { + name: "Generic link text - learn more", + href: "https://example.com/info", + linkText: "Learn More", + expected: false, + reason: "Generic text doesn't contain a domain (case insensitive)", + }, + { + name: "No domain in link text", + href: "https://example.com/page", + linkText: "Click to continue", + expected: false, + reason: "Link text has no domain reference", + }, + { + name: "Short link text", + href: "https://example.com", + linkText: "Go", + expected: false, + reason: "Text too short to contain meaningful domain", + }, + { + name: "Empty link text", + href: "https://example.com", + linkText: "", + expected: false, + reason: "Empty text cannot contain domain", + }, + { + name: "Mailto link - matching domain", + href: "mailto:support@example.com", + linkText: "Email support@example.com", + expected: false, + reason: "Mailto email matches text email", + }, + { + name: "Mailto link - domain mismatch (phishing)", + href: "mailto:attacker@evil.com", + linkText: "Contact support@paypal.com for help", + expected: true, + reason: "Mailto domain 'evil.com' doesn't match text domain 'paypal.com'", + }, + { + name: "Mailto link - generic text", + href: "mailto:info@example.com", + linkText: "Contact us", + expected: false, + reason: "Generic text without domain reference", + }, + { + name: "Mailto link - same domain different user", + href: "mailto:sales@example.com", + linkText: "Email support@example.com", + expected: false, + reason: "Both emails share the same domain", + }, + { + name: "Mailto link - text shows only domain", + href: "mailto:info@example.com", + linkText: "Write to example.com", + expected: false, + reason: "Text domain matches mailto domain", + }, + { + name: "Mailto link - domain in text doesn't match", + href: "mailto:scam@phishing.net", + linkText: "Reply to customer-service@amazon.com", + expected: true, + reason: "Mailto domain 'phishing.net' doesn't match 'amazon.com' in text", + }, + { + name: "Tel link", + href: "tel:+1234567890", + linkText: "Call example.com support", + expected: false, + reason: "Non-HTTP(S) links are excluded", + }, + { + name: "Same base domain with different subdomains", + href: "https://www.example.com/page", + linkText: "Visit blog.example.com", + expected: false, + reason: "Both share same base domain 'example.com'", + }, + { + name: "URL with path matches domain in text", + href: "https://example.com/section/page", + linkText: "Go to example.com", + expected: false, + reason: "Domain matches, path doesn't matter", + }, + { + name: "Generic text - subscribe", + href: "https://newsletter.example.com/signup", + linkText: "Subscribe", + expected: false, + reason: "Generic call-to-action text", + }, + { + name: "Generic text - unsubscribe", + href: "https://example.com/unsubscribe?id=123", + linkText: "Unsubscribe", + expected: false, + reason: "Generic unsubscribe text", + }, + { + name: "Generic text - download", + href: "https://files.example.com/document.pdf", + linkText: "Download", + expected: false, + reason: "Generic action text", + }, + { + name: "Descriptive text without domain", + href: "https://shop.example.com/products", + linkText: "View our latest products", + expected: false, + reason: "No domain mentioned in text", + }, + + // Edge cases + { + name: "Domain-like text but not valid domain", + href: "https://example.com", + linkText: "Save up to 50.00 dollars", + expected: false, + reason: "50.00 looks like domain but isn't", + }, + { + name: "Text with http prefix matching domain", + href: "https://example.com/page", + linkText: "Visit http://example.com", + expected: false, + reason: "Domains match despite different protocols in display", + }, + { + name: "Port in URL should not affect matching", + href: "https://example.com:8080/page", + linkText: "Go to example.com", + expected: false, + reason: "Port number doesn't affect domain matching", + }, + { + name: "Whitespace in link text", + href: "https://example.com", + linkText: " example.com ", + expected: false, + reason: "Whitespace should be trimmed", + }, + { + name: "Multiple spaces in generic text", + href: "https://example.com", + linkText: "click here", + expected: false, + reason: "Generic text with extra spaces", + }, + { + name: "Anchor fragment in URL", + href: "https://example.com/page#section", + linkText: "example.com section", + expected: false, + reason: "Fragment doesn't affect domain matching", + }, + { + name: "Query parameters in URL", + href: "https://example.com/page?utm_source=email", + linkText: "Visit example.com", + expected: false, + reason: "Query params don't affect domain matching", + }, + } + + analyzer := NewContentAnalyzer(5 * time.Second) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := analyzer.hasDomainMisalignment(tt.href, tt.linkText) + if result != tt.expected { + t.Errorf("hasDomainMisalignment(%q, %q) = %v, want %v\nReason: %s", + tt.href, tt.linkText, result, tt.expected, tt.reason) + } + }) + } +} From 099965c1f993a8c9faccd4b887f39baee8337b49 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 30 Oct 2025 14:06:28 +0700 Subject: [PATCH 004/102] Report BIMI issues --- web/src/lib/components/SummaryCard.svelte | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/web/src/lib/components/SummaryCard.svelte b/web/src/lib/components/SummaryCard.svelte index cfac2a7..8817004 100644 --- a/web/src/lib/components/SummaryCard.svelte +++ b/web/src/lib/components/SummaryCard.svelte @@ -331,6 +331,13 @@ }); if (bimiResult.details && bimiResult.details.indexOf("declined") == 0) { segments.push({ text: " declined to participate" }); + } else if (bimiResult?.result === "fail") { + segments.push({ text: " but " }); + segments.push({ + text: "has issues", + highlight: { color: "danger", bold: true }, + link: "#authentication-bimi", + }); } else { segments.push({ text: " for brand indicator display" }); } From 718b624fb86deb985b81eb8987bf250aeea4bed0 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Fri, 31 Oct 2025 10:10:58 +0700 Subject: [PATCH 005/102] Add domain only tests --- api/openapi.yaml | 70 +++++++ internal/api/handlers.go | 51 ++++++ pkg/analyzer/analyzer.go | 11 ++ pkg/analyzer/dns.go | 64 +++++++ pkg/analyzer/scoring.go | 20 ++ web/src/lib/components/DnsRecordsCard.svelte | 147 ++++++++------- web/src/lib/components/index.ts | 8 + web/src/routes/+page.svelte | 7 + web/src/routes/domain/+page.svelte | 176 ++++++++++++++++++ web/src/routes/domain/[domain]/+page.svelte | 181 +++++++++++++++++++ 10 files changed, 665 insertions(+), 70 deletions(-) create mode 100644 web/src/routes/domain/+page.svelte create mode 100644 web/src/routes/domain/[domain]/+page.svelte diff --git a/api/openapi.yaml b/api/openapi.yaml index 7d2ec2c..8c8a836 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -169,6 +169,39 @@ paths: schema: $ref: '#/components/schemas/Error' + /domain: + post: + tags: + - tests + summary: Test a domain's email configuration + description: Analyzes DNS records (MX, SPF, DMARC, BIMI) for a domain without requiring an actual email to be sent. Returns results immediately. + operationId: testDomain + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DomainTestRequest' + responses: + '200': + description: Domain test completed successfully + content: + application/json: + schema: + $ref: '#/components/schemas/DomainTestResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /status: get: tags: @@ -1112,3 +1145,40 @@ components: details: type: string description: Additional error details + + DomainTestRequest: + type: object + required: + - domain + properties: + domain: + type: string + pattern: '^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$' + description: Domain name to test (e.g., example.com) + example: "example.com" + + DomainTestResponse: + type: object + required: + - domain + - score + - grade + - dns_results + properties: + domain: + type: string + description: The tested domain name + example: "example.com" + score: + type: integer + minimum: 0 + maximum: 100 + description: Overall domain configuration score (0-100) + example: 85 + grade: + type: string + enum: [A+, A, B, C, D, E, F] + description: Letter grade representation of the score + example: "A" + dns_results: + $ref: '#/components/schemas/DNSResults' diff --git a/internal/api/handlers.go b/internal/api/handlers.go index 7489f99..fd57579 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -40,6 +40,7 @@ import ( // This interface breaks the circular dependency with pkg/analyzer type EmailAnalyzer interface { AnalyzeEmailBytes(rawEmail []byte, testID uuid.UUID) (reportJSON []byte, err error) + AnalyzeDomain(domain string) (dnsResults *DNSResults, score int, grade string) } // APIHandler implements the ServerInterface for handling API requests @@ -290,3 +291,53 @@ func (h *APIHandler) GetStatus(c *gin.Context) { Uptime: &uptime, }) } + +// TestDomain performs synchronous domain analysis +// (POST /domain) +func (h *APIHandler) TestDomain(c *gin.Context) { + var request DomainTestRequest + + // Bind and validate request + if err := c.ShouldBindJSON(&request); err != nil { + c.JSON(http.StatusBadRequest, Error{ + Error: "invalid_request", + Message: "Invalid request body", + Details: stringPtr(err.Error()), + }) + return + } + + // Perform domain analysis + dnsResults, score, grade := h.analyzer.AnalyzeDomain(request.Domain) + + // Convert grade string to DomainTestResponseGrade enum + var responseGrade DomainTestResponseGrade + switch grade { + case "A+": + responseGrade = DomainTestResponseGradeA + case "A": + responseGrade = DomainTestResponseGradeA1 + case "B": + responseGrade = DomainTestResponseGradeB + case "C": + responseGrade = DomainTestResponseGradeC + case "D": + responseGrade = DomainTestResponseGradeD + case "E": + responseGrade = DomainTestResponseGradeE + case "F": + responseGrade = DomainTestResponseGradeF + default: + responseGrade = DomainTestResponseGradeF + } + + // Build response + response := DomainTestResponse{ + Domain: request.Domain, + Score: score, + Grade: responseGrade, + DnsResults: *dnsResults, + } + + c.JSON(http.StatusOK, response) +} diff --git a/pkg/analyzer/analyzer.go b/pkg/analyzer/analyzer.go index 99b7b52..1cc5bf1 100644 --- a/pkg/analyzer/analyzer.go +++ b/pkg/analyzer/analyzer.go @@ -108,3 +108,14 @@ func (a *APIAdapter) AnalyzeEmailBytes(rawEmail []byte, testID uuid.UUID) ([]byt return reportJSON, nil } + +// AnalyzeDomain performs DNS analysis for a domain and returns the results +func (a *APIAdapter) AnalyzeDomain(domain string) (*api.DNSResults, int, string) { + // Perform DNS analysis + dnsResults := a.analyzer.generator.dnsAnalyzer.AnalyzeDomainOnly(domain) + + // Calculate score + score, grade := a.analyzer.generator.dnsAnalyzer.CalculateDomainOnlyScore(dnsResults) + + return dnsResults, score, grade +} diff --git a/pkg/analyzer/dns.go b/pkg/analyzer/dns.go index c76359c..57226c6 100644 --- a/pkg/analyzer/dns.go +++ b/pkg/analyzer/dns.go @@ -124,6 +124,70 @@ func (d *DNSAnalyzer) AnalyzeDNS(email *EmailMessage, authResults *api.Authentic return results } +// AnalyzeDomainOnly performs DNS validation for a domain without email context +// This is useful for checking domain configuration without sending an actual email +func (d *DNSAnalyzer) AnalyzeDomainOnly(domain string) *api.DNSResults { + results := &api.DNSResults{ + FromDomain: domain, + } + + // Check MX records + results.FromMxRecords = d.checkMXRecords(domain) + + // Check SPF records + results.SpfRecords = d.checkSPFRecords(domain) + + // Check DMARC record + results.DmarcRecord = d.checkDMARCRecord(domain) + + // Check BIMI record with default selector + results.BimiRecord = d.checkBIMIRecord(domain, "default") + + return results +} + +// CalculateDomainOnlyScore calculates the DNS score for domain-only tests +// Returns a score from 0-100 where higher is better +// This version excludes PTR and DKIM checks since they require email context +func (d *DNSAnalyzer) CalculateDomainOnlyScore(results *api.DNSResults) (int, string) { + if results == nil { + return 0, "" + } + + score := 0 + + // MX Records: 30 points (only one domain to check) + mxScore := d.calculateMXScore(results) + // Since calculateMXScore checks both From and RP domains, + // and we only have From domain, we use the full score + score += 30 * mxScore / 100 + + // SPF Records: 30 points + score += 30 * d.calculateSPFScore(results) / 100 + + // DMARC Record: 40 points + score += 40 * d.calculateDMARCScore(results) / 100 + + // BIMI Record: only bonus + if results.BimiRecord != nil && results.BimiRecord.Valid { + if score >= 100 { + return 100, "A+" + } + } + + // Ensure score doesn't exceed maximum + if score > 100 { + score = 100 + } + + // Ensure score is non-negative + if score < 0 { + score = 0 + } + + return score, ScoreToGradeKind(score) +} + // CalculateDNSScore calculates the DNS score from records results // Returns a score from 0-100 where higher is better // senderIP is the original sender IP address used for FCrDNS verification diff --git a/pkg/analyzer/scoring.go b/pkg/analyzer/scoring.go index ae91d4f..0a23388 100644 --- a/pkg/analyzer/scoring.go +++ b/pkg/analyzer/scoring.go @@ -45,6 +45,26 @@ func ScoreToGrade(score int) string { } } +// ScoreToGradeKind converts a percentage score (0-100) to a letter grade, be kind in gradation +func ScoreToGradeKind(score int) string { + switch { + case score > 100: + return "A+" + case score >= 90: + return "A" + case score >= 80: + return "B" + case score >= 60: + return "C" + case score >= 45: + return "D" + case score >= 30: + return "E" + default: + return "F" + } +} + // ScoreToReportGrade converts a percentage score to an api.ReportGrade func ScoreToReportGrade(score int) api.ReportGrade { return api.ReportGrade(ScoreToGrade(score)) diff --git a/web/src/lib/components/DnsRecordsCard.svelte b/web/src/lib/components/DnsRecordsCard.svelte index 2b3c99c..337f7c1 100644 --- a/web/src/lib/components/DnsRecordsCard.svelte +++ b/web/src/lib/components/DnsRecordsCard.svelte @@ -17,9 +17,10 @@ dnsGrade?: string; dnsScore?: number; receivedChain?: ReceivedHop[]; + domainOnly?: boolean; // If true, only shows domain-level DNS records (no PTR, no DKIM, simplified view) } - let { domainAlignment, dnsResults, dnsGrade, dnsScore, receivedChain }: Props = $props(); + let { domainAlignment, dnsResults, dnsGrade, dnsScore, receivedChain, domainOnly = false }: Props = $props(); // Extract sender IP from first hop const senderIp = $derived( @@ -61,88 +62,94 @@
{/if} - - {#if receivedChain && receivedChain.length > 0} -
-

- Received from: {receivedChain[0].from} ({receivedChain[0].reverse || "Unknown"} [{receivedChain[0].ip}]) -

-
- {/if} + {#if !domainOnly} + + {#if receivedChain && receivedChain.length > 0} +
+

+ Received from: {receivedChain[0].from} ({receivedChain[0].reverse || "Unknown"} [{receivedChain[0].ip}]) +

+
+ {/if} - - + + - - - -
- - -
-
-

- Return-Path Domain: {dnsResults.rp_domain || dnsResults.from_domain} -

- {#if (domainAlignment && !domainAlignment.aligned && !domainAlignment.relaxed_aligned) || (domainAlignment && !domainAlignment.aligned && domainAlignment.relaxed_aligned && dnsResults.dmarc_record && dnsResults.dmarc_record.spf_alignment === "strict") || (!domainAlignment && dnsResults.rp_domain && dnsResults.rp_domain !== dnsResults.from_domain)} - Differs from From domain - - - See domain alignment - - {:else} - Same as From domain - {/if} -
-
- - - {#if dnsResults.rp_mx_records && dnsResults.rp_mx_records.length > 0} - + + +
+ + +
+
+

+ Return-Path Domain: {dnsResults.rp_domain || dnsResults.from_domain} +

+ {#if (domainAlignment && !domainAlignment.aligned && !domainAlignment.relaxed_aligned) || (domainAlignment && !domainAlignment.aligned && domainAlignment.relaxed_aligned && dnsResults.dmarc_record && dnsResults.dmarc_record.spf_alignment === "strict") || (!domainAlignment && dnsResults.rp_domain && dnsResults.rp_domain !== dnsResults.from_domain)} + Differs from From domain + + + See domain alignment + + {:else} + Same as From domain + {/if} +
+
+ + + {#if dnsResults.rp_mx_records && dnsResults.rp_mx_records.length > 0} + + {/if} {/if} -
+ {#if !domainOnly} +
- -
-

- From Domain: {dnsResults.from_domain} -

- {#if dnsResults.rp_domain && dnsResults.rp_domain !== dnsResults.from_domain} - Differs from Return-Path domain - {/if} -
- - - {#if dnsResults.from_mx_records && dnsResults.from_mx_records.length > 0} - + +
+

+ From Domain: {dnsResults.from_domain} +

+ {#if dnsResults.rp_domain && dnsResults.rp_domain !== dnsResults.from_domain} + Differs from Return-Path domain + {/if} +
{/if} - - + + {#if dnsResults.from_mx_records && dnsResults.from_mx_records.length > 0} + + {/if} - - + {#if !domainOnly} + + + {/if} - - + + + + + {/if}
diff --git a/web/src/lib/components/index.ts b/web/src/lib/components/index.ts index dadab9e..fd4f3c9 100644 --- a/web/src/lib/components/index.ts +++ b/web/src/lib/components/index.ts @@ -15,3 +15,11 @@ export { default as PtrRecordsDisplay } from "./PtrRecordsDisplay.svelte"; export { default as PtrForwardRecordsDisplay } from "./PtrForwardRecordsDisplay.svelte"; export { default as TinySurvey } from "./TinySurvey.svelte"; export { default as ErrorDisplay } from "./ErrorDisplay.svelte"; +export { default as GradeDisplay } from "./GradeDisplay.svelte"; +export { default as MxRecordsDisplay } from "./MxRecordsDisplay.svelte"; +export { default as SpfRecordsDisplay } from "./SpfRecordsDisplay.svelte"; +export { default as DmarcRecordDisplay } from "./DmarcRecordDisplay.svelte"; +export { default as BimiRecordDisplay } from "./BimiRecordDisplay.svelte"; +export { default as DkimRecordsDisplay } from "./DkimRecordsDisplay.svelte"; +export { default as Logo } from "./Logo.svelte"; +export { default as EmailPathCard } from "./EmailPathCard.svelte"; diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index 765b03d..2cf556b 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -233,6 +233,13 @@ {/if} + + diff --git a/web/src/routes/domain/+page.svelte b/web/src/routes/domain/+page.svelte new file mode 100644 index 0000000..fe51876 --- /dev/null +++ b/web/src/routes/domain/+page.svelte @@ -0,0 +1,176 @@ + + + + Domain Test - happyDeliver + + +
+
+
+ +
+

+ + Test Domain Configuration +

+

+ Check your domain's email DNS records (MX, SPF, DMARC, BIMI) without sending an + email. +

+
+ + +
+
+

Enter Domain Name

+
+ + + + + +
+ + {#if error} + + {/if} + + + + Enter a domain name like "example.com" or "mail.example.org" + +
+
+ + +
+
+
+
+

+ + What's Checked +

+
    +
  • MX Records
  • +
  • SPF Records
  • +
  • DMARC Policy
  • +
  • BIMI Support
  • +
  • + Disposable Domain Check +
  • +
+
+
+
+ +
+
+
+

+ + Need More? +

+

+ For complete email deliverability analysis including: +

+
    +
  • + DKIM Verification +
  • +
  • + Content & Header Analysis +
  • +
  • + Spam Scoring +
  • +
  • + Blacklist Checks +
  • +
+ + + Send Test Email + +
+
+
+
+
+
+
+ + diff --git a/web/src/routes/domain/[domain]/+page.svelte b/web/src/routes/domain/[domain]/+page.svelte new file mode 100644 index 0000000..7ce9ee4 --- /dev/null +++ b/web/src/routes/domain/[domain]/+page.svelte @@ -0,0 +1,181 @@ + + + + {domain} - Domain Test - happyDeliver + + +
+
+
+ +
+
+

+ + Domain Analysis +

+ + + Test Another Domain + +
+
+ + {#if loading} + +
+
+
+ Loading... +
+

Analyzing {domain}...

+

Checking DNS records and configuration

+
+
+ {:else if error} + +
+
+ +

Analysis Failed

+

{error}

+ +
+
+ {:else if result} + +
+ +
+
+
+
+

+ {result.domain} +

+ {#if result.is_disposable} +
+ + Disposable Email Provider Detected +

+ This domain is a known temporary/disposable email service. + Emails from this domain may have lower deliverability. +

+
+ {:else} +

Domain Configuration Score

+ {/if} +
+
+
+ + DNS +
+
+
+
+
+ + + + + +
+
+

+ + Want Complete Email Analysis? +

+

+ This domain-only test checks DNS configuration. For comprehensive + deliverability testing including DKIM verification, content analysis, + spam scoring, and blacklist checks: +

+ + + Send a Test Email + +
+
+
+ {/if} +
+
+
+ + From 65c8e9a528e10863d932012e24e56e52de3c7522 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 28 Oct 2025 00:11:40 +0000 Subject: [PATCH 006/102] Update Node.js to v24 --- .drone.yml | 2 +- Dockerfile | 2 +- web/package-lock.json | 92 ++++++++++++------------------------------- web/package.json | 2 +- 4 files changed, 29 insertions(+), 69 deletions(-) diff --git a/.drone.yml b/.drone.yml index 5696614..779952f 100644 --- a/.drone.yml +++ b/.drone.yml @@ -9,7 +9,7 @@ platform: steps: - name: frontend - image: node:22-alpine + image: node:24-alpine commands: - cd web - npm install --network-timeout=100000 diff --git a/Dockerfile b/Dockerfile index 6e099f6..5cb9c9e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # Multi-stage Dockerfile for happyDeliver with integrated MTA # Stage 1: Build the Svelte application -FROM node:22-alpine AS nodebuild +FROM node:24-alpine AS nodebuild WORKDIR /build diff --git a/web/package-lock.json b/web/package-lock.json index f1c42fd..e5be2ba 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -19,7 +19,7 @@ "@sveltejs/adapter-static": "^3.0.9", "@sveltejs/kit": "^2.43.2", "@sveltejs/vite-plugin-svelte": "^6.2.0", - "@types/node": "^22", + "@types/node": "^24.0.0", "eslint": "^9.38.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-svelte": "^3.12.4", @@ -1339,14 +1339,14 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.18.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.12.tgz", - "integrity": "sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog==", + "version": "24.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", + "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.16.0" } }, "node_modules/@typescript-eslint/eslint-plugin": { @@ -3132,6 +3132,19 @@ "node": ">=8.6" } }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3383,13 +3396,14 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -4002,19 +4016,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/tinypool": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", @@ -4148,9 +4149,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" }, @@ -4270,19 +4271,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/vitefu": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", @@ -4376,19 +4364,6 @@ } } }, - "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/vitest/node_modules/tinyexec": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", @@ -4462,21 +4437,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/web/package.json b/web/package.json index 8deb4f4..943195f 100644 --- a/web/package.json +++ b/web/package.json @@ -22,7 +22,7 @@ "@sveltejs/adapter-static": "^3.0.9", "@sveltejs/kit": "^2.43.2", "@sveltejs/vite-plugin-svelte": "^6.2.0", - "@types/node": "^22", + "@types/node": "^24.0.0", "eslint": "^9.38.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-svelte": "^3.12.4", From 9e9e76cf428792f9b858edf019a1c69aa6f06b80 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 31 Oct 2025 00:10:31 +0000 Subject: [PATCH 007/102] Update dependency @hey-api/openapi-ts to v0.86.10 --- web/package-lock.json | 18 +++++++++--------- web/package.json | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index e5be2ba..ac4e9f3 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -15,7 +15,7 @@ "devDependencies": { "@eslint/compat": "^1.4.0", "@eslint/js": "^9.36.0", - "@hey-api/openapi-ts": "0.86.4", + "@hey-api/openapi-ts": "0.86.10", "@sveltejs/adapter-static": "^3.0.9", "@sveltejs/kit": "^2.43.2", "@sveltejs/vite-plugin-svelte": "^6.2.0", @@ -655,9 +655,9 @@ } }, "node_modules/@hey-api/codegen-core": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.3.1.tgz", - "integrity": "sha512-iLG9uRJdmQf83sCZ8WsDR6RXQep0X+D1t1mxuzhrSS9zVL4NvnjTQD6PNnQNPymJyss/mdPf7f7kbmcCK7DVmw==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.3.2.tgz", + "integrity": "sha512-DhfftvmoJyfMiiNHhfU7xrDxrjMjPKex1g064RfE6HjNEsFYwK36J2yKfkn8I1mrYWHPmS5ZV3GarMZajsYEEQ==", "dev": true, "license": "MIT", "engines": { @@ -690,13 +690,13 @@ } }, "node_modules/@hey-api/openapi-ts": { - "version": "0.86.4", - "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.86.4.tgz", - "integrity": "sha512-TxQw+2IAykRrHlJwNU68rGjkuL92FhL4TDfkGCzj4dRxo+P4oiBOKSkxSNKUvolDQSdnsq1G71ynEkXoI7BJUg==", + "version": "0.86.10", + "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.86.10.tgz", + "integrity": "sha512-Ns0dTJp/RUrOMPiJsO4/1E2Sa3VZ1iw2KCdG6PDbd9vLwOXEYW2UmiWMDPOTInLCYB+f8FLMF9T25jtfQe7AZg==", "dev": true, "license": "MIT", "dependencies": { - "@hey-api/codegen-core": "^0.3.1", + "@hey-api/codegen-core": "^0.3.2", "@hey-api/json-schema-ref-parser": "1.2.1", "ansi-colors": "4.1.3", "c12": "3.3.1", @@ -707,7 +707,7 @@ "semver": "7.7.2" }, "bin": { - "openapi-ts": "bin/index.cjs" + "openapi-ts": "bin/run.js" }, "engines": { "node": ">=20.19.0" diff --git a/web/package.json b/web/package.json index 943195f..c1efabe 100644 --- a/web/package.json +++ b/web/package.json @@ -18,7 +18,7 @@ "devDependencies": { "@eslint/compat": "^1.4.0", "@eslint/js": "^9.36.0", - "@hey-api/openapi-ts": "0.86.4", + "@hey-api/openapi-ts": "0.86.10", "@sveltejs/adapter-static": "^3.0.9", "@sveltejs/kit": "^2.43.2", "@sveltejs/vite-plugin-svelte": "^6.2.0", From d3f69630c9faa5bbb739bc663ee33be08c488772 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 30 Oct 2025 09:10:11 +0000 Subject: [PATCH 008/102] Update dependency eslint-plugin-svelte to v3.13.0 --- web/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index ac4e9f3..819ca90 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -2402,9 +2402,9 @@ } }, "node_modules/eslint-plugin-svelte": { - "version": "3.12.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.12.5.tgz", - "integrity": "sha512-4KRG84eAHQfYd9OjZ1K7sCHy0nox+9KwT+s5WCCku3jTim5RV4tVENob274nCwIaApXsYPKAUAZFBxKZ3Wyfjw==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.13.0.tgz", + "integrity": "sha512-2ohCCQJJTNbIpQCSDSTWj+FN0OVfPmSO03lmSNT7ytqMaWF6kpT86LdzDqtm4sh7TVPl/OEWJ/d7R87bXP2Vjg==", "dev": true, "license": "MIT", "dependencies": { From e166e75e426784a786984aa8009fb4eb87066cc5 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 30 Oct 2025 07:10:05 +0000 Subject: [PATCH 009/102] Update dependency @eslint/compat to v1.4.1 --- web/package-lock.json | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 819ca90..01d6a6d 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -519,13 +519,13 @@ } }, "node_modules/@eslint/compat": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.4.0.tgz", - "integrity": "sha512-DEzm5dKeDBPm3r08Ixli/0cmxr8LkRdwxMRUIJBlSCpAwSrvFEJpVBzV+66JhDxiaqKxnRzCXhtiMiczF7Hglg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.4.1.tgz", + "integrity": "sha512-cfO82V9zxxGBxcQDr1lfaYB7wykTa0b00mGa36FrJl7iTFd0Z2cHfEYuxcBRP/iNijCsWsEkA+jzT8hGYmv33w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.16.0" + "@eslint/core": "^0.17.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -539,6 +539,19 @@ } } }, + "node_modules/@eslint/compat/node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/config-array": { "version": "0.21.1", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", From bc6a6397ad14284c9f4ea86af38e2b316d20ab9a Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Fri, 31 Oct 2025 11:01:58 +0700 Subject: [PATCH 010/102] New route to check blacklist only --- api/openapi.yaml | 78 +++++++ internal/api/handlers.go | 39 ++++ pkg/analyzer/analyzer.go | 20 ++ pkg/analyzer/rbl.go | 22 ++ web/routes.go | 4 + web/src/lib/stores/config.ts | 1 + web/src/routes/+page.svelte | 8 +- web/src/routes/blacklist/+page.svelte | 186 +++++++++++++++++ web/src/routes/blacklist/[ip]/+page.svelte | 230 +++++++++++++++++++++ 9 files changed, 586 insertions(+), 2 deletions(-) create mode 100644 web/src/routes/blacklist/+page.svelte create mode 100644 web/src/routes/blacklist/[ip]/+page.svelte diff --git a/api/openapi.yaml b/api/openapi.yaml index 8c8a836..92bf3e3 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -202,6 +202,39 @@ paths: schema: $ref: '#/components/schemas/Error' + /blacklist: + post: + tags: + - tests + summary: Check an IP address against DNS blacklists + description: Tests a single IP address (IPv4 or IPv6) against configured DNS-based blacklists (RBLs) without requiring an actual email to be sent. Returns results immediately. + operationId: checkBlacklist + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BlacklistCheckRequest' + responses: + '200': + description: Blacklist check completed successfully + content: + application/json: + schema: + $ref: '#/components/schemas/BlacklistCheckResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /status: get: tags: @@ -1182,3 +1215,48 @@ components: example: "A" dns_results: $ref: '#/components/schemas/DNSResults' + + BlacklistCheckRequest: + type: object + required: + - ip + properties: + ip: + type: string + description: IPv4 or IPv6 address to check against blacklists + example: "192.0.2.1" + pattern: '^([0-9]{1,3}\.){3}[0-9]{1,3}$|^([0-9a-fA-F]{0,4}:){7}[0-9a-fA-F]{0,4}$|^::([0-9a-fA-F]{0,4}:){0,6}[0-9a-fA-F]{0,4}$|^([0-9a-fA-F]{0,4}:){1,6}:([0-9a-fA-F]{0,4}:){0,5}[0-9a-fA-F]{0,4}$' + + BlacklistCheckResponse: + type: object + required: + - ip + - checks + - listed_count + - score + - grade + properties: + ip: + type: string + description: The IP address that was checked + example: "192.0.2.1" + checks: + type: array + items: + $ref: '#/components/schemas/BlacklistCheck' + description: List of blacklist check results + listed_count: + type: integer + description: Number of blacklists that have this IP listed + example: 0 + score: + type: integer + minimum: 0 + maximum: 100 + description: Blacklist score (0-100, higher is better) + example: 100 + grade: + type: string + enum: [A+, A, B, C, D, E, F] + description: Letter grade representation of the score + example: "A+" diff --git a/internal/api/handlers.go b/internal/api/handlers.go index fd57579..80c8f9a 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -41,6 +41,7 @@ import ( type EmailAnalyzer interface { AnalyzeEmailBytes(rawEmail []byte, testID uuid.UUID) (reportJSON []byte, err error) AnalyzeDomain(domain string) (dnsResults *DNSResults, score int, grade string) + CheckBlacklistIP(ip string) (checks []BlacklistCheck, listedCount int, score int, grade string, err error) } // APIHandler implements the ServerInterface for handling API requests @@ -341,3 +342,41 @@ func (h *APIHandler) TestDomain(c *gin.Context) { c.JSON(http.StatusOK, response) } + +// CheckBlacklist checks an IP address against DNS blacklists +// (POST /blacklist) +func (h *APIHandler) CheckBlacklist(c *gin.Context) { + var request BlacklistCheckRequest + + // Bind and validate request + if err := c.ShouldBindJSON(&request); err != nil { + c.JSON(http.StatusBadRequest, Error{ + Error: "invalid_request", + Message: "Invalid request body", + Details: stringPtr(err.Error()), + }) + return + } + + // Perform blacklist check using analyzer + checks, listedCount, score, grade, err := h.analyzer.CheckBlacklistIP(request.Ip) + if err != nil { + c.JSON(http.StatusBadRequest, Error{ + Error: "invalid_ip", + Message: "Invalid IP address", + Details: stringPtr(err.Error()), + }) + return + } + + // Build response + response := BlacklistCheckResponse{ + Ip: request.Ip, + Checks: checks, + ListedCount: listedCount, + Score: score, + Grade: BlacklistCheckResponseGrade(grade), + } + + c.JSON(http.StatusOK, response) +} diff --git a/pkg/analyzer/analyzer.go b/pkg/analyzer/analyzer.go index 1cc5bf1..e7ae561 100644 --- a/pkg/analyzer/analyzer.go +++ b/pkg/analyzer/analyzer.go @@ -119,3 +119,23 @@ func (a *APIAdapter) AnalyzeDomain(domain string) (*api.DNSResults, int, string) return dnsResults, score, grade } + +// CheckBlacklistIP checks a single IP address against DNS blacklists +func (a *APIAdapter) CheckBlacklistIP(ip string) ([]api.BlacklistCheck, int, int, string, error) { + // Check the IP against all configured RBLs + checks, listedCount, err := a.analyzer.generator.rblChecker.CheckIP(ip) + if err != nil { + return nil, 0, 0, "", err + } + + // Calculate score using the existing function + // Create a minimal RBLResults structure for scoring + results := &RBLResults{ + Checks: map[string][]api.BlacklistCheck{ip: checks}, + IPsChecked: []string{ip}, + ListedCount: listedCount, + } + score, grade := a.analyzer.generator.rblChecker.CalculateRBLScore(results) + + return checks, listedCount, score, grade, nil +} diff --git a/pkg/analyzer/rbl.go b/pkg/analyzer/rbl.go index 5e8b503..5fcb939 100644 --- a/pkg/analyzer/rbl.go +++ b/pkg/analyzer/rbl.go @@ -108,6 +108,28 @@ func (r *RBLChecker) CheckEmail(email *EmailMessage) *RBLResults { return results } +// CheckIP checks a single IP address against all configured RBLs +func (r *RBLChecker) CheckIP(ip string) ([]api.BlacklistCheck, int, error) { + // Validate that it's a valid IP address + if !r.isPublicIP(ip) { + return nil, 0, fmt.Errorf("invalid or non-public IP address: %s", ip) + } + + var checks []api.BlacklistCheck + listedCount := 0 + + // Check the IP against all RBLs + for _, rbl := range r.RBLs { + check := r.checkIP(ip, rbl) + checks = append(checks, check) + if check.Listed { + listedCount++ + } + } + + return checks, listedCount, nil +} + // extractIPs extracts IP addresses from Received headers func (r *RBLChecker) extractIPs(email *EmailMessage) []string { var ips []string diff --git a/web/routes.go b/web/routes.go index c60cb11..44b1cb2 100644 --- a/web/routes.go +++ b/web/routes.go @@ -63,6 +63,10 @@ func DeclareRoutes(cfg *config.Config, router *gin.Engine) { appConfig["survey_url"] = cfg.SurveyURL.String() } + if len(cfg.Analysis.RBLs) > 0 { + appConfig["rbls"] = cfg.Analysis.RBLs + } + if appcfg, err := json.MarshalIndent(appConfig, "", " "); err != nil { log.Println("Unable to generate JSON config to inject in web application") } else { diff --git a/web/src/lib/stores/config.ts b/web/src/lib/stores/config.ts index 4187307..8a978e0 100644 --- a/web/src/lib/stores/config.ts +++ b/web/src/lib/stores/config.ts @@ -29,6 +29,7 @@ interface AppConfig { const defaultConfig: AppConfig = { report_retention: 0, survey_url: "", + rbls: [], }; function getConfigFromScriptTag(): AppConfig | null { diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index 2cf556b..d3f17a3 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -235,9 +235,13 @@ diff --git a/web/src/routes/blacklist/+page.svelte b/web/src/routes/blacklist/+page.svelte new file mode 100644 index 0000000..f104e73 --- /dev/null +++ b/web/src/routes/blacklist/+page.svelte @@ -0,0 +1,186 @@ + + + + Blacklist Check - happyDeliver + + +
+
+
+ +
+

+ + Check IP Blacklist Status +

+

+ Test an IP address against multiple DNS-based blacklists (RBLs) to check its reputation. +

+
+ + +
+
+

Enter IP Address

+
+ + + + + +
+ + {#if error} + + {/if} + + + + Enter an IPv4 address (e.g., 192.0.2.1) or IPv6 address (e.g., 2001:db8::1) + +
+
+ + +
+
+
+
+

+ + What's Checked +

+
    + {#each $appConfig.rbls as rbl} +
  • {rbl}
  • + {/each} +
+
+
+
+ +
+
+
+

+ + Why Check Blacklists? +

+

+ DNS-based blacklists (RBLs) are used by email servers to identify and block spam sources. Being listed can severely impact email deliverability. +

+

+ This tool checks your IP against multiple popular RBLs to help you: +

+
    +
  • + Monitor IP reputation +
  • +
  • + Identify deliverability issues +
  • +
  • + Take corrective action +
  • +
+
+
+
+
+ + +
+

+ + Need Complete Email Analysis? +

+

+ For comprehensive deliverability testing including DKIM verification, content analysis, spam scoring, and more: +

+ + + Send Test Email + +
+
+
+
+ + diff --git a/web/src/routes/blacklist/[ip]/+page.svelte b/web/src/routes/blacklist/[ip]/+page.svelte new file mode 100644 index 0000000..4556552 --- /dev/null +++ b/web/src/routes/blacklist/[ip]/+page.svelte @@ -0,0 +1,230 @@ + + + + {ip} - Blacklist Check - happyDeliver + + +
+
+
+ +
+
+

+ + Blacklist Analysis +

+ + + Check Another IP + +
+
+ + {#if loading} + +
+
+
+ Loading... +
+

Checking {ip}...

+

Querying DNS-based blacklists

+
+
+ {:else if error} + +
+
+ +

Check Failed

+

{error}

+ +
+
+ {:else if result} + +
+ +
+
+
+
+

+ {result.ip} +

+ {#if result.listed_count === 0} +
+ + Not Listed +

+ This IP address is not listed on any checked blacklists. +

+
+ {:else} +
+ + Listed on {result.listed_count} blacklist{result.listed_count > 1 ? "s" : ""} +

+ This IP address is listed on {result.listed_count} of {result.checks.length} checked blacklist{result.checks.length > 1 ? "s" : ""}. +

+
+ {/if} +
+
+
+ + Blacklist Score +
+
+
+
+
+ + + + + +
+
+

+ + What This Means +

+ {#if result.listed_count === 0} +

+ Good news! This IP address is not currently listed on any of the + checked DNS-based blacklists (RBLs). This indicates a good sender reputation + and should not negatively impact email deliverability. +

+ {:else} +

+ Warning: This IP address is listed on {result.listed_count} blacklist{result.listed_count > 1 ? "s" : ""}. + Being listed can significantly impact email deliverability as many mail servers + use these blacklists to filter incoming mail. +

+
+

Recommended Actions:

+
    +
  • Investigate the cause of the listing (compromised system, spam complaints, etc.)
  • +
  • Fix any security issues or stop sending practices that led to the listing
  • +
  • Request delisting from each RBL (check their websites for removal procedures)
  • +
  • Monitor your IP reputation regularly to prevent future listings
  • +
+
+ {/if} +
+
+ + +
+
+

+ + Want Complete Email Analysis? +

+

+ This blacklist check tests IP reputation only. For comprehensive + deliverability testing including DKIM verification, content analysis, + spam scoring, and DNS configuration: +

+ + + Send Test Email + +
+
+
+ {/if} +
+
+
+ + From 723166936246d3e7db838bcca766f9d73c6346ed Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Fri, 31 Oct 2025 11:11:36 +0700 Subject: [PATCH 011/102] Add survey on RBL report and Domain report page --- web/src/routes/blacklist/[ip]/+page.svelte | 8 +++++++- web/src/routes/domain/[domain]/+page.svelte | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/web/src/routes/blacklist/[ip]/+page.svelte b/web/src/routes/blacklist/[ip]/+page.svelte index 4556552..1516751 100644 --- a/web/src/routes/blacklist/[ip]/+page.svelte +++ b/web/src/routes/blacklist/[ip]/+page.svelte @@ -3,7 +3,7 @@ import { onMount } from "svelte"; import { checkBlacklist } from "$lib/api"; import type { BlacklistCheckResponse } from "$lib/api/types.gen"; - import { GradeDisplay, BlacklistCard } from "$lib/components"; + import { BlacklistCard, GradeDisplay, TinySurvey } from "$lib/components"; import { theme } from "$lib/stores/theme"; let ip = $derived($page.params.ip); @@ -129,6 +129,12 @@ +
+ +
diff --git a/web/src/routes/domain/[domain]/+page.svelte b/web/src/routes/domain/[domain]/+page.svelte index 7ce9ee4..424b848 100644 --- a/web/src/routes/domain/[domain]/+page.svelte +++ b/web/src/routes/domain/[domain]/+page.svelte @@ -3,7 +3,7 @@ import { onMount } from "svelte"; import { testDomain } from "$lib/api"; import type { DomainTestResponse } from "$lib/api/types.gen"; - import { GradeDisplay, DnsRecordsCard } from "$lib/components"; + import { DnsRecordsCard, GradeDisplay, TinySurvey } from "$lib/components"; import { theme } from "$lib/stores/theme"; let domain = $derived($page.params.domain); @@ -124,6 +124,12 @@ +
+ +
From 3b301a415fa91f250adf4c8a94ea000679be6f66 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 1 Nov 2025 15:46:48 +0700 Subject: [PATCH 012/102] Protonmail is now the best mailbox provider I tested --- web/static/img/report.webp | Bin 86668 -> 85254 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/web/static/img/report.webp b/web/static/img/report.webp index 97c3b8c416e2cbf4158a26f81f196d51e9fd3d23..d3df7a93bd2c1df1d755285eb1f024ef4785ea28 100644 GIT binary patch literal 85254 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

AmC0+yaX!S$hD!{Jc# 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 z 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 literal 86668 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

7NK}hT>rnJZ+Nq6AulYk97ZAPC>K=DP^3Z4F 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 Date: Sat, 1 Nov 2025 17:52:28 +0700 Subject: [PATCH 013/102] Handle all options of x-aligned-from --- api/openapi.yaml | 2 +- web/src/lib/components/AuthenticationCard.svelte | 12 ++++++++++++ web/src/lib/components/HeaderAnalysisCard.svelte | 7 +------ web/src/routes/test/[test]/+page.svelte | 1 - 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/api/openapi.yaml b/api/openapi.yaml index 92bf3e3..25c1b90 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -760,7 +760,7 @@ components: properties: result: type: string - enum: [pass, fail, invalid, missing, none, neutral, softfail, temperror, permerror, declined] + enum: [pass, fail, invalid, missing, none, neutral, softfail, temperror, permerror, declined, domain_pass, orgdomain_pass] description: Authentication result example: "pass" domain: diff --git a/web/src/lib/components/AuthenticationCard.svelte b/web/src/lib/components/AuthenticationCard.svelte index 0b36dd0..8f22eac 100644 --- a/web/src/lib/components/AuthenticationCard.svelte +++ b/web/src/lib/components/AuthenticationCard.svelte @@ -16,10 +16,16 @@ function getAuthResultClass(result: string, noneIsFail: boolean): string { switch (result) { case "pass": + case "domain_pass": + case "orgdomain_pass": return "text-success"; + case "error": case "fail": case "missing": case "invalid": + case "null": + case "null_smtp": + case "null_header": return "text-danger"; case "softfail": case "neutral": @@ -36,12 +42,18 @@ function getAuthResultIcon(result: string, noneIsFail: boolean): string { switch (result) { case "pass": + case "domain_pass": + case "orgdomain_pass": return "bi-check-circle-fill"; case "fail": return "bi-x-circle-fill"; case "softfail": case "neutral": case "invalid": + case "null": + case "error": + case "null_smtp": + case "null_header": return "bi-exclamation-circle-fill"; case "missing": return "bi-dash-circle-fill"; diff --git a/web/src/lib/components/HeaderAnalysisCard.svelte b/web/src/lib/components/HeaderAnalysisCard.svelte index 36e173b..306260e 100644 --- a/web/src/lib/components/HeaderAnalysisCard.svelte +++ b/web/src/lib/components/HeaderAnalysisCard.svelte @@ -9,7 +9,6 @@ headerAnalysis: HeaderAnalysis; headerGrade?: string; headerScore?: number; - xAlignedFrom?: AuthResult; } let { dmarcRecord, headerAnalysis, headerGrade, headerScore, xAlignedFrom }: Props = $props(); @@ -62,11 +61,7 @@

- {#if xAlignedFrom} - - {:else} - - {/if} + Domain Alignment
diff --git a/web/src/routes/test/[test]/+page.svelte b/web/src/routes/test/[test]/+page.svelte index 82ff49d..8e78be7 100644 --- a/web/src/routes/test/[test]/+page.svelte +++ b/web/src/routes/test/[test]/+page.svelte @@ -335,7 +335,6 @@ headerAnalysis={report.header_analysis} headerGrade={report.summary?.header_grade} headerScore={report.summary?.header_score} - xAlignedFrom={report.authentication?.x_aligned_from} />
From 1c4eb0653ed7e72ab6404e98cb35fbd268ebf2c2 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 1 Nov 2025 17:57:57 +0700 Subject: [PATCH 014/102] Don't alert on missing -all on included SPF records --- pkg/analyzer/dns_spf.go | 39 +++++++++++--------- pkg/analyzer/dns_spf_test.go | 71 +++++++++++++++++++++++++++++++++++- 2 files changed, 92 insertions(+), 18 deletions(-) diff --git a/pkg/analyzer/dns_spf.go b/pkg/analyzer/dns_spf.go index fa819c1..a6b74c1 100644 --- a/pkg/analyzer/dns_spf.go +++ b/pkg/analyzer/dns_spf.go @@ -33,11 +33,12 @@ import ( // checkSPFRecords looks up and validates SPF records for a domain, including resolving include: directives func (d *DNSAnalyzer) checkSPFRecords(domain string) *[]api.SPFRecord { visited := make(map[string]bool) - return d.resolveSPFRecords(domain, visited, 0) + return d.resolveSPFRecords(domain, visited, 0, true) } // resolveSPFRecords recursively resolves SPF records including include: directives -func (d *DNSAnalyzer) resolveSPFRecords(domain string, visited map[string]bool, depth int) *[]api.SPFRecord { +// isMainRecord indicates if this is the primary domain's record (not an included one) +func (d *DNSAnalyzer) resolveSPFRecords(domain string, visited map[string]bool, depth int, isMainRecord bool) *[]api.SPFRecord { const maxDepth = 10 // Prevent infinite recursion if depth > maxDepth { @@ -103,7 +104,7 @@ func (d *DNSAnalyzer) resolveSPFRecords(domain string, visited map[string]bool, } // Basic validation - validationErr := d.validateSPF(spfRecord) + validationErr := d.validateSPF(spfRecord, isMainRecord) // Extract the "all" mechanism qualifier var allQualifier *api.SPFRecordAllQualifier @@ -140,7 +141,7 @@ func (d *DNSAnalyzer) resolveSPFRecords(domain string, visited map[string]bool, if redirectDomain != "" { // redirect= replaces the current domain's policy entirely // Only follow if no other mechanisms matched (per RFC 7208) - redirectRecords := d.resolveSPFRecords(redirectDomain, visited, depth+1) + redirectRecords := d.resolveSPFRecords(redirectDomain, visited, depth+1, false) if redirectRecords != nil { results = append(results, *redirectRecords...) } @@ -150,7 +151,7 @@ func (d *DNSAnalyzer) resolveSPFRecords(domain string, visited map[string]bool, // Extract and resolve include: directives includes := d.extractSPFIncludes(spfRecord) for _, includeDomain := range includes { - includedRecords := d.resolveSPFRecords(includeDomain, visited, depth+1) + includedRecords := d.resolveSPFRecords(includeDomain, visited, depth+1, false) if includedRecords != nil { results = append(results, *includedRecords...) } @@ -236,7 +237,8 @@ func (d *DNSAnalyzer) isValidSPFMechanism(token string) error { } // validateSPF performs basic SPF record validation -func (d *DNSAnalyzer) validateSPF(record string) error { +// isMainRecord indicates if this is the primary domain's record (not an included one) +func (d *DNSAnalyzer) validateSPF(record string, isMainRecord bool) error { // Must start with v=spf1 if !strings.HasPrefix(record, "v=spf1") { return fmt.Errorf("SPF record must start with 'v=spf1'") @@ -269,19 +271,22 @@ func (d *DNSAnalyzer) validateSPF(record string) error { return nil } - // Check for common syntax issues - // Should have a final mechanism (all, +all, -all, ~all, ?all) - validEndings := []string{" all", " +all", " -all", " ~all", " ?all"} - hasValidEnding := false - for _, ending := range validEndings { - if strings.HasSuffix(record, ending) { - hasValidEnding = true - break + // Only check for 'all' mechanism on the main record, not on included records + if isMainRecord { + // Check for common syntax issues + // Should have a final mechanism (all, +all, -all, ~all, ?all) + validEndings := []string{" all", " +all", " -all", " ~all", " ?all"} + hasValidEnding := false + for _, ending := range validEndings { + if strings.HasSuffix(record, ending) { + hasValidEnding = true + break + } } - } - if !hasValidEnding { - return fmt.Errorf("SPF record should end with an 'all' mechanism (e.g., '-all', '~all') or have a 'redirect=' modifier") + if !hasValidEnding { + return fmt.Errorf("SPF record should end with an 'all' mechanism (e.g., '-all', '~all') or have a 'redirect=' modifier") + } } return nil diff --git a/pkg/analyzer/dns_spf_test.go b/pkg/analyzer/dns_spf_test.go index bc51a6f..b1195cb 100644 --- a/pkg/analyzer/dns_spf_test.go +++ b/pkg/analyzer/dns_spf_test.go @@ -128,7 +128,8 @@ func TestValidateSPF(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := analyzer.validateSPF(tt.record) + // Test as main record (isMainRecord = true) since these tests check overall SPF validity + err := analyzer.validateSPF(tt.record, true) if tt.expectError { if err == nil { t.Errorf("validateSPF(%q) expected error but got nil", tt.record) @@ -144,6 +145,74 @@ func TestValidateSPF(t *testing.T) { } } +func TestValidateSPF_IncludedRecords(t *testing.T) { + tests := []struct { + name string + record string + isMainRecord bool + expectError bool + errorMsg string + }{ + { + name: "Main record without 'all' - should error", + record: "v=spf1 include:_spf.example.com", + isMainRecord: true, + expectError: true, + errorMsg: "should end with an 'all' mechanism", + }, + { + name: "Included record without 'all' - should NOT error", + record: "v=spf1 include:_spf.example.com", + isMainRecord: false, + expectError: false, + }, + { + name: "Included record with only mechanisms - should NOT error", + record: "v=spf1 ip4:192.0.2.0/24 mx", + isMainRecord: false, + expectError: false, + }, + { + name: "Main record with only mechanisms - should error", + record: "v=spf1 ip4:192.0.2.0/24 mx", + isMainRecord: true, + expectError: true, + errorMsg: "should end with an 'all' mechanism", + }, + { + name: "Included record with 'all' - valid", + record: "v=spf1 ip4:192.0.2.0/24 -all", + isMainRecord: false, + expectError: false, + }, + { + name: "Main record with 'all' - valid", + record: "v=spf1 ip4:192.0.2.0/24 -all", + isMainRecord: true, + expectError: false, + }, + } + + analyzer := NewDNSAnalyzer(5 * time.Second) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := analyzer.validateSPF(tt.record, tt.isMainRecord) + if tt.expectError { + if err == nil { + t.Errorf("validateSPF(%q, isMainRecord=%v) expected error but got nil", tt.record, tt.isMainRecord) + } else if tt.errorMsg != "" && !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("validateSPF(%q, isMainRecord=%v) error = %q, want error containing %q", tt.record, tt.isMainRecord, err.Error(), tt.errorMsg) + } + } else { + if err != nil { + t.Errorf("validateSPF(%q, isMainRecord=%v) unexpected error: %v", tt.record, tt.isMainRecord, err) + } + } + }) + } +} + func TestExtractSPFRedirect(t *testing.T) { tests := []struct { name string From d870fc81306ccf33fc9f1259be2d8315f1dd16c0 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sun, 2 Nov 2025 10:36:17 +0700 Subject: [PATCH 015/102] Add backup/restore commands --- cmd/happyDeliver/main.go | 24 ++++-- internal/app/cli_backup.go | 156 ++++++++++++++++++++++++++++++++++++ internal/storage/storage.go | 30 +++++++ 3 files changed, 205 insertions(+), 5 deletions(-) create mode 100644 internal/app/cli_backup.go diff --git a/cmd/happyDeliver/main.go b/cmd/happyDeliver/main.go index af1d30f..3caf4d1 100644 --- a/cmd/happyDeliver/main.go +++ b/cmd/happyDeliver/main.go @@ -33,8 +33,8 @@ import ( ) func main() { - fmt.Println("happyDeliver - Email Deliverability Testing Platform") - fmt.Printf("Version: %s\n", version.Version) + fmt.Fprintln(os.Stderr, "happyDeliver - Email Deliverability Testing Platform") + fmt.Fprintf(os.Stderr, "Version: %s\n", version.Version) cfg, err := config.ConsolidateConfig() if err != nil { @@ -52,6 +52,18 @@ func main() { if err := app.RunAnalyzer(cfg, flag.Args()[1:], os.Stdin, os.Stdout); err != nil { log.Fatalf("Analyzer error: %v", err) } + case "backup": + if err := app.RunBackup(cfg); err != nil { + log.Fatalf("Backup error: %v", err) + } + case "restore": + inputFile := "" + if len(flag.Args()) >= 2 { + inputFile = flag.Args()[1] + } + if err := app.RunRestore(cfg, inputFile); err != nil { + log.Fatalf("Restore error: %v", err) + } case "version": fmt.Println(version.Version) default: @@ -63,9 +75,11 @@ func main() { func printUsage() { fmt.Println("\nCommand availables:") - fmt.Println(" happyDeliver server - Start the API server") - fmt.Println(" happyDeliver analyze [-json] - Analyze email from stdin and output results to terminal") - fmt.Println(" happyDeliver version - Print version information") + fmt.Println(" happyDeliver server - Start the API server") + fmt.Println(" happyDeliver analyze [-json] - Analyze email from stdin and output results to terminal") + fmt.Println(" happyDeliver backup - Backup database to stdout as JSON") + fmt.Println(" happyDeliver restore [file] - Restore database from JSON file or stdin") + fmt.Println(" happyDeliver version - Print version information") fmt.Println("") flag.Usage() } diff --git a/internal/app/cli_backup.go b/internal/app/cli_backup.go new file mode 100644 index 0000000..4b01fbb --- /dev/null +++ b/internal/app/cli_backup.go @@ -0,0 +1,156 @@ +// This file is part of the happyDeliver (R) project. +// Copyright (c) 2025 happyDomain +// Authors: Pierre-Olivier Mercier, et al. +// +// This program is offered under a commercial and under the AGPL license. +// For commercial licensing, contact us at . +// +// For AGPL licensing: +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package app + +import ( + "encoding/json" + "fmt" + "io" + "os" + + "git.happydns.org/happyDeliver/internal/config" + "git.happydns.org/happyDeliver/internal/storage" +) + +// BackupData represents the structure of a backup file +type BackupData struct { + Version string `json:"version"` + Reports []storage.Report `json:"reports"` +} + +// RunBackup exports the database to stdout as JSON +func RunBackup(cfg *config.Config) error { + if err := cfg.Validate(); err != nil { + return err + } + + // Initialize storage + store, err := storage.NewStorage(cfg.Database.Type, cfg.Database.DSN) + if err != nil { + return fmt.Errorf("failed to connect to database: %w", err) + } + defer store.Close() + + fmt.Fprintf(os.Stderr, "Connected to %s database\n", cfg.Database.Type) + + // Get all reports from the database + reports, err := storage.GetAllReports(store) + if err != nil { + return fmt.Errorf("failed to retrieve reports: %w", err) + } + + fmt.Fprintf(os.Stderr, "Found %d reports to backup\n", len(reports)) + + // Create backup data structure + backup := BackupData{ + Version: "1.0", + Reports: reports, + } + + // Encode to JSON and write to stdout + encoder := json.NewEncoder(os.Stdout) + encoder.SetIndent("", " ") + if err := encoder.Encode(backup); err != nil { + return fmt.Errorf("failed to encode backup data: %w", err) + } + + return nil +} + +// RunRestore imports the database from a JSON file or stdin +func RunRestore(cfg *config.Config, inputPath string) error { + if err := cfg.Validate(); err != nil { + return err + } + + // Determine input source + var reader io.Reader + if inputPath == "" || inputPath == "-" { + fmt.Fprintln(os.Stderr, "Reading backup from stdin...") + reader = os.Stdin + } else { + inFile, err := os.Open(inputPath) + if err != nil { + return fmt.Errorf("failed to open backup file: %w", err) + } + defer inFile.Close() + fmt.Fprintf(os.Stderr, "Reading backup from file: %s\n", inputPath) + reader = inFile + } + + // Decode JSON + var backup BackupData + decoder := json.NewDecoder(reader) + if err := decoder.Decode(&backup); err != nil { + if err == io.EOF { + return fmt.Errorf("backup file is empty or corrupted") + } + return fmt.Errorf("failed to decode backup data: %w", err) + } + + fmt.Fprintf(os.Stderr, "Backup version: %s\n", backup.Version) + fmt.Fprintf(os.Stderr, "Found %d reports in backup\n", len(backup.Reports)) + + // Initialize storage + store, err := storage.NewStorage(cfg.Database.Type, cfg.Database.DSN) + if err != nil { + return fmt.Errorf("failed to connect to database: %w", err) + } + defer store.Close() + + fmt.Fprintf(os.Stderr, "Connected to %s database\n", cfg.Database.Type) + + // Restore reports + restored, skipped, failed := 0, 0, 0 + for _, report := range backup.Reports { + // Check if report already exists + exists, err := store.ReportExists(report.TestID) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: Failed to check if report %s exists: %v\n", report.TestID, err) + failed++ + continue + } + + if exists { + fmt.Fprintf(os.Stderr, "Report %s already exists, skipping\n", report.TestID) + skipped++ + continue + } + + // Create the report + _, err = storage.CreateReportFromBackup(store, &report) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: Failed to restore report %s: %v\n", report.TestID, err) + failed++ + continue + } + + restored++ + } + + fmt.Fprintf(os.Stderr, "Restore completed: %d restored, %d skipped, %d failed\n", restored, skipped, failed) + if failed > 0 { + return fmt.Errorf("restore completed with %d failures", failed) + } + + return nil +} diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 35aa0df..39b2eb6 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -147,3 +147,33 @@ func (s *DBStorage) Close() error { } return sqlDB.Close() } + +// GetAllReports retrieves all reports from the database +func GetAllReports(s Storage) ([]Report, error) { + dbStorage, ok := s.(*DBStorage) + if !ok { + return nil, fmt.Errorf("storage type does not support GetAllReports") + } + + var reports []Report + if err := dbStorage.db.Find(&reports).Error; err != nil { + return nil, fmt.Errorf("failed to retrieve reports: %w", err) + } + + return reports, nil +} + +// CreateReportFromBackup creates a report from backup data, preserving timestamps +func CreateReportFromBackup(s Storage, report *Report) (*Report, error) { + dbStorage, ok := s.(*DBStorage) + if !ok { + return nil, fmt.Errorf("storage type does not support CreateReportFromBackup") + } + + // Use Create to insert the report with all fields including timestamps + if err := dbStorage.db.Create(report).Error; err != nil { + return nil, fmt.Errorf("failed to create report from backup: %w", err) + } + + return report, nil +} From 465da6d16a08e7d4f252a18b234b073cb8e48bb6 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Mon, 3 Nov 2025 13:22:40 +0700 Subject: [PATCH 016/102] Don't look at original DKIM keys headers --- pkg/analyzer/authentication.go | 7 - pkg/analyzer/authentication_dkim.go | 34 ---- pkg/analyzer/authentication_dkim_test.go | 244 ----------------------- 3 files changed, 285 deletions(-) diff --git a/pkg/analyzer/authentication.go b/pkg/analyzer/authentication.go index 02f8b28..07f7794 100644 --- a/pkg/analyzer/authentication.go +++ b/pkg/analyzer/authentication.go @@ -50,13 +50,6 @@ func (a *AuthenticationAnalyzer) AnalyzeAuthentication(email *EmailMessage) *api results.Spf = a.parseLegacySPF(email) } - if results.Dkim == nil || len(*results.Dkim) == 0 { - dkimResults := a.parseLegacyDKIM(email) - if len(dkimResults) > 0 { - results.Dkim = &dkimResults - } - } - // Parse ARC headers if not already parsed from Authentication-Results if results.Arc == nil { results.Arc = a.parseARCHeaders(email) diff --git a/pkg/analyzer/authentication_dkim.go b/pkg/analyzer/authentication_dkim.go index 9f1774b..b6cf5f8 100644 --- a/pkg/analyzer/authentication_dkim.go +++ b/pkg/analyzer/authentication_dkim.go @@ -59,40 +59,6 @@ func (a *AuthenticationAnalyzer) parseDKIMResult(part string) *api.AuthResult { return result } -// parseLegacyDKIM attempts to parse DKIM from DKIM-Signature header -func (a *AuthenticationAnalyzer) parseLegacyDKIM(email *EmailMessage) []api.AuthResult { - var results []api.AuthResult - - // Get all DKIM-Signature headers - dkimHeaders := email.Header[textprotoCanonical("DKIM-Signature")] - for _, dkimHeader := range dkimHeaders { - result := api.AuthResult{ - Result: api.AuthResultResultNone, // We can't determine pass/fail from signature alone - } - - // Extract domain (d=) - domainRe := regexp.MustCompile(`d=([^\s;]+)`) - if matches := domainRe.FindStringSubmatch(dkimHeader); len(matches) > 1 { - domain := matches[1] - result.Domain = &domain - } - - // Extract selector (s=) - selectorRe := regexp.MustCompile(`s=([^\s;]+)`) - if matches := selectorRe.FindStringSubmatch(dkimHeader); len(matches) > 1 { - selector := matches[1] - result.Selector = &selector - } - - details := "DKIM signature present (verification status unknown)" - result.Details = &details - - results = append(results, result) - } - - return results -} - func (a *AuthenticationAnalyzer) calculateDKIMScore(results *api.AuthenticationResults) (score int) { // Expect at least one passing signature if results.Dkim != nil && len(*results.Dkim) > 0 { diff --git a/pkg/analyzer/authentication_dkim_test.go b/pkg/analyzer/authentication_dkim_test.go index 323e421..2aab530 100644 --- a/pkg/analyzer/authentication_dkim_test.go +++ b/pkg/analyzer/authentication_dkim_test.go @@ -22,7 +22,6 @@ package analyzer import ( - "strings" "testing" "git.happydns.org/happyDeliver/internal/api" @@ -85,246 +84,3 @@ func TestParseDKIMResult(t *testing.T) { }) } } - -func TestParseLegacyDKIM(t *testing.T) { - tests := []struct { - name string - dkimSignatures []string - expectedCount int - expectedDomains []string - expectedSelector []string - }{ - { - name: "Single DKIM signature with domain and selector", - dkimSignatures: []string{ - "v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=selector1; h=from:to:subject:date; bh=xyz; b=abc", - }, - expectedCount: 1, - expectedDomains: []string{"example.com"}, - expectedSelector: []string{"selector1"}, - }, - { - name: "Multiple DKIM signatures", - dkimSignatures: []string{ - "v=1; a=rsa-sha256; d=example.com; s=selector1; b=abc123", - "v=1; a=rsa-sha256; d=example.com; s=selector2; b=def456", - }, - expectedCount: 2, - expectedDomains: []string{"example.com", "example.com"}, - expectedSelector: []string{"selector1", "selector2"}, - }, - { - name: "DKIM signature with different domain", - dkimSignatures: []string{ - "v=1; a=rsa-sha256; d=mail.example.org; s=default; b=xyz789", - }, - expectedCount: 1, - expectedDomains: []string{"mail.example.org"}, - expectedSelector: []string{"default"}, - }, - { - name: "DKIM signature with subdomain", - dkimSignatures: []string{ - "v=1; a=rsa-sha256; d=newsletters.example.com; s=marketing; b=aaa", - }, - expectedCount: 1, - expectedDomains: []string{"newsletters.example.com"}, - expectedSelector: []string{"marketing"}, - }, - { - name: "Multiple signatures from different domains", - dkimSignatures: []string{ - "v=1; a=rsa-sha256; d=example.com; s=s1; b=abc", - "v=1; a=rsa-sha256; d=relay.com; s=s2; b=def", - }, - expectedCount: 2, - expectedDomains: []string{"example.com", "relay.com"}, - expectedSelector: []string{"s1", "s2"}, - }, - { - name: "No DKIM signatures", - dkimSignatures: []string{}, - expectedCount: 0, - expectedDomains: []string{}, - expectedSelector: []string{}, - }, - { - name: "DKIM signature without selector", - dkimSignatures: []string{ - "v=1; a=rsa-sha256; d=example.com; b=abc123", - }, - expectedCount: 1, - expectedDomains: []string{"example.com"}, - expectedSelector: []string{""}, - }, - { - name: "DKIM signature without domain", - dkimSignatures: []string{ - "v=1; a=rsa-sha256; s=selector1; b=abc123", - }, - expectedCount: 1, - expectedDomains: []string{""}, - expectedSelector: []string{"selector1"}, - }, - { - name: "DKIM signature with whitespace in parameters", - dkimSignatures: []string{ - "v=1; a=rsa-sha256; d=example.com ; s=selector1 ; b=abc123", - }, - expectedCount: 1, - expectedDomains: []string{"example.com"}, - expectedSelector: []string{"selector1"}, - }, - { - name: "DKIM signature with multiline format", - dkimSignatures: []string{ - "v=1; a=rsa-sha256; c=relaxed/relaxed;\r\n\td=example.com; s=selector1;\r\n\th=from:to:subject:date;\r\n\tb=abc123def456ghi789", - }, - expectedCount: 1, - expectedDomains: []string{"example.com"}, - expectedSelector: []string{"selector1"}, - }, - { - name: "DKIM signature with ed25519 algorithm", - dkimSignatures: []string{ - "v=1; a=ed25519-sha256; d=example.com; s=ed25519; b=xyz", - }, - expectedCount: 1, - expectedDomains: []string{"example.com"}, - expectedSelector: []string{"ed25519"}, - }, - { - name: "Complex real-world DKIM signature", - dkimSignatures: []string{ - "v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20230601; t=1234567890; x=1234567950; darn=example.com; h=to:subject:message-id:date:from:mime-version:from:to:cc:subject:date:message-id:reply-to; bh=abc123def456==; b=longsignaturehere==", - }, - expectedCount: 1, - expectedDomains: []string{"google.com"}, - expectedSelector: []string{"20230601"}, - }, - } - - analyzer := NewAuthenticationAnalyzer() - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock email message with DKIM-Signature headers - email := &EmailMessage{ - Header: make(map[string][]string), - } - if len(tt.dkimSignatures) > 0 { - email.Header["Dkim-Signature"] = tt.dkimSignatures - } - - results := analyzer.parseLegacyDKIM(email) - - // Check count - if len(results) != tt.expectedCount { - t.Errorf("Expected %d results, got %d", tt.expectedCount, len(results)) - return - } - - // Check each result - for i, result := range results { - // All legacy DKIM results should have Result = none - if result.Result != api.AuthResultResultNone { - t.Errorf("Result[%d].Result = %v, want %v", i, result.Result, api.AuthResultResultNone) - } - - // Check domain - if i < len(tt.expectedDomains) { - expectedDomain := tt.expectedDomains[i] - if expectedDomain != "" { - if result.Domain == nil { - t.Errorf("Result[%d].Domain = nil, want %v", i, expectedDomain) - } else if strings.TrimSpace(*result.Domain) != expectedDomain { - t.Errorf("Result[%d].Domain = %v, want %v", i, *result.Domain, expectedDomain) - } - } - } - - // Check selector - if i < len(tt.expectedSelector) { - expectedSelector := tt.expectedSelector[i] - if expectedSelector != "" { - if result.Selector == nil { - t.Errorf("Result[%d].Selector = nil, want %v", i, expectedSelector) - } else if strings.TrimSpace(*result.Selector) != expectedSelector { - t.Errorf("Result[%d].Selector = %v, want %v", i, *result.Selector, expectedSelector) - } - } - } - - // Check that Details is set - if result.Details == nil { - t.Errorf("Result[%d].Details = nil, expected non-nil", i) - } else { - expectedDetails := "DKIM signature present (verification status unknown)" - if *result.Details != expectedDetails { - t.Errorf("Result[%d].Details = %v, want %v", i, *result.Details, expectedDetails) - } - } - } - }) - } -} - -func TestParseLegacyDKIM_Integration(t *testing.T) { - hostname = "" - - // Test that parseLegacyDKIM is properly integrated into AnalyzeAuthentication - t.Run("Legacy DKIM is used when no Authentication-Results", func(t *testing.T) { - analyzer := NewAuthenticationAnalyzer() - email := &EmailMessage{ - Header: make(map[string][]string), - } - email.Header["Dkim-Signature"] = []string{ - "v=1; a=rsa-sha256; d=example.com; s=selector1; b=abc", - } - - results := analyzer.AnalyzeAuthentication(email) - - if results.Dkim == nil { - t.Fatal("Expected DKIM results, got nil") - } - if len(*results.Dkim) != 1 { - t.Errorf("Expected 1 DKIM result, got %d", len(*results.Dkim)) - } - if (*results.Dkim)[0].Result != api.AuthResultResultNone { - t.Errorf("Expected DKIM result to be 'none', got %v", (*results.Dkim)[0].Result) - } - if (*results.Dkim)[0].Domain == nil || *(*results.Dkim)[0].Domain != "example.com" { - t.Error("Expected domain to be 'example.com'") - } - }) - - t.Run("Legacy DKIM is NOT used when Authentication-Results present", func(t *testing.T) { - analyzer := NewAuthenticationAnalyzer() - email := &EmailMessage{ - Header: make(map[string][]string), - } - // Both Authentication-Results and DKIM-Signature headers - email.Header["Authentication-Results"] = []string{ - "mx.example.com; dkim=pass header.d=verified.com header.s=s1", - } - email.Header["Dkim-Signature"] = []string{ - "v=1; a=rsa-sha256; d=example.com; s=selector1; b=abc", - } - - results := analyzer.AnalyzeAuthentication(email) - - // Should use the Authentication-Results DKIM (pass from verified.com), not the legacy signature - if results.Dkim == nil { - t.Fatal("Expected DKIM results, got nil") - } - if len(*results.Dkim) != 1 { - t.Errorf("Expected 1 DKIM result, got %d", len(*results.Dkim)) - } - if (*results.Dkim)[0].Result != api.AuthResultResultPass { - t.Errorf("Expected DKIM result to be 'pass', got %v", (*results.Dkim)[0].Result) - } - if (*results.Dkim)[0].Domain == nil || *(*results.Dkim)[0].Domain != "verified.com" { - t.Error("Expected domain to be 'verified.com' from Authentication-Results, not 'example.com' from legacy") - } - }) -} From 5b179e7b93ba8d277eca793699b2acccf8d74617 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Mon, 3 Nov 2025 14:58:48 +0700 Subject: [PATCH 017/102] Domain alignment checks for DKIM --- api/openapi.yaml | 20 +- pkg/analyzer/headers.go | 119 +++++++++-- pkg/analyzer/headers_test.go | 160 ++++++++++++++- pkg/analyzer/report.go | 2 +- .../lib/components/HeaderAnalysisCard.svelte | 192 +++++++++++++----- 5 files changed, 410 insertions(+), 83 deletions(-) diff --git a/api/openapi.yaml b/api/openapi.yaml index 25c1b90..8463007 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -664,6 +664,21 @@ components: description: Reverse DNS (PTR record) for the IP address example: "mail.example.com" + DKIMDomainInfo: + type: object + required: + - domain + - org_domain + properties: + domain: + type: string + description: DKIM signature domain + example: "mail.example.com" + org_domain: + type: string + description: Organizational domain extracted from DKIM domain (using Public Suffix List) + example: "example.com" + DomainAlignment: type: object properties: @@ -686,9 +701,8 @@ components: dkim_domains: type: array items: - type: string - description: Domains from DKIM signatures - example: ["example.com"] + $ref: '#/components/schemas/DKIMDomainInfo' + description: Domains from DKIM signatures with their organizational domains aligned: type: boolean description: Whether all domains align (strict alignment - exact match) diff --git a/pkg/analyzer/headers.go b/pkg/analyzer/headers.go index 7e65571..b7ff3bb 100644 --- a/pkg/analyzer/headers.go +++ b/pkg/analyzer/headers.go @@ -52,13 +52,14 @@ func (h *HeaderAnalyzer) CalculateHeaderScore(analysis *api.HeaderAnalysis) (int maxGrade := 6 headers := *analysis.Headers - // RP and From alignment (20 points) - if analysis.DomainAlignment.Aligned != nil && *analysis.DomainAlignment.Aligned { - score += 20 - } else if analysis.DomainAlignment.RelaxedAligned != nil && *analysis.DomainAlignment.RelaxedAligned { - score += 15 - } else { + // RP and From alignment (25 points) + if analysis.DomainAlignment.Aligned == nil || !*analysis.DomainAlignment.RelaxedAligned { + // Bad domain alignment, cap grade to C maxGrade -= 2 + } else if *analysis.DomainAlignment.Aligned { + score += 25 + } else if *analysis.DomainAlignment.RelaxedAligned { + score += 20 } // Check required headers (RFC 5322) - 30 points @@ -79,7 +80,7 @@ func (h *HeaderAnalyzer) CalculateHeaderScore(analysis *api.HeaderAnalysis) (int maxGrade = 1 } - // Check recommended headers (20 points) + // Check recommended headers (15 points) recommendedHeaders := []string{"subject", "to"} // Add reply-to when from is a no-reply address @@ -95,7 +96,7 @@ func (h *HeaderAnalyzer) CalculateHeaderScore(analysis *api.HeaderAnalysis) (int presentRecommended++ } } - score += presentRecommended * 20 / recommendedCount + score += presentRecommended * 15 / recommendedCount if presentRecommended < recommendedCount { maxGrade -= 1 @@ -235,7 +236,7 @@ func (h *HeaderAnalyzer) formatAddress(addr *mail.Address) string { } // GenerateHeaderAnalysis creates structured header analysis from email -func (h *HeaderAnalyzer) GenerateHeaderAnalysis(email *EmailMessage) *api.HeaderAnalysis { +func (h *HeaderAnalyzer) GenerateHeaderAnalysis(email *EmailMessage, authResults *api.AuthenticationResults) *api.HeaderAnalysis { if email == nil { return nil } @@ -281,7 +282,7 @@ func (h *HeaderAnalyzer) GenerateHeaderAnalysis(email *EmailMessage) *api.Header } // Domain alignment - domainAlignment := h.analyzeDomainAlignment(email) + domainAlignment := h.analyzeDomainAlignment(email, authResults) if domainAlignment != nil { analysis.DomainAlignment = domainAlignment } @@ -352,8 +353,8 @@ func (h *HeaderAnalyzer) checkHeader(email *EmailMessage, headerName string, imp return check } -// analyzeDomainAlignment checks domain alignment between headers -func (h *HeaderAnalyzer) analyzeDomainAlignment(email *EmailMessage) *api.DomainAlignment { +// analyzeDomainAlignment checks domain alignment between headers and DKIM signatures +func (h *HeaderAnalyzer) analyzeDomainAlignment(email *EmailMessage, authResults *api.AuthenticationResults) *api.DomainAlignment { alignment := &api.DomainAlignment{ Aligned: api.PtrTo(true), RelaxedAligned: api.PtrTo(true), @@ -383,14 +384,45 @@ func (h *HeaderAnalyzer) analyzeDomainAlignment(email *EmailMessage) *api.Domain } } + // Extract DKIM domains from authentication results + var dkimDomains []api.DKIMDomainInfo + if authResults != nil && authResults.Dkim != nil { + for _, dkim := range *authResults.Dkim { + if dkim.Domain != nil && *dkim.Domain != "" { + domain := *dkim.Domain + orgDomain := h.getOrganizationalDomain(domain) + dkimDomains = append(dkimDomains, api.DKIMDomainInfo{ + Domain: domain, + OrgDomain: orgDomain, + }) + } + } + } + if len(dkimDomains) > 0 { + alignment.DkimDomains = &dkimDomains + } + // Check alignment (strict and relaxed) issues := []string{} - if alignment.FromDomain != nil && alignment.ReturnPathDomain != nil { + + // hasReturnPath and hasDKIM track whether we have these fields to check + hasReturnPath := alignment.FromDomain != nil && alignment.ReturnPathDomain != nil + hasDKIM := alignment.FromDomain != nil && len(dkimDomains) > 0 + + // If neither Return-Path nor DKIM is present, keep default alignment (true) + // Otherwise, at least one must be aligned for overall alignment to be true + strictAligned := !hasReturnPath && !hasDKIM + relaxedAligned := !hasReturnPath && !hasDKIM + + // Check Return-Path alignment + rpStrictAligned := false + rpRelaxedAligned := false + if hasReturnPath { fromDomain := *alignment.FromDomain rpDomain := *alignment.ReturnPathDomain // Strict alignment: exact match (case-insensitive) - strictAligned := strings.EqualFold(fromDomain, rpDomain) + rpStrictAligned = strings.EqualFold(fromDomain, rpDomain) // Relaxed alignment: organizational domain match var fromOrgDomain, rpOrgDomain string @@ -400,20 +432,67 @@ func (h *HeaderAnalyzer) analyzeDomainAlignment(email *EmailMessage) *api.Domain if alignment.ReturnPathOrgDomain != nil { rpOrgDomain = *alignment.ReturnPathOrgDomain } - relaxedAligned := strings.EqualFold(fromOrgDomain, rpOrgDomain) + rpRelaxedAligned = strings.EqualFold(fromOrgDomain, rpOrgDomain) - *alignment.Aligned = strictAligned - *alignment.RelaxedAligned = relaxedAligned - - if !strictAligned { - if relaxedAligned { + if !rpStrictAligned { + if rpRelaxedAligned { issues = append(issues, fmt.Sprintf("Return-Path domain (%s) does not exactly match From domain (%s), but satisfies relaxed alignment (organizational domain: %s)", rpDomain, fromDomain, fromOrgDomain)) } else { issues = append(issues, fmt.Sprintf("Return-Path domain (%s) does not match From domain (%s) - neither strict nor relaxed alignment", rpDomain, fromDomain)) } } + + strictAligned = rpStrictAligned + relaxedAligned = rpRelaxedAligned } + // Check DKIM alignment + dkimStrictAligned := false + dkimRelaxedAligned := false + if hasDKIM { + fromDomain := *alignment.FromDomain + var fromOrgDomain string + if alignment.FromOrgDomain != nil { + fromOrgDomain = *alignment.FromOrgDomain + } + + for _, dkimDomain := range dkimDomains { + // Check strict alignment for this DKIM signature + if strings.EqualFold(fromDomain, dkimDomain.Domain) { + dkimStrictAligned = true + } + + // Check relaxed alignment for this DKIM signature + if strings.EqualFold(fromOrgDomain, dkimDomain.OrgDomain) { + dkimRelaxedAligned = true + } + } + + if !dkimStrictAligned && !dkimRelaxedAligned { + // List all DKIM domains that failed alignment + dkimDomainsList := []string{} + for _, dkimDomain := range dkimDomains { + dkimDomainsList = append(dkimDomainsList, dkimDomain.Domain) + } + issues = append(issues, fmt.Sprintf("DKIM signature domains (%s) do not align with From domain (%s) - neither strict nor relaxed alignment", strings.Join(dkimDomainsList, ", "), fromDomain)) + } else if !dkimStrictAligned && dkimRelaxedAligned { + // DKIM has relaxed alignment but not strict + issues = append(issues, fmt.Sprintf("DKIM signature domains satisfy relaxed alignment with From domain (%s) but not strict alignment (organizational domain: %s)", fromDomain, fromOrgDomain)) + } + + // Overall alignment requires at least one method (Return-Path OR DKIM) to be aligned + // For DMARC compliance, at least one of SPF or DKIM must be aligned + if dkimStrictAligned { + strictAligned = true + } + if dkimRelaxedAligned { + relaxedAligned = true + } + } + + *alignment.Aligned = strictAligned + *alignment.RelaxedAligned = relaxedAligned + if len(issues) > 0 { alignment.Issues = &issues } diff --git a/pkg/analyzer/headers_test.go b/pkg/analyzer/headers_test.go index 7896a5c..6a35d18 100644 --- a/pkg/analyzer/headers_test.go +++ b/pkg/analyzer/headers_test.go @@ -24,6 +24,7 @@ package analyzer import ( "net/mail" "net/textproto" + "strings" "testing" "git.happydns.org/happyDeliver/internal/api" @@ -110,7 +111,7 @@ func TestCalculateHeaderScore(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Generate header analysis first - analysis := analyzer.GenerateHeaderAnalysis(tt.email) + analysis := analyzer.GenerateHeaderAnalysis(tt.email, nil) score, _ := analyzer.CalculateHeaderScore(analysis) if score < tt.minScore || score > tt.maxScore { t.Errorf("CalculateHeaderScore() = %v, want between %v and %v", score, tt.minScore, tt.maxScore) @@ -360,7 +361,7 @@ func TestAnalyzeDomainAlignment(t *testing.T) { }), } - alignment := analyzer.analyzeDomainAlignment(email) + alignment := analyzer.analyzeDomainAlignment(email, nil) if alignment == nil { t.Fatal("Expected non-nil alignment") @@ -698,7 +699,7 @@ func TestGenerateHeaderAnalysis_WithReceivedChain(t *testing.T) { "from relay.example.com (relay.example.com [192.0.2.2]) by mail.example.com with SMTP id DEF456; Mon, 01 Jan 2024 11:59:00 +0000", } - analysis := analyzer.GenerateHeaderAnalysis(email) + analysis := analyzer.GenerateHeaderAnalysis(email, nil) if analysis == nil { t.Fatal("GenerateHeaderAnalysis returned nil") @@ -923,3 +924,156 @@ func equalStrPtr(a, b *string) bool { } return *a == *b } + +func TestAnalyzeDomainAlignment_WithDKIM(t *testing.T) { + tests := []struct { + name string + fromHeader string + returnPath string + dkimDomains []string + expectStrictAligned bool + expectRelaxedAligned bool + expectIssuesContain string + }{ + { + name: "DKIM strict alignment with From domain", + fromHeader: "sender@example.com", + returnPath: "", + dkimDomains: []string{"example.com"}, + expectStrictAligned: true, + expectRelaxedAligned: true, + expectIssuesContain: "", + }, + { + name: "DKIM relaxed alignment only", + fromHeader: "sender@mail.example.com", + returnPath: "", + dkimDomains: []string{"example.com"}, + expectStrictAligned: false, + expectRelaxedAligned: true, + expectIssuesContain: "relaxed alignment", + }, + { + name: "DKIM no alignment", + fromHeader: "sender@example.com", + returnPath: "", + dkimDomains: []string{"different.com"}, + expectStrictAligned: false, + expectRelaxedAligned: false, + expectIssuesContain: "do not align", + }, + { + name: "Multiple DKIM signatures - one aligns", + fromHeader: "sender@example.com", + returnPath: "", + dkimDomains: []string{"different.com", "example.com"}, + expectStrictAligned: true, + expectRelaxedAligned: true, + expectIssuesContain: "", + }, + { + name: "Return-Path misaligned but DKIM aligned", + fromHeader: "sender@example.com", + returnPath: "bounce@different.com", + dkimDomains: []string{"example.com"}, + expectStrictAligned: true, + expectRelaxedAligned: true, + expectIssuesContain: "Return-Path", + }, + { + name: "Return-Path aligned, no DKIM", + fromHeader: "sender@example.com", + returnPath: "bounce@example.com", + dkimDomains: []string{}, + expectStrictAligned: true, + expectRelaxedAligned: true, + expectIssuesContain: "", + }, + { + name: "Both Return-Path and DKIM misaligned", + fromHeader: "sender@example.com", + returnPath: "bounce@other.com", + dkimDomains: []string{"different.com"}, + expectStrictAligned: false, + expectRelaxedAligned: false, + expectIssuesContain: "do not", + }, + } + + analyzer := NewHeaderAnalyzer() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + email := &EmailMessage{ + Header: createHeaderWithFields(map[string]string{ + "From": tt.fromHeader, + "Return-Path": tt.returnPath, + }), + } + + // Create authentication results with DKIM signatures + var authResults *api.AuthenticationResults + if len(tt.dkimDomains) > 0 { + dkimResults := make([]api.AuthResult, 0, len(tt.dkimDomains)) + for _, domain := range tt.dkimDomains { + dkimResults = append(dkimResults, api.AuthResult{ + Result: api.AuthResultResultPass, + Domain: &domain, + }) + } + authResults = &api.AuthenticationResults{ + Dkim: &dkimResults, + } + } + + alignment := analyzer.analyzeDomainAlignment(email, authResults) + + if alignment == nil { + t.Fatal("Expected non-nil alignment") + } + + if alignment.Aligned == nil { + t.Fatal("Expected non-nil Aligned field") + } + + if *alignment.Aligned != tt.expectStrictAligned { + t.Errorf("Aligned = %v, want %v", *alignment.Aligned, tt.expectStrictAligned) + } + + if alignment.RelaxedAligned == nil { + t.Fatal("Expected non-nil RelaxedAligned field") + } + + if *alignment.RelaxedAligned != tt.expectRelaxedAligned { + t.Errorf("RelaxedAligned = %v, want %v", *alignment.RelaxedAligned, tt.expectRelaxedAligned) + } + + // Check DKIM domains are populated + if len(tt.dkimDomains) > 0 { + if alignment.DkimDomains == nil { + t.Error("Expected DkimDomains to be populated") + } else if len(*alignment.DkimDomains) != len(tt.dkimDomains) { + t.Errorf("Expected %d DKIM domains, got %d", len(tt.dkimDomains), len(*alignment.DkimDomains)) + } + } + + // Check issues contain expected string + if tt.expectIssuesContain != "" { + if alignment.Issues == nil || len(*alignment.Issues) == 0 { + t.Errorf("Expected issues to contain '%s', but no issues found", tt.expectIssuesContain) + } else { + found := false + for _, issue := range *alignment.Issues { + if strings.Contains(strings.ToLower(issue), strings.ToLower(tt.expectIssuesContain)) { + found = true + break + } + } + if !found { + t.Errorf("Expected issues to contain '%s', but found: %v", tt.expectIssuesContain, *alignment.Issues) + } + } + } + }) + } +} diff --git a/pkg/analyzer/report.go b/pkg/analyzer/report.go index a39a98a..39871fe 100644 --- a/pkg/analyzer/report.go +++ b/pkg/analyzer/report.go @@ -75,7 +75,7 @@ func (r *ReportGenerator) AnalyzeEmail(email *EmailMessage) *AnalysisResults { // Run all analyzers results.Authentication = r.authAnalyzer.AnalyzeAuthentication(email) - results.Headers = r.headerAnalyzer.GenerateHeaderAnalysis(email) + results.Headers = r.headerAnalyzer.GenerateHeaderAnalysis(email, results.Authentication) results.DNS = r.dnsAnalyzer.AnalyzeDNS(email, results.Authentication, results.Headers) results.RBL = r.rblChecker.CheckEmail(email) results.SpamAssassin = r.spamAnalyzer.AnalyzeSpamAssassin(email) diff --git a/web/src/lib/components/HeaderAnalysisCard.svelte b/web/src/lib/components/HeaderAnalysisCard.svelte index 306260e..3cfe287 100644 --- a/web/src/lib/components/HeaderAnalysisCard.svelte +++ b/web/src/lib/components/HeaderAnalysisCard.svelte @@ -66,68 +66,148 @@
-

- Domain alignment ensures that the visible "From" domain matches the domain used for authentication (Return-Path). Proper alignment is crucial for DMARC compliance and helps prevent email spoofing by verifying that the sender domain is consistent across all authentication layers. +

+ Domain alignment ensures that the visible "From" domain matches the domain used for authentication (Return-Path or DKIM signature). Proper alignment is crucial for DMARC compliance, regardless of the policy. It helps prevent email spoofing by verifying that the sender domain is consistent across all authentication layers. Only one of the following lines needs to pass.

-
-
- Strict Alignment -
- - - {headerAnalysis.domain_alignment.aligned ? 'Pass' : 'Fail'} - -
-
Exact domain match
+
+
+
+
+ SPF
-
- Relaxed Alignment -
- - - {headerAnalysis.domain_alignment.relaxed_aligned ? 'Pass' : 'Fail'} - -
-
Organizational domain match
-
-
- From Domain -
{headerAnalysis.domain_alignment.from_domain || '-'}
- {#if headerAnalysis.domain_alignment.from_org_domain && headerAnalysis.domain_alignment.from_org_domain !== headerAnalysis.domain_alignment.from_domain} -
Org: {headerAnalysis.domain_alignment.from_org_domain}
- {/if} -
-
- Return-Path Domain -
{headerAnalysis.domain_alignment.return_path_domain || '-'}
- {#if headerAnalysis.domain_alignment.return_path_org_domain && headerAnalysis.domain_alignment.return_path_org_domain !== headerAnalysis.domain_alignment.return_path_domain} -
Org: {headerAnalysis.domain_alignment.return_path_org_domain}
- {/if} -
-
- {#if headerAnalysis.domain_alignment.issues && headerAnalysis.domain_alignment.issues.length > 0} -
- {#each headerAnalysis.domain_alignment.issues as issue} -
- - {issue} +
+
+ Strict Alignment +
+ + + {headerAnalysis.domain_alignment.aligned ? 'Pass' : 'Fail'} +
- {/each} +
Exact domain match
+
+
+ Relaxed Alignment +
+ + + {headerAnalysis.domain_alignment.relaxed_aligned ? 'Pass' : 'Fail'} + +
+
Organizational domain match
+
+
+ From Domain +
{headerAnalysis.domain_alignment.from_domain || '-'}
+ {#if headerAnalysis.domain_alignment.from_org_domain && headerAnalysis.domain_alignment.from_org_domain !== headerAnalysis.domain_alignment.from_domain} +
Org: {headerAnalysis.domain_alignment.from_org_domain}
+ {/if} +
+
+ Return-Path Domain +
{headerAnalysis.domain_alignment.return_path_domain || '-'}
+ {#if headerAnalysis.domain_alignment.return_path_org_domain && headerAnalysis.domain_alignment.return_path_org_domain !== headerAnalysis.domain_alignment.return_path_domain} +
Org: {headerAnalysis.domain_alignment.return_path_org_domain}
+ {/if} +
- {/if} + {#if headerAnalysis.domain_alignment.issues && headerAnalysis.domain_alignment.issues.length > 0} +
+ {#each headerAnalysis.domain_alignment.issues as issue} +
+ + {issue} +
+ {/each} +
+ {/if} - - {#if dmarcRecord && headerAnalysis.domain_alignment.return_path_domain && headerAnalysis.domain_alignment.return_path_domain !== headerAnalysis.domain_alignment.from_domain} -
- {#if dmarcRecord.spf_alignment === 'strict'} - - Strict SPF alignment required — Your DMARC policy requires exact domain match. The Return-Path domain must exactly match the From domain for SPF to pass DMARC alignment. - {:else} - - Relaxed SPF alignment allowed — Your DMARC policy allows organizational domain matching. As long as both domains share the same organizational domain (e.g., mail.example.com and example.com), SPF alignment can pass. - {/if} + + {#if dmarcRecord && headerAnalysis.domain_alignment.return_path_domain && headerAnalysis.domain_alignment.return_path_domain !== headerAnalysis.domain_alignment.from_domain} +
+ {#if dmarcRecord.spf_alignment === 'strict'} + + Strict SPF alignment required — Your DMARC policy requires exact domain match. The Return-Path domain must exactly match the From domain for SPF to pass DMARC alignment. + {:else} + + Relaxed SPF alignment allowed — Your DMARC policy allows organizational domain matching. As long as both domains share the same organizational domain (e.g., mail.example.com and example.com), SPF alignment can pass. + {/if} +
+ {/if} +
+ + {#each headerAnalysis.domain_alignment.dkim_domains as dkim_domain} + {@const dkim_aligned = dkim_domain.domain === headerAnalysis.domain_alignment.from_domain} + {@const dkim_relaxed_aligned = dkim_domain.org_domain === headerAnalysis.domain_alignment.from_org_domain} +
+
+ DKIM +
+
+
+
+ Strict Alignment +
+ + + {dkim_aligned ? 'Pass' : 'Fail'} + +
+
Exact domain match
+
+
+ Relaxed Alignment +
+ + + {dkim_relaxed_aligned ? 'Pass' : 'Fail'} + +
+
Organizational domain match
+
+
+ From Domain +
{headerAnalysis.domain_alignment.from_domain || '-'}
+ {#if headerAnalysis.domain_alignment.from_org_domain && headerAnalysis.domain_alignment.from_org_domain !== headerAnalysis.domain_alignment.from_domain} +
Org: {headerAnalysis.domain_alignment.from_org_domain}
+ {/if} +
+
+ Signature Domain +
{dkim_domain.domain || '-'}
+ {#if dkim_domain.domain !== dkim_domain.org_domain} +
Org: {dkim_domain.org_domain}
+ {/if} +
+
+ {#if headerAnalysis.domain_alignment.issues && headerAnalysis.domain_alignment.issues.length > 0} +
+ {#each headerAnalysis.domain_alignment.issues as issue} +
+ + {issue} +
+ {/each} +
+ {/if} + + + {#if dmarcRecord && dkim_domain.domain !== headerAnalysis.domain_alignment.from_domain} + {#if dkim_domain.org_domain === headerAnalysis.domain_alignment.from_org_domain} +
+ {#if dmarcRecord.dkim_alignment === 'strict'} + + Strict DKIM alignment required — Your DMARC policy requires exact domain match. The DKIM signature domain must exactly match the From domain for DKIM to pass DMARC alignment. + {:else} + + Relaxed DKIM alignment allowed — Your DMARC policy allows organizational domain matching. As long as both domains share the same organizational domain (e.g., mail.example.com and example.com), DKIM alignment can pass. + {/if} +
+ {/if} + {/if} +
- {/if} + {/each}
{/if} From c52a3aa8a769bf9d2e635aa10e6987e24ecc2bc6 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Mon, 3 Nov 2025 14:59:18 +0700 Subject: [PATCH 018/102] Improve DMARC description --- web/src/lib/components/DmarcRecordDisplay.svelte | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/web/src/lib/components/DmarcRecordDisplay.svelte b/web/src/lib/components/DmarcRecordDisplay.svelte index 09a10c7..b7a3e7b 100644 --- a/web/src/lib/components/DmarcRecordDisplay.svelte +++ b/web/src/lib/components/DmarcRecordDisplay.svelte @@ -34,9 +34,10 @@

- DMARC builds on SPF and DKIM by telling receiving servers what to do with emails - that fail authentication checks. It also enables reporting so you can monitor your - email security. + DMARC enforces domain alignment requirements (regardless of the policy). It builds + on SPF and DKIM by telling receiving servers what to do with emails that fail + authentication checks. It also enables reporting so you can monitor your email + security.


From deb9fd4f512565615ae9d2e017f7e899c80afc37 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Fri, 7 Nov 2025 14:09:05 +0700 Subject: [PATCH 019/102] Handle RFC6652 Closes: https://framagit.org/happyDomain/happydeliver/-/issues/1 --- pkg/analyzer/dns_spf.go | 8 ++++++-- pkg/analyzer/dns_spf_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/pkg/analyzer/dns_spf.go b/pkg/analyzer/dns_spf.go index a6b74c1..bfa1640 100644 --- a/pkg/analyzer/dns_spf.go +++ b/pkg/analyzer/dns_spf.go @@ -191,8 +191,12 @@ func (d *DNSAnalyzer) isValidSPFMechanism(token string) error { // Check if it's a modifier (contains =) if strings.Contains(mechanism, "=") { - // Only allow known modifiers: redirect= and exp= - if strings.HasPrefix(mechanism, "redirect=") || strings.HasPrefix(mechanism, "exp=") { + // Allow known modifiers: redirect=, exp=, and RFC 6652 modifiers (ra=, rp=, rr=) + if strings.HasPrefix(mechanism, "redirect=") || + strings.HasPrefix(mechanism, "exp=") || + strings.HasPrefix(mechanism, "ra=") || + strings.HasPrefix(mechanism, "rp=") || + strings.HasPrefix(mechanism, "rr=") { return nil } diff --git a/pkg/analyzer/dns_spf_test.go b/pkg/analyzer/dns_spf_test.go index b1195cb..2e794ce 100644 --- a/pkg/analyzer/dns_spf_test.go +++ b/pkg/analyzer/dns_spf_test.go @@ -122,6 +122,31 @@ func TestValidateSPF(t *testing.T) { expectError: true, errorMsg: "unknown modifier", }, + { + name: "Valid SPF with RFC 6652 ra modifier", + record: "v=spf1 mx ra=postmaster -all", + expectError: false, + }, + { + name: "Valid SPF with RFC 6652 rp modifier", + record: "v=spf1 mx rp=100 -all", + expectError: false, + }, + { + name: "Valid SPF with RFC 6652 rr modifier", + record: "v=spf1 mx rr=all -all", + expectError: false, + }, + { + name: "Valid SPF with all RFC 6652 modifiers", + record: "v=spf1 mx ra=postmaster rp=50 rr=fail -all", + expectError: false, + }, + { + name: "Valid SPF with RFC 6652 modifiers and redirect", + record: "v=spf1 ip4:192.0.2.0/24 ra=abuse redirect=_spf.example.com", + expectError: false, + }, } analyzer := NewDNSAnalyzer(5 * time.Second) From 18c86225137b5d01291d8e612bbddd4de620dced Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Fri, 7 Nov 2025 14:22:58 +0700 Subject: [PATCH 020/102] Don't require docker-compose to build the image, use docker hub published --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 87521ef..9071f16 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ services: build: context: . dockerfile: Dockerfile - image: happydeliver:latest + image: happydomain/happydeliver:latest container_name: happydeliver hostname: mail.happydeliver.local From c91ab96642451cf25e4102922a6a45b0bd0b0101 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Fri, 7 Nov 2025 14:23:29 +0700 Subject: [PATCH 021/102] Include the HEALTHCHECK command in Dockerfile --- Dockerfile | 4 ++++ docker-compose.yml | 7 ------- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5cb9c9e..93ae993 100644 --- a/Dockerfile +++ b/Dockerfile @@ -170,6 +170,10 @@ ENV HAPPYDELIVER_DATABASE_TYPE=sqlite HAPPYDELIVER_DATABASE_DSN=/var/lib/happyde # Volume for persistent data VOLUME ["/var/lib/happydeliver", "/var/log/happydeliver"] +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD wget --quiet --tries=1 --spider http://localhost:8080/api/status || exit 1 + # Set entrypoint ENTRYPOINT ["/entrypoint.sh"] CMD ["supervisord", "-c", "/etc/supervisor/supervisord.conf"] diff --git a/docker-compose.yml b/docker-compose.yml index 9071f16..fa27c5c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,13 +26,6 @@ services: restart: unless-stopped - healthcheck: - test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/api/status"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - volumes: data: logs: From 2172603ad58009cb3c7fca3efe6372206231d91a Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Fri, 7 Nov 2025 15:14:15 +0700 Subject: [PATCH 022/102] content: Add spaces behind each node to reduce gap with plain text --- pkg/analyzer/content.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/analyzer/content.go b/pkg/analyzer/content.go index 87c423f..4a3b5b8 100644 --- a/pkg/analyzer/content.go +++ b/pkg/analyzer/content.go @@ -627,7 +627,7 @@ func (c *ContentAnalyzer) extractTextFromHTML(htmlContent string) string { var extract func(*html.Node) extract = func(n *html.Node) { if n.Type == html.TextNode { - text.WriteString(n.Data) + text.WriteString(" " + n.Data) } // Skip script and style tags if n.Type == html.ElementNode && (n.Data == "script" || n.Data == "style") { @@ -639,7 +639,7 @@ func (c *ContentAnalyzer) extractTextFromHTML(htmlContent string) string { } extract(doc) - return text.String() + return strings.TrimSpace(text.String()) } // calculateTextPlainConsistency compares plain text and HTML versions From 447a666ae7d560ee565b442d3c8d869fc55c27e4 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Fri, 7 Nov 2025 17:07:31 +0700 Subject: [PATCH 023/102] Fix Domain Alignment align issue when error messages --- .../lib/components/HeaderAnalysisCard.svelte | 222 +++++++++--------- 1 file changed, 109 insertions(+), 113 deletions(-) diff --git a/web/src/lib/components/HeaderAnalysisCard.svelte b/web/src/lib/components/HeaderAnalysisCard.svelte index 3cfe287..e0ecb58 100644 --- a/web/src/lib/components/HeaderAnalysisCard.svelte +++ b/web/src/lib/components/HeaderAnalysisCard.svelte @@ -58,6 +58,8 @@ {/if} {#if headerAnalysis.domain_alignment} + {@const spfStrictAligned = headerAnalysis.domain_alignment.from_domain === headerAnalysis.domain_alignment.return_path_domain} + {@const spfRelaxedAligned = headerAnalysis.domain_alignment.from_org_domain === headerAnalysis.domain_alignment.return_path_org_domain}
@@ -69,71 +71,73 @@

Domain alignment ensures that the visible "From" domain matches the domain used for authentication (Return-Path or DKIM signature). Proper alignment is crucial for DMARC compliance, regardless of the policy. It helps prevent email spoofing by verifying that the sender domain is consistent across all authentication layers. Only one of the following lines needs to pass.

+ {#if headerAnalysis.domain_alignment.issues && headerAnalysis.domain_alignment.issues.length > 0} +
+ {#each headerAnalysis.domain_alignment.issues as issue} +
+ + {issue} +
+ {/each} +
+ {/if}
SPF
-
-
- Strict Alignment -
- - - {headerAnalysis.domain_alignment.aligned ? 'Pass' : 'Fail'} - -
-
Exact domain match
-
-
- Relaxed Alignment -
- - - {headerAnalysis.domain_alignment.relaxed_aligned ? 'Pass' : 'Fail'} - -
-
Organizational domain match
-
-
- From Domain -
{headerAnalysis.domain_alignment.from_domain || '-'}
- {#if headerAnalysis.domain_alignment.from_org_domain && headerAnalysis.domain_alignment.from_org_domain !== headerAnalysis.domain_alignment.from_domain} -
Org: {headerAnalysis.domain_alignment.from_org_domain}
- {/if} -
-
- Return-Path Domain -
{headerAnalysis.domain_alignment.return_path_domain || '-'}
- {#if headerAnalysis.domain_alignment.return_path_org_domain && headerAnalysis.domain_alignment.return_path_org_domain !== headerAnalysis.domain_alignment.return_path_domain} -
Org: {headerAnalysis.domain_alignment.return_path_org_domain}
- {/if} -
-
- {#if headerAnalysis.domain_alignment.issues && headerAnalysis.domain_alignment.issues.length > 0} -
- {#each headerAnalysis.domain_alignment.issues as issue} -
- - {issue} +
+
+
+ Strict Alignment +
+ + + {spfStrictAligned ? 'Pass' : 'Fail'} +
- {/each} +
Exact domain match
+
+
+ Relaxed Alignment +
+ + + {spfRelaxedAligned ? 'Pass' : 'Fail'} + +
+
Organizational domain match
+
+
+ From Domain +
{headerAnalysis.domain_alignment.from_domain || '-'}
+ {#if headerAnalysis.domain_alignment.from_org_domain && headerAnalysis.domain_alignment.from_org_domain !== headerAnalysis.domain_alignment.from_domain} +
Org: {headerAnalysis.domain_alignment.from_org_domain}
+ {/if} +
+
+ Return-Path Domain +
{headerAnalysis.domain_alignment.return_path_domain || '-'}
+ {#if headerAnalysis.domain_alignment.return_path_org_domain && headerAnalysis.domain_alignment.return_path_org_domain !== headerAnalysis.domain_alignment.return_path_domain} +
Org: {headerAnalysis.domain_alignment.return_path_org_domain}
+ {/if} +
- {/if} - - {#if dmarcRecord && headerAnalysis.domain_alignment.return_path_domain && headerAnalysis.domain_alignment.return_path_domain !== headerAnalysis.domain_alignment.from_domain} -
- {#if dmarcRecord.spf_alignment === 'strict'} - - Strict SPF alignment required — Your DMARC policy requires exact domain match. The Return-Path domain must exactly match the From domain for SPF to pass DMARC alignment. - {:else} - - Relaxed SPF alignment allowed — Your DMARC policy allows organizational domain matching. As long as both domains share the same organizational domain (e.g., mail.example.com and example.com), SPF alignment can pass. - {/if} -
- {/if} + + {#if dmarcRecord && headerAnalysis.domain_alignment.return_path_domain && headerAnalysis.domain_alignment.return_path_domain !== headerAnalysis.domain_alignment.from_domain} +
+ {#if dmarcRecord.spf_alignment === 'strict'} + + Strict SPF alignment required — Your DMARC policy requires exact domain match. The Return-Path domain must exactly match the From domain for SPF to pass DMARC alignment. + {:else} + + Relaxed SPF alignment allowed — Your DMARC policy allows organizational domain matching. As long as both domains share the same organizational domain (e.g., mail.example.com and example.com), SPF alignment can pass. + {/if} +
+ {/if} +
{#each headerAnalysis.domain_alignment.dkim_domains as dkim_domain} @@ -144,67 +148,59 @@ DKIM
-
-
- Strict Alignment -
- - - {dkim_aligned ? 'Pass' : 'Fail'} - -
-
Exact domain match
-
-
- Relaxed Alignment -
- - - {dkim_relaxed_aligned ? 'Pass' : 'Fail'} - -
-
Organizational domain match
-
-
- From Domain -
{headerAnalysis.domain_alignment.from_domain || '-'}
- {#if headerAnalysis.domain_alignment.from_org_domain && headerAnalysis.domain_alignment.from_org_domain !== headerAnalysis.domain_alignment.from_domain} -
Org: {headerAnalysis.domain_alignment.from_org_domain}
- {/if} -
-
- Signature Domain -
{dkim_domain.domain || '-'}
- {#if dkim_domain.domain !== dkim_domain.org_domain} -
Org: {dkim_domain.org_domain}
- {/if} -
-
- {#if headerAnalysis.domain_alignment.issues && headerAnalysis.domain_alignment.issues.length > 0} -
- {#each headerAnalysis.domain_alignment.issues as issue} -
- - {issue} +
+
+
+ Strict Alignment +
+ + + {dkim_aligned ? 'Pass' : 'Fail'} +
- {/each} -
- {/if} - - - {#if dmarcRecord && dkim_domain.domain !== headerAnalysis.domain_alignment.from_domain} - {#if dkim_domain.org_domain === headerAnalysis.domain_alignment.from_org_domain} -
- {#if dmarcRecord.dkim_alignment === 'strict'} - - Strict DKIM alignment required — Your DMARC policy requires exact domain match. The DKIM signature domain must exactly match the From domain for DKIM to pass DMARC alignment. - {:else} - - Relaxed DKIM alignment allowed — Your DMARC policy allows organizational domain matching. As long as both domains share the same organizational domain (e.g., mail.example.com and example.com), DKIM alignment can pass. +
Exact domain match
+
+
+ Relaxed Alignment +
+ + + {dkim_relaxed_aligned ? 'Pass' : 'Fail'} + +
+
Organizational domain match
+
+
+ From Domain +
{headerAnalysis.domain_alignment.from_domain || '-'}
+ {#if headerAnalysis.domain_alignment.from_org_domain && headerAnalysis.domain_alignment.from_org_domain !== headerAnalysis.domain_alignment.from_domain} +
Org: {headerAnalysis.domain_alignment.from_org_domain}
{/if}
+
+ Signature Domain +
{dkim_domain.domain || '-'}
+ {#if dkim_domain.domain !== dkim_domain.org_domain} +
Org: {dkim_domain.org_domain}
+ {/if} +
+
+ + + {#if dmarcRecord && dkim_domain.domain !== headerAnalysis.domain_alignment.from_domain} + {#if dkim_domain.org_domain === headerAnalysis.domain_alignment.from_org_domain} +
+ {#if dmarcRecord.dkim_alignment === 'strict'} + + Strict DKIM alignment required — Your DMARC policy requires exact domain match. The DKIM signature domain must exactly match the From domain for DKIM to pass DMARC alignment. + {:else} + + Relaxed DKIM alignment allowed — Your DMARC policy allows organizational domain matching. As long as both domains share the same organizational domain (e.g., mail.example.com and example.com), DKIM alignment can pass. + {/if} +
+ {/if} {/if} - {/if} +
{/each} From 644dfda2232b9d1c301e5411b5ded813d849f45d Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 13 Nov 2025 10:53:59 +0700 Subject: [PATCH 024/102] Don't stop polling report if response is not ok Bug: https://github.com/happyDomain/happydeliver/issues/2 --- web/src/routes/test/[test]/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/routes/test/[test]/+page.svelte b/web/src/routes/test/[test]/+page.svelte index 8e78be7..054dc23 100644 --- a/web/src/routes/test/[test]/+page.svelte +++ b/web/src/routes/test/[test]/+page.svelte @@ -84,8 +84,8 @@ const reportResponse = await getReport({ path: { id: testId } }); if (reportResponse.data) { report = reportResponse.data; + stopPolling(); } - stopPolling(); } } else if (testResponse.error) { handleApiError(testResponse.error, "Failed to fetch test"); From ea71074cc89ebe38d393f473dda9cda2b2e143d6 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 11 Nov 2025 12:10:25 +0000 Subject: [PATCH 025/102] chore(deps): update dependency svelte-check to v4.3.4 --- web/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 01d6a6d..10c565e 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -3945,9 +3945,9 @@ } }, "node_modules/svelte-check": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.3.tgz", - "integrity": "sha512-RYP0bEwenDXzfv0P1sKAwjZSlaRyqBn0Fz1TVni58lqyEiqgwztTpmodJrGzP6ZT2aHl4MbTvWP6gbmQ3FOnBg==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.4.tgz", + "integrity": "sha512-DVWvxhBrDsd+0hHWKfjP99lsSXASeOhHJYyuKOFYJcP7ThfSCKgjVarE8XfuMWpS5JV3AlDf+iK1YGGo2TACdw==", "dev": true, "license": "MIT", "dependencies": { From e28a96508d35eec77270335d9b2396334306929e Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Fri, 14 Nov 2025 15:33:52 +0700 Subject: [PATCH 026/102] Respond with HTTP 200 on blacklist, domain and test pages Bug: https://github.com/happyDomain/happydeliver/issues/2 --- web/routes.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/web/routes.go b/web/routes.go index 44b1cb2..23a9bbb 100644 --- a/web/routes.go +++ b/web/routes.go @@ -86,6 +86,12 @@ func DeclareRoutes(cfg *config.Config, router *gin.Engine) { router.GET("/_app/immutable/*_", func(c *gin.Context) { c.Writer.Header().Set("Cache-Control", "public, max-age=604800, immutable") }, serveOrReverse("", cfg)) router.GET("/", serveOrReverse("/", cfg)) + router.GET("/blacklist/", serveOrReverse("/", cfg)) + router.GET("/blacklist/:ip", serveOrReverse("/", cfg)) + router.GET("/domain/", serveOrReverse("/", cfg)) + router.GET("/domain/:domain", serveOrReverse("/", cfg)) + router.GET("/test/", serveOrReverse("/", cfg)) + router.GET("/test/:testid", serveOrReverse("/", cfg)) router.GET("/favicon.png", func(c *gin.Context) { c.Writer.Header().Set("Cache-Control", "public, max-age=604800, immutable") }, serveOrReverse("", cfg)) router.GET("/img/*path", serveOrReverse("", cfg)) From 04d8b150b4b84e5327334e826bcb12f1dcd28457 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 14 Nov 2025 09:11:24 +0000 Subject: [PATCH 027/102] chore(deps): update module golang.org/x/net to v0.47.0 --- go.mod | 17 ++++++++--------- go.sum | 34 ++++++++++++++-------------------- 2 files changed, 22 insertions(+), 29 deletions(-) diff --git a/go.mod b/go.mod index db2ac1d..e20b404 100644 --- a/go.mod +++ b/go.mod @@ -5,18 +5,16 @@ go 1.24.6 require ( github.com/JGLTechnologies/gin-rate-limit v1.5.6 github.com/emersion/go-smtp v0.24.0 - github.com/getkin/kin-openapi v0.133.0 github.com/gin-gonic/gin v1.11.0 github.com/google/uuid v1.6.0 github.com/oapi-codegen/runtime v1.1.2 - golang.org/x/net v0.46.0 + golang.org/x/net v0.47.0 gorm.io/driver/postgres v1.6.0 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.31.0 ) require ( - github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -25,6 +23,7 @@ require ( github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/getkin/kin-openapi v0.133.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect @@ -65,12 +64,12 @@ require ( github.com/woodsbury/decimal128 v1.3.0 // indirect go.uber.org/mock v0.5.0 // indirect golang.org/x/arch v0.20.0 // indirect - golang.org/x/crypto v0.43.0 // indirect - golang.org/x/mod v0.28.0 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.37.0 // indirect - golang.org/x/text v0.30.0 // indirect - golang.org/x/tools v0.37.0 // indirect + golang.org/x/crypto v0.44.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/tools v0.38.0 // indirect google.golang.org/protobuf v1.36.9 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 266785d..1f557d7 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,5 @@ github.com/JGLTechnologies/gin-rate-limit v1.5.6 h1:BrL2wXrF7SSqmB88YTGFVKMGVcjURMUeKqwQrlmzweI= github.com/JGLTechnologies/gin-rate-limit v1.5.6/go.mod h1:fwUuBegxLKm8+/4ST0zDFssRFTFaVZ7bH3ApK7iNZww= -github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= -github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= -github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= -github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -98,7 +94,6 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -166,7 +161,6 @@ github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU= github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg= -github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -195,11 +189,11 @@ golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= +golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -207,13 +201,13 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -229,21 +223,21 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From e05c6d0bc29714fc7508fa0d78d5d321f02646df Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 1 Nov 2025 18:08:06 +0700 Subject: [PATCH 028/102] Fix calculateTextPlainConsistency algorithm --- pkg/analyzer/content.go | 45 ++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/pkg/analyzer/content.go b/pkg/analyzer/content.go index 4a3b5b8..95e32aa 100644 --- a/pkg/analyzer/content.go +++ b/pkg/analyzer/content.go @@ -659,30 +659,47 @@ func (c *ContentAnalyzer) calculateTextPlainConsistency(plainText, htmlText stri return 0.0 } - // Count common words - commonWords := 0 - plainWordSet := make(map[string]bool) + // Count common words by building sets + plainWordSet := make(map[string]int) for _, word := range plainWords { - plainWordSet[word] = true + plainWordSet[word]++ } + htmlWordSet := make(map[string]int) for _, word := range htmlWords { - if plainWordSet[word] { - commonWords++ + htmlWordSet[word]++ + } + + // Count matches: for each unique word, count minimum occurrences in both texts + commonWords := 0 + for word, plainCount := range plainWordSet { + if htmlCount, exists := htmlWordSet[word]; exists { + // Count the minimum occurrences between both texts + if plainCount < htmlCount { + commonWords += plainCount + } else { + commonWords += htmlCount + } } } - // Calculate ratio (Jaccard similarity approximation) - maxWords := len(plainWords) - if len(htmlWords) > maxWords { - maxWords = len(htmlWords) - } - - if maxWords == 0 { + // Calculate ratio using total words from both texts (union approach) + // This provides a balanced measure: perfect match = 1.0, partial overlap = 0.3-0.8 + totalWords := len(plainWords) + len(htmlWords) + if totalWords == 0 { return 0.0 } - return float32(commonWords) / float32(maxWords) + // Divide by average word count for better scoring + avgWords := float32(totalWords) / 2.0 + ratio := float32(commonWords) / avgWords + + // Cap at 1.0 for perfect matches + if ratio > 1.0 { + ratio = 1.0 + } + + return ratio } // normalizeText normalizes text for comparison From ee9fa59dbc8bc4a28b472bc5daf21c87aa1d54ee Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 1 Nov 2025 13:11:17 +0000 Subject: [PATCH 029/102] Update eslint monorepo to v9.39.0 --- web/package-lock.json | 55 +++++++++++++++++-------------------------- 1 file changed, 21 insertions(+), 34 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 10c565e..46186e5 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -539,19 +539,6 @@ } } }, - "node_modules/@eslint/compat/node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@eslint/config-array": { "version": "0.21.1", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", @@ -568,22 +555,22 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz", - "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.16.0" + "@eslint/core": "^0.17.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", - "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -631,9 +618,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.38.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", - "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", + "version": "9.39.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.0.tgz", + "integrity": "sha512-BIhe0sW91JGPiaF1mOuPy5v8NflqfjIcDNpC+LbW9f609WVRX1rArrhi6Z2ymvrAry9jw+5POTj4t2t62o8Bmw==", "dev": true, "license": "MIT", "engines": { @@ -654,13 +641,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", - "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.16.0", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { @@ -2338,9 +2325,9 @@ } }, "node_modules/eslint": { - "version": "9.38.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", - "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", + "version": "9.39.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.0.tgz", + "integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==", "dev": true, "license": "MIT", "peer": true, @@ -2348,11 +2335,11 @@ "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.1", - "@eslint/core": "^0.16.0", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.38.0", - "@eslint/plugin-kit": "^0.4.0", + "@eslint/js": "9.39.0", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", From 723bec622afd919bd0f5cbb35928aa629a6e3315 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 1 Nov 2025 18:08:06 +0700 Subject: [PATCH 030/102] Fix calculateTextPlainConsistency algorithm From 27d5220687493ebab7ea0b9d1faf946d2d518352 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 1 Nov 2025 13:10:56 +0000 Subject: [PATCH 031/102] Update dependency globals to v16.5.0 --- web/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 46186e5..96ce1f6 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -2763,9 +2763,9 @@ } }, "node_modules/globals": { - "version": "16.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", - "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", "dev": true, "license": "MIT", "engines": { From a3ca8ffb4876dea771a5f85789921bfc667f4c2d Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 1 Nov 2025 18:08:06 +0700 Subject: [PATCH 032/102] Fix calculateTextPlainConsistency algorithm From 03b58b6f19093ef935c56483f08a7ca91388ce7a Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 1 Nov 2025 17:10:46 +0000 Subject: [PATCH 033/102] Update module github.com/oapi-codegen/oapi-codegen/v2 to v2.5.1 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e20b404..5cef1e4 100644 --- a/go.mod +++ b/go.mod @@ -48,7 +48,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect - github.com/oapi-codegen/oapi-codegen/v2 v2.5.0 // indirect + github.com/oapi-codegen/oapi-codegen/v2 v2.5.1 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect diff --git a/go.sum b/go.sum index 1f557d7..1def6c0 100644 --- a/go.sum +++ b/go.sum @@ -121,8 +121,8 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwd github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 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.5.0 h1:iJvF8SdB/3/+eGOXEpsWkD8FQAHj6mqkb6Fnsoc8MFU= -github.com/oapi-codegen/oapi-codegen/v2 v2.5.0/go.mod h1:fwlMxUEMuQK5ih9aymrxKPQqNm2n8bdLk1ppjH+lr9w= +github.com/oapi-codegen/oapi-codegen/v2 v2.5.1 h1:5vHNY1uuPBRBWqB2Dp0G7YB03phxLQZupZTIZaeorjc= +github.com/oapi-codegen/oapi-codegen/v2 v2.5.1/go.mod h1:ro0npU1BWkcGpCgGD9QwPp44l5OIZ94tB3eabnT7DjQ= github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI= github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= From c19f545df0fde0182e2724a1196709657371623b Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 14 Nov 2025 09:10:35 +0000 Subject: [PATCH 034/102] chore(deps): update dependency typescript-eslint to v8.46.4 --- web/package-lock.json | 122 +++++++++++++++++++++--------------------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 96ce1f6..129c2c5 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1350,17 +1350,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", - "integrity": "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.4.tgz", + "integrity": "sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.46.2", - "@typescript-eslint/type-utils": "8.46.2", - "@typescript-eslint/utils": "8.46.2", - "@typescript-eslint/visitor-keys": "8.46.2", + "@typescript-eslint/scope-manager": "8.46.4", + "@typescript-eslint/type-utils": "8.46.4", + "@typescript-eslint/utils": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1374,7 +1374,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.46.2", + "@typescript-eslint/parser": "^8.46.4", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -1390,17 +1390,17 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz", - "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.4.tgz", + "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.46.2", - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/typescript-estree": "8.46.2", - "@typescript-eslint/visitor-keys": "8.46.2", + "@typescript-eslint/scope-manager": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", "debug": "^4.3.4" }, "engines": { @@ -1416,14 +1416,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz", - "integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.4.tgz", + "integrity": "sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.2", - "@typescript-eslint/types": "^8.46.2", + "@typescript-eslint/tsconfig-utils": "^8.46.4", + "@typescript-eslint/types": "^8.46.4", "debug": "^4.3.4" }, "engines": { @@ -1438,14 +1438,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz", - "integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.4.tgz", + "integrity": "sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/visitor-keys": "8.46.2" + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1456,9 +1456,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz", - "integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.4.tgz", + "integrity": "sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==", "dev": true, "license": "MIT", "engines": { @@ -1473,15 +1473,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz", - "integrity": "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.4.tgz", + "integrity": "sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/typescript-estree": "8.46.2", - "@typescript-eslint/utils": "8.46.2", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/utils": "8.46.4", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1498,9 +1498,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz", - "integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.4.tgz", + "integrity": "sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==", "dev": true, "license": "MIT", "engines": { @@ -1512,16 +1512,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz", - "integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.4.tgz", + "integrity": "sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.46.2", - "@typescript-eslint/tsconfig-utils": "8.46.2", - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/visitor-keys": "8.46.2", + "@typescript-eslint/project-service": "8.46.4", + "@typescript-eslint/tsconfig-utils": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1567,16 +1567,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz", - "integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.4.tgz", + "integrity": "sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.2", - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/typescript-estree": "8.46.2" + "@typescript-eslint/scope-manager": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1591,13 +1591,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz", - "integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.4.tgz", + "integrity": "sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/types": "8.46.4", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -4111,16 +4111,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.2.tgz", - "integrity": "sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg==", + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.4.tgz", + "integrity": "sha512-KALyxkpYV5Ix7UhvjTwJXZv76VWsHG+NjNlt/z+a17SOQSiOcBdUXdbJdyXi7RPxrBFECtFOiPwUJQusJuCqrg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.46.2", - "@typescript-eslint/parser": "8.46.2", - "@typescript-eslint/typescript-estree": "8.46.2", - "@typescript-eslint/utils": "8.46.2" + "@typescript-eslint/eslint-plugin": "8.46.4", + "@typescript-eslint/parser": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/utils": "8.46.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" From e194fcc5b1a3e8ad60e64033b4d5113e9fded57d Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 1 Nov 2025 18:08:06 +0700 Subject: [PATCH 035/102] Fix calculateTextPlainConsistency algorithm From a1e8dd35bd4dcc83e44f1a61bfea4f92237d1041 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 1 Nov 2025 13:10:36 +0000 Subject: [PATCH 036/102] Update dependency @types/node to v24.9.2 --- web/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 129c2c5..2579fe0 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1339,9 +1339,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", - "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", + "version": "24.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", + "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", "dev": true, "license": "MIT", "peer": true, From 5ac0e2a8bf9280ab39c361db1f4e5a3407417751 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 14 Nov 2025 09:11:07 +0000 Subject: [PATCH 037/102] chore(deps): update dependency vite to v7.2.2 --- web/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 2579fe0..769b905 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -4173,9 +4173,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.1.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", - "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", + "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", "peer": true, From 3bcbb5814d9f372e1c01a2a7b29b780def54bb48 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 14 Nov 2025 13:12:20 +0000 Subject: [PATCH 038/102] chore(deps): lock file maintenance --- web/package-lock.json | 563 +++++++++++++++++++++++------------------- 1 file changed, 303 insertions(+), 260 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 769b905..4ac32c2 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -35,9 +35,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", - "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], @@ -52,9 +52,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", - "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -69,9 +69,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", - "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -86,9 +86,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", - "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -103,9 +103,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", - "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -120,9 +120,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", - "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -137,9 +137,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", - "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -154,9 +154,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", - "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -171,9 +171,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", - "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -188,9 +188,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", - "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -205,9 +205,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", - "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -222,9 +222,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", - "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -239,9 +239,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", - "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -256,9 +256,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", - "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -273,9 +273,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", - "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -290,9 +290,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", - "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -307,9 +307,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", - "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -324,9 +324,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", - "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ "arm64" ], @@ -341,9 +341,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", - "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -358,9 +358,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", - "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "cpu": [ "arm64" ], @@ -375,9 +375,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", - "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -392,9 +392,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", - "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", "cpu": [ "arm64" ], @@ -409,9 +409,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", - "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -426,9 +426,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", - "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -443,9 +443,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", - "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -460,9 +460,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", - "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -618,9 +618,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.0.tgz", - "integrity": "sha512-BIhe0sW91JGPiaF1mOuPy5v8NflqfjIcDNpC+LbW9f609WVRX1rArrhi6Z2ymvrAry9jw+5POTj4t2t62o8Bmw==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", "dev": true, "license": "MIT", "engines": { @@ -655,9 +655,9 @@ } }, "node_modules/@hey-api/codegen-core": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.3.2.tgz", - "integrity": "sha512-DhfftvmoJyfMiiNHhfU7xrDxrjMjPKex1g064RfE6HjNEsFYwK36J2yKfkn8I1mrYWHPmS5ZV3GarMZajsYEEQ==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.3.3.tgz", + "integrity": "sha512-vArVDtrvdzFewu1hnjUm4jX1NBITlSCeO81EdWq676MxQbyxsGcDPAgohaSA+Wvr4HjPSvsg2/1s2zYxUtXebg==", "dev": true, "license": "MIT", "engines": { @@ -885,9 +885,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", - "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", + "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", "cpu": [ "arm" ], @@ -899,9 +899,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", - "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", + "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", "cpu": [ "arm64" ], @@ -913,9 +913,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", - "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", + "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", "cpu": [ "arm64" ], @@ -927,9 +927,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", - "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", + "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", "cpu": [ "x64" ], @@ -941,9 +941,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", - "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", + "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", "cpu": [ "arm64" ], @@ -955,9 +955,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", - "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", + "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", "cpu": [ "x64" ], @@ -969,9 +969,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", - "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", + "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", "cpu": [ "arm" ], @@ -983,9 +983,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", - "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", + "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", "cpu": [ "arm" ], @@ -997,9 +997,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", - "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", + "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", "cpu": [ "arm64" ], @@ -1011,9 +1011,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", - "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", + "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", "cpu": [ "arm64" ], @@ -1025,9 +1025,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", - "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", + "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", "cpu": [ "loong64" ], @@ -1039,9 +1039,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", - "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", + "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", "cpu": [ "ppc64" ], @@ -1053,9 +1053,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", - "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", + "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", "cpu": [ "riscv64" ], @@ -1067,9 +1067,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", - "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", + "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", "cpu": [ "riscv64" ], @@ -1081,9 +1081,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", - "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", + "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", "cpu": [ "s390x" ], @@ -1095,9 +1095,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", - "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", + "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", "cpu": [ "x64" ], @@ -1109,9 +1109,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", - "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", + "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", "cpu": [ "x64" ], @@ -1123,9 +1123,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", - "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", + "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", "cpu": [ "arm64" ], @@ -1137,9 +1137,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", - "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", + "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", "cpu": [ "arm64" ], @@ -1151,9 +1151,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", - "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", + "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", "cpu": [ "ia32" ], @@ -1165,9 +1165,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", - "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", + "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", "cpu": [ "x64" ], @@ -1179,9 +1179,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", - "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", + "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", "cpu": [ "x64" ], @@ -1220,9 +1220,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.48.0", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.48.0.tgz", - "integrity": "sha512-GAAbkWrbRJvysL7+HOWs5v/+TmnRcEQPeED2sUcDFTHpPvRYADEtScL6x8hWuKp0DKauJVaVJLTjQVy9e7cMiw==", + "version": "2.48.5", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.48.5.tgz", + "integrity": "sha512-/rnwfSWS3qwUSzvHynUTORF9xSJi7PCR9yXkxUOnRrNqyKmCmh3FPHH+E9BbgqxXfTevGXBqgnlh9kMb+9T5XA==", "dev": true, "license": "MIT", "peer": true, @@ -1339,9 +1339,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.9.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", - "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", "peer": true, @@ -2186,9 +2186,9 @@ } }, "node_modules/default-browser": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", - "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.3.0.tgz", + "integrity": "sha512-Qq68+VkJlc8tjnPV1i7HtbIn7ohmjZa88qUvHMIK0ZKUXMCuV45cT7cEXALPUmeXCe0q1DWQkQTemHVaLIFSrg==", "dev": true, "license": "MIT", "dependencies": { @@ -2203,9 +2203,9 @@ } }, "node_modules/default-browser-id": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", - "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", "dev": true, "license": "MIT", "engines": { @@ -2243,9 +2243,9 @@ "license": "MIT" }, "node_modules/devalue": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.4.2.tgz", - "integrity": "sha512-MwPZTKEPK2k8Qgfmqrd48ZKVvzSQjgW0lXLxiIBA8dQjtf/6mw6pggHNLcyDKyf+fI6eXxlQwPsfaCMTU5U+Bw==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.5.0.tgz", + "integrity": "sha512-69sM5yrHfFLJt0AZ9QqZXGCPfJ7fQjvpln3Rq5+PS03LD32Ost1Q9N+eEnaQwGRIriKkMImXD56ocjQmfjbV3w==", "dev": true, "license": "MIT" }, @@ -2270,9 +2270,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", - "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2283,32 +2283,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.11", - "@esbuild/android-arm": "0.25.11", - "@esbuild/android-arm64": "0.25.11", - "@esbuild/android-x64": "0.25.11", - "@esbuild/darwin-arm64": "0.25.11", - "@esbuild/darwin-x64": "0.25.11", - "@esbuild/freebsd-arm64": "0.25.11", - "@esbuild/freebsd-x64": "0.25.11", - "@esbuild/linux-arm": "0.25.11", - "@esbuild/linux-arm64": "0.25.11", - "@esbuild/linux-ia32": "0.25.11", - "@esbuild/linux-loong64": "0.25.11", - "@esbuild/linux-mips64el": "0.25.11", - "@esbuild/linux-ppc64": "0.25.11", - "@esbuild/linux-riscv64": "0.25.11", - "@esbuild/linux-s390x": "0.25.11", - "@esbuild/linux-x64": "0.25.11", - "@esbuild/netbsd-arm64": "0.25.11", - "@esbuild/netbsd-x64": "0.25.11", - "@esbuild/openbsd-arm64": "0.25.11", - "@esbuild/openbsd-x64": "0.25.11", - "@esbuild/openharmony-arm64": "0.25.11", - "@esbuild/sunos-x64": "0.25.11", - "@esbuild/win32-arm64": "0.25.11", - "@esbuild/win32-ia32": "0.25.11", - "@esbuild/win32-x64": "0.25.11" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/escape-string-regexp": { @@ -2325,9 +2325,9 @@ } }, "node_modules/eslint": { - "version": "9.39.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.0.tgz", - "integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", "peer": true, @@ -2338,7 +2338,7 @@ "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.0", + "@eslint/js": "9.39.1", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -2504,9 +2504,9 @@ } }, "node_modules/esrap": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.1.tgz", - "integrity": "sha512-ebTT9B6lOtZGMgJ3o5r12wBacHctG7oEWazIda8UlPfA3HD/Wrv8FdXoVo73vzdpwCxNyXjPauyN2bbJzMkB9A==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.2.tgz", + "integrity": "sha512-DgvlIQeowRNyvLPWW4PT7Gu13WznY288Du086E751mwwbsgr29ytBiYeLzAGIo0qk3Ujob0SDk8TiSaM5WQzNg==", "dev": true, "license": "MIT", "dependencies": { @@ -2567,9 +2567,9 @@ } }, "node_modules/exsolve": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", - "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", "dev": true, "license": "MIT" }, @@ -2970,9 +2970,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -3132,19 +3132,6 @@ "node": ">=8.6" } }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3396,14 +3383,13 @@ "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": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "license": "MIT", - "peer": true, "engines": { - "node": ">=12" + "node": ">=8.6" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -3675,9 +3661,9 @@ } }, "node_modules/rollup": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", - "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", + "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", "dev": true, "license": "MIT", "dependencies": { @@ -3691,28 +3677,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.5", - "@rollup/rollup-android-arm64": "4.52.5", - "@rollup/rollup-darwin-arm64": "4.52.5", - "@rollup/rollup-darwin-x64": "4.52.5", - "@rollup/rollup-freebsd-arm64": "4.52.5", - "@rollup/rollup-freebsd-x64": "4.52.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", - "@rollup/rollup-linux-arm-musleabihf": "4.52.5", - "@rollup/rollup-linux-arm64-gnu": "4.52.5", - "@rollup/rollup-linux-arm64-musl": "4.52.5", - "@rollup/rollup-linux-loong64-gnu": "4.52.5", - "@rollup/rollup-linux-ppc64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-musl": "4.52.5", - "@rollup/rollup-linux-s390x-gnu": "4.52.5", - "@rollup/rollup-linux-x64-gnu": "4.52.5", - "@rollup/rollup-linux-x64-musl": "4.52.5", - "@rollup/rollup-openharmony-arm64": "4.52.5", - "@rollup/rollup-win32-arm64-msvc": "4.52.5", - "@rollup/rollup-win32-ia32-msvc": "4.52.5", - "@rollup/rollup-win32-x64-gnu": "4.52.5", - "@rollup/rollup-win32-x64-msvc": "4.52.5", + "@rollup/rollup-android-arm-eabi": "4.53.2", + "@rollup/rollup-android-arm64": "4.53.2", + "@rollup/rollup-darwin-arm64": "4.53.2", + "@rollup/rollup-darwin-x64": "4.53.2", + "@rollup/rollup-freebsd-arm64": "4.53.2", + "@rollup/rollup-freebsd-x64": "4.53.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", + "@rollup/rollup-linux-arm-musleabihf": "4.53.2", + "@rollup/rollup-linux-arm64-gnu": "4.53.2", + "@rollup/rollup-linux-arm64-musl": "4.53.2", + "@rollup/rollup-linux-loong64-gnu": "4.53.2", + "@rollup/rollup-linux-ppc64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-musl": "4.53.2", + "@rollup/rollup-linux-s390x-gnu": "4.53.2", + "@rollup/rollup-linux-x64-gnu": "4.53.2", + "@rollup/rollup-linux-x64-musl": "4.53.2", + "@rollup/rollup-openharmony-arm64": "4.53.2", + "@rollup/rollup-win32-arm64-msvc": "4.53.2", + "@rollup/rollup-win32-ia32-msvc": "4.53.2", + "@rollup/rollup-win32-x64-gnu": "4.53.2", + "@rollup/rollup-win32-x64-msvc": "4.53.2", "fsevents": "~2.3.2" } }, @@ -3780,9 +3766,9 @@ } }, "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "dev": true, "license": "MIT" }, @@ -3905,9 +3891,9 @@ } }, "node_modules/svelte": { - "version": "5.42.2", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.42.2.tgz", - "integrity": "sha512-iSry5jsBHispVczyt9UrBX/1qu3HQ/UyKPAIjqlvlu3o/eUvc+kpyMyRS2O4HLLx4MvLurLGIUOyyP11pyD59g==", + "version": "5.43.6", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.43.6.tgz", + "integrity": "sha512-RnyO9VXI85Bmsf4b8AuQFBKFYL3LKUl+ZrifOjvlrQoboAROj5IITVLK1yOXBjwUWUn2BI5cfmurktgCzuZ5QA==", "dev": true, "license": "MIT", "peer": true, @@ -3993,11 +3979,14 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", - "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/tinyglobby": { "version": "0.2.15", @@ -4016,6 +4005,19 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tinypool": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", @@ -4271,6 +4273,19 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vitefu": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", @@ -4364,6 +4379,19 @@ } } }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vitest/node_modules/tinyexec": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", @@ -4437,6 +4465,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", From f2261adb54974a1655270c257511b29252f24ae7 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Mon, 17 Nov 2025 10:15:11 +0700 Subject: [PATCH 039/102] Update go dependancies --- go.mod | 38 +++++++++++++------------- go.sum | 84 ++++++++++++++++++++++++++++++++++------------------------ 2 files changed, 69 insertions(+), 53 deletions(-) diff --git a/go.mod b/go.mod index 5cef1e4..ebf21a7 100644 --- a/go.mod +++ b/go.mod @@ -5,31 +5,33 @@ go 1.24.6 require ( github.com/JGLTechnologies/gin-rate-limit v1.5.6 github.com/emersion/go-smtp v0.24.0 + github.com/getkin/kin-openapi v0.133.0 github.com/gin-gonic/gin v1.11.0 github.com/google/uuid v1.6.0 github.com/oapi-codegen/runtime v1.1.2 golang.org/x/net v0.47.0 gorm.io/driver/postgres v1.6.0 gorm.io/driver/sqlite v1.6.0 - gorm.io/gorm v1.31.0 + gorm.io/gorm v1.31.1 ) require ( - github.com/bytedance/sonic v1.14.0 // indirect - github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.14.2 // indirect + github.com/bytedance/sonic/loader v0.4.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect - github.com/gabriel-vasile/mimetype v1.4.8 // indirect - github.com/getkin/kin-openapi v0.133.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.11 // indirect github.com/gin-contrib/sse v1.1.0 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-openapi/jsonpointer v0.22.2 // indirect + github.com/go-openapi/swag/jsonname v0.25.1 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/go-playground/validator/v10 v10.28.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.18.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect @@ -42,7 +44,7 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/mailru/easyjson v0.9.1 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.32 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -54,23 +56,23 @@ require ( github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/quic-go/qpack v0.5.1 // indirect - github.com/quic-go/quic-go v0.54.1 // indirect - github.com/redis/go-redis/v9 v9.7.3 // indirect + github.com/quic-go/quic-go v0.56.0 // indirect + github.com/redis/go-redis/v9 v9.16.0 // indirect github.com/speakeasy-api/jsonpath v0.6.0 // indirect github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.3.0 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect - github.com/woodsbury/decimal128 v1.3.0 // indirect - go.uber.org/mock v0.5.0 // indirect - golang.org/x/arch v0.20.0 // indirect + github.com/woodsbury/decimal128 v1.4.0 // indirect + go.uber.org/mock v0.6.0 // indirect + golang.org/x/arch v0.23.0 // indirect golang.org/x/crypto v0.44.0 // indirect - golang.org/x/mod v0.29.0 // indirect + golang.org/x/mod v0.30.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/text v0.31.0 // indirect - golang.org/x/tools v0.38.0 // indirect - google.golang.org/protobuf v1.36.9 // indirect + golang.org/x/tools v0.39.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 1def6c0..cd951e8 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,19 @@ github.com/JGLTechnologies/gin-rate-limit v1.5.6 h1:BrL2wXrF7SSqmB88YTGFVKMGVcjURMUeKqwQrlmzweI= github.com/JGLTechnologies/gin-rate-limit v1.5.6/go.mod h1:fwUuBegxLKm8+/4ST0zDFssRFTFaVZ7bH3ApK7iNZww= +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= -github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= -github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= -github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= +github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= +github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= +github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -30,26 +36,28 @@ github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4Mc github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik= +github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/jsonpointer v0.22.2 h1:JDQEe4B9j6K3tQ7HQQTZfjR59IURhjjLxet2FB4KHyg= +github.com/go-openapi/jsonpointer v0.22.2/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo= +github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= +github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= -github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= +github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= @@ -94,6 +102,7 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -105,8 +114,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= @@ -149,10 +158,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.54.1 h1:4ZAWm0AhCb6+hE+l5Q1NAL0iRn/ZrMwqHRGQiFwj2eg= -github.com/quic-go/quic-go v0.54.1/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= -github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= -github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= +github.com/quic-go/quic-go v0.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY= +github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c= +github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4= +github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= @@ -161,39 +170,42 @@ github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU= github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= -github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= -github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= -github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= +github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= -go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= -golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= -golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= +golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -233,11 +245,13 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -250,8 +264,8 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= -google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -273,5 +287,5 @@ gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= -gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY= -gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= From eef6480e75936b7bce5601859e6f4acff8ec12e5 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Mon, 17 Nov 2025 10:15:40 +0700 Subject: [PATCH 040/102] Refactor DNS resolution: create an interface to have multiple implementations --- pkg/analyzer/dns.go | 18 +++++--- pkg/analyzer/dns_resolver.go | 80 ++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 pkg/analyzer/dns_resolver.go diff --git a/pkg/analyzer/dns.go b/pkg/analyzer/dns.go index 57226c6..3098934 100644 --- a/pkg/analyzer/dns.go +++ b/pkg/analyzer/dns.go @@ -22,7 +22,6 @@ package analyzer import ( - "net" "time" "git.happydns.org/happyDeliver/internal/api" @@ -31,19 +30,26 @@ import ( // DNSAnalyzer analyzes DNS records for email domains type DNSAnalyzer struct { Timeout time.Duration - resolver *net.Resolver + resolver DNSResolver } // NewDNSAnalyzer creates a new DNS analyzer with configurable timeout func NewDNSAnalyzer(timeout time.Duration) *DNSAnalyzer { + return NewDNSAnalyzerWithResolver(timeout, NewStandardDNSResolver()) +} + +// NewDNSAnalyzerWithResolver creates a new DNS analyzer with a custom resolver. +// If resolver is nil, a StandardDNSResolver will be used. +func NewDNSAnalyzerWithResolver(timeout time.Duration, resolver DNSResolver) *DNSAnalyzer { if timeout == 0 { timeout = 10 * time.Second // Default timeout } + if resolver == nil { + resolver = NewStandardDNSResolver() + } return &DNSAnalyzer{ - Timeout: timeout, - resolver: &net.Resolver{ - PreferGo: true, - }, + Timeout: timeout, + resolver: resolver, } } diff --git a/pkg/analyzer/dns_resolver.go b/pkg/analyzer/dns_resolver.go new file mode 100644 index 0000000..f60484f --- /dev/null +++ b/pkg/analyzer/dns_resolver.go @@ -0,0 +1,80 @@ +// This file is part of the happyDeliver (R) project. +// Copyright (c) 2025 happyDomain +// Authors: Pierre-Olivier Mercier, et al. +// +// This program is offered under a commercial and under the AGPL license. +// For commercial licensing, contact us at . +// +// For AGPL licensing: +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package analyzer + +import ( + "context" + "net" +) + +// DNSResolver defines the interface for DNS resolution operations. +// This interface abstracts DNS lookups to allow for custom implementations, +// such as mock resolvers for testing or caching resolvers for performance. +type DNSResolver interface { + // LookupMX returns the DNS MX records for the given domain. + LookupMX(ctx context.Context, name string) ([]*net.MX, error) + + // LookupTXT returns the DNS TXT records for the given domain. + LookupTXT(ctx context.Context, name string) ([]string, error) + + // LookupAddr performs a reverse lookup for the given IP address, + // returning a list of hostnames mapping to that address. + LookupAddr(ctx context.Context, addr string) ([]string, error) + + // LookupHost looks up the given hostname using the local resolver. + // It returns a slice of that host's addresses (IPv4 and IPv6). + LookupHost(ctx context.Context, host string) ([]string, error) +} + +// StandardDNSResolver is the default DNS resolver implementation that uses net.Resolver. +type StandardDNSResolver struct { + resolver *net.Resolver +} + +// NewStandardDNSResolver creates a new StandardDNSResolver with default settings. +func NewStandardDNSResolver() DNSResolver { + return &StandardDNSResolver{ + resolver: &net.Resolver{ + PreferGo: true, + }, + } +} + +// LookupMX implements DNSResolver.LookupMX using net.Resolver. +func (r *StandardDNSResolver) LookupMX(ctx context.Context, name string) ([]*net.MX, error) { + return r.resolver.LookupMX(ctx, name) +} + +// LookupTXT implements DNSResolver.LookupTXT using net.Resolver. +func (r *StandardDNSResolver) LookupTXT(ctx context.Context, name string) ([]string, error) { + return r.resolver.LookupTXT(ctx, name) +} + +// LookupAddr implements DNSResolver.LookupAddr using net.Resolver. +func (r *StandardDNSResolver) LookupAddr(ctx context.Context, addr string) ([]string, error) { + return r.resolver.LookupAddr(ctx, addr) +} + +// LookupHost implements DNSResolver.LookupHost using net.Resolver. +func (r *StandardDNSResolver) LookupHost(ctx context.Context, host string) ([]string, error) { + return r.resolver.LookupHost(ctx, host) +} From d81ff1731c840c175bb115ef1b5afcfef1c8959d Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Mon, 17 Nov 2025 10:28:32 +0700 Subject: [PATCH 041/102] Fix tests --- pkg/analyzer/content_test.go | 6 +++--- pkg/analyzer/headers_test.go | 4 ++-- pkg/analyzer/parser_test.go | 3 +++ 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pkg/analyzer/content_test.go b/pkg/analyzer/content_test.go index 0aa7ff9..9289d95 100644 --- a/pkg/analyzer/content_test.go +++ b/pkg/analyzer/content_test.go @@ -76,17 +76,17 @@ func TestExtractTextFromHTML(t *testing.T) { { name: "Multiple elements", html: "

Title

Paragraph

", - expectedText: "TitleParagraph", + expectedText: "Title Paragraph", }, { name: "With script tag", html: "

Text

More

", - expectedText: "TextMore", + expectedText: "Text More", }, { name: "With style tag", html: "

Text

More

", - expectedText: "TextMore", + expectedText: "Text More", }, { name: "Empty HTML", diff --git a/pkg/analyzer/headers_test.go b/pkg/analyzer/headers_test.go index 6a35d18..2513e6f 100644 --- a/pkg/analyzer/headers_test.go +++ b/pkg/analyzer/headers_test.go @@ -83,8 +83,8 @@ func TestCalculateHeaderScore(t *testing.T) { Date: "Mon, 01 Jan 2024 12:00:00 +0000", Parts: []MessagePart{{ContentType: "text/plain", Content: "test"}}, }, - minScore: 40, - maxScore: 80, + minScore: 80, + maxScore: 90, }, { name: "Invalid Message-ID format", diff --git a/pkg/analyzer/parser_test.go b/pkg/analyzer/parser_test.go index 571f542..eb1fc6a 100644 --- a/pkg/analyzer/parser_test.go +++ b/pkg/analyzer/parser_test.go @@ -106,6 +106,9 @@ Content-Type: text/html; charset=utf-8 } func TestGetAuthenticationResults(t *testing.T) { + // Force hostname + hostname = "example.com" + rawEmail := `From: sender@example.com To: recipient@example.com Subject: Test Email From e23afcc77cc5ffb1c4c930ec446ec175123e0986 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Tue, 18 Nov 2025 14:37:39 +0700 Subject: [PATCH 042/102] Add container options to use certificates in postfix --- README.md | 43 ++++++++++++++++++++++++++++++++++++++++++- docker/entrypoint.sh | 9 +++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a8f79e3..1f330c4 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,48 @@ docker run -d \ happydeliver:latest ``` -#### 3. Configure Network and DNS +#### 3. Configure TLS Certificates (Optional but Recommended) + +To enable TLS encryption for incoming SMTP connections, you can configure Postfix to use your SSL/TLS certificates. This is highly recommended for production deployments. + +##### Using docker-compose + +Add the certificate paths to your `docker-compose.yml`: + +```yaml +environment: + - POSTFIX_CERT_FILE=/etc/ssl/certs/mail.yourdomain.com.crt + - POSTFIX_KEY_FILE=/etc/ssl/private/mail.yourdomain.com.key +volumes: + - /path/to/your/certificate.crt:/etc/ssl/certs/mail.yourdomain.com.crt:ro + - /path/to/your/private.key:/etc/ssl/private/mail.yourdomain.com.key:ro +``` + +##### Using docker run + +```bash +docker run -d \ + --name happydeliver \ + -p 25:25 \ + -p 8080:8080 \ + -e HAPPYDELIVER_DOMAIN=yourdomain.com \ + -e HOSTNAME=mail.yourdomain.com \ + -e POSTFIX_CERT_FILE=/etc/ssl/certs/mail.yourdomain.com.crt \ + -e POSTFIX_KEY_FILE=/etc/ssl/private/mail.yourdomain.com.key \ + -v /path/to/your/certificate.crt:/etc/ssl/certs/mail.yourdomain.com.crt:ro \ + -v /path/to/your/private.key:/etc/ssl/private/mail.yourdomain.com.key:ro \ + -v $(pwd)/data:/var/lib/happydeliver \ + -v $(pwd)/logs:/var/log/happydeliver \ + happydeliver:latest +``` + +**Notes:** +- The certificate file should contain the full certificate chain (certificate + intermediate CAs) +- The private key file must be readable by the postfix user inside the container +- TLS is configured with `smtpd_tls_security_level = may`, which means it's opportunistic (STARTTLS supported but not required) +- If both environment variables are not set, Postfix will run without TLS support + +#### 4. Configure Network and DNS ##### Open SMTP Port diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 99744f6..bfe6088 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -25,6 +25,15 @@ echo "Configuring Postfix..." sed -i "s/__HOSTNAME__/${HOSTNAME}/g" /etc/postfix/main.cf sed -i "s/__DOMAIN__/${HAPPYDELIVER_DOMAIN}/g" /etc/postfix/main.cf +# Add certificates to postfix +[ -n "${POSTFIX_CERT_FILE}" ] && [ -n "${POSTFIX_KEY_FILE}" ] && { + cat <> /etc/postfix/main.cf +smtpd_tls_cert_file = ${POSTFIX_CERT_FILE} +smtpd_tls_key_file = ${POSTFIX_KEY_FILE} +smtpd_tls_security_level = may +EOF +} + # Replace placeholders in configurations sed -i "s/__HOSTNAME__/${HOSTNAME}/g" /etc/authentication_milter.json From 3e766924482a5104fd98c7870c93cab3dc8df230 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 17 Nov 2025 00:11:15 +0000 Subject: [PATCH 043/102] chore(deps): lock file maintenance --- web/package-lock.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 4ac32c2..91a57e4 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1200,9 +1200,9 @@ "license": "MIT" }, "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.6.tgz", - "integrity": "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.7.tgz", + "integrity": "sha512-znp1A/Y1Jj4l/Zy7PX5DZKBE0ZNY+5QBngiE21NJkfSTyzzC5iKNWOtwFXKtIrn7MXEFBck4jD95iBNkGjK92Q==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2186,9 +2186,9 @@ } }, "node_modules/default-browser": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.3.0.tgz", - "integrity": "sha512-Qq68+VkJlc8tjnPV1i7HtbIn7ohmjZa88qUvHMIK0ZKUXMCuV45cT7cEXALPUmeXCe0q1DWQkQTemHVaLIFSrg==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.4.0.tgz", + "integrity": "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==", "dev": true, "license": "MIT", "dependencies": { @@ -2504,9 +2504,9 @@ } }, "node_modules/esrap": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.2.tgz", - "integrity": "sha512-DgvlIQeowRNyvLPWW4PT7Gu13WznY288Du086E751mwwbsgr29ytBiYeLzAGIo0qk3Ujob0SDk8TiSaM5WQzNg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.3.tgz", + "integrity": "sha512-T/Dhhv/QH+yYmiaLz9SA3PW+YyenlnRKDNdtlYJrSOBmNsH4nvPux+mTwx7p+wAedlJrGoZtXNI0a0MjQ2QkVg==", "dev": true, "license": "MIT", "dependencies": { @@ -3891,9 +3891,9 @@ } }, "node_modules/svelte": { - "version": "5.43.6", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.43.6.tgz", - "integrity": "sha512-RnyO9VXI85Bmsf4b8AuQFBKFYL3LKUl+ZrifOjvlrQoboAROj5IITVLK1yOXBjwUWUn2BI5cfmurktgCzuZ5QA==", + "version": "5.43.8", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.43.8.tgz", + "integrity": "sha512-d53/xClCjHsuFXuHsn7+F/0NKkkwgRv8kLg2his5YBYqVtfIrBqkvWd+5ZjYN6ryk/jv/rJF00vexXHkK8ofXA==", "dev": true, "license": "MIT", "peer": true, From 016ed7180eaa3203065609202c6dc87cf2a5ab3c Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sun, 23 Nov 2025 19:41:31 +0700 Subject: [PATCH 044/102] Simplify docker usage, HOSTNAME variable is taken from container hostname Bug: https://github.com/happyDomain/happydeliver/issues/3 --- README.md | 6 +++--- docker-compose.yml | 6 +++--- docker/README.md | 7 ++++--- docker/entrypoint.sh | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 1f330c4..3b28292 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ git clone https://git.nemunai.re/happyDomain/happyDeliver.git cd happydeliver # Edit docker-compose.yml to set your domain -# Change HAPPYDELIVER_DOMAIN and HOSTNAME environment variables +# Change HAPPYDELIVER_DOMAIN environment variable and hostname # Build and start docker-compose up -d @@ -63,7 +63,7 @@ docker run -d \ -p 25:25 \ -p 8080:8080 \ -e HAPPYDELIVER_DOMAIN=yourdomain.com \ - -e HOSTNAME=mail.yourdomain.com \ + --hostname mail.yourdomain.com \ -v $(pwd)/data:/var/lib/happydeliver \ -v $(pwd)/logs:/var/log/happydeliver \ happydeliver:latest @@ -94,9 +94,9 @@ docker run -d \ -p 25:25 \ -p 8080:8080 \ -e HAPPYDELIVER_DOMAIN=yourdomain.com \ - -e HOSTNAME=mail.yourdomain.com \ -e POSTFIX_CERT_FILE=/etc/ssl/certs/mail.yourdomain.com.crt \ -e POSTFIX_KEY_FILE=/etc/ssl/private/mail.yourdomain.com.key \ + --hostname mail.yourdomain.com \ -v /path/to/your/certificate.crt:/etc/ssl/certs/mail.yourdomain.com.crt:ro \ -v /path/to/your/private.key:/etc/ssl/private/mail.yourdomain.com.key:ro \ -v $(pwd)/data:/var/lib/happydeliver \ diff --git a/docker-compose.yml b/docker-compose.yml index fa27c5c..ccfd313 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,12 +5,12 @@ services: dockerfile: Dockerfile image: happydomain/happydeliver:latest container_name: happydeliver + # Set a hostname hostname: mail.happydeliver.local environment: - # Set your domain and hostname - DOMAIN: happydeliver.local - HOSTNAME: mail.happydeliver.local + # Set your domain + HAPPYDELIVER_DOMAIN: happydeliver.local ports: # SMTP port diff --git a/docker/README.md b/docker/README.md index 45cce6b..3769365 100644 --- a/docker/README.md +++ b/docker/README.md @@ -109,12 +109,13 @@ Default configuration for the Docker environment: The container accepts these environment variables: -- `DOMAIN`: Email domain for test addresses (default: happydeliver.local) -- `HOSTNAME`: Container hostname (default: mail.happydeliver.local) +- `HAPPYDELIVER_DOMAIN`: Email domain for test addresses (default: happydeliver.local) + +Note that the hostname of the container is used to filter the authentication tests results. Example: ```bash -docker run -e DOMAIN=example.com -e HOSTNAME=mail.example.com ... +docker run -e HAPPYDELIVER_DOMAIN=example.com --hostname mail.example.com ... ``` ## Volumes diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index bfe6088..1bc3eff 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -4,7 +4,7 @@ set -e echo "Starting happyDeliver container..." # Get environment variables with defaults -HOSTNAME="${HOSTNAME:-mail.happydeliver.local}" +[ -n "${HOSTNAME}" ] || HOSTNAME=$(hostname) HAPPYDELIVER_DOMAIN="${HAPPYDELIVER_DOMAIN:-happydeliver.local}" echo "Hostname: $HOSTNAME" From ca2ac3df7c4dd608c9b577add6af4fb6be778c20 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 28 Nov 2025 21:15:01 +0000 Subject: [PATCH 045/102] chore(deps): update dependency prettier to v3.7.2 --- web/package-lock.json | 82 +++++++++++-------------------------------- 1 file changed, 21 insertions(+), 61 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 91a57e4..55170c9 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -3132,6 +3132,19 @@ "node": ">=8.6" } }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3383,13 +3396,14 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -3556,9 +3570,9 @@ } }, "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.2.tgz", + "integrity": "sha512-n3HV2J6QhItCXndGa3oMWvWFAgN1ibnS7R9mt6iokScBOC0Ul9/iZORmU2IWUMcyAQaMPjTlY3uT34TqocUxMA==", "dev": true, "license": "MIT", "peer": true, @@ -4005,19 +4019,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/tinypool": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", @@ -4273,19 +4274,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/vitefu": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", @@ -4379,19 +4367,6 @@ } } }, - "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/vitest/node_modules/tinyexec": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", @@ -4465,21 +4440,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", From 954cbe29fca46bb9bf75096cd9254dee7c8de0a5 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 25 Nov 2025 02:19:42 +0000 Subject: [PATCH 046/102] chore(deps): update module golang.org/x/crypto to v0.45.0 [security] --- go.mod | 5 ++--- go.sum | 10 ++-------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index ebf21a7..1a62c95 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.24.6 require ( github.com/JGLTechnologies/gin-rate-limit v1.5.6 github.com/emersion/go-smtp v0.24.0 - github.com/getkin/kin-openapi v0.133.0 github.com/gin-gonic/gin v1.11.0 github.com/google/uuid v1.6.0 github.com/oapi-codegen/runtime v1.1.2 @@ -16,7 +15,6 @@ require ( ) require ( - github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.14.2 // indirect github.com/bytedance/sonic/loader v0.4.0 // indirect @@ -26,6 +24,7 @@ require ( github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect github.com/gabriel-vasile/mimetype v1.4.11 // indirect + github.com/getkin/kin-openapi v0.133.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-openapi/jsonpointer v0.22.2 // indirect github.com/go-openapi/swag/jsonname v0.25.1 // indirect @@ -66,7 +65,7 @@ require ( github.com/woodsbury/decimal128 v1.4.0 // indirect go.uber.org/mock v0.6.0 // indirect golang.org/x/arch v0.23.0 // indirect - golang.org/x/crypto v0.44.0 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/mod v0.30.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect diff --git a/go.sum b/go.sum index cd951e8..f79c10e 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,5 @@ github.com/JGLTechnologies/gin-rate-limit v1.5.6 h1:BrL2wXrF7SSqmB88YTGFVKMGVcjURMUeKqwQrlmzweI= github.com/JGLTechnologies/gin-rate-limit v1.5.6/go.mod h1:fwUuBegxLKm8+/4ST0zDFssRFTFaVZ7bH3ApK7iNZww= -github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= -github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= -github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= -github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -102,7 +98,6 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -170,7 +165,6 @@ github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU= github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg= -github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -201,8 +195,8 @@ golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= From 5701070cc1b1e1918927e4bfd8b0d86e092f4b9b Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 1 Dec 2025 07:14:52 +0000 Subject: [PATCH 047/102] chore(deps): update dependency vite to v7.2.6 --- web/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 55170c9..bd96852 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -4176,9 +4176,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", - "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz", + "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", "dev": true, "license": "MIT", "peer": true, From 5d02070100e841f2af9c0f0ab1f0916f779ab285 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 29 Nov 2025 19:14:29 +0000 Subject: [PATCH 048/102] chore(deps): update dependency prettier to v3.7.3 --- web/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index bd96852..1a35357 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -3570,9 +3570,9 @@ } }, "node_modules/prettier": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.2.tgz", - "integrity": "sha512-n3HV2J6QhItCXndGa3oMWvWFAgN1ibnS7R9mt6iokScBOC0Ul9/iZORmU2IWUMcyAQaMPjTlY3uT34TqocUxMA==", + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.3.tgz", + "integrity": "sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==", "dev": true, "license": "MIT", "peer": true, From 926796b79e33fe2157909ad04ba531dbf6a76444 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 2 Dec 2025 05:15:50 +0000 Subject: [PATCH 049/102] chore(deps): lock file maintenance --- web/package-lock.json | 586 ++++++++++++++---------------------------- 1 file changed, 192 insertions(+), 394 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 1a35357..5634377 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -581,9 +581,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", "dev": true, "license": "MIT", "dependencies": { @@ -593,7 +593,7 @@ "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", + "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, @@ -828,44 +828,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -885,9 +847,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", - "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", "cpu": [ "arm" ], @@ -899,9 +861,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", - "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", "cpu": [ "arm64" ], @@ -913,9 +875,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", - "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", "cpu": [ "arm64" ], @@ -927,9 +889,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", - "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", "cpu": [ "x64" ], @@ -941,9 +903,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", - "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", "cpu": [ "arm64" ], @@ -955,9 +917,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", - "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", "cpu": [ "x64" ], @@ -969,9 +931,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", - "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", "cpu": [ "arm" ], @@ -983,9 +945,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", - "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", "cpu": [ "arm" ], @@ -997,9 +959,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", - "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", "cpu": [ "arm64" ], @@ -1011,9 +973,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", - "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", "cpu": [ "arm64" ], @@ -1025,9 +987,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", - "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", "cpu": [ "loong64" ], @@ -1039,9 +1001,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", - "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", "cpu": [ "ppc64" ], @@ -1053,9 +1015,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", - "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", "cpu": [ "riscv64" ], @@ -1067,9 +1029,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", - "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", "cpu": [ "riscv64" ], @@ -1081,9 +1043,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", - "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", "cpu": [ "s390x" ], @@ -1095,9 +1057,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", - "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", "cpu": [ "x64" ], @@ -1109,9 +1071,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", - "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", "cpu": [ "x64" ], @@ -1123,9 +1085,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", - "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", "cpu": [ "arm64" ], @@ -1137,9 +1099,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", - "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", "cpu": [ "arm64" ], @@ -1151,9 +1113,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", - "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", "cpu": [ "ia32" ], @@ -1165,9 +1127,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", - "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", "cpu": [ "x64" ], @@ -1179,9 +1141,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", - "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", "cpu": [ "x64" ], @@ -1200,9 +1162,9 @@ "license": "MIT" }, "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.7.tgz", - "integrity": "sha512-znp1A/Y1Jj4l/Zy7PX5DZKBE0ZNY+5QBngiE21NJkfSTyzzC5iKNWOtwFXKtIrn7MXEFBck4jD95iBNkGjK92Q==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.8.tgz", + "integrity": "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1220,9 +1182,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.48.5", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.48.5.tgz", - "integrity": "sha512-/rnwfSWS3qwUSzvHynUTORF9xSJi7PCR9yXkxUOnRrNqyKmCmh3FPHH+E9BbgqxXfTevGXBqgnlh9kMb+9T5XA==", + "version": "2.49.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.49.0.tgz", + "integrity": "sha512-oH8tXw7EZnie8FdOWYrF7Yn4IKrqTFHhXvl8YxXxbKwTMcD/5NNCryUSEXRk2ZR4ojnub0P8rNrsVGHXWqIDtA==", "dev": true, "license": "MIT", "peer": true, @@ -1350,17 +1312,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.4.tgz", - "integrity": "sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz", + "integrity": "sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.46.4", - "@typescript-eslint/type-utils": "8.46.4", - "@typescript-eslint/utils": "8.46.4", - "@typescript-eslint/visitor-keys": "8.46.4", + "@typescript-eslint/scope-manager": "8.48.0", + "@typescript-eslint/type-utils": "8.48.0", + "@typescript-eslint/utils": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1374,7 +1336,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.46.4", + "@typescript-eslint/parser": "^8.48.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -1390,17 +1352,17 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.4.tgz", - "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.0.tgz", + "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.46.4", - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/typescript-estree": "8.46.4", - "@typescript-eslint/visitor-keys": "8.46.4", + "@typescript-eslint/scope-manager": "8.48.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0", "debug": "^4.3.4" }, "engines": { @@ -1416,14 +1378,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.4.tgz", - "integrity": "sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.0.tgz", + "integrity": "sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.4", - "@typescript-eslint/types": "^8.46.4", + "@typescript-eslint/tsconfig-utils": "^8.48.0", + "@typescript-eslint/types": "^8.48.0", "debug": "^4.3.4" }, "engines": { @@ -1438,14 +1400,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.4.tgz", - "integrity": "sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.0.tgz", + "integrity": "sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/visitor-keys": "8.46.4" + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1456,9 +1418,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.4.tgz", - "integrity": "sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.0.tgz", + "integrity": "sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==", "dev": true, "license": "MIT", "engines": { @@ -1473,15 +1435,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.4.tgz", - "integrity": "sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.0.tgz", + "integrity": "sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/typescript-estree": "8.46.4", - "@typescript-eslint/utils": "8.46.4", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0", + "@typescript-eslint/utils": "8.48.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1498,9 +1460,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.4.tgz", - "integrity": "sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.0.tgz", + "integrity": "sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==", "dev": true, "license": "MIT", "engines": { @@ -1512,21 +1474,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.4.tgz", - "integrity": "sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.0.tgz", + "integrity": "sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.46.4", - "@typescript-eslint/tsconfig-utils": "8.46.4", - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/visitor-keys": "8.46.4", + "@typescript-eslint/project-service": "8.48.0", + "@typescript-eslint/tsconfig-utils": "8.48.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0", "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", + "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "engines": { @@ -1567,16 +1528,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.4.tgz", - "integrity": "sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.0.tgz", + "integrity": "sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.4", - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/typescript-estree": "8.46.4" + "@typescript-eslint/scope-manager": "8.48.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1591,13 +1552,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.4.tgz", - "integrity": "sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.0.tgz", + "integrity": "sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/types": "8.48.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -1880,19 +1841,6 @@ "concat-map": "0.0.1" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -2504,9 +2452,9 @@ } }, "node_modules/esrap": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.3.tgz", - "integrity": "sha512-T/Dhhv/QH+yYmiaLz9SA3PW+YyenlnRKDNdtlYJrSOBmNsH4nvPux+mTwx7p+wAedlJrGoZtXNI0a0MjQ2QkVg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.1.tgz", + "integrity": "sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg==", "dev": true, "license": "MIT", "dependencies": { @@ -2580,36 +2528,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2624,16 +2542,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2665,19 +2573,6 @@ "node": ">=16.0.0" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2909,16 +2804,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/is-reference": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", @@ -3108,43 +2993,6 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3546,9 +3394,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "dev": true, "license": "MIT", "dependencies": { @@ -3607,27 +3455,6 @@ "node": ">=6" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/rc9": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", @@ -3663,21 +3490,10 @@ "node": ">=4" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rollup": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", - "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", "dev": true, "license": "MIT", "dependencies": { @@ -3691,28 +3507,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.53.2", - "@rollup/rollup-android-arm64": "4.53.2", - "@rollup/rollup-darwin-arm64": "4.53.2", - "@rollup/rollup-darwin-x64": "4.53.2", - "@rollup/rollup-freebsd-arm64": "4.53.2", - "@rollup/rollup-freebsd-x64": "4.53.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", - "@rollup/rollup-linux-arm-musleabihf": "4.53.2", - "@rollup/rollup-linux-arm64-gnu": "4.53.2", - "@rollup/rollup-linux-arm64-musl": "4.53.2", - "@rollup/rollup-linux-loong64-gnu": "4.53.2", - "@rollup/rollup-linux-ppc64-gnu": "4.53.2", - "@rollup/rollup-linux-riscv64-gnu": "4.53.2", - "@rollup/rollup-linux-riscv64-musl": "4.53.2", - "@rollup/rollup-linux-s390x-gnu": "4.53.2", - "@rollup/rollup-linux-x64-gnu": "4.53.2", - "@rollup/rollup-linux-x64-musl": "4.53.2", - "@rollup/rollup-openharmony-arm64": "4.53.2", - "@rollup/rollup-win32-arm64-msvc": "4.53.2", - "@rollup/rollup-win32-ia32-msvc": "4.53.2", - "@rollup/rollup-win32-x64-gnu": "4.53.2", - "@rollup/rollup-win32-x64-msvc": "4.53.2", + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" } }, @@ -3729,30 +3545,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -3905,9 +3697,9 @@ } }, "node_modules/svelte": { - "version": "5.43.8", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.43.8.tgz", - "integrity": "sha512-d53/xClCjHsuFXuHsn7+F/0NKkkwgRv8kLg2his5YBYqVtfIrBqkvWd+5ZjYN6ryk/jv/rJF00vexXHkK8ofXA==", + "version": "5.45.3", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.45.3.tgz", + "integrity": "sha512-ngKXNhNvwPzF43QqEhDOue7TQTrG09em1sd4HBxVF0Wr2gopAmdEWan+rgbdgK4fhBtSOTJO8bYU4chUG7VXZQ==", "dev": true, "license": "MIT", "peer": true, @@ -3920,8 +3712,9 @@ "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", + "devalue": "^5.5.0", "esm-env": "^1.2.1", - "esrap": "^2.1.0", + "esrap": "^2.2.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", @@ -4049,19 +3842,6 @@ "node": ">=14.0.0" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -4114,16 +3894,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.4.tgz", - "integrity": "sha512-KALyxkpYV5Ix7UhvjTwJXZv76VWsHG+NjNlt/z+a17SOQSiOcBdUXdbJdyXi7RPxrBFECtFOiPwUJQusJuCqrg==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.48.0.tgz", + "integrity": "sha512-fcKOvQD9GUn3Xw63EgiDqhvWJ5jsyZUaekl3KVpGsDJnN46WJTe3jWxtQP9lMZm1LJNkFLlTaWAxK2vUQR+cqw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.46.4", - "@typescript-eslint/parser": "8.46.4", - "@typescript-eslint/typescript-estree": "8.46.4", - "@typescript-eslint/utils": "8.46.4" + "@typescript-eslint/eslint-plugin": "8.48.0", + "@typescript-eslint/parser": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0", + "@typescript-eslint/utils": "8.48.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4440,6 +4220,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", From 528a65ca0483a4c445a8a0296ff93f6078194d7b Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 8 Dec 2025 01:15:59 +0000 Subject: [PATCH 050/102] chore(deps): lock file maintenance --- web/package-lock.json | 156 +++++++++++++++++++++--------------------- 1 file changed, 78 insertions(+), 78 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 5634377..e7bf363 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1182,9 +1182,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.49.0", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.49.0.tgz", - "integrity": "sha512-oH8tXw7EZnie8FdOWYrF7Yn4IKrqTFHhXvl8YxXxbKwTMcD/5NNCryUSEXRk2ZR4ojnub0P8rNrsVGHXWqIDtA==", + "version": "2.49.1", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.49.1.tgz", + "integrity": "sha512-vByReCTTdlNM80vva8alAQC80HcOiHLkd8XAxIiKghKSHcqeNfyhp3VsYAV8VSiPKu4Jc8wWCfsZNAIvd1uCqA==", "dev": true, "license": "MIT", "peer": true, @@ -1312,17 +1312,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz", - "integrity": "sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz", + "integrity": "sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.48.0", - "@typescript-eslint/type-utils": "8.48.0", - "@typescript-eslint/utils": "8.48.0", - "@typescript-eslint/visitor-keys": "8.48.0", + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/type-utils": "8.48.1", + "@typescript-eslint/utils": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1336,7 +1336,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.48.0", + "@typescript-eslint/parser": "^8.48.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -1352,17 +1352,17 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.0.tgz", - "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.1.tgz", + "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.48.0", - "@typescript-eslint/types": "8.48.0", - "@typescript-eslint/typescript-estree": "8.48.0", - "@typescript-eslint/visitor-keys": "8.48.0", + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", "debug": "^4.3.4" }, "engines": { @@ -1378,14 +1378,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.0.tgz", - "integrity": "sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz", + "integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.48.0", - "@typescript-eslint/types": "^8.48.0", + "@typescript-eslint/tsconfig-utils": "^8.48.1", + "@typescript-eslint/types": "^8.48.1", "debug": "^4.3.4" }, "engines": { @@ -1400,14 +1400,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.0.tgz", - "integrity": "sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz", + "integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.0", - "@typescript-eslint/visitor-keys": "8.48.0" + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1418,9 +1418,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.0.tgz", - "integrity": "sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz", + "integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==", "dev": true, "license": "MIT", "engines": { @@ -1435,15 +1435,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.0.tgz", - "integrity": "sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.1.tgz", + "integrity": "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.0", - "@typescript-eslint/typescript-estree": "8.48.0", - "@typescript-eslint/utils": "8.48.0", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1", + "@typescript-eslint/utils": "8.48.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1460,9 +1460,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.0.tgz", - "integrity": "sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", + "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", "dev": true, "license": "MIT", "engines": { @@ -1474,16 +1474,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.0.tgz", - "integrity": "sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz", + "integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.48.0", - "@typescript-eslint/tsconfig-utils": "8.48.0", - "@typescript-eslint/types": "8.48.0", - "@typescript-eslint/visitor-keys": "8.48.0", + "@typescript-eslint/project-service": "8.48.1", + "@typescript-eslint/tsconfig-utils": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", @@ -1528,16 +1528,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.0.tgz", - "integrity": "sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.1.tgz", + "integrity": "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.48.0", - "@typescript-eslint/types": "8.48.0", - "@typescript-eslint/typescript-estree": "8.48.0" + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1552,13 +1552,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.0.tgz", - "integrity": "sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz", + "integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/types": "8.48.1", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -2350,9 +2350,9 @@ } }, "node_modules/eslint-plugin-svelte": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.13.0.tgz", - "integrity": "sha512-2ohCCQJJTNbIpQCSDSTWj+FN0OVfPmSO03lmSNT7ytqMaWF6kpT86LdzDqtm4sh7TVPl/OEWJ/d7R87bXP2Vjg==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.13.1.tgz", + "integrity": "sha512-Ng+kV/qGS8P/isbNYVE3sJORtubB+yLEcYICMkUWNaDTb0SwZni/JhAYXh/Dz/q2eThUwWY0VMPZ//KYD1n3eQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3418,9 +3418,9 @@ } }, "node_modules/prettier": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.3.tgz", - "integrity": "sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==", + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, "license": "MIT", "peer": true, @@ -3697,9 +3697,9 @@ } }, "node_modules/svelte": { - "version": "5.45.3", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.45.3.tgz", - "integrity": "sha512-ngKXNhNvwPzF43QqEhDOue7TQTrG09em1sd4HBxVF0Wr2gopAmdEWan+rgbdgK4fhBtSOTJO8bYU4chUG7VXZQ==", + "version": "5.45.6", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.45.6.tgz", + "integrity": "sha512-V3aVXthzPyPt1UB1wLEoXnEXpwPsvs7NHrR0xkCor8c11v71VqBj477MClqPZYyrcXrAH21sNGhOj9FJvSwXfQ==", "dev": true, "license": "MIT", "peer": true, @@ -3714,7 +3714,7 @@ "clsx": "^2.1.1", "devalue": "^5.5.0", "esm-env": "^1.2.1", - "esrap": "^2.2.0", + "esrap": "^2.2.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", @@ -3749,9 +3749,9 @@ } }, "node_modules/svelte-eslint-parser": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.4.0.tgz", - "integrity": "sha512-fjPzOfipR5S7gQ/JvI9r2H8y9gMGXO3JtmrylHLLyahEMquXI0lrebcjT+9/hNgDej0H7abTyox5HpHmW1PSWA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.4.1.tgz", + "integrity": "sha512-1eqkfQ93goAhjAXxZiu1SaKI9+0/sxp4JIWQwUpsz7ybehRE5L8dNuz7Iry7K22R47p5/+s9EM+38nHV2OlgXA==", "dev": true, "license": "MIT", "dependencies": { @@ -3764,7 +3764,7 @@ }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0", - "pnpm": "10.18.3" + "pnpm": "10.24.0" }, "funding": { "url": "https://github.com/sponsors/ota-meshi" @@ -3894,16 +3894,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.48.0.tgz", - "integrity": "sha512-fcKOvQD9GUn3Xw63EgiDqhvWJ5jsyZUaekl3KVpGsDJnN46WJTe3jWxtQP9lMZm1LJNkFLlTaWAxK2vUQR+cqw==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.48.1.tgz", + "integrity": "sha512-FbOKN1fqNoXp1hIl5KYpObVrp0mCn+CLgn479nmu2IsRMrx2vyv74MmsBLVlhg8qVwNFGbXSp8fh1zp8pEoC2A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.48.0", - "@typescript-eslint/parser": "8.48.0", - "@typescript-eslint/typescript-estree": "8.48.0", - "@typescript-eslint/utils": "8.48.0" + "@typescript-eslint/eslint-plugin": "8.48.1", + "@typescript-eslint/parser": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1", + "@typescript-eslint/utils": "8.48.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" From 6081e486bfa757c83e23c54baec3b669779fb316 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 20 Dec 2025 07:15:11 +0000 Subject: [PATCH 051/102] chore(deps): update dependency svelte-check to v4.3.5 --- web/package-lock.json | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index e7bf363..8791088 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -3725,9 +3725,9 @@ } }, "node_modules/svelte-check": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.4.tgz", - "integrity": "sha512-DVWvxhBrDsd+0hHWKfjP99lsSXASeOhHJYyuKOFYJcP7ThfSCKgjVarE8XfuMWpS5JV3AlDf+iK1YGGo2TACdw==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.5.tgz", + "integrity": "sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4220,24 +4220,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", From 11d46de033bf2f1b3a8293e3eb19464395ad2645 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sun, 14 Dec 2025 21:15:06 +0000 Subject: [PATCH 052/102] chore(deps): update dependency prettier-plugin-svelte to v3.4.1 --- web/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 8791088..4ed6e7b 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -3435,9 +3435,9 @@ } }, "node_modules/prettier-plugin-svelte": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.4.0.tgz", - "integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.4.1.tgz", + "integrity": "sha512-xL49LCloMoZRvSwa6IEdN2GV6cq2IqpYGstYtMT+5wmml1/dClEoI0MZR78MiVPpu6BdQFfN0/y73yO6+br5Pg==", "dev": true, "license": "MIT", "peerDependencies": { From 57a3774d2845216bf7b2c29dbe578196c132977e Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 8 Dec 2025 23:16:01 +0000 Subject: [PATCH 053/102] chore(deps): update module golang.org/x/net to v0.48.0 --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 1a62c95..e035147 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/gin-gonic/gin v1.11.0 github.com/google/uuid v1.6.0 github.com/oapi-codegen/runtime v1.1.2 - golang.org/x/net v0.47.0 + golang.org/x/net v0.48.0 gorm.io/driver/postgres v1.6.0 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.31.1 @@ -65,11 +65,11 @@ require ( github.com/woodsbury/decimal128 v1.4.0 // indirect go.uber.org/mock v0.6.0 // indirect golang.org/x/arch v0.23.0 // indirect - golang.org/x/crypto v0.45.0 // indirect + golang.org/x/crypto v0.46.0 // indirect golang.org/x/mod v0.30.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect golang.org/x/tools v0.39.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index f79c10e..0d141fe 100644 --- a/go.sum +++ b/go.sum @@ -195,8 +195,8 @@ golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= @@ -207,13 +207,13 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -229,16 +229,16 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From 0fda0f88c1646df0a52dcb527c745d875db5ce39 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 2 Dec 2025 07:15:00 +0000 Subject: [PATCH 054/102] chore(deps): update dependency @eslint/compat to v2 --- web/package-lock.json | 25 +++++++++++++++++++------ web/package.json | 2 +- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 4ed6e7b..e020b39 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -13,7 +13,7 @@ "bootstrap-icons": "^1.13.1" }, "devDependencies": { - "@eslint/compat": "^1.4.0", + "@eslint/compat": "^2.0.0", "@eslint/js": "^9.36.0", "@hey-api/openapi-ts": "0.86.10", "@sveltejs/adapter-static": "^3.0.9", @@ -519,16 +519,16 @@ } }, "node_modules/@eslint/compat": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.4.1.tgz", - "integrity": "sha512-cfO82V9zxxGBxcQDr1lfaYB7wykTa0b00mGa36FrJl7iTFd0Z2cHfEYuxcBRP/iNijCsWsEkA+jzT8hGYmv33w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-2.0.0.tgz", + "integrity": "sha512-T9AfE1G1uv4wwq94ozgTGio5EUQBqAVe1X9qsQtSNVEYW6j3hvtZVm8Smr4qL1qDPFg+lOB2cL5RxTRMzq4CTA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0" + "@eslint/core": "^1.0.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "peerDependencies": { "eslint": "^8.40 || 9" @@ -539,6 +539,19 @@ } } }, + "node_modules/@eslint/compat/node_modules/@eslint/core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.0.0.tgz", + "integrity": "sha512-PRfWP+8FOldvbApr6xL7mNCw4cJcSTq4GA7tYbgq15mRb0kWKO/wEB2jr+uwjFH3sZvEZneZyCUGTxsv4Sahyw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, "node_modules/@eslint/config-array": { "version": "0.21.1", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", diff --git a/web/package.json b/web/package.json index c1efabe..8ba17ca 100644 --- a/web/package.json +++ b/web/package.json @@ -16,7 +16,7 @@ "generate:api": "openapi-ts" }, "devDependencies": { - "@eslint/compat": "^1.4.0", + "@eslint/compat": "^2.0.0", "@eslint/js": "^9.36.0", "@hey-api/openapi-ts": "0.86.10", "@sveltejs/adapter-static": "^3.0.9", From 1ba35c6f9f315fb0ace535f2273f161b48eaf8b0 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 1 Jan 2026 19:16:13 +0000 Subject: [PATCH 055/102] chore(deps): update dependency globals to v17 --- web/package-lock.json | 21 +++++++++++++++++---- web/package.json | 2 +- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index e020b39..675e31a 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -23,7 +23,7 @@ "eslint": "^9.38.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-svelte": "^3.12.4", - "globals": "^16.4.0", + "globals": "^17.0.0", "prettier": "^3.6.2", "prettier-plugin-svelte": "^3.4.0", "svelte": "^5.39.5", @@ -2396,6 +2396,19 @@ } } }, + "node_modules/eslint-plugin-svelte/node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-scope": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", @@ -2671,9 +2684,9 @@ } }, "node_modules/globals": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", - "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.0.0.tgz", + "integrity": "sha512-gv5BeD2EssA793rlFWVPMMCqefTlpusw6/2TbAVMy0FzcG8wKJn4O+NqJ4+XWmmwrayJgw5TzrmWjFgmz1XPqw==", "dev": true, "license": "MIT", "engines": { diff --git a/web/package.json b/web/package.json index 8ba17ca..e5b88f2 100644 --- a/web/package.json +++ b/web/package.json @@ -26,7 +26,7 @@ "eslint": "^9.38.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-svelte": "^3.12.4", - "globals": "^16.4.0", + "globals": "^17.0.0", "prettier": "^3.6.2", "prettier-plugin-svelte": "^3.4.0", "svelte": "^5.39.5", From dc21b72f52f3a1b1fdb77fa3754d586ce38d6c83 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 29 Dec 2025 03:52:30 +0000 Subject: [PATCH 056/102] chore(deps): lock file maintenance --- web/package-lock.json | 662 ++++++++++++++++++++++-------------------- 1 file changed, 349 insertions(+), 313 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 675e31a..2edd780 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -35,9 +35,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", "cpu": [ "ppc64" ], @@ -52,9 +52,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", "cpu": [ "arm" ], @@ -69,9 +69,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", "cpu": [ "arm64" ], @@ -86,9 +86,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", "cpu": [ "x64" ], @@ -103,9 +103,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", "cpu": [ "arm64" ], @@ -120,9 +120,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", "cpu": [ "x64" ], @@ -137,9 +137,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", "cpu": [ "arm64" ], @@ -154,9 +154,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", "cpu": [ "x64" ], @@ -171,9 +171,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", "cpu": [ "arm" ], @@ -188,9 +188,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", "cpu": [ "arm64" ], @@ -205,9 +205,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", "cpu": [ "ia32" ], @@ -222,9 +222,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", "cpu": [ "loong64" ], @@ -239,9 +239,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", "cpu": [ "mips64el" ], @@ -256,9 +256,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", "cpu": [ "ppc64" ], @@ -273,9 +273,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", "cpu": [ "riscv64" ], @@ -290,9 +290,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", "cpu": [ "s390x" ], @@ -307,9 +307,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", "cpu": [ "x64" ], @@ -324,9 +324,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", "cpu": [ "arm64" ], @@ -341,9 +341,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", "cpu": [ "x64" ], @@ -358,9 +358,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", "cpu": [ "arm64" ], @@ -375,9 +375,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", "cpu": [ "x64" ], @@ -392,9 +392,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", "cpu": [ "arm64" ], @@ -409,9 +409,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", "cpu": [ "x64" ], @@ -426,9 +426,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", "cpu": [ "arm64" ], @@ -443,9 +443,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", "cpu": [ "ia32" ], @@ -460,9 +460,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", "cpu": [ "x64" ], @@ -539,19 +539,6 @@ } } }, - "node_modules/@eslint/compat/node_modules/@eslint/core": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.0.0.tgz", - "integrity": "sha512-PRfWP+8FOldvbApr6xL7mNCw4cJcSTq4GA7tYbgq15mRb0kWKO/wEB2jr+uwjFH3sZvEZneZyCUGTxsv4Sahyw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, "node_modules/@eslint/config-array": { "version": "0.21.1", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", @@ -580,7 +567,7 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/core": { + "node_modules/@eslint/config-helpers/node_modules/@eslint/core": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", @@ -593,6 +580,19 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.0.0.tgz", + "integrity": "sha512-PRfWP+8FOldvbApr6xL7mNCw4cJcSTq4GA7tYbgq15mRb0kWKO/wEB2jr+uwjFH3sZvEZneZyCUGTxsv4Sahyw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, "node_modules/@eslint/eslintrc": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", @@ -631,9 +631,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", - "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", "dev": true, "license": "MIT", "engines": { @@ -667,6 +667,19 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@hey-api/codegen-core": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.3.3.tgz", @@ -860,9 +873,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", - "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", "cpu": [ "arm" ], @@ -874,9 +887,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", - "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", "cpu": [ "arm64" ], @@ -888,9 +901,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", - "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", "cpu": [ "arm64" ], @@ -902,9 +915,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", - "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", "cpu": [ "x64" ], @@ -916,9 +929,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", - "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", "cpu": [ "arm64" ], @@ -930,9 +943,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", - "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", "cpu": [ "x64" ], @@ -944,9 +957,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", - "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", "cpu": [ "arm" ], @@ -958,9 +971,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", - "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", "cpu": [ "arm" ], @@ -972,9 +985,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", - "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", "cpu": [ "arm64" ], @@ -986,9 +999,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", - "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", "cpu": [ "arm64" ], @@ -1000,9 +1013,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", - "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", "cpu": [ "loong64" ], @@ -1014,9 +1027,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", - "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", "cpu": [ "ppc64" ], @@ -1028,9 +1041,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", - "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", "cpu": [ "riscv64" ], @@ -1042,9 +1055,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", - "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", "cpu": [ "riscv64" ], @@ -1056,9 +1069,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", - "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", "cpu": [ "s390x" ], @@ -1070,9 +1083,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", - "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", "cpu": [ "x64" ], @@ -1084,9 +1097,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", - "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", "cpu": [ "x64" ], @@ -1098,9 +1111,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", - "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", "cpu": [ "arm64" ], @@ -1112,9 +1125,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", - "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", "cpu": [ "arm64" ], @@ -1126,9 +1139,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", - "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", "cpu": [ "ia32" ], @@ -1140,9 +1153,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", - "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", "cpu": [ "x64" ], @@ -1154,9 +1167,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", - "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", "cpu": [ "x64" ], @@ -1168,9 +1181,9 @@ ] }, "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, "license": "MIT" }, @@ -1195,9 +1208,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.49.1", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.49.1.tgz", - "integrity": "sha512-vByReCTTdlNM80vva8alAQC80HcOiHLkd8XAxIiKghKSHcqeNfyhp3VsYAV8VSiPKu4Jc8wWCfsZNAIvd1uCqA==", + "version": "2.49.2", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.49.2.tgz", + "integrity": "sha512-Vp3zX/qlwerQmHMP6x0Ry1oY7eKKRcOWGc2P59srOp4zcqyn+etJyQpELgOi4+ZSUgteX8Y387NuwruLgGXLUQ==", "dev": true, "license": "MIT", "peer": true, @@ -1314,9 +1327,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", - "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "version": "24.10.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz", + "integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==", "dev": true, "license": "MIT", "peer": true, @@ -1325,18 +1338,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz", - "integrity": "sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.1.tgz", + "integrity": "sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.48.1", - "@typescript-eslint/type-utils": "8.48.1", - "@typescript-eslint/utils": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1", - "graphemer": "^1.4.0", + "@typescript-eslint/scope-manager": "8.50.1", + "@typescript-eslint/type-utils": "8.50.1", + "@typescript-eslint/utils": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" @@ -1349,7 +1361,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.48.1", + "@typescript-eslint/parser": "^8.50.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -1365,17 +1377,17 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.1.tgz", - "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.1.tgz", + "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.48.1", - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1", + "@typescript-eslint/scope-manager": "8.50.1", + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/typescript-estree": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1", "debug": "^4.3.4" }, "engines": { @@ -1391,14 +1403,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz", - "integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.1.tgz", + "integrity": "sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.48.1", - "@typescript-eslint/types": "^8.48.1", + "@typescript-eslint/tsconfig-utils": "^8.50.1", + "@typescript-eslint/types": "^8.50.1", "debug": "^4.3.4" }, "engines": { @@ -1413,14 +1425,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz", - "integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.1.tgz", + "integrity": "sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1" + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1431,9 +1443,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz", - "integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.1.tgz", + "integrity": "sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw==", "dev": true, "license": "MIT", "engines": { @@ -1448,15 +1460,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.1.tgz", - "integrity": "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.1.tgz", + "integrity": "sha512-7J3bf022QZE42tYMO6SL+6lTPKFk/WphhRPe9Tw/el+cEwzLz1Jjz2PX3GtGQVxooLDKeMVmMt7fWpYRdG5Etg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1", - "@typescript-eslint/utils": "8.48.1", + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/typescript-estree": "8.50.1", + "@typescript-eslint/utils": "8.50.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1473,9 +1485,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", - "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.1.tgz", + "integrity": "sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==", "dev": true, "license": "MIT", "engines": { @@ -1487,16 +1499,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz", - "integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.1.tgz", + "integrity": "sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.48.1", - "@typescript-eslint/tsconfig-utils": "8.48.1", - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1", + "@typescript-eslint/project-service": "8.50.1", + "@typescript-eslint/tsconfig-utils": "8.50.1", + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", @@ -1541,16 +1553,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.1.tgz", - "integrity": "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.1.tgz", + "integrity": "sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.48.1", - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1" + "@typescript-eslint/scope-manager": "8.50.1", + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/typescript-estree": "8.50.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1565,13 +1577,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz", - "integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.1.tgz", + "integrity": "sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/types": "8.50.1", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -2204,9 +2216,9 @@ "license": "MIT" }, "node_modules/devalue": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.5.0.tgz", - "integrity": "sha512-69sM5yrHfFLJt0AZ9QqZXGCPfJ7fQjvpln3Rq5+PS03LD32Ost1Q9N+eEnaQwGRIriKkMImXD56ocjQmfjbV3w==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.1.tgz", + "integrity": "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A==", "dev": true, "license": "MIT" }, @@ -2231,9 +2243,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2244,32 +2256,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" } }, "node_modules/escape-string-regexp": { @@ -2286,9 +2298,9 @@ } }, "node_modules/eslint": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", - "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", "peer": true, @@ -2299,7 +2311,7 @@ "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.1", + "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -2439,6 +2451,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/esm-env": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", @@ -2531,9 +2556,9 @@ } }, "node_modules/expect-type": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", - "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2696,13 +2721,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -3517,9 +3535,9 @@ } }, "node_modules/rollup": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", - "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", "dev": true, "license": "MIT", "dependencies": { @@ -3533,28 +3551,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.53.3", - "@rollup/rollup-android-arm64": "4.53.3", - "@rollup/rollup-darwin-arm64": "4.53.3", - "@rollup/rollup-darwin-x64": "4.53.3", - "@rollup/rollup-freebsd-arm64": "4.53.3", - "@rollup/rollup-freebsd-x64": "4.53.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", - "@rollup/rollup-linux-arm-musleabihf": "4.53.3", - "@rollup/rollup-linux-arm64-gnu": "4.53.3", - "@rollup/rollup-linux-arm64-musl": "4.53.3", - "@rollup/rollup-linux-loong64-gnu": "4.53.3", - "@rollup/rollup-linux-ppc64-gnu": "4.53.3", - "@rollup/rollup-linux-riscv64-gnu": "4.53.3", - "@rollup/rollup-linux-riscv64-musl": "4.53.3", - "@rollup/rollup-linux-s390x-gnu": "4.53.3", - "@rollup/rollup-linux-x64-gnu": "4.53.3", - "@rollup/rollup-linux-x64-musl": "4.53.3", - "@rollup/rollup-openharmony-arm64": "4.53.3", - "@rollup/rollup-win32-arm64-msvc": "4.53.3", - "@rollup/rollup-win32-ia32-msvc": "4.53.3", - "@rollup/rollup-win32-x64-gnu": "4.53.3", - "@rollup/rollup-win32-x64-msvc": "4.53.3", + "@rollup/rollup-android-arm-eabi": "4.54.0", + "@rollup/rollup-android-arm64": "4.54.0", + "@rollup/rollup-darwin-arm64": "4.54.0", + "@rollup/rollup-darwin-x64": "4.54.0", + "@rollup/rollup-freebsd-arm64": "4.54.0", + "@rollup/rollup-freebsd-x64": "4.54.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", + "@rollup/rollup-linux-arm-musleabihf": "4.54.0", + "@rollup/rollup-linux-arm64-gnu": "4.54.0", + "@rollup/rollup-linux-arm64-musl": "4.54.0", + "@rollup/rollup-linux-loong64-gnu": "4.54.0", + "@rollup/rollup-linux-ppc64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-musl": "4.54.0", + "@rollup/rollup-linux-s390x-gnu": "4.54.0", + "@rollup/rollup-linux-x64-gnu": "4.54.0", + "@rollup/rollup-linux-x64-musl": "4.54.0", + "@rollup/rollup-openharmony-arm64": "4.54.0", + "@rollup/rollup-win32-arm64-msvc": "4.54.0", + "@rollup/rollup-win32-ia32-msvc": "4.54.0", + "@rollup/rollup-win32-x64-gnu": "4.54.0", + "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" } }, @@ -3723,9 +3741,9 @@ } }, "node_modules/svelte": { - "version": "5.45.6", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.45.6.tgz", - "integrity": "sha512-V3aVXthzPyPt1UB1wLEoXnEXpwPsvs7NHrR0xkCor8c11v71VqBj477MClqPZYyrcXrAH21sNGhOj9FJvSwXfQ==", + "version": "5.46.1", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.46.1.tgz", + "integrity": "sha512-ynjfCHD3nP2el70kN5Pmg37sSi0EjOm9FgHYQdC4giWG/hzO3AatzXXJJgP305uIhGQxSufJLuYWtkY8uK/8RA==", "dev": true, "license": "MIT", "peer": true, @@ -3879,9 +3897,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.3.0.tgz", + "integrity": "sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg==", "dev": true, "license": "MIT", "engines": { @@ -3920,16 +3938,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.48.1.tgz", - "integrity": "sha512-FbOKN1fqNoXp1hIl5KYpObVrp0mCn+CLgn479nmu2IsRMrx2vyv74MmsBLVlhg8qVwNFGbXSp8fh1zp8pEoC2A==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.50.1.tgz", + "integrity": "sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.48.1", - "@typescript-eslint/parser": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1", - "@typescript-eslint/utils": "8.48.1" + "@typescript-eslint/eslint-plugin": "8.50.1", + "@typescript-eslint/parser": "8.50.1", + "@typescript-eslint/typescript-estree": "8.50.1", + "@typescript-eslint/utils": "8.50.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3982,14 +4000,14 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.2.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz", - "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", + "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "esbuild": "^0.25.0", + "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -4246,6 +4264,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", From 9ac3e165fa01d7fa66f0492af30c40275c92098e Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 3 Jan 2026 12:18:07 +0700 Subject: [PATCH 057/102] Readd missing go dep --- go.mod | 3 ++- go.sum | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index e035147..3cdc587 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.24.6 require ( github.com/JGLTechnologies/gin-rate-limit v1.5.6 github.com/emersion/go-smtp v0.24.0 + github.com/getkin/kin-openapi v0.133.0 github.com/gin-gonic/gin v1.11.0 github.com/google/uuid v1.6.0 github.com/oapi-codegen/runtime v1.1.2 @@ -15,6 +16,7 @@ require ( ) require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.14.2 // indirect github.com/bytedance/sonic/loader v0.4.0 // indirect @@ -24,7 +26,6 @@ require ( github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect github.com/gabriel-vasile/mimetype v1.4.11 // indirect - github.com/getkin/kin-openapi v0.133.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-openapi/jsonpointer v0.22.2 // indirect github.com/go-openapi/swag/jsonname v0.25.1 // indirect diff --git a/go.sum b/go.sum index 0d141fe..9c5081a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,9 @@ github.com/JGLTechnologies/gin-rate-limit v1.5.6 h1:BrL2wXrF7SSqmB88YTGFVKMGVcjURMUeKqwQrlmzweI= github.com/JGLTechnologies/gin-rate-limit v1.5.6/go.mod h1:fwUuBegxLKm8+/4ST0zDFssRFTFaVZ7bH3ApK7iNZww= +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -98,6 +102,7 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -165,6 +170,7 @@ github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU= github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= From d1e48b9885e7b850c714ec496007f49ea2f27be6 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 5 Jan 2026 01:15:53 +0000 Subject: [PATCH 058/102] chore(deps): lock file maintenance --- web/package-lock.json | 152 +++++++++++++++++++++--------------------- 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 2edd780..6f88380 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -477,9 +477,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1338,20 +1338,20 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.50.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.1.tgz", - "integrity": "sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.51.0.tgz", + "integrity": "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.50.1", - "@typescript-eslint/type-utils": "8.50.1", - "@typescript-eslint/utils": "8.50.1", - "@typescript-eslint/visitor-keys": "8.50.1", + "@typescript-eslint/scope-manager": "8.51.0", + "@typescript-eslint/type-utils": "8.51.0", + "@typescript-eslint/utils": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1361,7 +1361,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.50.1", + "@typescript-eslint/parser": "^8.51.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -1377,17 +1377,17 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.50.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.1.tgz", - "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.51.0.tgz", + "integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.50.1", - "@typescript-eslint/types": "8.50.1", - "@typescript-eslint/typescript-estree": "8.50.1", - "@typescript-eslint/visitor-keys": "8.50.1", + "@typescript-eslint/scope-manager": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0", "debug": "^4.3.4" }, "engines": { @@ -1403,14 +1403,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.50.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.1.tgz", - "integrity": "sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.51.0.tgz", + "integrity": "sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.50.1", - "@typescript-eslint/types": "^8.50.1", + "@typescript-eslint/tsconfig-utils": "^8.51.0", + "@typescript-eslint/types": "^8.51.0", "debug": "^4.3.4" }, "engines": { @@ -1425,14 +1425,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.50.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.1.tgz", - "integrity": "sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.51.0.tgz", + "integrity": "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.1", - "@typescript-eslint/visitor-keys": "8.50.1" + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1443,9 +1443,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.50.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.1.tgz", - "integrity": "sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.51.0.tgz", + "integrity": "sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw==", "dev": true, "license": "MIT", "engines": { @@ -1460,17 +1460,17 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.50.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.1.tgz", - "integrity": "sha512-7J3bf022QZE42tYMO6SL+6lTPKFk/WphhRPe9Tw/el+cEwzLz1Jjz2PX3GtGQVxooLDKeMVmMt7fWpYRdG5Etg==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.51.0.tgz", + "integrity": "sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.1", - "@typescript-eslint/typescript-estree": "8.50.1", - "@typescript-eslint/utils": "8.50.1", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0", + "@typescript-eslint/utils": "8.51.0", "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1485,9 +1485,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.50.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.1.tgz", - "integrity": "sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.51.0.tgz", + "integrity": "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==", "dev": true, "license": "MIT", "engines": { @@ -1499,21 +1499,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.50.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.1.tgz", - "integrity": "sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.51.0.tgz", + "integrity": "sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.50.1", - "@typescript-eslint/tsconfig-utils": "8.50.1", - "@typescript-eslint/types": "8.50.1", - "@typescript-eslint/visitor-keys": "8.50.1", + "@typescript-eslint/project-service": "8.51.0", + "@typescript-eslint/tsconfig-utils": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1553,16 +1553,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.50.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.1.tgz", - "integrity": "sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.51.0.tgz", + "integrity": "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.50.1", - "@typescript-eslint/types": "8.50.1", - "@typescript-eslint/typescript-estree": "8.50.1" + "@typescript-eslint/scope-manager": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1577,13 +1577,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.50.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.1.tgz", - "integrity": "sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.51.0.tgz", + "integrity": "sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/types": "8.51.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -1966,9 +1966,9 @@ } }, "node_modules/check-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", "dev": true, "license": "MIT", "engines": { @@ -2490,9 +2490,9 @@ } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -3897,9 +3897,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.3.0.tgz", - "integrity": "sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { @@ -3938,16 +3938,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.50.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.50.1.tgz", - "integrity": "sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.51.0.tgz", + "integrity": "sha512-jh8ZuM5oEh2PSdyQG9YAEM1TCGuWenLSuSUhf/irbVUNW9O5FhbFVONviN2TgMTBnUmyHv7E56rYnfLZK6TkiA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.50.1", - "@typescript-eslint/parser": "8.50.1", - "@typescript-eslint/typescript-estree": "8.50.1", - "@typescript-eslint/utils": "8.50.1" + "@typescript-eslint/eslint-plugin": "8.51.0", + "@typescript-eslint/parser": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0", + "@typescript-eslint/utils": "8.51.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" From e6746a1382a1905d6bd6d9d38e5d9215a92eb997 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 12 Jan 2026 17:13:54 +0000 Subject: [PATCH 059/102] chore(deps): update module golang.org/x/net to v0.49.0 --- go.mod | 15 +++++++-------- go.sum | 30 ++++++++++++------------------ 2 files changed, 19 insertions(+), 26 deletions(-) diff --git a/go.mod b/go.mod index 3cdc587..04c9d76 100644 --- a/go.mod +++ b/go.mod @@ -5,18 +5,16 @@ go 1.24.6 require ( github.com/JGLTechnologies/gin-rate-limit v1.5.6 github.com/emersion/go-smtp v0.24.0 - github.com/getkin/kin-openapi v0.133.0 github.com/gin-gonic/gin v1.11.0 github.com/google/uuid v1.6.0 github.com/oapi-codegen/runtime v1.1.2 - golang.org/x/net v0.48.0 + golang.org/x/net v0.49.0 gorm.io/driver/postgres v1.6.0 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.31.1 ) require ( - github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.14.2 // indirect github.com/bytedance/sonic/loader v0.4.0 // indirect @@ -26,6 +24,7 @@ require ( github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect github.com/gabriel-vasile/mimetype v1.4.11 // indirect + github.com/getkin/kin-openapi v0.133.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-openapi/jsonpointer v0.22.2 // indirect github.com/go-openapi/swag/jsonname v0.25.1 // indirect @@ -66,12 +65,12 @@ require ( github.com/woodsbury/decimal128 v1.4.0 // indirect go.uber.org/mock v0.6.0 // indirect golang.org/x/arch v0.23.0 // indirect - golang.org/x/crypto v0.46.0 // indirect - golang.org/x/mod v0.30.0 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/mod v0.31.0 // indirect golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.39.0 // indirect - golang.org/x/text v0.32.0 // indirect - golang.org/x/tools v0.39.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/tools v0.40.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 9c5081a..a2fbfe7 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,5 @@ github.com/JGLTechnologies/gin-rate-limit v1.5.6 h1:BrL2wXrF7SSqmB88YTGFVKMGVcjURMUeKqwQrlmzweI= github.com/JGLTechnologies/gin-rate-limit v1.5.6/go.mod h1:fwUuBegxLKm8+/4ST0zDFssRFTFaVZ7bH3ApK7iNZww= -github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= -github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= -github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= -github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -102,7 +98,6 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -170,7 +165,6 @@ github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU= github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg= -github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -201,11 +195,11 @@ golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -213,8 +207,8 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -235,23 +229,23 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From a6efd7710e09a4c5ed27ff1a0646def81ab3c2fd Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 3 Jan 2026 07:14:34 +0000 Subject: [PATCH 060/102] chore(deps): update module github.com/quic-go/quic-go to v0.57.0 [security] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 04c9d76..14f4c0d 100644 --- a/go.mod +++ b/go.mod @@ -54,8 +54,8 @@ require ( github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect - github.com/quic-go/qpack v0.5.1 // indirect - github.com/quic-go/quic-go v0.56.0 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.57.0 // indirect github.com/redis/go-redis/v9 v9.16.0 // indirect github.com/speakeasy-api/jsonpath v0.6.0 // indirect github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect diff --git a/go.sum b/go.sum index a2fbfe7..e17672d 100644 --- a/go.sum +++ b/go.sum @@ -151,10 +151,10 @@ github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= -github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY= -github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE= +github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4= github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= From 035e864de46e24dbd23592b457909e8c7757319b Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 24 Jan 2026 18:19:06 +0800 Subject: [PATCH 061/102] Update go modules --- go.mod | 27 ++++++++++++++------------- go.sum | 31 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index 14f4c0d..e9da3d6 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.24.6 require ( github.com/JGLTechnologies/gin-rate-limit v1.5.6 github.com/emersion/go-smtp v0.24.0 + github.com/getkin/kin-openapi v0.133.0 github.com/gin-gonic/gin v1.11.0 github.com/google/uuid v1.6.0 github.com/oapi-codegen/runtime v1.1.2 @@ -15,27 +16,27 @@ require ( ) require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/bytedance/gopkg v0.1.3 // indirect - github.com/bytedance/sonic v1.14.2 // indirect - github.com/bytedance/sonic/loader v0.4.0 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect - github.com/gabriel-vasile/mimetype v1.4.11 // indirect - github.com/getkin/kin-openapi v0.133.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/gin-contrib/sse v1.1.0 // indirect - github.com/go-openapi/jsonpointer v0.22.2 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.28.0 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/goccy/go-json v0.10.5 // indirect - github.com/goccy/go-yaml v1.18.0 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.7.6 // indirect + github.com/jackc/pgx/v5 v5.8.0 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect @@ -45,7 +46,7 @@ require ( github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-sqlite3 v1.14.32 // indirect + github.com/mattn/go-sqlite3 v1.14.33 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect @@ -55,8 +56,8 @@ require ( github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/quic-go/qpack v0.6.0 // indirect - github.com/quic-go/quic-go v0.57.0 // indirect - github.com/redis/go-redis/v9 v9.16.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/redis/go-redis/v9 v9.17.2 // indirect github.com/speakeasy-api/jsonpath v0.6.0 // indirect github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect @@ -71,7 +72,7 @@ require ( golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/tools v0.40.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e17672d..96ea7bc 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,9 @@ github.com/JGLTechnologies/gin-rate-limit v1.5.6 h1:BrL2wXrF7SSqmB88YTGFVKMGVcjURMUeKqwQrlmzweI= github.com/JGLTechnologies/gin-rate-limit v1.5.6/go.mod h1:fwUuBegxLKm8+/4ST0zDFssRFTFaVZ7bH3ApK7iNZww= +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -8,8 +12,12 @@ github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -34,6 +42,8 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik= github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= @@ -42,8 +52,12 @@ github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= github.com/go-openapi/jsonpointer v0.22.2 h1:JDQEe4B9j6K3tQ7HQQTZfjR59IURhjjLxet2FB4KHyg= github.com/go-openapi/jsonpointer v0.22.2/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo= +github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= @@ -54,6 +68,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= @@ -61,6 +77,8 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -88,6 +106,8 @@ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7Ulw github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= +github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= @@ -98,6 +118,7 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -115,6 +136,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= +github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -155,8 +178,12 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE= github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4= github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= +github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= +github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= @@ -165,6 +192,7 @@ github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU= github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -239,6 +267,7 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -260,6 +289,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= From ac9b567025d3cad6c5449b66b7c270c844433d5f Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 24 Jan 2026 19:18:26 +0800 Subject: [PATCH 062/102] web: Format code files --- .../lib/components/AuthenticationCard.svelte | 649 +++++++++++------- web/src/lib/components/BlacklistCard.svelte | 24 +- .../lib/components/ContentAnalysisCard.svelte | 43 +- web/src/lib/components/DnsRecordsCard.svelte | 71 +- web/src/lib/components/EmailPathCard.svelte | 16 +- web/src/lib/components/GradeDisplay.svelte | 5 +- .../lib/components/HeaderAnalysisCard.svelte | 253 +++++-- .../lib/components/MxRecordsDisplay.svelte | 1 + web/src/lib/components/ScoreCard.svelte | 36 +- .../lib/components/SpamAssassinCard.svelte | 31 +- .../lib/components/SpfRecordsDisplay.svelte | 53 +- web/src/lib/components/SummaryCard.svelte | 30 +- web/src/lib/components/index.ts | 38 +- web/src/lib/stores/theme.ts | 23 +- web/src/routes/+error.svelte | 6 +- web/src/routes/+layout.svelte | 6 +- web/src/routes/+page.svelte | 3 +- web/src/routes/blacklist/+page.svelte | 27 +- web/src/routes/blacklist/[ip]/+page.svelte | 61 +- web/src/routes/domain/+page.svelte | 23 +- web/src/routes/domain/[domain]/+page.svelte | 21 +- web/src/routes/test/[test]/+page.svelte | 29 +- 22 files changed, 977 insertions(+), 472 deletions(-) diff --git a/web/src/lib/components/AuthenticationCard.svelte b/web/src/lib/components/AuthenticationCard.svelte index 8f22eac..097dff1 100644 --- a/web/src/lib/components/AuthenticationCard.svelte +++ b/web/src/lib/components/AuthenticationCard.svelte @@ -96,281 +96,442 @@
- - {#if authentication.iprev} -
-
- -
- IP Reverse DNS - - {authentication.iprev.result} - - {#if authentication.iprev.ip} -
- IP Address: - {authentication.iprev.ip} -
- {/if} - {#if authentication.iprev.hostname} -
- Hostname: - {authentication.iprev.hostname} -
- {/if} - {#if authentication.iprev.details} -
{authentication.iprev.details}
- {/if} -
+ + {#if authentication.iprev} +
+
+ +
+ IP Reverse DNS + + {authentication.iprev.result} + + {#if authentication.iprev.ip} +
+ IP Address: + {authentication.iprev.ip} +
+ {/if} + {#if authentication.iprev.hostname} +
+ Hostname: + {authentication.iprev.hostname} +
+ {/if} + {#if authentication.iprev.details} +
{authentication.iprev.details}
+ {/if}
- {/if} - - -
-
- {#if authentication.spf} - -
- SPF - - {authentication.spf.result} - - {#if authentication.spf.domain} -
- Domain: - {authentication.spf.domain} -
- {/if} - {#if authentication.spf.details} -
{authentication.spf.details}
- {/if} -
- {:else} - -
- SPF - - {getAuthResultText('missing')} - -
SPF record is required for proper email authentication
-
- {/if} -
+ {/if} - -
- {#if authentication.dkim && authentication.dkim.length > 0} - {#each authentication.dkim as dkim, i} -
0}> - -
- DKIM{authentication.dkim.length > 1 ? ` #${i + 1}` : ''} - - {dkim.result} - - {#if dkim.domain} -
- Domain: - {dkim.domain} -
- {/if} - {#if dkim.selector} -
- Selector: - {dkim.selector} -
- {/if} - {#if dkim.details} -
{dkim.details}
- {/if} + +
+
+ {#if authentication.spf} + +
+ SPF + + {authentication.spf.result} + + {#if authentication.spf.domain} +
+ Domain: + {authentication.spf.domain}
-
- {/each} + {/if} + {#if authentication.spf.details} +
{authentication.spf.details}
+ {/if} +
{:else} -
- -
- DKIM - - {getAuthResultText('missing')} - -
DKIM signature is required for proper email authentication
+ +
+ SPF + + {getAuthResultText("missing")} + +
+ SPF record is required for proper email authentication
{/if}
+
- - {#if authentication.x_google_dkim} -
-
- + +
+ {#if authentication.dkim && authentication.dkim.length > 0} + {#each authentication.dkim as dkim, i} +
0}> +
- X-Google-DKIM - - - {authentication.x_google_dkim.result} + DKIM{authentication.dkim.length > 1 ? ` #${i + 1}` : ""} + + {dkim.result} - {#if authentication.x_google_dkim.domain} + {#if dkim.domain}
Domain: - {authentication.x_google_dkim.domain} + {dkim.domain}
{/if} - {#if authentication.x_google_dkim.selector} + {#if dkim.selector}
Selector: - {authentication.x_google_dkim.selector} + {dkim.selector}
{/if} - {#if authentication.x_google_dkim.details} -
{authentication.x_google_dkim.details}
+ {#if dkim.details} +
{dkim.details}
{/if}
-
- {/if} - - - {#if authentication.x_aligned_from} -
-
- -
- X-Aligned-From - - - {authentication.x_aligned_from.result} - - {#if authentication.x_aligned_from.domain} -
- Domain: - {authentication.x_aligned_from.domain} -
- {/if} - {#if authentication.x_aligned_from.details} -
{authentication.x_aligned_from.details}
- {/if} -
-
-
- {/if} - - -
+ {/each} + {:else}
- {#if authentication.dmarc} - -
- DMARC - - {authentication.dmarc.result} - - {#if authentication.dmarc.domain} -
- Domain: - {authentication.dmarc.domain} -
- {/if} - {#snippet DMARCPolicy(policy: string)} -
- Policy: - - {policy} - -
- {/snippet} - {#if authentication.dmarc.result != "none"} - {#if authentication.dmarc.details && authentication.dmarc.details.indexOf("policy.published-domain-policy=") > 0} - {@const policy = authentication.dmarc.details.replace(/^.*policy.published-domain-policy=([^\s]+).*$/, "$1")} - {@render DMARCPolicy(policy)} - {:else if authentication.dmarc.domain && dnsResults?.dmarc_record?.policy} - {@render DMARCPolicy(dnsResults.dmarc_record.policy)} - {/if} - {/if} - {#if authentication.dmarc.details} -
{authentication.dmarc.details}
- {/if} + +
+ DKIM + + {getAuthResultText("missing")} + +
+ DKIM signature is required for proper email authentication
- {:else} - -
- DMARC - - {getAuthResultText('missing')} - -
DMARC policy is required for proper email authentication
-
- {/if} +
+
+ {/if} +
+ + + {#if authentication.x_google_dkim} +
+
+ +
+ X-Google-DKIM + + + {authentication.x_google_dkim.result} + + {#if authentication.x_google_dkim.domain} +
+ Domain: + {authentication.x_google_dkim.domain} +
+ {/if} + {#if authentication.x_google_dkim.selector} +
+ Selector: + {authentication.x_google_dkim.selector} +
+ {/if} + {#if authentication.x_google_dkim.details} +
{authentication.x_google_dkim
+                                    .details}
+ {/if} +
+ {/if} - -
+ + {#if authentication.x_aligned_from} +
- {#if authentication.bimi && authentication.bimi.result != "none"} - -
- BIMI - - {authentication.bimi.result} - - {#if authentication.bimi.details} -
{authentication.bimi.details}
- {/if} -
- {:else if authentication.bimi && authentication.bimi.result == "none"} - -
- BIMI - - NONE - -
Brand Indicators for Message Identification
- {#if authentication.bimi.details} -
{authentication.bimi.details}
- {/if} -
- {:else} - -
- BIMI - - Optional - -
Brand Indicators for Message Identification (optional enhancement)
-
- {/if} -
-
- - - {#if authentication.arc} -
-
- -
- ARC - - {authentication.arc.result} - - {#if authentication.arc.chain_length} -
Chain length: {authentication.arc.chain_length}
- {/if} - {#if authentication.arc.details} -
{authentication.arc.details}
- {/if} -
+ +
+ X-Aligned-From + + + {authentication.x_aligned_from.result} + + {#if authentication.x_aligned_from.domain} +
+ Domain: + {authentication.x_aligned_from.domain} +
+ {/if} + {#if authentication.x_aligned_from.details} +
{authentication.x_aligned_from
+                                    .details}
+ {/if}
- {/if} +
+ {/if} + + +
+
+ {#if authentication.dmarc} + +
+ DMARC + + {authentication.dmarc.result} + + {#if authentication.dmarc.domain} +
+ Domain: + {authentication.dmarc.domain} +
+ {/if} + {#snippet DMARCPolicy(policy: string)} +
+ Policy: + + {policy} + +
+ {/snippet} + {#if authentication.dmarc.result != "none"} + {#if authentication.dmarc.details && authentication.dmarc.details.indexOf("policy.published-domain-policy=") > 0} + {@const policy = authentication.dmarc.details.replace( + /^.*policy.published-domain-policy=([^\s]+).*$/, + "$1", + )} + {@render DMARCPolicy(policy)} + {:else if authentication.dmarc.domain && dnsResults?.dmarc_record?.policy} + {@render DMARCPolicy(dnsResults.dmarc_record.policy)} + {/if} + {/if} + {#if authentication.dmarc.details} +
{authentication.dmarc.details}
+ {/if} +
+ {:else} + +
+ DMARC + + {getAuthResultText("missing")} + +
+ DMARC policy is required for proper email authentication +
+
+ {/if} +
+
+ + +
+
+ {#if authentication.bimi && authentication.bimi.result != "none"} + +
+ BIMI + + {authentication.bimi.result} + + {#if authentication.bimi.details} +
{authentication.bimi.details}
+ {/if} +
+ {:else if authentication.bimi && authentication.bimi.result == "none"} + +
+ BIMI + NONE +
+ Brand Indicators for Message Identification +
+ {#if authentication.bimi.details} +
{authentication.bimi.details}
+ {/if} +
+ {:else} + +
+ BIMI + Optional +
+ Brand Indicators for Message Identification (optional enhancement) +
+
+ {/if} +
+
+ + + {#if authentication.arc} +
+
+ +
+ ARC + + {authentication.arc.result} + + {#if authentication.arc.chain_length} +
+ Chain length: {authentication.arc.chain_length} +
+ {/if} + {#if authentication.arc.details} +
{authentication.arc.details}
+ {/if} +
+
+
+ {/if}
diff --git a/web/src/lib/components/BlacklistCard.svelte b/web/src/lib/components/BlacklistCard.svelte index bb0a160..7f9b7f2 100644 --- a/web/src/lib/components/BlacklistCard.svelte +++ b/web/src/lib/components/BlacklistCard.svelte @@ -2,8 +2,8 @@ import type { BlacklistCheck, ReceivedHop } from "$lib/api/types.gen"; import { getScoreColorClass } from "$lib/score"; import { theme } from "$lib/stores/theme"; - import GradeDisplay from "./GradeDisplay.svelte"; import EmailPathCard from "./EmailPathCard.svelte"; + import GradeDisplay from "./GradeDisplay.svelte"; interface Props { blacklists: Record; @@ -16,11 +16,7 @@
-
+

@@ -54,9 +50,19 @@ {#each checks as check} - - - {check.error ? 'Error' : (check.listed ? 'Listed' : 'Clean')} + + + {check.error + ? "Error" + : check.listed + ? "Listed" + : "Clean"} {check.rbl} diff --git a/web/src/lib/components/ContentAnalysisCard.svelte b/web/src/lib/components/ContentAnalysisCard.svelte index 87cfd5e..51c4e5b 100644 --- a/web/src/lib/components/ContentAnalysisCard.svelte +++ b/web/src/lib/components/ContentAnalysisCard.svelte @@ -36,16 +36,28 @@
- + HTML Part
- + Plaintext Part
- {#if typeof contentAnalysis.has_unsubscribe_link === 'boolean'} + {#if typeof contentAnalysis.has_unsubscribe_link === "boolean"}
- + Unsubscribe Link
{/if} @@ -74,7 +86,14 @@
Content Issues
{#each contentAnalysis.html_issues as issue} -
+
{issue.type} @@ -118,11 +137,17 @@ {/if} - + {link.status} - {link.http_code || '-'} + {link.http_code || "-"} {/each} @@ -146,11 +171,11 @@ {#each contentAnalysis.images as image} - {image.src || '-'} + {image.src || "-"} {#if image.has_alt} - {image.alt_text || 'Present'} + {image.alt_text || "Present"} {:else} Missing diff --git a/web/src/lib/components/DnsRecordsCard.svelte b/web/src/lib/components/DnsRecordsCard.svelte index 337f7c1..b7997b0 100644 --- a/web/src/lib/components/DnsRecordsCard.svelte +++ b/web/src/lib/components/DnsRecordsCard.svelte @@ -1,15 +1,15 @@ - + {#if grade} {grade} {:else} diff --git a/web/src/lib/components/HeaderAnalysisCard.svelte b/web/src/lib/components/HeaderAnalysisCard.svelte index e0ecb58..b26b492 100644 --- a/web/src/lib/components/HeaderAnalysisCard.svelte +++ b/web/src/lib/components/HeaderAnalysisCard.svelte @@ -1,5 +1,5 @@ - +
diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index d3f17a3..7c23d10 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -1,8 +1,9 @@ - {report ? `Test${report.dns_results ? ` of ${report.dns_results.from_domain}` : ''} ${report.test_id?.slice(0, 7) || ''}` : (test ? `Test ${test.id.slice(0, 7)}` : "Loading...")} - happyDeliver + + {report + ? `Test${report.dns_results ? ` of ${report.dns_results.from_domain}` : ""} ${report.test_id?.slice(0, 7) || ""}` + : test + ? `Test ${test.id.slice(0, 7)}` + : "Loading..."} - happyDeliver +
From 6b4ca126b07a86d18d7f469d0af57eaceab06106 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 24 Jan 2026 21:23:40 +0800 Subject: [PATCH 063/102] Add colors to css --- web/src/app.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/web/src/app.css b/web/src/app.css index 1472994..dca80a5 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -1,6 +1,9 @@ :root { --bs-primary: #1cb487; --bs-primary-rgb: 28, 180, 135; + --bs-link-color-rgb: 28, 180, 135; + --bs-link-hover-color-rgb: 17, 112, 84; + --bs-tertiary-bg: #e7e8e8; } body { @@ -8,6 +11,10 @@ body { -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } +.bg-tertiary { + background-color: var(--bs-tertiary-bg); +} + /* Animations */ @keyframes fadeIn { from { From 5453c0942057322e0cb674167ef3c4cd0b107116 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 24 Jan 2026 21:24:20 +0800 Subject: [PATCH 064/102] Use slimmer footer by default Bug: https://github.com/happyDomain/happydeliver/issues/6 --- web/src/routes/+layout.svelte | 43 +++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index 652fb88..0d3fb23 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -55,7 +55,26 @@ {@render children?.()} -
+ + +
@@ -144,6 +163,27 @@ border-top: 3px solid #9332bb; } + footer a { + text-decoration: none; + } + + .footer-nav { + list-style: none; + padding: 0; + margin: 0; + gap: 0; + } + + .footer-nav li { + display: flex; + align-items: center; + } + + .footer-nav li:not(:last-child)::after { + content: "|"; + margin: 0 0.5rem; + } + .footer-links { list-style: none; padding: 0; @@ -155,7 +195,6 @@ .footer-links a { color: rgba(255, 255, 255, 0.7); - text-decoration: none; transition: color 0.3s; } From 64ba6932f7815ee6dd4800feb12968f8b783a3c6 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 24 Jan 2026 21:35:30 +0800 Subject: [PATCH 065/102] Use io instead of deprecated ioutil --- web/routes.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/web/routes.go b/web/routes.go index 23a9bbb..ea9929d 100644 --- a/web/routes.go +++ b/web/routes.go @@ -27,7 +27,6 @@ import ( "fmt" "io" "io/fs" - "io/ioutil" "log" "net/http" "net/url" @@ -140,7 +139,7 @@ func serveOrReverse(forced_url string, cfg *config.Config) gin.HandlerFunc { } } - v, _ := ioutil.ReadAll(resp.Body) + v, _ := io.ReadAll(resp.Body) v2 := strings.Replace(strings.Replace(string(v), "", `{{ .Head }}`, 1), "", "{{ .Body }}", 1) @@ -167,7 +166,7 @@ func serveOrReverse(forced_url string, cfg *config.Config) gin.HandlerFunc { if indexTpl == nil { // Create template from file f, _ := Assets.Open("index.html") - v, _ := ioutil.ReadAll(f) + v, _ := io.ReadAll(f) v2 := strings.Replace(strings.Replace(string(v), "", `{{ .Head }}`, 1), "", "{{ .Body }}", 1) From 8a10eef2f53d39f0095b645844e2f3d9f73e9038 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 24 Jan 2026 21:42:35 +0800 Subject: [PATCH 066/102] Add custom logo URL configuration option Bug: https://github.com/happyDomain/happydeliver/issues/6 --- internal/config/cli.go | 1 + internal/config/config.go | 1 + web/routes.go | 4 ++++ web/src/lib/stores/config.ts | 1 + web/src/routes/+layout.svelte | 9 +++++++-- 5 files changed, 14 insertions(+), 2 deletions(-) diff --git a/internal/config/cli.go b/internal/config/cli.go index 2a61bad..3accc99 100644 --- a/internal/config/cli.go +++ b/internal/config/cli.go @@ -41,6 +41,7 @@ func declareFlags(o *Config) { flag.DurationVar(&o.ReportRetention, "report-retention", o.ReportRetention, "How long to keep reports (e.g., 720h, 30d). 0 = keep forever") flag.UintVar(&o.RateLimit, "rate-limit", o.RateLimit, "API rate limit (requests per second per IP)") flag.Var(&URL{&o.SurveyURL}, "survey-url", "URL for user feedback survey") + flag.StringVar(&o.CustomLogoURL, "custom-logo-url", o.CustomLogoURL, "URL for custom logo image in the web UI") // Others flags are declared in some other files likes sources, storages, ... when they need specials configurations } diff --git a/internal/config/config.go b/internal/config/config.go index be5e63a..4a335c9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -44,6 +44,7 @@ type Config struct { ReportRetention time.Duration // How long to keep reports. 0 = keep forever RateLimit uint // API rate limit (requests per second per IP) SurveyURL url.URL // URL for user feedback survey + CustomLogoURL string // URL for custom logo image in the web UI } // DatabaseConfig contains database connection settings diff --git a/web/routes.go b/web/routes.go index ea9929d..876954c 100644 --- a/web/routes.go +++ b/web/routes.go @@ -66,6 +66,10 @@ func DeclareRoutes(cfg *config.Config, router *gin.Engine) { appConfig["rbls"] = cfg.Analysis.RBLs } + if cfg.CustomLogoURL != "" { + appConfig["custom_logo_url"] = cfg.CustomLogoURL + } + if appcfg, err := json.MarshalIndent(appConfig, "", " "); err != nil { log.Println("Unable to generate JSON config to inject in web application") } else { diff --git a/web/src/lib/stores/config.ts b/web/src/lib/stores/config.ts index 8a978e0..87662ba 100644 --- a/web/src/lib/stores/config.ts +++ b/web/src/lib/stores/config.ts @@ -24,6 +24,7 @@ import { writable } from "svelte/store"; interface AppConfig { report_retention?: number; survey_url?: string; + custom_logo_url?: string; } const defaultConfig: AppConfig = { diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index 0d3fb23..077f340 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -6,6 +6,7 @@ import favicon from "$lib/assets/favicon.svg"; import Logo from "$lib/components/Logo.svelte"; + import { appConfig } from "$lib/stores/config"; import { theme } from "$lib/stores/theme"; import { onMount } from "svelte"; @@ -32,8 +33,12 @@

diff --git a/web/src/lib/components/index.ts b/web/src/lib/components/index.ts index 3c76feb..d577399 100644 --- a/web/src/lib/components/index.ts +++ b/web/src/lib/components/index.ts @@ -19,6 +19,7 @@ export { default as PendingState } from "./PendingState.svelte"; export { default as PtrForwardRecordsDisplay } from "./PtrForwardRecordsDisplay.svelte"; export { default as PtrRecordsDisplay } from "./PtrRecordsDisplay.svelte"; export { default as ScoreCard } from "./ScoreCard.svelte"; +export { default as RspamdCard } from "./RspamdCard.svelte"; export { default as SpamAssassinCard } from "./SpamAssassinCard.svelte"; export { default as SpfRecordsDisplay } from "./SpfRecordsDisplay.svelte"; export { default as SummaryCard } from "./SummaryCard.svelte"; diff --git a/web/src/routes/test/[test]/+page.svelte b/web/src/routes/test/[test]/+page.svelte index bf44d20..c5add96 100644 --- a/web/src/routes/test/[test]/+page.svelte +++ b/web/src/routes/test/[test]/+page.svelte @@ -12,6 +12,7 @@ ErrorDisplay, HeaderAnalysisCard, PendingState, + RspamdCard, ScoreCard, SpamAssassinCard, SummaryCard, @@ -347,16 +348,19 @@
{/if} - - {#if report.spamassassin} + + {#if report.spamassassin || report.rspamd}
-
- -
+ {#if report.spamassassin} +
+ +
+ {/if} + {#if report.rspamd} +
+ +
+ {/if}
{/if} From a146940a65be5766f85f7c3c93168f4c1502ce96 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Mon, 23 Feb 2026 04:25:43 +0700 Subject: [PATCH 076/102] Improve FCrDNS UI: hide non-matching IPs when match exists Closes: https://github.com/happyDomain/happydeliver/issues/4 --- .../PtrForwardRecordsDisplay.svelte | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/web/src/lib/components/PtrForwardRecordsDisplay.svelte b/web/src/lib/components/PtrForwardRecordsDisplay.svelte index 77ce6c8..8ed723b 100644 --- a/web/src/lib/components/PtrForwardRecordsDisplay.svelte +++ b/web/src/lib/components/PtrForwardRecordsDisplay.svelte @@ -21,6 +21,11 @@ ); const hasForwardRecords = $derived(ptrForwardRecords && ptrForwardRecords.length > 0); + + let showDifferent = $state(false); + const differentCount = $derived( + ptrForwardRecords ? ptrForwardRecords.filter((ip) => ip !== senderIp).length : 0, + ); {#if ptrRecords && ptrRecords.length > 0} @@ -63,15 +68,31 @@
Forward Resolution (A/AAAA): {#each ptrForwardRecords as ip} -
- {#if senderIp && ip === senderIp} - Match - {:else} - Different - {/if} - {ip} -
+ {#if ip === senderIp || !fcrDnsIsValid || showDifferent} +
+ {#if senderIp && ip === senderIp} + Match + {:else} + Different + {/if} + {ip} +
+ {/if} {/each} + {#if fcrDnsIsValid && differentCount > 0} +
+ +
+ {/if}
{#if fcrDnsIsValid}
From b619ebf8c3696c08278e4406ef81882d42159de6 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 7 Mar 2026 11:38:09 +0700 Subject: [PATCH 077/102] Display permerror (SPF test) as error: text-danger --- web/src/lib/components/AuthenticationCard.svelte | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/src/lib/components/AuthenticationCard.svelte b/web/src/lib/components/AuthenticationCard.svelte index 097dff1..93531e7 100644 --- a/web/src/lib/components/AuthenticationCard.svelte +++ b/web/src/lib/components/AuthenticationCard.svelte @@ -19,6 +19,7 @@ case "domain_pass": case "orgdomain_pass": return "text-success"; + case "permerror": case "error": case "fail": case "missing": @@ -51,6 +52,7 @@ case "neutral": case "invalid": case "null": + case "permerror": case "error": case "null_smtp": case "null_header": From 7b9c45fb68189e6d1c1986f4c31d084be8dde293 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 7 Mar 2026 11:42:28 +0700 Subject: [PATCH 078/102] summary: color SPF error in red --- web/src/lib/components/SummaryCard.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/components/SummaryCard.svelte b/web/src/lib/components/SummaryCard.svelte index dd0637a..fe8af8e 100644 --- a/web/src/lib/components/SummaryCard.svelte +++ b/web/src/lib/components/SummaryCard.svelte @@ -113,7 +113,7 @@ } else if (spfResult === "temperror" || spfResult === "permerror") { segments.push({ text: "encountered an error", - highlight: { color: "warning", bold: true }, + highlight: { color: "danger", bold: true }, link: "#authentication-spf", }); segments.push({ text: ", check your SPF record configuration" }); From 9679b381c7a225af2bf8153defcda2ddcf44be71 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 7 Mar 2026 12:04:43 +0700 Subject: [PATCH 079/102] fix: mark Message-ID as invalid when multiple headers are present --- pkg/analyzer/headers.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/analyzer/headers.go b/pkg/analyzer/headers.go index b7ff3bb..2a1bae4 100644 --- a/pkg/analyzer/headers.go +++ b/pkg/analyzer/headers.go @@ -320,6 +320,10 @@ func (h *HeaderAnalyzer) checkHeader(email *EmailMessage, headerName string, imp valid = false headerIssues = append(headerIssues, "Invalid Message-ID format (should be )") } + if len(email.Header["Message-Id"]) > 1 { + valid = false + headerIssues = append(headerIssues, fmt.Sprintf("Multiple Message-ID headers found (%d); only one is allowed", len(email.Header["Message-Id"]))) + } case "Date": // Validate date format if _, err := h.parseEmailDate(value); err != nil { From 4245f93ce4fddffbd151ec1b871ca61a920ecfb1 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 7 Mar 2026 12:14:45 +0700 Subject: [PATCH 080/102] Add MIME-Version recommended header check Validate MIME-Version header value equals "1.0" and subtract 5 points from the score if the header is present but invalid. Absence is not penalized. --- pkg/analyzer/headers.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pkg/analyzer/headers.go b/pkg/analyzer/headers.go index 2a1bae4..37718bb 100644 --- a/pkg/analyzer/headers.go +++ b/pkg/analyzer/headers.go @@ -109,6 +109,13 @@ func (h *HeaderAnalyzer) CalculateHeaderScore(analysis *api.HeaderAnalysis) (int maxGrade -= 1 } + // Check MIME-Version header (-5 points if present but not "1.0") + if check, exists := headers["mime-version"]; exists && check.Present { + if check.Valid != nil && !*check.Valid { + score -= 5 + } + } + // Check Message-ID format (10 points) if check, exists := headers["message-id"]; exists && check.Present { // If Valid is set and true, award points @@ -266,6 +273,10 @@ func (h *HeaderAnalyzer) GenerateHeaderAnalysis(email *EmailMessage, authResults headers[strings.ToLower(headerName)] = *check } + // Check MIME-Version header (recommended but absence is not penalized) + mimeVersionCheck := h.checkHeader(email, "MIME-Version", "recommended") + headers[strings.ToLower("MIME-Version")] = *mimeVersionCheck + // Check optional headers optionalHeaders := []string{"List-Unsubscribe", "List-Unsubscribe-Post"} for _, headerName := range optionalHeaders { @@ -330,6 +341,11 @@ func (h *HeaderAnalyzer) checkHeader(email *EmailMessage, headerName string, imp valid = false headerIssues = append(headerIssues, fmt.Sprintf("Invalid date format: %v", err)) } + case "MIME-Version": + if value != "1.0" { + valid = false + headerIssues = append(headerIssues, fmt.Sprintf("MIME-Version should be '1.0', got '%s'", value)) + } case "From", "To", "Cc", "Bcc", "Reply-To", "Sender", "Resent-From", "Resent-To", "Return-Path": // Parse address header using net/mail and get normalized address if normalizedAddr, err := h.validateAddressHeader(value); err != nil { From f9c5c815d1b95a4e1853a1888fd9714e1b7f0edd Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 7 Mar 2026 12:22:41 +0700 Subject: [PATCH 081/102] spamassassin: disable Validity network rules scoring --- docker/spamassassin/local.cf | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docker/spamassassin/local.cf b/docker/spamassassin/local.cf index c248ef6..ce9a31c 100644 --- a/docker/spamassassin/local.cf +++ b/docker/spamassassin/local.cf @@ -48,3 +48,14 @@ rbl_timeout 5 # Don't use user-specific rules user_scores_dsn_timeout 3 user_scores_sql_override 0 + +# Disable Validity network rules +dns_query_restriction deny sa-trusted.bondedsender.org +dns_query_restriction deny sa-accredit.habeas.com +dns_query_restriction deny bl.score.senderscore.com +score RCVD_IN_VALIDITY_CERTIFIED_BLOCKED 0 +score RCVD_IN_VALIDITY_RPBL_BLOCKED 0 +score RCVD_IN_VALIDITY_SAFE_BLOCKED 0 +score RCVD_IN_VALIDITY_CERTIFIED 0 +score RCVD_IN_VALIDITY_RPBL 0 +score RCVD_IN_VALIDITY_SAFE 0 \ No newline at end of file From 3cc39c9c544a98fc00c677110d76b5c33ee3b0fb Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 7 Mar 2026 14:23:47 +0700 Subject: [PATCH 082/102] rbl: add more RBL providers Add 8 new RBL providers (SpamRats, PSBL, DroneBL, Mailspike, RBL-DNS and NSZones). --- pkg/analyzer/rbl.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/analyzer/rbl.go b/pkg/analyzer/rbl.go index 5fcb939..923f939 100644 --- a/pkg/analyzer/rbl.go +++ b/pkg/analyzer/rbl.go @@ -48,6 +48,14 @@ var DefaultRBLs = []string{ "b.barracudacentral.org", // Barracuda "cbl.abuseat.org", // CBL (Composite Blocking List) "dnsbl-1.uceprotect.net", // UCEPROTECT Level 1 + "spam.spamrats.com", // SpamRats SPAM + "dyna.spamrats.com", // SpamRats dynamic IPs + "psbl.surriel.com", // PSBL + "dnsbl.dronebl.org", // DroneBL + "bl.mailspike.net", // Mailspike BL + "z.mailspike.net", // Mailspike Z + "bl.rbl-dns.com", // RBL-DNS + "bl.nszones.com", // NSZones } // NewRBLChecker creates a new RBL checker with configurable timeout and RBL list From 28424729a5909ba7489d2a4e0a0dd66b6e6407a6 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 7 Mar 2026 14:24:33 +0700 Subject: [PATCH 083/102] rbl: support informational-only RBL entries Add DefaultInformationalRBLs (UCEPROTECT L2/L3) and track listings separately via RelevantListedCount so these broader lists are displayed but excluded from the deliverability score calculation. --- pkg/analyzer/rbl.go | 53 ++++++++++++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/pkg/analyzer/rbl.go b/pkg/analyzer/rbl.go index 923f939..ff0e813 100644 --- a/pkg/analyzer/rbl.go +++ b/pkg/analyzer/rbl.go @@ -34,10 +34,11 @@ import ( // RBLChecker checks IP addresses against DNS-based blacklists type RBLChecker struct { - Timeout time.Duration - RBLs []string - CheckAllIPs bool // Check all IPs found in headers, not just the first one - resolver *net.Resolver + Timeout time.Duration + RBLs []string + CheckAllIPs bool // Check all IPs found in headers, not just the first one + resolver *net.Resolver + informationalSet map[string]bool } // DefaultRBLs is a list of commonly used RBL providers @@ -48,6 +49,8 @@ var DefaultRBLs = []string{ "b.barracudacentral.org", // Barracuda "cbl.abuseat.org", // CBL (Composite Blocking List) "dnsbl-1.uceprotect.net", // UCEPROTECT Level 1 + "dnsbl-2.uceprotect.net", // UCEPROTECT Level 2 (informational) + "dnsbl-3.uceprotect.net", // UCEPROTECT Level 3 (informational) "spam.spamrats.com", // SpamRats SPAM "dyna.spamrats.com", // SpamRats dynamic IPs "psbl.surriel.com", // PSBL @@ -58,6 +61,13 @@ var DefaultRBLs = []string{ "bl.nszones.com", // NSZones } +// DefaultInformationalRBLs lists RBLs that are checked but not counted in the score. +// These are typically broader lists where being listed is less definitive. +var DefaultInformationalRBLs = []string{ + "dnsbl-2.uceprotect.net", // UCEPROTECT Level 2: entire netblocks, may cause false positives + "dnsbl-3.uceprotect.net", // UCEPROTECT Level 3: entire ASes, too broad for scoring +} + // NewRBLChecker creates a new RBL checker with configurable timeout and RBL list func NewRBLChecker(timeout time.Duration, rbls []string, checkAllIPs bool) *RBLChecker { if timeout == 0 { @@ -66,21 +76,25 @@ func NewRBLChecker(timeout time.Duration, rbls []string, checkAllIPs bool) *RBLC if len(rbls) == 0 { rbls = DefaultRBLs } + informationalSet := make(map[string]bool, len(DefaultInformationalRBLs)) + for _, rbl := range DefaultInformationalRBLs { + informationalSet[rbl] = true + } return &RBLChecker{ - Timeout: timeout, - RBLs: rbls, - CheckAllIPs: checkAllIPs, - resolver: &net.Resolver{ - PreferGo: true, - }, + Timeout: timeout, + RBLs: rbls, + CheckAllIPs: checkAllIPs, + resolver: &net.Resolver{PreferGo: true}, + informationalSet: informationalSet, } } // RBLResults represents the results of RBL checks type RBLResults struct { - Checks map[string][]api.BlacklistCheck // Map of IP -> list of RBL checks for that IP - IPsChecked []string - ListedCount int + Checks map[string][]api.BlacklistCheck // Map of IP -> list of RBL checks for that IP + IPsChecked []string + ListedCount int // Total listings including informational RBLs + RelevantListedCount int // Listings on scoring (non-informational) RBLs only } // CheckEmail checks all IPs found in the email headers against RBLs @@ -104,6 +118,9 @@ func (r *RBLChecker) CheckEmail(email *EmailMessage) *RBLResults { results.Checks[ip] = append(results.Checks[ip], check) if check.Listed { results.ListedCount++ + if !r.informationalSet[rbl] { + results.RelevantListedCount++ + } } } @@ -276,14 +293,20 @@ func (r *RBLChecker) reverseIP(ipStr string) string { return fmt.Sprintf("%d.%d.%d.%d", ipv4[3], ipv4[2], ipv4[1], ipv4[0]) } -// CalculateRBLScore calculates the blacklist contribution to deliverability +// CalculateRBLScore calculates the blacklist contribution to deliverability. +// Informational RBLs are not counted in the score. func (r *RBLChecker) CalculateRBLScore(results *RBLResults) (int, string) { if results == nil || len(results.IPsChecked) == 0 { // No IPs to check, give benefit of doubt return 100, "" } - percentage := 100 - results.ListedCount*100/len(r.RBLs) + scoringRBLCount := len(r.RBLs) - len(r.informationalSet) + if scoringRBLCount <= 0 { + return 100, "A+" + } + + percentage := 100 - results.RelevantListedCount*100/scoringRBLCount return percentage, ScoreToGrade(percentage) } From 55e9bcd3d043988cb7edb061f9661e40e4f00f9c Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 7 Mar 2026 16:18:10 +0700 Subject: [PATCH 084/102] refactor: handle DNS whitelists Introduce a single DNSListChecker struct with flags to avoid code duplication with already existing RBL checker. --- api/openapi.yaml | 13 ++ internal/config/config.go | 2 + pkg/analyzer/analyzer.go | 5 +- pkg/analyzer/rbl.go | 165 ++++++++++---------- pkg/analyzer/rbl_test.go | 26 +-- pkg/analyzer/report.go | 16 +- pkg/analyzer/report_test.go | 10 +- web/src/lib/components/WhitelistCard.svelte | 62 ++++++++ web/src/lib/components/index.ts | 1 + web/src/routes/test/[test]/+page.svelte | 52 ++++-- 10 files changed, 235 insertions(+), 117 deletions(-) create mode 100644 web/src/lib/components/WhitelistCard.svelte diff --git a/api/openapi.yaml b/api/openapi.yaml index 5c628fd..f724ae6 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -350,6 +350,19 @@ components: listed: false - rbl: "bl.spamcop.net" listed: false + whitelists: + type: object + additionalProperties: + type: array + items: + $ref: '#/components/schemas/BlacklistCheck' + description: Map of IP addresses to their DNS whitelist check results (informational only) + example: + "192.0.2.1": + - rbl: "list.dnswl.org" + listed: false + - rbl: "swl.spamhaus.org" + listed: false content_analysis: $ref: '#/components/schemas/ContentAnalysis' header_analysis: diff --git a/internal/config/config.go b/internal/config/config.go index 4a335c9..468a2aa 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -65,6 +65,7 @@ type AnalysisConfig struct { DNSTimeout time.Duration HTTPTimeout time.Duration RBLs []string + DNSWLs []string CheckAllIPs bool // Check all IPs found in headers, not just the first one } @@ -88,6 +89,7 @@ func DefaultConfig() *Config { DNSTimeout: 5 * time.Second, HTTPTimeout: 10 * time.Second, RBLs: []string{}, + DNSWLs: []string{}, CheckAllIPs: false, // By default, only check the first IP }, } diff --git a/pkg/analyzer/analyzer.go b/pkg/analyzer/analyzer.go index e7ae561..83eafe6 100644 --- a/pkg/analyzer/analyzer.go +++ b/pkg/analyzer/analyzer.go @@ -44,6 +44,7 @@ func NewEmailAnalyzer(cfg *config.Config) *EmailAnalyzer { cfg.Analysis.DNSTimeout, cfg.Analysis.HTTPTimeout, cfg.Analysis.RBLs, + cfg.Analysis.DNSWLs, cfg.Analysis.CheckAllIPs, ) @@ -130,12 +131,12 @@ func (a *APIAdapter) CheckBlacklistIP(ip string) ([]api.BlacklistCheck, int, int // Calculate score using the existing function // Create a minimal RBLResults structure for scoring - results := &RBLResults{ + results := &DNSListResults{ Checks: map[string][]api.BlacklistCheck{ip: checks}, IPsChecked: []string{ip}, ListedCount: listedCount, } - score, grade := a.analyzer.generator.rblChecker.CalculateRBLScore(results) + score, grade := a.analyzer.generator.rblChecker.CalculateScore(results) return checks, listedCount, score, grade, nil } diff --git a/pkg/analyzer/rbl.go b/pkg/analyzer/rbl.go index ff0e813..44c6e99 100644 --- a/pkg/analyzer/rbl.go +++ b/pkg/analyzer/rbl.go @@ -32,13 +32,15 @@ import ( "git.happydns.org/happyDeliver/internal/api" ) -// RBLChecker checks IP addresses against DNS-based blacklists -type RBLChecker struct { +// DNSListChecker checks IP addresses against DNS-based block/allow lists. +// It handles both RBL (blacklist) and DNSWL (whitelist) semantics via flags. +type DNSListChecker struct { Timeout time.Duration - RBLs []string + Lists []string CheckAllIPs bool // Check all IPs found in headers, not just the first one + filterErrorCodes bool // When true (RBL mode), treat 127.255.255.253/254/255 as operational errors resolver *net.Resolver - informationalSet map[string]bool + informationalSet map[string]bool // Lists whose hits don't count toward the score } // DefaultRBLs is a list of commonly used RBL providers @@ -68,10 +70,16 @@ var DefaultInformationalRBLs = []string{ "dnsbl-3.uceprotect.net", // UCEPROTECT Level 3: entire ASes, too broad for scoring } +// DefaultDNSWLs is a list of commonly used DNSWL providers +var DefaultDNSWLs = []string{ + "list.dnswl.org", // DNSWL.org — the main DNS whitelist + "swl.spamhaus.org", // Spamhaus Safe Whitelist +} + // NewRBLChecker creates a new RBL checker with configurable timeout and RBL list -func NewRBLChecker(timeout time.Duration, rbls []string, checkAllIPs bool) *RBLChecker { +func NewRBLChecker(timeout time.Duration, rbls []string, checkAllIPs bool) *DNSListChecker { if timeout == 0 { - timeout = 5 * time.Second // Default timeout + timeout = 5 * time.Second } if len(rbls) == 0 { rbls = DefaultRBLs @@ -80,30 +88,48 @@ func NewRBLChecker(timeout time.Duration, rbls []string, checkAllIPs bool) *RBLC for _, rbl := range DefaultInformationalRBLs { informationalSet[rbl] = true } - return &RBLChecker{ + return &DNSListChecker{ Timeout: timeout, - RBLs: rbls, + Lists: rbls, CheckAllIPs: checkAllIPs, + filterErrorCodes: true, resolver: &net.Resolver{PreferGo: true}, informationalSet: informationalSet, } } -// RBLResults represents the results of RBL checks -type RBLResults struct { - Checks map[string][]api.BlacklistCheck // Map of IP -> list of RBL checks for that IP - IPsChecked []string - ListedCount int // Total listings including informational RBLs - RelevantListedCount int // Listings on scoring (non-informational) RBLs only +// NewDNSWLChecker creates a new DNSWL checker with configurable timeout and DNSWL list +func NewDNSWLChecker(timeout time.Duration, dnswls []string, checkAllIPs bool) *DNSListChecker { + if timeout == 0 { + timeout = 5 * time.Second + } + if len(dnswls) == 0 { + dnswls = DefaultDNSWLs + } + return &DNSListChecker{ + Timeout: timeout, + Lists: dnswls, + CheckAllIPs: checkAllIPs, + filterErrorCodes: false, + resolver: &net.Resolver{PreferGo: true}, + informationalSet: make(map[string]bool), + } } -// CheckEmail checks all IPs found in the email headers against RBLs -func (r *RBLChecker) CheckEmail(email *EmailMessage) *RBLResults { - results := &RBLResults{ +// DNSListResults represents the results of DNS list checks +type DNSListResults struct { + Checks map[string][]api.BlacklistCheck // Map of IP -> list of checks for that IP + IPsChecked []string + ListedCount int // Total listings including informational entries + RelevantListedCount int // Listings on scoring (non-informational) lists only +} + +// CheckEmail checks all IPs found in the email headers against the configured lists +func (r *DNSListChecker) CheckEmail(email *EmailMessage) *DNSListResults { + results := &DNSListResults{ Checks: make(map[string][]api.BlacklistCheck), } - // Extract IPs from Received headers ips := r.extractIPs(email) if len(ips) == 0 { return results @@ -111,20 +137,18 @@ func (r *RBLChecker) CheckEmail(email *EmailMessage) *RBLResults { results.IPsChecked = ips - // Check each IP against all RBLs for _, ip := range ips { - for _, rbl := range r.RBLs { - check := r.checkIP(ip, rbl) + for _, list := range r.Lists { + check := r.checkIP(ip, list) results.Checks[ip] = append(results.Checks[ip], check) if check.Listed { results.ListedCount++ - if !r.informationalSet[rbl] { + if !r.informationalSet[list] { results.RelevantListedCount++ } } } - // Only check the first IP unless CheckAllIPs is enabled if !r.CheckAllIPs { break } @@ -133,9 +157,8 @@ func (r *RBLChecker) CheckEmail(email *EmailMessage) *RBLResults { return results } -// CheckIP checks a single IP address against all configured RBLs -func (r *RBLChecker) CheckIP(ip string) ([]api.BlacklistCheck, int, error) { - // Validate that it's a valid IP address +// CheckIP checks a single IP address against all configured lists +func (r *DNSListChecker) CheckIP(ip string) ([]api.BlacklistCheck, int, error) { if !r.isPublicIP(ip) { return nil, 0, fmt.Errorf("invalid or non-public IP address: %s", ip) } @@ -143,9 +166,8 @@ func (r *RBLChecker) CheckIP(ip string) ([]api.BlacklistCheck, int, error) { var checks []api.BlacklistCheck listedCount := 0 - // Check the IP against all RBLs - for _, rbl := range r.RBLs { - check := r.checkIP(ip, rbl) + for _, list := range r.Lists { + check := r.checkIP(ip, list) checks = append(checks, check) if check.Listed { listedCount++ @@ -156,27 +178,19 @@ func (r *RBLChecker) CheckIP(ip string) ([]api.BlacklistCheck, int, error) { } // extractIPs extracts IP addresses from Received headers -func (r *RBLChecker) extractIPs(email *EmailMessage) []string { +func (r *DNSListChecker) extractIPs(email *EmailMessage) []string { var ips []string seenIPs := make(map[string]bool) - // Get all Received headers receivedHeaders := email.Header["Received"] - - // Regex patterns for IP addresses - // Match IPv4: xxx.xxx.xxx.xxx ipv4Pattern := regexp.MustCompile(`\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b`) - // Look for IPs in Received headers for _, received := range receivedHeaders { - // Find all IPv4 addresses matches := ipv4Pattern.FindAllString(received, -1) for _, match := range matches { - // Skip private/reserved IPs if !r.isPublicIP(match) { continue } - // Avoid duplicates if !seenIPs[match] { ips = append(ips, match) seenIPs[match] = true @@ -184,13 +198,10 @@ func (r *RBLChecker) extractIPs(email *EmailMessage) []string { } } - // If no IPs found in Received headers, try X-Originating-IP if len(ips) == 0 { originatingIP := email.Header.Get("X-Originating-IP") if originatingIP != "" { - // Extract IP from formats like "[192.0.2.1]" or "192.0.2.1" cleanIP := strings.TrimSuffix(strings.TrimPrefix(originatingIP, "["), "]") - // Remove any whitespace cleanIP = strings.TrimSpace(cleanIP) matches := ipv4Pattern.FindString(cleanIP) if matches != "" && r.isPublicIP(matches) { @@ -203,19 +214,16 @@ func (r *RBLChecker) extractIPs(email *EmailMessage) []string { } // isPublicIP checks if an IP address is public (not private, loopback, or reserved) -func (r *RBLChecker) isPublicIP(ipStr string) bool { +func (r *DNSListChecker) isPublicIP(ipStr string) bool { ip := net.ParseIP(ipStr) if ip == nil { return false } - // Check if it's a private network if ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() { return false } - // Additional checks for reserved ranges - // 0.0.0.0/8, 192.0.0.0/24, 192.0.2.0/24 (TEST-NET-1), 198.51.100.0/24 (TEST-NET-2), 203.0.113.0/24 (TEST-NET-3) if ip.IsUnspecified() { return false } @@ -223,51 +231,43 @@ func (r *RBLChecker) isPublicIP(ipStr string) bool { return true } -// checkIP checks a single IP against a single RBL -func (r *RBLChecker) checkIP(ip, rbl string) api.BlacklistCheck { +// checkIP checks a single IP against a single DNS list +func (r *DNSListChecker) checkIP(ip, list string) api.BlacklistCheck { check := api.BlacklistCheck{ - Rbl: rbl, + Rbl: list, } - // Reverse the IP for DNSBL query reversedIP := r.reverseIP(ip) if reversedIP == "" { check.Error = api.PtrTo("Failed to reverse IP address") return check } - // Construct DNSBL query: reversed-ip.rbl-domain - query := fmt.Sprintf("%s.%s", reversedIP, rbl) + query := fmt.Sprintf("%s.%s", reversedIP, list) - // Perform DNS lookup with timeout ctx, cancel := context.WithTimeout(context.Background(), r.Timeout) defer cancel() addrs, err := r.resolver.LookupHost(ctx, query) if err != nil { - // Most likely not listed (NXDOMAIN) if dnsErr, ok := err.(*net.DNSError); ok { if dnsErr.IsNotFound { check.Listed = false return check } } - // Other DNS errors check.Error = api.PtrTo(fmt.Sprintf("DNS lookup failed: %v", err)) return check } - // If we got a response, check the return code if len(addrs) > 0 { - check.Response = api.PtrTo(addrs[0]) // Return code (e.g., 127.0.0.2) + check.Response = api.PtrTo(addrs[0]) - // Check for RBL error codes: 127.255.255.253, 127.255.255.254, 127.255.255.255 - // These indicate RBL operational issues, not actual listings - if addrs[0] == "127.255.255.253" || addrs[0] == "127.255.255.254" || addrs[0] == "127.255.255.255" { + // In RBL mode, 127.255.255.253/254/255 indicate operational errors, not real listings. + if r.filterErrorCodes && (addrs[0] == "127.255.255.253" || addrs[0] == "127.255.255.254" || addrs[0] == "127.255.255.255") { check.Listed = false - check.Error = api.PtrTo(fmt.Sprintf("RBL %s returned error code %s (RBL operational issue)", rbl, addrs[0])) + check.Error = api.PtrTo(fmt.Sprintf("RBL %s returned error code %s (RBL operational issue)", list, addrs[0])) } else { - // Normal listing response check.Listed = true } } @@ -275,50 +275,47 @@ func (r *RBLChecker) checkIP(ip, rbl string) api.BlacklistCheck { return check } -// reverseIP reverses an IPv4 address for DNSBL queries +// reverseIP reverses an IPv4 address for DNSBL/DNSWL queries // Example: 192.0.2.1 -> 1.2.0.192 -func (r *RBLChecker) reverseIP(ipStr string) string { +func (r *DNSListChecker) reverseIP(ipStr string) string { ip := net.ParseIP(ipStr) if ip == nil { return "" } - // Convert to IPv4 ipv4 := ip.To4() if ipv4 == nil { return "" // IPv6 not supported yet } - // Reverse the octets return fmt.Sprintf("%d.%d.%d.%d", ipv4[3], ipv4[2], ipv4[1], ipv4[0]) } -// CalculateRBLScore calculates the blacklist contribution to deliverability. -// Informational RBLs are not counted in the score. -func (r *RBLChecker) CalculateRBLScore(results *RBLResults) (int, string) { +// CalculateScore calculates the list contribution to deliverability. +// Informational lists are not counted in the score. +func (r *DNSListChecker) CalculateScore(results *DNSListResults) (int, string) { if results == nil || len(results.IPsChecked) == 0 { - // No IPs to check, give benefit of doubt return 100, "" } - scoringRBLCount := len(r.RBLs) - len(r.informationalSet) - if scoringRBLCount <= 0 { + scoringListCount := len(r.Lists) - len(r.informationalSet) + if scoringListCount <= 0 { return 100, "A+" } - percentage := 100 - results.RelevantListedCount*100/scoringRBLCount + percentage := 100 - results.RelevantListedCount*100/scoringListCount return percentage, ScoreToGrade(percentage) } -// GetUniqueListedIPs returns a list of unique IPs that are listed on at least one RBL -func (r *RBLChecker) GetUniqueListedIPs(results *RBLResults) []string { +// GetUniqueListedIPs returns a list of unique IPs that are listed on at least one entry +func (r *DNSListChecker) GetUniqueListedIPs(results *DNSListResults) []string { var listedIPs []string - for ip, rblChecks := range results.Checks { - for _, check := range rblChecks { + for ip, checks := range results.Checks { + for _, check := range checks { if check.Listed { listedIPs = append(listedIPs, ip) - break // Only add the IP once + break } } } @@ -326,17 +323,17 @@ func (r *RBLChecker) GetUniqueListedIPs(results *RBLResults) []string { return listedIPs } -// GetRBLsForIP returns all RBLs that list a specific IP -func (r *RBLChecker) GetRBLsForIP(results *RBLResults, ip string) []string { - var rbls []string +// GetListsForIP returns all lists that match a specific IP +func (r *DNSListChecker) GetListsForIP(results *DNSListResults, ip string) []string { + var lists []string - if rblChecks, exists := results.Checks[ip]; exists { - for _, check := range rblChecks { + if checks, exists := results.Checks[ip]; exists { + for _, check := range checks { if check.Listed { - rbls = append(rbls, check.Rbl) + lists = append(lists, check.Rbl) } } } - return rbls + return lists } diff --git a/pkg/analyzer/rbl_test.go b/pkg/analyzer/rbl_test.go index a1de270..1dd1262 100644 --- a/pkg/analyzer/rbl_test.go +++ b/pkg/analyzer/rbl_test.go @@ -59,8 +59,8 @@ func TestNewRBLChecker(t *testing.T) { if checker.Timeout != tt.expectedTimeout { t.Errorf("Timeout = %v, want %v", checker.Timeout, tt.expectedTimeout) } - if len(checker.RBLs) != tt.expectedRBLs { - t.Errorf("RBLs count = %d, want %d", len(checker.RBLs), tt.expectedRBLs) + if len(checker.Lists) != tt.expectedRBLs { + t.Errorf("RBLs count = %d, want %d", len(checker.Lists), tt.expectedRBLs) } if checker.resolver == nil { t.Error("Resolver should not be nil") @@ -265,7 +265,7 @@ func TestExtractIPs(t *testing.T) { func TestGetBlacklistScore(t *testing.T) { tests := []struct { name string - results *RBLResults + results *DNSListResults expectedScore int }{ { @@ -275,14 +275,14 @@ func TestGetBlacklistScore(t *testing.T) { }, { name: "No IPs checked", - results: &RBLResults{ + results: &DNSListResults{ IPsChecked: []string{}, }, expectedScore: 100, }, { name: "Not listed on any RBL", - results: &RBLResults{ + results: &DNSListResults{ IPsChecked: []string{"198.51.100.1"}, ListedCount: 0, }, @@ -290,7 +290,7 @@ func TestGetBlacklistScore(t *testing.T) { }, { name: "Listed on 1 RBL", - results: &RBLResults{ + results: &DNSListResults{ IPsChecked: []string{"198.51.100.1"}, ListedCount: 1, }, @@ -298,7 +298,7 @@ func TestGetBlacklistScore(t *testing.T) { }, { name: "Listed on 2 RBLs", - results: &RBLResults{ + results: &DNSListResults{ IPsChecked: []string{"198.51.100.1"}, ListedCount: 2, }, @@ -306,7 +306,7 @@ func TestGetBlacklistScore(t *testing.T) { }, { name: "Listed on 3 RBLs", - results: &RBLResults{ + results: &DNSListResults{ IPsChecked: []string{"198.51.100.1"}, ListedCount: 3, }, @@ -314,7 +314,7 @@ func TestGetBlacklistScore(t *testing.T) { }, { name: "Listed on 4+ RBLs", - results: &RBLResults{ + results: &DNSListResults{ IPsChecked: []string{"198.51.100.1"}, ListedCount: 4, }, @@ -326,7 +326,7 @@ func TestGetBlacklistScore(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - score, _ := checker.CalculateRBLScore(tt.results) + score, _ := checker.CalculateScore(tt.results) if score != tt.expectedScore { t.Errorf("GetBlacklistScore() = %v, want %v", score, tt.expectedScore) } @@ -335,7 +335,7 @@ func TestGetBlacklistScore(t *testing.T) { } func TestGetUniqueListedIPs(t *testing.T) { - results := &RBLResults{ + results := &DNSListResults{ Checks: map[string][]api.BlacklistCheck{ "198.51.100.1": { {Rbl: "zen.spamhaus.org", Listed: true}, @@ -363,7 +363,7 @@ func TestGetUniqueListedIPs(t *testing.T) { } func TestGetRBLsForIP(t *testing.T) { - results := &RBLResults{ + results := &DNSListResults{ Checks: map[string][]api.BlacklistCheck{ "198.51.100.1": { {Rbl: "zen.spamhaus.org", Listed: true}, @@ -402,7 +402,7 @@ func TestGetRBLsForIP(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - rbls := checker.GetRBLsForIP(results, tt.ip) + rbls := checker.GetListsForIP(results, tt.ip) if len(rbls) != len(tt.expectedRBLs) { t.Errorf("Got %d RBLs, want %d", len(rbls), len(tt.expectedRBLs)) diff --git a/pkg/analyzer/report.go b/pkg/analyzer/report.go index dc420fb..bd12960 100644 --- a/pkg/analyzer/report.go +++ b/pkg/analyzer/report.go @@ -35,7 +35,8 @@ type ReportGenerator struct { spamAnalyzer *SpamAssassinAnalyzer rspamdAnalyzer *RspamdAnalyzer dnsAnalyzer *DNSAnalyzer - rblChecker *RBLChecker + rblChecker *DNSListChecker + dnswlChecker *DNSListChecker contentAnalyzer *ContentAnalyzer headerAnalyzer *HeaderAnalyzer } @@ -45,6 +46,7 @@ func NewReportGenerator( dnsTimeout time.Duration, httpTimeout time.Duration, rbls []string, + dnswls []string, checkAllIPs bool, ) *ReportGenerator { return &ReportGenerator{ @@ -53,6 +55,7 @@ func NewReportGenerator( rspamdAnalyzer: NewRspamdAnalyzer(), dnsAnalyzer: NewDNSAnalyzer(dnsTimeout), rblChecker: NewRBLChecker(dnsTimeout, rbls, checkAllIPs), + dnswlChecker: NewDNSWLChecker(dnsTimeout, dnswls, checkAllIPs), contentAnalyzer: NewContentAnalyzer(httpTimeout), headerAnalyzer: NewHeaderAnalyzer(), } @@ -65,7 +68,8 @@ type AnalysisResults struct { Content *ContentResults DNS *api.DNSResults Headers *api.HeaderAnalysis - RBL *RBLResults + RBL *DNSListResults + DNSWL *DNSListResults SpamAssassin *api.SpamAssassinResult Rspamd *api.RspamdResult } @@ -81,6 +85,7 @@ func (r *ReportGenerator) AnalyzeEmail(email *EmailMessage) *AnalysisResults { results.Headers = r.headerAnalyzer.GenerateHeaderAnalysis(email, results.Authentication) results.DNS = r.dnsAnalyzer.AnalyzeDNS(email, results.Authentication, results.Headers) results.RBL = r.rblChecker.CheckEmail(email) + results.DNSWL = r.dnswlChecker.CheckEmail(email) results.SpamAssassin = r.spamAnalyzer.AnalyzeSpamAssassin(email) results.Rspamd = r.rspamdAnalyzer.AnalyzeRspamd(email) results.Content = r.contentAnalyzer.AnalyzeContent(email) @@ -135,7 +140,7 @@ func (r *ReportGenerator) GenerateReport(testID uuid.UUID, results *AnalysisResu blacklistScore := 0 var blacklistGrade string if results.RBL != nil { - blacklistScore, blacklistGrade = r.rblChecker.CalculateRBLScore(results.RBL) + blacklistScore, blacklistGrade = r.rblChecker.CalculateScore(results.RBL) } saScore, saGrade := r.spamAnalyzer.CalculateSpamAssassinScore(results.SpamAssassin) @@ -197,6 +202,11 @@ func (r *ReportGenerator) GenerateReport(testID uuid.UUID, results *AnalysisResu report.Blacklists = &results.RBL.Checks } + // Add whitelist checks as a map of IP -> array of BlacklistCheck (informational only) + if results.DNSWL != nil && len(results.DNSWL.Checks) > 0 { + report.Whitelists = &results.DNSWL.Checks + } + // Add SpamAssassin result with individual deliverability score if results.SpamAssassin != nil { saGradeTyped := api.SpamAssassinResultDeliverabilityGrade(saGrade) diff --git a/pkg/analyzer/report_test.go b/pkg/analyzer/report_test.go index 5a325b1..82e923e 100644 --- a/pkg/analyzer/report_test.go +++ b/pkg/analyzer/report_test.go @@ -32,7 +32,7 @@ import ( ) func TestNewReportGenerator(t *testing.T) { - gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, false) + gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, DefaultDNSWLs, false) if gen == nil { t.Fatal("Expected report generator, got nil") } @@ -55,7 +55,7 @@ func TestNewReportGenerator(t *testing.T) { } func TestAnalyzeEmail(t *testing.T) { - gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, false) + gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, DefaultDNSWLs, false) email := createTestEmail() @@ -75,7 +75,7 @@ func TestAnalyzeEmail(t *testing.T) { } func TestGenerateReport(t *testing.T) { - gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, false) + gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, DefaultDNSWLs, false) testID := uuid.New() email := createTestEmail() @@ -130,7 +130,7 @@ func TestGenerateReport(t *testing.T) { } func TestGenerateReportWithSpamAssassin(t *testing.T) { - gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, false) + gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, DefaultDNSWLs, false) testID := uuid.New() email := createTestEmailWithSpamAssassin() @@ -150,7 +150,7 @@ func TestGenerateReportWithSpamAssassin(t *testing.T) { } func TestGenerateRawEmail(t *testing.T) { - gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, false) + gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, DefaultDNSWLs, false) tests := []struct { name string diff --git a/web/src/lib/components/WhitelistCard.svelte b/web/src/lib/components/WhitelistCard.svelte new file mode 100644 index 0000000..ee0b0e2 --- /dev/null +++ b/web/src/lib/components/WhitelistCard.svelte @@ -0,0 +1,62 @@ + + +
+
+

+ + + Whitelist Checks + + Informational +

+
+
+

+ DNS whitelists identify trusted senders. Being listed here is a positive signal, but has + no impact on the overall score. +

+ +
+ {#each Object.entries(whitelists) as [ip, checks]} +
+
+ + {ip} +
+ + + {#each checks as check} + + + + + {/each} + +
+ + {check.error + ? "Error" + : check.listed + ? "Listed" + : "Not listed"} + + {check.rbl}
+
+ {/each} +
+
+
diff --git a/web/src/lib/components/index.ts b/web/src/lib/components/index.ts index d577399..8ed409c 100644 --- a/web/src/lib/components/index.ts +++ b/web/src/lib/components/index.ts @@ -24,3 +24,4 @@ export { default as SpamAssassinCard } from "./SpamAssassinCard.svelte"; export { default as SpfRecordsDisplay } from "./SpfRecordsDisplay.svelte"; export { default as SummaryCard } from "./SummaryCard.svelte"; export { default as TinySurvey } from "./TinySurvey.svelte"; +export { default as WhitelistCard } from "./WhitelistCard.svelte"; diff --git a/web/src/routes/test/[test]/+page.svelte b/web/src/routes/test/[test]/+page.svelte index c5add96..0c8ea9d 100644 --- a/web/src/routes/test/[test]/+page.svelte +++ b/web/src/routes/test/[test]/+page.svelte @@ -3,7 +3,7 @@ import { onDestroy } from "svelte"; import { getReport, getTest, reanalyzeReport } from "$lib/api"; - import type { Report, Test } from "$lib/api/types.gen"; + import type { BlacklistCheck, Report, Test } from "$lib/api/types.gen"; import { AuthenticationCard, BlacklistCard, @@ -17,8 +17,11 @@ SpamAssassinCard, SummaryCard, TinySurvey, + WhitelistCard, } from "$lib/components"; + type BlacklistRecords = Record; + let testId = $derived(page.params.test); let test = $state(null); let report = $state(null); @@ -321,17 +324,46 @@ {/if} - {#if report.blacklists && Object.keys(report.blacklists).length > 0} -
-
- + {#snippet blacklistChecks(blacklists: BlacklistRecords, report: Report)} + + {/snippet} + + + {#snippet whitelistChecks(whitelists: BlacklistRecords)} + + {/snippet} + + + {#if report.blacklists && report.whitelists && Object.keys(report.blacklists).length == 1 && Object.keys(report.whitelists).length == 1} +
+
+ {@render blacklistChecks(report.blacklists, report)} +
+
+ {@render whitelistChecks(report.whitelists)}
+ {:else} + {#if report.blacklists && Object.keys(report.blacklists).length > 0} +
+
+ {@render blacklistChecks(report.blacklists, report)} +
+
+ {/if} + + {#if report.whitelists && Object.keys(report.whitelists).length > 0} +
+
+ {@render whitelistChecks(report.whitelists)} +
+
+ {/if} {/if} From 2a2bfe46a858b7cc2030f97e166337867a86732b Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 7 Mar 2026 16:26:40 +0700 Subject: [PATCH 085/102] fix: various small fixes and improvements - Add 'skipped' to authentication result enum in OpenAPI spec - Fix optional chaining on bimiResult.details check - Add rbls field to AppConfig interface - Restrict theme storage to valid 'light'/'dark' values only - Fix null coalescing for blacklist result data - Fix survey source to use domain instead of ip --- api/openapi.yaml | 2 +- web/src/lib/components/SummaryCard.svelte | 2 +- web/src/lib/stores/config.ts | 1 + web/src/lib/stores/theme.ts | 2 +- web/src/routes/blacklist/[ip]/+page.svelte | 2 +- web/src/routes/domain/[domain]/+page.svelte | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/api/openapi.yaml b/api/openapi.yaml index f724ae6..c0c3c70 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -789,7 +789,7 @@ components: properties: result: type: string - enum: [pass, fail, invalid, missing, none, neutral, softfail, temperror, permerror, declined, domain_pass, orgdomain_pass] + enum: [pass, fail, invalid, missing, none, neutral, softfail, temperror, permerror, declined, domain_pass, orgdomain_pass, skipped] description: Authentication result example: "pass" domain: diff --git a/web/src/lib/components/SummaryCard.svelte b/web/src/lib/components/SummaryCard.svelte index fe8af8e..5d93513 100644 --- a/web/src/lib/components/SummaryCard.svelte +++ b/web/src/lib/components/SummaryCard.svelte @@ -331,7 +331,7 @@ highlight: { color: "good", bold: true }, link: "#dns-bimi", }); - if (bimiResult.details && bimiResult.details.indexOf("declined") == 0) { + if (bimiResult?.details && bimiResult.details.indexOf("declined") == 0) { segments.push({ text: " declined to participate" }); } else if (bimiResult?.result === "fail") { segments.push({ text: " but " }); diff --git a/web/src/lib/stores/config.ts b/web/src/lib/stores/config.ts index 87662ba..c393dd2 100644 --- a/web/src/lib/stores/config.ts +++ b/web/src/lib/stores/config.ts @@ -25,6 +25,7 @@ interface AppConfig { report_retention?: number; survey_url?: string; custom_logo_url?: string; + rbls?: string[]; } const defaultConfig: AppConfig = { diff --git a/web/src/lib/stores/theme.ts b/web/src/lib/stores/theme.ts index 362202b..ea24293 100644 --- a/web/src/lib/stores/theme.ts +++ b/web/src/lib/stores/theme.ts @@ -26,7 +26,7 @@ const getInitialTheme = () => { if (!browser) return "light"; const stored = localStorage.getItem("theme"); - if (stored) return stored; + if (stored === "light" || stored === "dark") return stored; return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; }; diff --git a/web/src/routes/blacklist/[ip]/+page.svelte b/web/src/routes/blacklist/[ip]/+page.svelte index 180bfde..0cddb22 100644 --- a/web/src/routes/blacklist/[ip]/+page.svelte +++ b/web/src/routes/blacklist/[ip]/+page.svelte @@ -28,7 +28,7 @@ }); if (response.response.ok) { - result = response.data; + result = response.data ?? null; } else if (response.error) { error = response.error.message || "Failed to check IP address"; } diff --git a/web/src/routes/domain/[domain]/+page.svelte b/web/src/routes/domain/[domain]/+page.svelte index e191192..d866e21 100644 --- a/web/src/routes/domain/[domain]/+page.svelte +++ b/web/src/routes/domain/[domain]/+page.svelte @@ -130,7 +130,7 @@
From da93d6d706dedc3de744081172de9c0803276521 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Mon, 9 Mar 2026 12:47:24 +0700 Subject: [PATCH 086/102] Add rspamd tests --- pkg/analyzer/rspamd_test.go | 394 ++++++++++++++++++++++++++++++++++++ 1 file changed, 394 insertions(+) create mode 100644 pkg/analyzer/rspamd_test.go diff --git a/pkg/analyzer/rspamd_test.go b/pkg/analyzer/rspamd_test.go new file mode 100644 index 0000000..180bafd --- /dev/null +++ b/pkg/analyzer/rspamd_test.go @@ -0,0 +1,394 @@ +// This file is part of the happyDeliver (R) project. +// Copyright (c) 2026 happyDomain +// Authors: Pierre-Olivier Mercier, et al. +// +// This program is offered under a commercial and under the AGPL license. +// For commercial licensing, contact us at . +// +// For AGPL licensing: +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package analyzer + +import ( + "bytes" + "net/mail" + "testing" + + "git.happydns.org/happyDeliver/internal/api" +) + +func TestAnalyzeRspamdNoHeaders(t *testing.T) { + analyzer := NewRspamdAnalyzer() + email := &EmailMessage{Header: make(mail.Header)} + + result := analyzer.AnalyzeRspamd(email) + + if result != nil { + t.Errorf("Expected nil for email without rspamd headers, got %+v", result) + } +} + +func TestParseSpamdResult(t *testing.T) { + tests := []struct { + name string + header string + expectedScore float32 + expectedThreshold float32 + expectedIsSpam bool + expectedSymbols map[string]float32 + expectedSymParams map[string]string + }{ + { + name: "Clean email negative score", + header: "default: False [-3.91 / 15.00];\n\tDATE_IN_PAST(0.10); ALL_TRUSTED(-1.00)[trusted]", + expectedScore: -3.91, + expectedThreshold: 15.00, + expectedIsSpam: false, + expectedSymbols: map[string]float32{ + "DATE_IN_PAST": 0.10, + "ALL_TRUSTED": -1.00, + }, + expectedSymParams: map[string]string{ + "ALL_TRUSTED": "trusted", + }, + }, + { + name: "Spam email True flag", + header: "default: True [16.50 / 15.00];\n\tBAYES_99(5.00)[1.00]; SPOOFED_SENDER(3.50)", + expectedScore: 16.50, + expectedThreshold: 15.00, + expectedIsSpam: true, + expectedSymbols: map[string]float32{ + "BAYES_99": 5.00, + "SPOOFED_SENDER": 3.50, + }, + expectedSymParams: map[string]string{ + "BAYES_99": "1.00", + }, + }, + { + name: "Zero threshold uses default", + header: "default: False [1.00 / 0.00]", + expectedScore: 1.00, + expectedThreshold: rspamdDefaultAddHeaderThreshold, + expectedIsSpam: false, + expectedSymbols: map[string]float32{}, + }, + { + name: "Symbol without params", + header: "default: False [2.00 / 15.00];\n\tMISSING_DATE(1.00)", + expectedScore: 2.00, + expectedThreshold: 15.00, + expectedIsSpam: false, + expectedSymbols: map[string]float32{ + "MISSING_DATE": 1.00, + }, + }, + { + name: "Case-insensitive true flag", + header: "default: true [8.00 / 6.00]", + expectedScore: 8.00, + expectedThreshold: 6.00, + expectedIsSpam: true, + expectedSymbols: map[string]float32{}, + }, + } + + analyzer := NewRspamdAnalyzer() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := &api.RspamdResult{ + Symbols: make(map[string]api.RspamdSymbol), + } + analyzer.parseSpamdResult(tt.header, result) + + if result.Score != tt.expectedScore { + t.Errorf("Score = %v, want %v", result.Score, tt.expectedScore) + } + if result.Threshold != tt.expectedThreshold { + t.Errorf("Threshold = %v, want %v", result.Threshold, tt.expectedThreshold) + } + if result.IsSpam != tt.expectedIsSpam { + t.Errorf("IsSpam = %v, want %v", result.IsSpam, tt.expectedIsSpam) + } + for symName, expectedScore := range tt.expectedSymbols { + sym, ok := result.Symbols[symName] + if !ok { + t.Errorf("Symbol %s not found", symName) + continue + } + if sym.Score != expectedScore { + t.Errorf("Symbol %s score = %v, want %v", symName, sym.Score, expectedScore) + } + } + for symName, expectedParam := range tt.expectedSymParams { + sym, ok := result.Symbols[symName] + if !ok { + t.Errorf("Symbol %s not found for params check", symName) + continue + } + if sym.Params == nil { + t.Errorf("Symbol %s params = nil, want %q", symName, expectedParam) + } else if *sym.Params != expectedParam { + t.Errorf("Symbol %s params = %q, want %q", symName, *sym.Params, expectedParam) + } + } + }) + } +} + +func TestAnalyzeRspamd(t *testing.T) { + tests := []struct { + name string + headers map[string]string + expectedScore float32 + expectedThreshold float32 + expectedIsSpam bool + expectedServer *string + expectedSymCount int + }{ + { + name: "Full headers clean email", + headers: map[string]string{ + "X-Spamd-Result": "default: False [-3.91 / 15.00];\n\tALL_TRUSTED(-1.00)[local]", + "X-Rspamd-Score": "-3.91", + "X-Rspamd-Server": "mail.example.com", + }, + expectedScore: -3.91, + expectedThreshold: 15.00, + expectedIsSpam: false, + expectedServer: func() *string { s := "mail.example.com"; return &s }(), + expectedSymCount: 1, + }, + { + name: "X-Rspamd-Score overrides spamd result score", + headers: map[string]string{ + "X-Spamd-Result": "default: False [2.00 / 15.00]", + "X-Rspamd-Score": "3.50", + }, + expectedScore: 3.50, + expectedThreshold: 15.00, + expectedIsSpam: false, + }, + { + name: "Spam email above threshold", + headers: map[string]string{ + "X-Spamd-Result": "default: True [16.00 / 15.00];\n\tBAYES_99(5.00)", + "X-Rspamd-Score": "16.00", + }, + expectedScore: 16.00, + expectedThreshold: 15.00, + expectedIsSpam: true, + expectedSymCount: 1, + }, + { + name: "No X-Spamd-Result, only X-Rspamd-Score below default threshold", + headers: map[string]string{ + "X-Rspamd-Score": "2.00", + }, + expectedScore: 2.00, + expectedIsSpam: false, + }, + { + name: "No X-Spamd-Result, X-Rspamd-Score above default add-header threshold", + headers: map[string]string{ + "X-Rspamd-Score": "7.00", + }, + expectedScore: 7.00, + expectedIsSpam: true, + }, + { + name: "Server header is trimmed", + headers: map[string]string{ + "X-Rspamd-Score": "1.00", + "X-Rspamd-Server": " rspamd-01 ", + }, + expectedScore: 1.00, + expectedServer: func() *string { s := "rspamd-01"; return &s }(), + }, + } + + analyzer := NewRspamdAnalyzer() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + email := &EmailMessage{Header: make(mail.Header)} + for k, v := range tt.headers { + email.Header[k] = []string{v} + } + + result := analyzer.AnalyzeRspamd(email) + + if result == nil { + t.Fatal("Expected non-nil result") + } + if result.Score != tt.expectedScore { + t.Errorf("Score = %v, want %v", result.Score, tt.expectedScore) + } + if tt.expectedThreshold > 0 && result.Threshold != tt.expectedThreshold { + t.Errorf("Threshold = %v, want %v", result.Threshold, tt.expectedThreshold) + } + if result.IsSpam != tt.expectedIsSpam { + t.Errorf("IsSpam = %v, want %v", result.IsSpam, tt.expectedIsSpam) + } + if tt.expectedServer != nil { + if result.Server == nil { + t.Errorf("Server = nil, want %q", *tt.expectedServer) + } else if *result.Server != *tt.expectedServer { + t.Errorf("Server = %q, want %q", *result.Server, *tt.expectedServer) + } + } + if tt.expectedSymCount > 0 && len(result.Symbols) != tt.expectedSymCount { + t.Errorf("Symbol count = %d, want %d", len(result.Symbols), tt.expectedSymCount) + } + }) + } +} + +func TestCalculateRspamdScore(t *testing.T) { + tests := []struct { + name string + result *api.RspamdResult + expectedScore int + expectedGrade string + }{ + { + name: "Nil result (rspamd not installed)", + result: nil, + expectedScore: 100, + expectedGrade: "", + }, + { + name: "Score well below threshold", + result: &api.RspamdResult{ + Score: -3.91, + Threshold: 15.00, + }, + expectedScore: 100, + expectedGrade: "A+", + }, + { + name: "Score at zero", + result: &api.RspamdResult{ + Score: 0, + Threshold: 15.00, + }, + // 100 - round(0*100/30) = 100 → hits ScoreToGrade(100) = "A" + expectedScore: 100, + expectedGrade: "A", + }, + { + name: "Score at threshold (half of 2*threshold)", + result: &api.RspamdResult{ + Score: 15.00, + Threshold: 15.00, + }, + // 100 - round(15*100/(2*15)) = 100 - 50 = 50 + expectedScore: 50, + }, + { + name: "Score above 2*threshold", + result: &api.RspamdResult{ + Score: 31.00, + Threshold: 15.00, + }, + expectedScore: 0, + expectedGrade: "F", + }, + { + name: "Score exactly at 2*threshold", + result: &api.RspamdResult{ + Score: 30.00, + Threshold: 15.00, + }, + // 100 - round(30*100/30) = 100 - 100 = 0 + expectedScore: 0, + expectedGrade: "F", + }, + } + + analyzer := NewRspamdAnalyzer() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + score, grade := analyzer.CalculateRspamdScore(tt.result) + + if score != tt.expectedScore { + t.Errorf("Score = %d, want %d", score, tt.expectedScore) + } + if tt.expectedGrade != "" && grade != tt.expectedGrade { + t.Errorf("Grade = %q, want %q", grade, tt.expectedGrade) + } + }) + } +} + +const sampleEmailWithRspamdHeaders = `X-Spamd-Result: default: False [-3.91 / 15.00]; + BAYES_HAM(-3.00)[99%]; + RCVD_IN_DNSWL_MED(-0.01)[1.2.3.4:from]; + R_DKIM_ALLOW(-0.20)[example.com:s=dkim]; + FROM_HAS_DN(0.00)[]; + MIME_GOOD(-0.10)[text/plain]; +X-Rspamd-Score: -3.91 +X-Rspamd-Server: rspamd-01.example.com +Date: Mon, 09 Mar 2026 10:00:00 +0000 +From: sender@example.com +To: test@happydomain.org +Subject: Test email +Message-ID: +MIME-Version: 1.0 +Content-Type: text/plain + +Hello world` + +func TestAnalyzeRspamdRealEmail(t *testing.T) { + email, err := ParseEmail(bytes.NewBufferString(sampleEmailWithRspamdHeaders)) + if err != nil { + t.Fatalf("Failed to parse email: %v", err) + } + + analyzer := NewRspamdAnalyzer() + result := analyzer.AnalyzeRspamd(email) + + if result == nil { + t.Fatal("Expected non-nil result") + } + if result.IsSpam { + t.Error("Expected IsSpam=false") + } + if result.Score != -3.91 { + t.Errorf("Score = %v, want -3.91", result.Score) + } + if result.Threshold != 15.00 { + t.Errorf("Threshold = %v, want 15.00", result.Threshold) + } + if result.Server == nil || *result.Server != "rspamd-01.example.com" { + t.Errorf("Server = %v, want \"rspamd-01.example.com\"", result.Server) + } + + expectedSymbols := []string{"BAYES_HAM", "RCVD_IN_DNSWL_MED", "R_DKIM_ALLOW", "FROM_HAS_DN", "MIME_GOOD"} + for _, sym := range expectedSymbols { + if _, ok := result.Symbols[sym]; !ok { + t.Errorf("Symbol %s not found", sym) + } + } + + score, _ := analyzer.CalculateRspamdScore(result) + if score != 100 { + t.Errorf("CalculateRspamdScore = %d, want 100", score) + } +} + From bb47bb7c29eb9e59780d631772081daa42d111b7 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Mon, 9 Mar 2026 12:52:12 +0700 Subject: [PATCH 087/102] fix: handle nested brackets in rspamd symbol params --- pkg/analyzer/rspamd.go | 5 +++-- pkg/analyzer/rspamd_test.go | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/pkg/analyzer/rspamd.go b/pkg/analyzer/rspamd.go index d394c62..c2ea1cf 100644 --- a/pkg/analyzer/rspamd.go +++ b/pkg/analyzer/rspamd.go @@ -111,8 +111,9 @@ func (a *RspamdAnalyzer) parseSpamdResult(header string, result *api.RspamdResul } // Parse symbols: SYMBOL(score)[params] - // Each symbol entry is separated by ";" - symbolRe := regexp.MustCompile(`(\w+)\((-?\d+\.?\d*)\)(?:\[([^\]]*)\])?`) + // Each symbol entry is separated by ";", so within each part we use a + // greedy match to capture params that may contain nested brackets. + symbolRe := regexp.MustCompile(`(\w+)\((-?\d+\.?\d*)\)(?:\[(.*)\])?`) for _, part := range strings.Split(header, ";") { part = strings.TrimSpace(part) matches := symbolRe.FindStringSubmatch(part) diff --git a/pkg/analyzer/rspamd_test.go b/pkg/analyzer/rspamd_test.go index 180bafd..de98fe8 100644 --- a/pkg/analyzer/rspamd_test.go +++ b/pkg/analyzer/rspamd_test.go @@ -104,6 +104,26 @@ func TestParseSpamdResult(t *testing.T) { expectedIsSpam: true, expectedSymbols: map[string]float32{}, }, + { + name: "Zero threshold with symbols containing nested brackets in params", + header: "default: False [0.90 / 0.00];\n" + + "\tARC_REJECT(1.00)[cannot verify 1 of 1 signatures: {[1] = sig:mail-tester.local:signature has incorrect length: 12}];\n" + + "\tMIME_GOOD(-0.10)[multipart/alternative,text/plain];\n" + + "\tMIME_TRACE(0.00)[0:+,1:+,2:~]", + expectedScore: 0.90, + expectedThreshold: rspamdDefaultAddHeaderThreshold, + expectedIsSpam: false, + expectedSymbols: map[string]float32{ + "ARC_REJECT": 1.00, + "MIME_GOOD": -0.10, + "MIME_TRACE": 0.00, + }, + expectedSymParams: map[string]string{ + "ARC_REJECT": "cannot verify 1 of 1 signatures: {[1] = sig:mail-tester.local:signature has incorrect length: 12}", + "MIME_GOOD": "multipart/alternative,text/plain", + "MIME_TRACE": "0:+,1:+,2:~", + }, + }, } analyzer := NewRspamdAnalyzer() From d9b9ea87c6dfab9f42a225ecd94c0bde8a19e7a6 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Mon, 9 Mar 2026 13:09:07 +0700 Subject: [PATCH 088/102] refactor: extract email path into standalone card component Move the received chain display out of BlacklistCard into EmailPathCard, giving it its own card styling and placing it as a dedicated section on the report page. --- web/src/lib/components/BlacklistCard.svelte | 10 ++-------- web/src/lib/components/EmailPathCard.svelte | 18 ++++++++++++++---- web/src/routes/test/[test]/+page.svelte | 11 ++++++++++- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/web/src/lib/components/BlacklistCard.svelte b/web/src/lib/components/BlacklistCard.svelte index 7f9b7f2..fec7b09 100644 --- a/web/src/lib/components/BlacklistCard.svelte +++ b/web/src/lib/components/BlacklistCard.svelte @@ -1,18 +1,16 @@
@@ -35,10 +33,6 @@
- {#if receivedChain} - - {/if} -
{#each Object.entries(blacklists) as [ip, checks]}
diff --git a/web/src/lib/components/EmailPathCard.svelte b/web/src/lib/components/EmailPathCard.svelte index 8dc57b0..a4fda45 100644 --- a/web/src/lib/components/EmailPathCard.svelte +++ b/web/src/lib/components/EmailPathCard.svelte @@ -1,5 +1,6 @@ {#if receivedChain && receivedChain.length > 0} -
-
Email Path (Received Chain)
-
+
+
+

+ + Email Path +

+
+
{#each receivedChain as hop, i}
@@ -30,7 +40,7 @@ : "-"}
- {#if hop.with || hop.id} + {#if hop.with || hop.id || hop.from}

{#if hop.with} diff --git a/web/src/routes/test/[test]/+page.svelte b/web/src/routes/test/[test]/+page.svelte index 0c8ea9d..10c4f22 100644 --- a/web/src/routes/test/[test]/+page.svelte +++ b/web/src/routes/test/[test]/+page.svelte @@ -9,6 +9,7 @@ BlacklistCard, ContentAnalysisCard, DnsRecordsCard, + EmailPathCard, ErrorDisplay, HeaderAnalysisCard, PendingState, @@ -294,6 +295,15 @@

+ + {#if report.header_analysis?.received_chain && report.header_analysis.received_chain.length > 0} +
+
+ +
+
+ {/if} + {#if report.dns_results}
@@ -329,7 +339,6 @@ {blacklists} blacklistGrade={report.summary?.blacklist_grade} blacklistScore={report.summary?.blacklist_score} - receivedChain={report.header_analysis?.received_chain} /> {/snippet} From 27650a3496ed6c6d5ab916d96d6495fc10be1877 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Mon, 9 Mar 2026 13:12:36 +0700 Subject: [PATCH 089/102] feat: add raw report display to rspamd card Add a collapsible Raw Report section to RspamdCard, storing the raw X-Spamd-Result header value and displaying it like SpamAssassin's report. --- api/openapi.yaml | 3 +++ pkg/analyzer/rspamd.go | 2 ++ web/src/lib/components/RspamdCard.svelte | 27 +++++++++++++++++++++--- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/api/openapi.yaml b/api/openapi.yaml index c0c3c70..1a9cbbf 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -982,6 +982,9 @@ components: name: "BAYES_HAM" score: -1.9 params: "0.02" + report: + type: string + description: Full rspamd report (raw X-Spamd-Result header) RspamdSymbol: type: object diff --git a/pkg/analyzer/rspamd.go b/pkg/analyzer/rspamd.go index c2ea1cf..f3f548b 100644 --- a/pkg/analyzer/rspamd.go +++ b/pkg/analyzer/rspamd.go @@ -58,6 +58,8 @@ func (a *RspamdAnalyzer) AnalyzeRspamd(email *EmailMessage) *api.RspamdResult { // Parse X-Spamd-Result header (primary source for score, threshold, and symbols) // Format: "default: False [-3.91 / 15.00];\n\tSYMBOL(score)[params]; ..." if spamdResult, ok := headers["X-Spamd-Result"]; ok { + report := strings.ReplaceAll(spamdResult, "; ", ";\n") + result.Report = &report a.parseSpamdResult(spamdResult, result) } diff --git a/web/src/lib/components/RspamdCard.svelte b/web/src/lib/components/RspamdCard.svelte index 2468f90..0db6378 100644 --- a/web/src/lib/components/RspamdCard.svelte +++ b/web/src/lib/components/RspamdCard.svelte @@ -17,8 +17,7 @@ const effectiveAction = $derived.by(() => { const rejectThreshold = rspamd.threshold > 0 ? rspamd.threshold : 15; - if (rspamd.score >= rejectThreshold) - return { label: "Reject", cls: "bg-danger" }; + if (rspamd.score >= rejectThreshold) return { label: "Reject", cls: "bg-danger" }; if (rspamd.score >= RSPAMD_ADD_HEADER_THRESHOLD) return { label: "Add header", cls: "bg-warning text-dark" }; if (rspamd.score >= RSPAMD_GREYLIST_THRESHOLD) @@ -31,7 +30,7 @@

- + rspamd Analysis @@ -108,10 +107,32 @@

{/if} + + {#if rspamd.report} +
+ Raw Report +
{rspamd.report}
+
+ {/if}