package app import ( "bytes" "context" "crypto/rand" "crypto/subtle" "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" "html" "html/template" "io" "io/fs" "log/slog" "net/http" "net/netip" "net/url" "runtime/debug" "sort" "strings" "time" "github.com/b1tsblog/ai-disclosure-standard/internal/background" "github.com/b1tsblog/ai-disclosure-standard/internal/badge" "github.com/b1tsblog/ai-disclosure-standard/internal/declaration" "github.com/b1tsblog/ai-disclosure-standard/internal/i18n" "github.com/b1tsblog/ai-disclosure-standard/internal/marketing" webassets "github.com/b1tsblog/ai-disclosure-standard/web" "github.com/b1tsblog/license-platform/sdk/go/licenseclient" ) type Server struct { cfg Config logger *slog.Logger templates *template.Template metrics *metrics licenses *licenseclient.Client mux *http.ServeMux validateSem chan struct{} } type option struct{ Value, Label string } type fact struct{ Label, Value, Link string } type componentRow struct{ Name, Extent, Activities, Review, Note string } type legalAssessmentView struct{ Heading, Text, Level string } type languageLink struct { Code, Name, URL, AbsoluteURL string Current bool } type pageData struct { Name string BaseURL string ContactURL string Lang string Text map[string]string Languages []i18n.LanguageOption Presets []option Components []option Extents []option Reviews []option Assurances []option Declaration declaration.Declaration Title string Description string BadgeURL string ManifestURL string CanonicalURL string JSONLD template.JS AppConfig template.JS Facts []fact ComponentRows []componentRow Summary []string LegalAssessment legalAssessmentView IsArticle bool License licenseclient.Status CustomText bool CustomBadge bool Marketing marketing.Page Background background.Page LanguageLinks []languageLink DefaultLanguageURL string LegalNav legalNav Legal legalPage CSPNonce string } type clientConfig struct { BaseURL string `json:"baseURL"` SelectedLanguage string `json:"selectedLanguage"` Locales map[string]i18n.Locale `json:"locales"` Capabilities map[string]bool `json:"capabilities"` BulkURL string `json:"bulkURL,omitempty"` } type publicLicenseStatus struct { Edition string `json:"edition"` Licensed bool `json:"licensed"` Features []string `json:"features"` Limits map[string]int64 `json:"limits,omitempty"` ExpiresAt string `json:"expiresAt,omitempty"` } func New(ctx context.Context, cfg Config, logger *slog.Logger) (http.Handler, error) { if err := validateConfig(cfg); err != nil { return nil, fmt.Errorf("invalid configuration: %w", err) } tmpl, err := template.New("root").ParseFS(webassets.Files, "templates/*.html") if err != nil { return nil, fmt.Errorf("parse templates: %w", err) } trustStore, err := productTrustStore() if err != nil { return nil, err } licenses := licenseclient.New(ctx, licenseclient.Config{ Product: ProductID, Token: cfg.LicenseToken, TrustStore: trustStore, BaseURL: cfg.BaseURL, InstanceID: cfg.LicenseInstanceID, Mode: cfg.LicenseMode, ServerURL: cfg.LicenseServerURL, CacheFile: cfg.LicenseCacheFile, RefreshEvery: cfg.LicenseRefreshEvery, RequestTimeout: cfg.LicenseTimeout, ClientVersion: ProductVersion, }) licenses.Start(ctx) s := &Server{cfg: cfg, logger: logger, templates: tmpl, metrics: newMetrics(), licenses: licenses, mux: http.NewServeMux(), validateSem: make(chan struct{}, 32)} s.routes() return s.middleware(s.mux), nil } func (s *Server) routes() { staticFS, _ := fs.Sub(webassets.Files, "static") fileServer := http.FileServer(http.FS(staticFS)) s.mux.HandleFunc("GET /", s.handleIndex) s.mux.HandleFunc("GET /product", s.handleMarketing) s.mux.HandleFunc("GET /background", s.handleBackground) s.mux.HandleFunc("GET /impressum", s.handleImprint) s.mux.HandleFunc("GET /datenschutz", s.handlePrivacy) s.mux.HandleFunc("GET /barrierefreiheit", s.handleAccessibility) s.mux.HandleFunc("GET /install", s.handleMarketingAlias) s.mux.Handle("GET /static/", http.StripPrefix("/static/", cacheStatic(fileServer))) s.mux.HandleFunc("GET /badge/{file}", s.handlePresetBadge) s.mux.HandleFunc("GET /v1/badge.svg", s.handleBadge) s.mux.HandleFunc("GET /declaration", s.handleDeclaration) s.mux.HandleFunc("GET /v1/declaration.json", s.handleManifest) s.mux.HandleFunc("GET /v1/render", s.handleRender) s.mux.HandleFunc("POST /v1/validate", s.handleValidate) s.mux.HandleFunc("GET /v1/capabilities", s.handleCapabilities) s.mux.HandleFunc("GET /schema/v1/declaration.schema.json", s.handleSchema) s.mux.HandleFunc("GET /context/v1", s.handleContext) s.mux.HandleFunc("GET /healthz", s.handleHealth) s.mux.HandleFunc("GET /readyz", s.handleReady) if s.cfg.MetricsEnabled { s.mux.HandleFunc("GET /metrics", s.handleMetrics) } } func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } lang := s.language(r) locale := i18n.Get(lang) cfg := clientConfig{ BaseURL: s.cfg.BaseURL, SelectedLanguage: lang, Locales: i18n.ClientCatalogs(), BulkURL: s.cfg.BulkURL, Capabilities: map[string]bool{ FeatureCustomText: s.licenses.Has(FeatureCustomText), FeatureCustomBadge: s.licenses.Has(FeatureCustomBadge), }, } appJSON, _ := json.Marshal(cfg) data := pageData{ Name: s.cfg.PublicName, BaseURL: s.cfg.BaseURL, ContactURL: s.cfg.ContactURL, Lang: lang, Text: locale.Text, Languages: i18n.Languages(), Presets: presetOptions(locale), Components: orderedOptions(locale.Components, []string{"text", "coverImage", "image", "audio", "video", "code", "other"}), Extents: orderedOptions(locale.Extents, []string{"assisted", "none", "partial", "mostly", "full"}), Reviews: orderedOptions(locale.Reviews, []string{"editorial", "expert", "basic", "none"}), Assurances: orderedOptions(locale.Assurances, []string{"selfDeclared", "technicallyRecorded", "signed", "verified"}), AppConfig: template.JS(appJSON), LegalNav: legalNavFor(lang), License: s.licenses.Status(), CustomText: s.licenses.Has(FeatureCustomText), CustomBadge: s.licenses.Has(FeatureCustomBadge), } s.renderHTML(w, "index.html", data) } func (s *Server) handleBackground(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/background" { http.NotFound(w, r) return } lang := s.language(r) languageLinks, defaultLanguageURL := s.staticLanguageLinks("/background", lang) data := pageData{ Name: s.cfg.PublicName, BaseURL: s.cfg.BaseURL, ContactURL: s.cfg.ContactURL, Lang: lang, Languages: i18n.Languages(), Background: background.Build(lang), License: s.licenses.Status(), LanguageLinks: languageLinks, DefaultLanguageURL: defaultLanguageURL, LegalNav: legalNavFor(lang), } s.renderHTML(w, "background.html", data) } func (s *Server) handleImprint(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/impressum" { http.NotFound(w, r) return } lang := s.language(r) s.renderHTML(w, "legal.html", pageData{Name: s.cfg.PublicName, BaseURL: s.cfg.BaseURL, CanonicalURL: s.cfg.BaseURL + "/impressum", Lang: lang, LegalNav: legalNavFor(lang), Legal: imprintPage(s.cfg, lang)}) } func (s *Server) handlePrivacy(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/datenschutz" { http.NotFound(w, r) return } lang := s.language(r) s.renderHTML(w, "legal.html", pageData{Name: s.cfg.PublicName, BaseURL: s.cfg.BaseURL, CanonicalURL: s.cfg.BaseURL + "/datenschutz", Lang: lang, LegalNav: legalNavFor(lang), Legal: privacyPage(s.cfg, lang)}) } func (s *Server) handleAccessibility(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/barrierefreiheit" { http.NotFound(w, r) return } lang := s.language(r) s.renderHTML(w, "legal.html", pageData{Name: s.cfg.PublicName, BaseURL: s.cfg.BaseURL, CanonicalURL: s.cfg.BaseURL + "/barrierefreiheit", Lang: lang, LegalNav: legalNavFor(lang), Legal: accessibilityPage(s.cfg, lang)}) } func (s *Server) staticLanguageLinks(path, current string) ([]languageLink, string) { links := make([]languageLink, 0, len(i18n.Languages())) for _, language := range i18n.Languages() { values := url.Values{"lang": []string{language.Code}} relative := path + "?" + values.Encode() links = append(links, languageLink{ Code: language.Code, Name: language.Name, URL: relative, AbsoluteURL: s.cfg.BaseURL + relative, Current: language.Code == current, }) } return links, s.cfg.BaseURL + path } func (s *Server) handleMarketing(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/product" { http.NotFound(w, r) return } lang := s.language(r) page := marketing.Build(lang, s.cfg.PublicName, s.cfg.BaseURL, s.cfg.SalesURL, s.cfg.ContactURL, marketing.Prices{ Community: s.cfg.CommunityPrice, Pro: s.cfg.ProPrice, Publisher: s.cfg.PublisherPrice, Agency: s.cfg.AgencyPrice, }) data := pageData{ Name: s.cfg.PublicName, BaseURL: s.cfg.BaseURL, ContactURL: s.cfg.ContactURL, Lang: lang, Languages: i18n.Languages(), Marketing: page, License: s.licenses.Status(), LegalNav: legalNavFor(lang), } s.renderHTML(w, "marketing.html", data) } func (s *Server) handleMarketingAlias(w http.ResponseWriter, r *http.Request) { target := "/product" if raw := r.URL.Query().Encode(); raw != "" { target += "?" + raw } if r.URL.Path == "/install" { target += "#install" } http.Redirect(w, r, target, http.StatusTemporaryRedirect) } func (s *Server) handlePresetBadge(w http.ResponseWriter, r *http.Request) { file := r.PathValue("file") if !strings.HasSuffix(file, ".svg") { http.NotFound(w, r) return } presetID := strings.TrimSuffix(file, ".svg") if _, ok := declaration.Presets[presetID]; !ok { s.problem(w, http.StatusNotFound, "unknown_preset", "Unknown badge preset.") return } q := cloneValues(r.URL.Query()) q.Set("preset", presetID) q.Set("lang", s.languageFromValues(r, q)) s.renderBadge(w, r, q) } func (s *Server) handleBadge(w http.ResponseWriter, r *http.Request) { q := cloneValues(r.URL.Query()) q.Set("lang", s.languageFromValues(r, q)) s.renderBadge(w, r, q) } func (s *Server) renderBadge(w http.ResponseWriter, r *http.Request, q url.Values) { if customTextRequested(q, "label", "badgeLabel", "message", "badgeMessage") && !s.licenses.Has(FeatureCustomBadge) { s.problem(w, http.StatusForbidden, "pro_feature_required", "Custom badge labels require the Pro feature custom_badge.") return } if customTextRequested(q, "leftColor", "rightColor") && !s.licenses.Has(FeatureCustomBadge) { s.problem(w, http.StatusForbidden, "pro_feature_required", "Custom badge colours require the Pro feature custom_badge.") return } leftColor, rightColor := strings.TrimSpace(q.Get("leftColor")), strings.TrimSpace(q.Get("rightColor")) if (leftColor != "" && !validHexColor(leftColor)) || (rightColor != "" && !validHexColor(rightColor)) { s.problem(w, http.StatusBadRequest, "invalid_colour", "Badge colours must use the form #RRGGBB.") return } extent := strings.TrimSpace(q.Get("extent")) presetID := strings.TrimSpace(q.Get("preset")) isArticle := q.Get("mode") == "article" var parsedDeclaration *declaration.Declaration if isArticle || strings.TrimSpace(q.Get("legalContext")) != "" { if parsed, err := s.declarationFromQuery(q); err == nil { parsedDeclaration = &parsed if isArticle { extent = overallExtent(parsed) } } } if p, ok := declaration.Presets[presetID]; ok && extent == "" { extent = p.Extent } if extent == "" { extent = "assisted" } if !declaration.IsValidExtent(extent) { s.problem(w, http.StatusBadRequest, "invalid_extent", "extent must be one of none, assisted, partial, mostly or full") return } locale := i18n.Get(q.Get("lang")) label := locale.Text["ai_label"] message := locale.Extents[extent] if preset, ok := locale.Presets[presetID]; ok { message = preset.Title } if isArticle { message = locale.Text["badge_article"] // Article-level declarations combine several AI-use states. Use a calm // violet as the neutral default instead of inheriting the strongest // component colour (which may be red). Explicit Pro colours still win. if rightColor == "" { rightColor = "#7c3aed" } } if v := firstNonEmpty(q.Get("badgeLabel"), q.Get("label")); v != "" { label = v } if v := firstNonEmpty(q.Get("badgeMessage"), q.Get("message")); v != "" { message = v } // When the supplied legal context indicates a likely Article 50(4) disclosure // case, keep the first-layer wording explicit even for Pro-customised badges. // Custom colours and the left label remain available, but an ambiguous custom // message must not silently replace the generated disclosure wording. if parsedDeclaration != nil { assessment := assessLegalContext(*parsedDeclaration, locale) if assessment.RequiresDisclosure { message = assessment.BadgeMessage } } link := strings.TrimSpace(q.Get("link")) if link == "auto" { copy := cloneValues(q) for _, key := range []string{"link", "label", "message", "style", "theme", "leftColor", "rightColor"} { copy.Del(key) } link = s.cfg.BaseURL + "/declaration?" + copy.Encode() } data, etag := badge.Render(badge.Options{Label: label, Message: message, Extent: extent, Preset: presetID, Article: isArticle, Style: q.Get("style"), Theme: q.Get("theme"), Link: link, LeftColor: leftColor, RightColor: rightColor}) if r.Header.Get("If-None-Match") == etag { w.WriteHeader(http.StatusNotModified) return } s.metrics.badgeRenders.Add(1) w.Header().Set("Content-Type", "image/svg+xml; charset=utf-8") w.Header().Set("Cache-Control", "public, max-age=300, stale-while-revalidate=86400") w.Header().Set("ETag", etag) w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Cross-Origin-Resource-Policy", "cross-origin") w.Header().Set("Content-Security-Policy", "default-src 'none'; sandbox") _, _ = w.Write(data) } func (s *Server) handleDeclaration(w http.ResponseWriter, r *http.Request) { q := cloneValues(r.URL.Query()) q.Set("lang", s.languageFromValues(r, q)) d, err := s.declarationFromQuery(q) if err != nil { s.declarationError(w, err) return } locale := i18n.Get(d.Language) presetText := locale.Presets[q.Get("preset")] title, description := presetText.Title, presetText.Description if title == "" { for _, c := range d.Components { title = locale.Extents[c.AIExtent] break } description = locale.Text["transparency_text"] } if d.Presentation != nil { if d.Presentation.Title != "" { title = d.Presentation.Title } if d.Presentation.Description != "" { description = d.Presentation.Description } } canonicalURL := s.cfg.BaseURL + "/declaration?" + q.Encode() manifestURL := s.cfg.BaseURL + "/v1/declaration.json?" + q.Encode() badgeQ := cloneValues(q) badgeQ.Set("link", canonicalURL) badgeURL := s.cfg.BaseURL + "/v1/badge.svg?" + badgeQ.Encode() jsonLD, _ := json.Marshal(d) isArticle := q.Get("mode") == "article" || len(d.Components) > 1 if isArticle && (d.Presentation == nil || d.Presentation.Title == "") { title = locale.Text["article_title"] description = articleShortDescription(d, locale) } languageLinks, defaultLanguageURL := s.declarationLanguageLinks(q, d.Language) data := pageData{ Name: s.cfg.PublicName, BaseURL: s.cfg.BaseURL, ContactURL: s.cfg.ContactURL, Lang: d.Language, Text: locale.Text, Languages: i18n.Languages(), Declaration: d, Title: title, Description: description, BadgeURL: badgeURL, ManifestURL: manifestURL, CanonicalURL: canonicalURL, JSONLD: template.JS(jsonLD), Facts: declarationFacts(d, locale), ComponentRows: declarationComponentRows(d, locale), Summary: declarationSummary(d, locale), LegalAssessment: legalAssessmentPage(d, locale), IsArticle: isArticle, License: s.licenses.Status(), LanguageLinks: languageLinks, DefaultLanguageURL: defaultLanguageURL, LegalNav: legalNavFor(d.Language), } w.Header().Set("Link", "<"+manifestURL+">; rel=describedby; type=application/ld+json") s.renderHTML(w, "declaration.html", data) } func (s *Server) declarationLanguageLinks(q url.Values, current string) ([]languageLink, string) { links := make([]languageLink, 0, len(i18n.Languages())) for _, language := range i18n.Languages() { values := cloneValues(q) values.Set("lang", language.Code) relative := "/declaration?" + values.Encode() links = append(links, languageLink{ Code: language.Code, Name: language.Name, URL: relative, AbsoluteURL: s.cfg.BaseURL + relative, Current: language.Code == current, }) } defaultValues := cloneValues(q) defaultValues.Del("lang") defaultURL := s.cfg.BaseURL + "/declaration" if encoded := defaultValues.Encode(); encoded != "" { defaultURL += "?" + encoded } return links, defaultURL } func (s *Server) handleManifest(w http.ResponseWriter, r *http.Request) { q := cloneValues(r.URL.Query()) q.Set("lang", s.languageFromValues(r, q)) d, err := s.declarationFromQuery(q) if err != nil { s.declarationError(w, err) return } w.Header().Set("Content-Type", "application/ld+json; charset=utf-8") w.Header().Set("Cache-Control", "public, max-age=300, stale-while-revalidate=86400") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Cross-Origin-Resource-Policy", "cross-origin") _ = json.NewEncoder(w).Encode(d) } type renderedArtifacts struct { Subject string `json:"subject,omitempty"` DeclarationURL string `json:"declarationUrl"` BadgeURL string `json:"badgeUrl"` ManifestURL string `json:"manifestUrl"` AccessibleText string `json:"accessibleText"` HTML string `json:"html"` EmbedHTML string `json:"embedHtml"` Markdown string `json:"markdown"` JSONLD declaration.Declaration `json:"jsonLd"` } func (s *Server) handleRender(w http.ResponseWriter, r *http.Request) { q := cloneValues(r.URL.Query()) theme := strings.TrimSpace(q.Get("theme")) q.Del("theme") if theme == "" { theme = "mono" } if theme != "mono" && theme != "color" && theme != "emoji" { s.problem(w, http.StatusBadRequest, "invalid_theme", "theme must be one of mono, color or emoji") return } q.Set("lang", s.languageFromValues(r, q)) d, err := s.declarationFromQuery(q) if err != nil { s.declarationError(w, err) return } locale := i18n.Get(d.Language) declarationURL := s.cfg.BaseURL + "/declaration?" + q.Encode() manifestURL := s.cfg.BaseURL + "/v1/declaration.json?" + q.Encode() badgeQ := cloneValues(q) badgeQ.Set("theme", theme) badgeQ.Set("link", declarationURL) badgeURL := s.cfg.BaseURL + "/v1/badge.svg?" + badgeQ.Encode() assessment := assessLegalContext(d, locale) badgeText := renderBadgeAlt(q, d, locale, assessment) detailsText := strings.TrimSpace(locale.Text["embed_details"]) if detailsText == "" { detailsText = "AI usage details" } accessibleText := badgeText + " – " + detailsText badgeHeight := 20 if theme == "emoji" { badgeHeight = 48 } htmlMarkup := fmt.Sprintf(`
`, html.EscapeString(declarationURL), html.EscapeString(badgeURL), badgeHeight, html.EscapeString(accessibleText)) markdown := fmt.Sprintf(`[ %s](%s)`, badgeURL, markdownText(accessibleText), declarationURL) jsonLDBytes, err := json.MarshalIndent(d, "", " ") if err != nil { s.logger.Error("could not marshal render JSON-LD", "error", err) s.problem(w, http.StatusInternalServerError, "render_failed", "Could not render JSON-LD.") return } embedMarkup := htmlMarkup + "\n" + `" w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Cache-Control", "no-store") _ = json.NewEncoder(w).Encode(renderedArtifacts{ Subject: d.Subject, DeclarationURL: declarationURL, BadgeURL: badgeURL, ManifestURL: manifestURL, AccessibleText: accessibleText, HTML: htmlMarkup, EmbedHTML: embedMarkup, Markdown: markdown, JSONLD: d, }) } func renderBadgeAlt(q url.Values, d declaration.Declaration, locale i18n.Locale, assessment legalAssessment) string { if assessment.RequiresDisclosure && assessment.BadgeMessage != "" { return assessment.BadgeMessage } if q.Get("mode") == "article" || len(d.Components) > 1 { if value := locale.Text["badge_article"]; value != "" { return value } } if preset, ok := locale.Presets[q.Get("preset")]; ok && preset.Title != "" { return preset.Title } for _, component := range d.Components { if value := locale.Extents[component.AIExtent]; value != "" { return value } } return "AI usage disclosure" } func markdownAlt(value string) string { value = strings.ReplaceAll(value, `\`, `\\`) value = strings.ReplaceAll(value, `]`, `\]`) return strings.ReplaceAll(value, `[`, `\[`) } func markdownText(value string) string { for _, marker := range []string{"\\", "*", "_", "`", "[", "]"} { value = strings.ReplaceAll(value, marker, "\\"+marker) } return value } func (s *Server) declarationFromQuery(q url.Values) (declaration.Declaration, error) { return declaration.NewFromQueryWithOptions(q, s.cfg.BaseURL+"/context/v1", declaration.ParseOptions{ AllowCustomText: s.licenses.Has(FeatureCustomText), AllowCustomBadge: s.licenses.Has(FeatureCustomBadge), DefaultLanguage: s.cfg.DefaultLanguage, }) } func (s *Server) declarationError(w http.ResponseWriter, err error) { if errors.Is(err, declaration.ErrCustomTextRequiresPro) || errors.Is(err, declaration.ErrCustomBadgeRequiresPro) { s.problem(w, http.StatusForbidden, "pro_feature_required", err.Error()) return } s.problem(w, http.StatusBadRequest, "invalid_declaration", err.Error()) } func (s *Server) handleValidate(w http.ResponseWriter, r *http.Request) { s.metrics.validationRequests.Add(1) if contentType := strings.ToLower(strings.TrimSpace(strings.Split(r.Header.Get("Content-Type"), ";")[0])); contentType != "" && contentType != "application/json" { s.metrics.validationFailures.Add(1) s.problem(w, http.StatusUnsupportedMediaType, "unsupported_media_type", "Content-Type must be application/json.") return } select { case s.validateSem <- struct{}{}: defer func() { <-s.validateSem }() default: w.Header().Set("Retry-After", "1") s.problem(w, http.StatusServiceUnavailable, "validation_busy", "Too many concurrent validation requests.") return } body := http.MaxBytesReader(w, r.Body, 1<<20) defer body.Close() dec := json.NewDecoder(body) dec.DisallowUnknownFields() var d declaration.Declaration if err := dec.Decode(&d); err != nil { s.metrics.validationFailures.Add(1) var tooLarge *http.MaxBytesError if errors.As(err, &tooLarge) { s.problem(w, http.StatusRequestEntityTooLarge, "request_too_large", "JSON request body exceeds 1 MiB.") return } s.problem(w, http.StatusBadRequest, "invalid_json", err.Error()) return } if err := ensureEOF(dec); err != nil { s.metrics.validationFailures.Add(1) s.problem(w, http.StatusBadRequest, "invalid_json", err.Error()) return } if err := declaration.Validate(d); err != nil { s.metrics.validationFailures.Add(1) s.problem(w, http.StatusUnprocessableEntity, "validation_failed", err.Error()) return } s.writeJSON(w, http.StatusOK, map[string]any{"valid": true, "schemaVersion": d.SchemaVersion}) } func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request) { if token := s.cfg.MetricsToken; token != "" { provided := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") if len(provided) != len(token) || subtle.ConstantTimeCompare([]byte(provided), []byte(token)) != 1 { w.Header().Set("WWW-Authenticate", `Bearer realm="metrics"`) http.Error(w, "unauthorized", http.StatusUnauthorized) return } } s.metrics.serveHTTP(w, r) } func (s *Server) handleCapabilities(w http.ResponseWriter, _ *http.Request) { s.writeJSON(w, http.StatusOK, map[string]any{"license": publicLicense(s.licenses.Status()), "supportedLanguages": languageCodes()}) } func publicLicense(status licenseclient.Status) publicLicenseStatus { return publicLicenseStatus{ Edition: status.Edition, Licensed: status.Licensed, Features: append([]string(nil), status.Features...), Limits: status.Limits, ExpiresAt: status.ExpiresAt, } } func (s *Server) handleSchema(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/schema+json; charset=utf-8") w.Header().Set("Cache-Control", "public, max-age=3600") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Cross-Origin-Resource-Policy", "cross-origin") _, _ = w.Write([]byte(strings.ReplaceAll(declarationSchema, "__BASE_URL__", s.cfg.BaseURL))) } func (s *Server) handleContext(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/ld+json; charset=utf-8") w.Header().Set("Cache-Control", "public, max-age=3600") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Cross-Origin-Resource-Policy", "cross-origin") _, _ = w.Write([]byte(strings.ReplaceAll(jsonLDContext, "__BASE_URL__", s.cfg.BaseURL))) } func (s *Server) handleHealth(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Cache-Control", "no-store") _, _ = io.WriteString(w, "ok\n") } func (s *Server) handleReady(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Cache-Control", "no-store") _, _ = io.WriteString(w, "ready\n") } func (s *Server) renderHTML(w http.ResponseWriter, name string, data pageData) { nonce := randomNonce() data.CSPNonce = nonce var buf bytes.Buffer if err := s.templates.ExecuteTemplate(&buf, name, data); err != nil { s.logger.Error("template render failed", "template", name, "error", err) http.Error(w, "internal server error", http.StatusInternalServerError) return } csp := "default-src 'self'; script-src 'self' 'nonce-" + nonce + "'; style-src 'self'; img-src 'self' data:; connect-src 'self'; font-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'" if strings.HasPrefix(s.cfg.BaseURL, "https://") { csp += "; upgrade-insecure-requests" } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "no-store") w.Header().Set("Content-Security-Policy", csp) w.Header().Set("Cross-Origin-Resource-Policy", "same-origin") w.Header().Set("Cross-Origin-Opener-Policy", "same-origin") _, _ = w.Write(buf.Bytes()) } func (s *Server) language(r *http.Request) string { return i18n.Resolve(r.URL.Query().Get("lang"), r.Header.Get("Accept-Language"), s.cfg.DefaultLanguage) } func (s *Server) languageFromValues(r *http.Request, values url.Values) string { return i18n.Resolve(values.Get("lang"), r.Header.Get("Accept-Language"), s.cfg.DefaultLanguage) } func (s *Server) middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { started := time.Now() s.metrics.requests.Add(1) requestID := strings.TrimSpace(r.Header.Get("X-Request-ID")) if !validRequestID(requestID) { requestID = randomID() } w.Header().Set("X-Request-ID", requestID) w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") w.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()") w.Header().Set("Cross-Origin-Resource-Policy", "same-origin") if s.cfg.EnableHSTS && strings.HasPrefix(s.cfg.BaseURL, "https://") { w.Header().Set("Strict-Transport-Security", "max-age=31536000") } if strings.HasPrefix(r.URL.Path, "/v1/") || strings.HasPrefix(r.URL.Path, "/schema/") || strings.HasPrefix(r.URL.Path, "/context/") { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } } rw := &responseWriter{ResponseWriter: w, status: http.StatusOK} defer func() { if recovered := recover(); recovered != nil { s.metrics.panics.Add(1) s.logger.Error("handler panic", "request_id", requestID, "panic", recovered, "stack", string(debug.Stack())) http.Error(rw, "internal server error", http.StatusInternalServerError) } attrs := []any{"request_id", requestID, "method", r.Method, "path", r.URL.Path, "status", rw.status, "bytes", rw.bytes, "duration_ms", time.Since(started).Milliseconds()} if s.cfg.LogClientIP { attrs = append(attrs, "remote", clientIP(r, s.cfg.TrustProxy, s.cfg.TrustedProxies)) } s.logger.Info("request", attrs...) }() next.ServeHTTP(rw, r) }) } func (s *Server) problem(w http.ResponseWriter, status int, code, detail string) { w.Header().Set("Content-Type", "application/problem+json; charset=utf-8") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(map[string]any{"type": s.cfg.BaseURL + "/problems/" + code, "title": http.StatusText(status), "status": status, "code": code, "detail": detail}) } func (s *Server) writeJSON(w http.ResponseWriter, status int, value any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(value) } type responseWriter struct { http.ResponseWriter status, bytes int } func (w *responseWriter) WriteHeader(status int) { w.status = status w.ResponseWriter.WriteHeader(status) } func (w *responseWriter) Write(b []byte) (int, error) { n, err := w.ResponseWriter.Write(b) w.bytes += n return n, err } func cacheStatic(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "public, max-age=3600") next.ServeHTTP(w, r) }) } func presetOptions(locale i18n.Locale) []option { ids := []string{"research", "no-ai", "summary", "full"} out := make([]option, 0, len(ids)) for _, id := range ids { out = append(out, option{Value: id, Label: locale.Presets[id].Title}) } return out } func orderedOptions(values map[string]string, order []string) []option { out := make([]option, 0, len(order)) for _, key := range order { out = append(out, option{Value: key, Label: values[key]}) } return out } func declarationFacts(d declaration.Declaration, locale i18n.Locale) []fact { facts := []fact{{Label: locale.Text["fact_assurance"], Value: locale.Assurances[d.Assurance]}} if d.Subject != "" { facts = append(facts, fact{Label: locale.Text["fact_subject"], Value: d.Subject, Link: d.Subject}) } if d.Author != nil && d.Author.Name != "" { facts = append(facts, fact{Label: locale.Text["fact_author"], Value: d.Author.Name, Link: d.Author.URL}) } if d.EditorialResponsibility != nil && d.EditorialResponsibility.Name != "" { facts = append(facts, fact{Label: locale.Text["fact_responsibility"], Value: d.EditorialResponsibility.Name, Link: d.EditorialResponsibility.URL}) if d.EditorialResponsibility.Role != "" { role := locale.Text["responsibility_"+d.EditorialResponsibility.Role] if role == "" { role = d.EditorialResponsibility.Role } facts = append(facts, fact{Label: locale.Text["fact_responsibility_type"], Value: role}) } } if d.ComplaintsContact != nil && d.ComplaintsContact.Name != "" { link := d.ComplaintsContact.URL if link == "" && d.ComplaintsContact.Email != "" { link = "mailto:" + d.ComplaintsContact.Email } facts = append(facts, fact{Label: locale.Text["fact_complaints_contact"], Value: d.ComplaintsContact.Name, Link: link}) if d.ComplaintsContact.Email != "" { facts = append(facts, fact{Label: locale.Text["fact_complaints_email"], Value: d.ComplaintsContact.Email, Link: "mailto:" + d.ComplaintsContact.Email}) } } if d.LegalContext != nil { if len(d.LegalContext.Categories) > 0 { labels := make([]string, 0, len(d.LegalContext.Categories)) for _, category := range d.LegalContext.Categories { if label := legalCategoryLabel(locale, category); label != "" { labels = append(labels, label) } } if len(labels) > 0 { facts = append(facts, fact{Label: locale.Text["fact_legal_context"], Value: strings.Join(labels, "; ")}) } } if d.LegalContext.Role != "" { facts = append(facts, fact{Label: locale.Text["fact_legal_role"], Value: locale.Text["legal_role_"+d.LegalContext.Role]}) } if d.LegalContext.UseContext != "" { facts = append(facts, fact{Label: locale.Text["fact_use_context"], Value: locale.Text["use_context_"+d.LegalContext.UseContext]}) } if d.LegalContext.OutputDate != "" { facts = append(facts, fact{Label: locale.Text["fact_output_date"], Value: d.LegalContext.OutputDate}) } for _, item := range []struct { label string value string }{ {locale.Text["fact_deepfake_assessment"], d.LegalContext.DeepfakeAssessment}, {locale.Text["fact_public_interest_assessment"], d.LegalContext.PublicInterestAssessment}, {locale.Text["fact_creative_work_assessment"], d.LegalContext.CreativeWorkAssessment}, } { if item.value == "" { continue } translated := locale.Text["assessment_"+item.value] if translated == "" { translated = item.value } facts = append(facts, fact{Label: item.label, Value: translated}) } if d.LegalContext.LawEnforcementAuthorization != "" { value := locale.Text["law_enforcement_"+d.LegalContext.LawEnforcementAuthorization] if value == "" { value = d.LegalContext.LawEnforcementAuthorization } facts = append(facts, fact{Label: locale.Text["fact_law_enforcement_authorization"], Value: value}) } } if d.DeclaredAt != "" { facts = append(facts, fact{Label: locale.Text["fact_declared_at"], Value: d.DeclaredAt}) } return facts } func declarationComponentRows(d declaration.Declaration, locale i18n.Locale) []componentRow { order := []string{"text", "coverImage", "image", "research", "translation", "audio", "video", "code", "other"} rows := make([]componentRow, 0, len(d.Components)) for _, name := range order { component, ok := d.Components[name] if !ok { continue } activities := make([]string, 0, len(component.Activities)) for _, activity := range component.Activities { if translated := locale.Activities[activity]; translated != "" { activities = append(activities, translated) } else { activities = append(activities, activity) } } activityText := locale.Text["not_specified"] if component.AIExtent == "none" { activityText = locale.Text["not_applicable"] } if activityText == "" { activityText = locale.Text["none_value"] } if len(activities) > 0 { activityText = strings.Join(activities, ", ") } rows = append(rows, componentRow{Name: locale.Components[name], Extent: locale.Extents[component.AIExtent], Activities: activityText, Review: locale.Reviews[component.HumanReview], Note: component.Note}) } return rows } func overallExtent(d declaration.Declaration) string { rank := map[string]int{"none": 0, "assisted": 1, "partial": 2, "mostly": 3, "full": 4} best, bestRank := "none", 0 for _, c := range d.Components { if rank[c.AIExtent] > bestRank { best, bestRank = c.AIExtent, rank[c.AIExtent] } } return best } func articleShortDescription(d declaration.Declaration, locale i18n.Locale) string { rows := declarationComponentRows(d, locale) parts := make([]string, 0, len(rows)) for _, row := range rows { parts = append(parts, row.Name+": "+row.Extent) } return strings.Join(parts, " · ") } func declarationSummary(d declaration.Declaration, locale i18n.Locale) []string { if len(d.Components) == 0 { return nil } intro := map[string]string{ "de": "Diese Erklärung dokumentiert nachvollziehbar, in welchen Bereichen künstliche Intelligenz bei der Erstellung und Veröffentlichung des gekennzeichneten Inhalts eingesetzt wurde.", "en": "This declaration documents, in a transparent and traceable form, where artificial intelligence was used in creating and publishing the labelled content.", "fr": "Cette déclaration documente de manière transparente et traçable les domaines dans lesquels l’intelligence artificielle a été utilisée pour créer et publier le contenu identifié.", "es": "Esta declaración documenta de forma transparente y trazable en qué áreas se utilizó inteligencia artificial para crear y publicar el contenido identificado.", "it": "La presente dichiarazione documenta in modo trasparente e tracciabile gli ambiti in cui l’intelligenza artificiale è stata utilizzata per creare e pubblicare il contenuto indicato.", "nl": "Deze verklaring documenteert op transparante en navolgbare wijze op welke onderdelen kunstmatige intelligentie is gebruikt bij het maken en publiceren van de aangeduide inhoud.", "pt": "Esta declaração documenta, de forma transparente e rastreável, as áreas em que a inteligência artificial foi utilizada na criação e publicação do conteúdo identificado.", "pl": "Niniejsza deklaracja w przejrzysty i możliwy do prześledzenia sposób dokumentuje obszary, w których wykorzystano sztuczną inteligencję podczas tworzenia i publikacji oznaczonej treści.", } lang := locale.Code if intro[lang] == "" { lang = "en" } statements := []string{intro[lang]} for _, name := range []string{"text", "coverImage", "image", "research", "translation", "audio", "video", "code", "other"} { component, ok := d.Components[name] if !ok { continue } statements = append(statements, componentSummaryStatement(locale, name, component)) } closing := map[string][]string{ "de": { "Sämtliche Angaben werden unter einem gemeinsamen Link veröffentlicht und stehen zusätzlich als maschinenlesbares JSON-LD zur Verfügung.", "Eine redaktionelle Verantwortung im Sinne von Art. 50 Abs. 4 wird nur dann als angegeben behandelt, wenn Rolle und verantwortliche Person oder Organisation in den Metadaten ausdrücklich benannt sind.", }, "en": { "All information is published under a single link and is also available as machine-readable JSON-LD.", "Editorial responsibility for the purposes of Article 50(4) is treated as stated only where both the role and the responsible person or organisation are expressly named in the metadata.", }, "fr": { "Toutes les informations sont publiées sous un lien unique et sont également disponibles au format JSON-LD lisible par machine.", "La responsabilité du contenu et de sa publication éditoriale demeure celle de la personne ou de l’organisation éditrice, indépendamment du niveau d’intervention de l’IA indiqué.", }, "es": { "Toda la información se publica mediante un único enlace y también está disponible como JSON-LD legible por máquina.", "La responsabilidad sobre el contenido y su publicación editorial permanece en la persona u organización editora, con independencia del nivel de intervención de la IA indicado.", }, "it": { "Tutte le informazioni sono pubblicate tramite un unico collegamento e sono disponibili anche come JSON-LD leggibile automaticamente.", "La responsabilità del contenuto e della sua pubblicazione editoriale resta in capo alla persona o all’organizzazione che lo pubblica, indipendentemente dal livello dichiarato di intervento dell’IA.", }, "nl": { "Alle gegevens worden via één gezamenlijke link gepubliceerd en zijn daarnaast beschikbaar als machineleesbare JSON-LD.", "De inhoudelijke en redactionele verantwoordelijkheid blijft, ongeacht de vermelde mate van AI-gebruik, bij de publicerende persoon of organisatie.", }, "pt": { "Todas as informações são publicadas através de uma única ligação e estão igualmente disponíveis como JSON-LD legível por máquina.", "A responsabilidade pelo conteúdo e pela sua publicação editorial permanece com a pessoa ou organização responsável pela publicação, independentemente do grau de utilização de IA indicado.", }, "pl": { "Wszystkie informacje są publikowane pod jednym wspólnym odsyłaczem i są również dostępne jako maszynowo czytelny JSON-LD.", "Odpowiedzialność za treść i jej redakcyjną publikację pozostaje po stronie publikującej osoby lub organizacji, niezależnie od wskazanego zakresu użycia AI.", }, } end := closing[lang] end = append(end, assuranceStatement(d.Assurance, locale)) return []string{strings.Join(statements, " "), strings.Join(end, " ")} } type legalAssessment struct { RequiresDisclosure bool BadgeMessage string Text string Level string } func legalCategorySet(d declaration.Declaration) map[string]bool { out := map[string]bool{} if d.LegalContext == nil { return out } for _, category := range d.LegalContext.Categories { out[category] = true } if d.LegalContext.DeepfakeAssessment == "yes" { out["deepfake"] = true } if d.LegalContext.PublicInterestAssessment == "yes" { out["publicInterestText"] = true } if d.LegalContext.CreativeWorkAssessment == "yes" { out["artisticCreativeSatiricalFictional"] = true } return out } func legalCategoryLabel(locale i18n.Locale, category string) string { switch category { case "deepfake": return locale.Text["legal_deepfake"] case "publicInterestText": return locale.Text["legal_public_interest"] case "artisticCreativeSatiricalFictional": return locale.Text["legal_creative"] case "otherVoluntary": return locale.Text["legal_other_voluntary"] default: return category } } func hasSubstantiveTextReview(d declaration.Declaration) bool { text, ok := d.Components["text"] if !ok { return false } return text.HumanReview == "editorial" || text.HumanReview == "expert" } func hasExplicitEditorialResponsibility(d declaration.Declaration) bool { return d.EditorialResponsibility != nil && strings.TrimSpace(d.EditorialResponsibility.Name) != "" && strings.TrimSpace(d.EditorialResponsibility.Role) != "" } func assessLegalContext(d declaration.Declaration, locale i18n.Locale) legalAssessment { categories := legalCategorySet(d) if d.LegalContext != nil { if d.LegalContext.UseContext == "personalNonProfessional" { return legalAssessment{Text: locale.Text["legal_assessment_personal"], Level: "info"} } if d.LegalContext.OutputDate != "" { if outputDate, err := time.Parse("2006-01-02", d.LegalContext.OutputDate); err == nil && outputDate.Before(time.Date(2026, 8, 2, 0, 0, 0, 0, time.UTC)) { return legalAssessment{Text: locale.Text["legal_assessment_pre_application"], Level: "info"} } } if d.LegalContext.Role == "provider" { return legalAssessment{Text: locale.Text["legal_assessment_provider"], Level: "attention"} } } deepfakeUnsure := d.LegalContext != nil && d.LegalContext.DeepfakeAssessment == "unsure" publicInterestUnsure := d.LegalContext != nil && d.LegalContext.PublicInterestAssessment == "unsure" if len(categories) == 0 && !deepfakeUnsure && !publicInterestUnsure { return legalAssessment{Text: locale.Text["legal_assessment_none"], Level: "info"} } if categories["otherVoluntary"] { return legalAssessment{Text: locale.Text["legal_assessment_voluntary"], Level: "info"} } if d.LegalContext != nil && d.LegalContext.LawEnforcementAuthorization == "yes" && (categories["deepfake"] || categories["publicInterestText"] || deepfakeUnsure || publicInterestUnsure) { return legalAssessment{Text: locale.Text["legal_assessment_law_enforcement"], Level: "info"} } if deepfakeUnsure { return legalAssessment{RequiresDisclosure: true, BadgeMessage: locale.Text["badge_legal_disclosure"], Text: locale.Text["legal_assessment_deepfake_unsure"], Level: "attention"} } if categories["deepfake"] { text := locale.Text["legal_assessment_deepfake"] if categories["artisticCreativeSatiricalFictional"] { text = locale.Text["legal_assessment_deepfake_creative"] } return legalAssessment{RequiresDisclosure: true, BadgeMessage: locale.Text["badge_legal_disclosure"], Text: text, Level: "attention"} } if publicInterestUnsure { return legalAssessment{RequiresDisclosure: true, BadgeMessage: locale.Text["badge_public_interest"], Text: locale.Text["legal_assessment_public_unsure"], Level: "attention"} } if categories["publicInterestText"] { if hasSubstantiveTextReview(d) && hasExplicitEditorialResponsibility(d) { return legalAssessment{Text: locale.Text["legal_assessment_public_exemption"], Level: "info"} } return legalAssessment{RequiresDisclosure: true, BadgeMessage: locale.Text["badge_public_interest"], Text: locale.Text["legal_assessment_public_required"], Level: "attention"} } return legalAssessment{Text: locale.Text["legal_assessment_voluntary"], Level: "info"} } func legalAssessmentPage(d declaration.Declaration, locale i18n.Locale) legalAssessmentView { a := assessLegalContext(d, locale) return legalAssessmentView{Heading: locale.Text["legal_assessment_heading"], Text: a.Text, Level: a.Level} } func componentSummaryStatement(locale i18n.Locale, name string, component declaration.Component) string { componentName := locale.Components[name] if componentName == "" { componentName = name } extent := locale.Extents[component.AIExtent] activities := make([]string, 0, len(component.Activities)) for _, activity := range component.Activities { if value := locale.Activities[activity]; value != "" { activities = append(activities, value) } else { activities = append(activities, activity) } } activityText := strings.Join(activities, ", ") review := locale.Reviews[component.HumanReview] switch locale.Code { case "de": if component.AIExtent == "none" { if component.HumanReview == "none" { return fmt.Sprintf("Für den Bereich „%s“ wurde nach den vorliegenden Angaben keine KI eingesetzt.", componentName) } return fmt.Sprintf("Für den Bereich „%s“ wurde nach den vorliegenden Angaben keine KI eingesetzt; ergänzend ist eine %s menschliche Prüfung dokumentiert.", componentName, germanReviewAdjective(component.HumanReview)) } purpose := "" if activityText != "" { purpose = " für " + activityText } if component.HumanReview == "none" { return fmt.Sprintf("Für den Bereich „%s“ ist %s%s dokumentiert; eine menschliche Prüfung der KI-bezogenen Ergebnisse ist nicht angegeben.", componentName, germanExtentPhrase(component.AIExtent), purpose) } return fmt.Sprintf("Für den Bereich „%s“ ist %s%s dokumentiert; die KI-bezogenen Ergebnisse wurden %s geprüft.", componentName, germanExtentPhrase(component.AIExtent), purpose, germanReviewAdverb(component.HumanReview)) case "en": if component.AIExtent == "none" { if component.HumanReview == "none" { return fmt.Sprintf("According to the information provided, no AI was used for “%s”.", componentName) } return fmt.Sprintf("According to the information provided, no AI was used for “%s”; a %s human review is nevertheless recorded.", componentName, strings.ToLower(review)) } purpose := "" if activityText != "" { purpose = " for " + activityText } if component.HumanReview == "none" { return fmt.Sprintf("For “%s”, %s%s is recorded; no human review of the AI-related output is specified.", componentName, strings.ToLower(extent), purpose) } return fmt.Sprintf("For “%s”, %s%s is recorded; the AI-related output underwent %s human review.", componentName, strings.ToLower(extent), purpose, strings.ToLower(review)) default: if component.AIExtent == "none" { return genericNoAIStatement(locale.Code, componentName, component.HumanReview, review) } return genericAIStatement(locale.Code, componentName, extent, activityText, component.HumanReview, review) } } func germanExtentPhrase(extent string) string { switch extent { case "assisted": return "eine KI-unterstützte Nutzung" case "partial": return "eine teilweise KI-generierte Erstellung" case "mostly": return "eine überwiegend KI-generierte Erstellung" case "full": return "eine vollständig KI-generierte Erstellung" default: return "eine KI-Nutzung" } } func germanReviewAdverb(review string) string { switch review { case "basic": return "grundlegend" case "editorial": return "redaktionell" case "expert": return "fachlich" default: return "menschlich" } } func germanReviewAdjective(review string) string { switch review { case "basic": return "grundlegende" case "editorial": return "redaktionelle" case "expert": return "fachliche" default: return "menschliche" } } func genericNoAIStatement(lang, componentName, reviewCode, review string) string { withReview := reviewCode != "none" switch lang { case "fr": if withReview { return fmt.Sprintf("Selon les informations fournies, aucune IA n’a été utilisée pour « %s » ; une vérification humaine %s est néanmoins documentée.", componentName, strings.ToLower(review)) } return fmt.Sprintf("Selon les informations fournies, aucune IA n’a été utilisée pour « %s ».", componentName) case "es": if withReview { return fmt.Sprintf("Según la información proporcionada, no se utilizó IA en «%s»; no obstante, se documenta una revisión humana %s.", componentName, strings.ToLower(review)) } return fmt.Sprintf("Según la información proporcionada, no se utilizó IA en «%s».", componentName) case "it": if withReview { return fmt.Sprintf("Secondo le informazioni fornite, per «%s» non è stata utilizzata l’IA; è comunque documentata una revisione umana %s.", componentName, strings.ToLower(review)) } return fmt.Sprintf("Secondo le informazioni fornite, per «%s» non è stata utilizzata l’IA.", componentName) case "nl": if withReview { return fmt.Sprintf("Volgens de verstrekte informatie is voor ‘%s’ geen AI gebruikt; er is wel een %s menselijke controle vastgelegd.", componentName, strings.ToLower(review)) } return fmt.Sprintf("Volgens de verstrekte informatie is voor ‘%s’ geen AI gebruikt.", componentName) case "pt": if withReview { return fmt.Sprintf("De acordo com as informações fornecidas, não foi utilizada IA em «%s»; está, ainda assim, documentada uma revisão humana %s.", componentName, strings.ToLower(review)) } return fmt.Sprintf("De acordo com as informações fornecidas, não foi utilizada IA em «%s».", componentName) case "pl": if withReview { return fmt.Sprintf("Zgodnie z podanymi informacjami w obszarze „%s” nie użyto AI; udokumentowano jednak weryfikację człowieka na poziomie %s.", componentName, review) } return fmt.Sprintf("Zgodnie z podanymi informacjami w obszarze „%s” nie użyto AI.", componentName) default: return fmt.Sprintf("According to the information provided, no AI was used for “%s”.", componentName) } } func genericAIStatement(lang, componentName, extent, activities string, reviewCode, review string) string { purpose := activities if purpose == "" { purpose = "—" } switch lang { case "fr": if reviewCode == "none" { return fmt.Sprintf("Pour « %s », l’utilisation déclarée est %s, avec pour finalité %s ; aucune vérification humaine des résultats liés à l’IA n’est indiquée.", componentName, strings.ToLower(extent), purpose) } return fmt.Sprintf("Pour « %s », l’utilisation déclarée est %s, avec pour finalité %s ; les résultats liés à l’IA ont fait l’objet d’une vérification humaine %s.", componentName, strings.ToLower(extent), purpose, strings.ToLower(review)) case "es": if reviewCode == "none" { return fmt.Sprintf("Para «%s» se declara %s, con la finalidad %s; no se indica una revisión humana de los resultados relacionados con la IA.", componentName, strings.ToLower(extent), purpose) } return fmt.Sprintf("Para «%s» se declara %s, con la finalidad %s; los resultados relacionados con la IA fueron objeto de una revisión humana %s.", componentName, strings.ToLower(extent), purpose, strings.ToLower(review)) case "it": if reviewCode == "none" { return fmt.Sprintf("Per «%s» è dichiarato %s, con finalità %s; non è indicata una revisione umana dei risultati legati all’IA.", componentName, strings.ToLower(extent), purpose) } return fmt.Sprintf("Per «%s» è dichiarato %s, con finalità %s; i risultati legati all’IA sono stati sottoposti a revisione umana %s.", componentName, strings.ToLower(extent), purpose, strings.ToLower(review)) case "nl": if reviewCode == "none" { return fmt.Sprintf("Voor ‘%s’ is %s vastgelegd, met als doel %s; er is geen menselijke controle van de AI-gerelateerde resultaten aangegeven.", componentName, strings.ToLower(extent), purpose) } return fmt.Sprintf("Voor ‘%s’ is %s vastgelegd, met als doel %s; de AI-gerelateerde resultaten hebben een %s menselijke controle ondergaan.", componentName, strings.ToLower(extent), purpose, strings.ToLower(review)) case "pt": if reviewCode == "none" { return fmt.Sprintf("Para «%s» está documentado %s, com a finalidade %s; não é indicada uma revisão humana dos resultados relacionados com IA.", componentName, strings.ToLower(extent), purpose) } return fmt.Sprintf("Para «%s» está documentado %s, com a finalidade %s; os resultados relacionados com IA foram sujeitos a uma revisão humana %s.", componentName, strings.ToLower(extent), purpose, strings.ToLower(review)) case "pl": if reviewCode == "none" { return fmt.Sprintf("Dla obszaru „%s” udokumentowano %s w celu %s; nie wskazano weryfikacji człowieka dla wyników związanych z AI.", componentName, extent, purpose) } return fmt.Sprintf("Dla obszaru „%s” udokumentowano %s w celu %s; wyniki związane z AI poddano weryfikacji człowieka na poziomie %s.", componentName, extent, purpose, review) default: return fmt.Sprintf("For “%s”, %s is recorded.", componentName, strings.ToLower(extent)) } } func assuranceStatement(assurance string, locale i18n.Locale) string { key := "assurance_" + assurance + "_statement" if value := locale.Text[key]; value != "" { if assurance == "signed" { switch locale.Code { case "de": return value + " Die Signatur bestätigt nicht automatisch die inhaltliche Richtigkeit der Angaben." case "en": return value + " The signature does not by itself confirm the substantive accuracy of the information." } } return value } if value := locale.Text["assurance_"+assurance+"_description"]; value != "" { return value } return locale.Assurances[assurance] } func customTextRequested(q url.Values, keys ...string) bool { for _, key := range keys { if strings.TrimSpace(q.Get(key)) != "" { return true } } return false } func firstNonEmpty(values ...string) string { for _, value := range values { if value = strings.TrimSpace(value); value != "" { return value } } return "" } func validHexColor(value string) bool { if len(value) != 7 || value[0] != '#' { return false } for _, r := range value[1:] { if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')) { return false } } return true } func languageCodes() []string { languages := i18n.Languages() out := make([]string, 0, len(languages)) for _, l := range languages { out = append(out, l.Code) } sort.Strings(out) return out } func cloneValues(in url.Values) url.Values { out := url.Values{} for k, v := range in { out[k] = append([]string(nil), v...) } return out } func randomID() string { b := make([]byte, 8) if _, err := rand.Read(b); err != nil { return fmt.Sprintf("%d", time.Now().UnixNano()) } return hex.EncodeToString(b) } func clientIP(r *http.Request, trustProxy bool, trusted []netip.Prefix) string { remote := strings.TrimSpace(r.RemoteAddr) var remoteAddr netip.Addr if addrPort, err := netip.ParseAddrPort(remote); err == nil { remoteAddr = addrPort.Addr() } else { remoteAddr, _ = netip.ParseAddr(strings.Trim(remote, "[]")) } if trustProxy && remoteAddr.IsValid() && prefixContains(trusted, remoteAddr) { parts := strings.Split(r.Header.Get("X-Forwarded-For"), ",") var forwarded []netip.Addr for _, part := range parts { part = strings.TrimSpace(part) if part == "" { continue } addr, err := netip.ParseAddr(strings.Trim(part, "[]")) if err != nil { return remoteAddr.String() } forwarded = append(forwarded, addr.Unmap()) } for index := len(forwarded) - 1; index >= 0; index-- { if !prefixContains(trusted, forwarded[index]) { return forwarded[index].String() } } if len(forwarded) > 0 { return forwarded[0].String() } } if remoteAddr.IsValid() { return remoteAddr.String() } return remote } func prefixContains(prefixes []netip.Prefix, addr netip.Addr) bool { for _, prefix := range prefixes { if prefix.Contains(addr) { return true } } return false } func validRequestID(value string) bool { if len(value) < 1 || len(value) > 64 { return false } for _, r := range value { if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || strings.ContainsRune("-_.:", r) { continue } return false } return true } func randomNonce() string { b := make([]byte, 18) if _, err := rand.Read(b); err != nil { return randomID() } return base64.RawStdEncoding.EncodeToString(b) } func ensureEOF(dec *json.Decoder) error { var extra any err := dec.Decode(&extra) if errors.Is(err, io.EOF) { return nil } if err == nil { return errors.New("request body must contain exactly one JSON value") } return err } const declarationSchema = `{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "__BASE_URL__/schema/v1/declaration.schema.json", "title": "AI Usage Declaration", "type": "object", "additionalProperties": false, "required": [ "@context", "@type", "schemaVersion", "language", "components", "assurance" ], "properties": { "@context": { "type": "string" }, "@type": { "const": "AIUsageDeclaration" }, "schemaVersion": { "const": "1.3" }, "subject": { "type": "string", "format": "uri" }, "declaredAt": { "type": "string", "format": "date-time" }, "language": { "enum": [ "de", "en", "fr", "es", "it", "nl", "pt", "pl" ] }, "components": { "type": "object", "minProperties": 1, "additionalProperties": { "$ref": "#/$defs/component" } }, "author": { "type": "object", "additionalProperties": false, "required": [ "name" ], "properties": { "name": { "type": "string", "maxLength": 200 }, "url": { "type": "string", "format": "uri" } } }, "editorialResponsibility": { "type": "object", "additionalProperties": false, "required": [ "name" ], "properties": { "name": { "type": "string" }, "url": { "type": "string", "format": "uri" }, "assumed": { "type": "boolean" }, "role": { "enum": [ "publisher", "other" ] } } }, "complaintsContact": { "type": "object", "additionalProperties": false, "required": [ "name" ], "properties": { "name": { "type": "string", "maxLength": 200 }, "email": { "type": "string", "format": "email", "maxLength": 320 }, "url": { "type": "string", "format": "uri" } }, "anyOf": [ { "required": [ "email" ] }, { "required": [ "url" ] } ] }, "assurance": { "enum": [ "selfDeclared", "technicallyRecorded", "signed", "verified" ] }, "presentation": { "type": "object", "additionalProperties": false, "properties": { "title": { "type": "string", "maxLength": 120 }, "description": { "type": "string", "maxLength": 500 }, "badgeLabel": { "type": "string", "maxLength": 40 }, "badgeMessage": { "type": "string", "maxLength": 80 }, "leftColor": { "type": "string", "pattern": "^#[0-9A-Fa-f]{6}$" }, "rightColor": { "type": "string", "pattern": "^#[0-9A-Fa-f]{6}$" } } }, "legalContext": { "type": "object", "additionalProperties": false, "properties": { "categories": { "type": "array", "uniqueItems": true, "items": { "enum": [ "deepfake", "publicInterestText", "artisticCreativeSatiricalFictional", "otherVoluntary" ] } }, "actorRole": { "enum": [ "deployer", "provider", "both", "unsure" ] }, "useContext": { "enum": [ "professional", "personalNonProfessional", "unsure" ] }, "outputDate": { "type": "string", "format": "date" }, "lawEnforcementAuthorization": { "type": "string", "enum": [ "yes", "no", "unsure" ], "description": "Self-declared assessment whether the use is authorised by law to detect, prevent, investigate or prosecute criminal offences for the Article 50(4) exception." }, "deepfakeAssessment": { "type": "string", "enum": [ "yes", "no", "unsure" ], "description": "Self-assessment whether the AI-involved image, audio or video qualifies as a deepfake for Article 50(4)." }, "publicInterestAssessment": { "type": "string", "enum": [ "yes", "no", "unsure" ], "description": "Self-assessment whether AI-involved text is published to inform the public on a matter of public interest for Article 50(4)." }, "creativeWorkAssessment": { "type": "string", "enum": [ "yes", "no", "unsure" ], "description": "Self-assessment whether a deepfake forms part of an evidently artistic, creative, satirical, fictional or analogous work or programme." } } } }, "$defs": { "component": { "type": "object", "additionalProperties": false, "required": [ "aiExtent", "humanReview" ], "properties": { "aiExtent": { "enum": [ "none", "assisted", "partial", "mostly", "full" ] }, "activities": { "type": "array", "uniqueItems": true, "items": { "enum": [ "research", "summarisation", "drafting", "generation", "translation", "editing", "imageGeneration", "codeGeneration", "transcription", "classification" ] } }, "humanReview": { "enum": [ "none", "basic", "editorial", "expert" ] }, "note": { "type": "string", "maxLength": 500 } } } } }` const jsonLDContext = `{ "@context": { "@version": 1.1, "AIUsageDeclaration": "__BASE_URL__/vocab/AIUsageDeclaration", "schemaVersion": "__BASE_URL__/vocab/schemaVersion", "subject": {"@id": "https://schema.org/about", "@type": "@id"}, "declaredAt": {"@id": "https://schema.org/dateCreated", "@type": "https://www.w3.org/2001/XMLSchema#dateTime"}, "language": "https://schema.org/inLanguage", "components": "__BASE_URL__/vocab/components", "author": "https://schema.org/author", "name": "https://schema.org/name", "url": {"@id": "https://schema.org/url", "@type": "@id"}, "email": "https://schema.org/email", "aiExtent": "__BASE_URL__/vocab/aiExtent", "activities": "__BASE_URL__/vocab/activities", "humanReview": "__BASE_URL__/vocab/humanReview", "assurance": "__BASE_URL__/vocab/assurance", "presentation": "__BASE_URL__/vocab/presentation", "editorialResponsibility": "https://schema.org/accountablePerson", "complaintsContact": "https://schema.org/contactPoint", "assumed": "__BASE_URL__/vocab/assumed", "role": "__BASE_URL__/vocab/responsibilityRole", "legalContext": "__BASE_URL__/vocab/legalContext", "categories": "__BASE_URL__/vocab/legalCategories", "actorRole": "__BASE_URL__/vocab/aiActActorRole", "useContext": "__BASE_URL__/vocab/useContext", "outputDate": {"@id": "__BASE_URL__/vocab/outputDate", "@type": "https://www.w3.org/2001/XMLSchema#date"}, "deepfakeAssessment": "__BASE_URL__/vocab/deepfakeAssessment", "publicInterestAssessment": "__BASE_URL__/vocab/publicInterestAssessment", "creativeWorkAssessment": "__BASE_URL__/vocab/creativeWorkAssessment", "lawEnforcementAuthorization": "__BASE_URL__/vocab/lawEnforcementAuthorization" } }`