package app import ( "context" "crypto/rand" "crypto/subtle" "encoding/hex" "encoding/json" "errors" "fmt" "html/template" "io" "io/fs" "log/slog" "net" "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 } type option struct{ Value, Label string } type fact struct{ Label, Value, Link string } type componentRow struct{ Name, Extent, Activities, Review, Note string } type assessmentView struct { Code, Severity, Title, Summary string Details []string Warnings []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 BundleURL string CanonicalURL string JSONLD template.JS AppConfig template.JS Facts []fact ComponentRows []componentRow Summary []string IsArticle bool License licenseclient.Status CustomText bool CustomBadge bool Marketing marketing.Page Background background.Page LanguageLinks []languageLink DefaultLanguageURL string Assessment assessmentView RegulatoryFacts []fact BulkAPI bool BulkMaxItems int BulkRequireAPIKey bool ExportBundle bool WhiteLabel bool LegalPage legalPage } type clientConfig struct { BaseURL string `json:"baseURL"` SelectedLanguage string `json:"selectedLanguage"` Locales map[string]i18n.Locale `json:"locales"` Capabilities map[string]bool `json:"capabilities"` } type bulkClientConfig struct { Endpoint string `json:"endpoint"` MaxItems int `json:"maxItems"` RequireAPIKey bool `json:"requireAPIKey"` Texts map[string]string `json:"texts"` } func New(ctx context.Context, cfg Config, logger *slog.Logger) (http.Handler, error) { if err := cfg.Validate(); err != nil { return nil, fmt.Errorf("invalid configuration: %w", err) } tmpl, err := template.New("root").Funcs(template.FuncMap{"legalLabel": legalLabel}).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()} s.routes() return s.middleware(s.mux), nil } func (s *Server) routes() { staticFS, _ := fs.Sub(webassets.Files, "static") fileServer := http.FileServer(http.FS(staticFS)) // Static assets are also required by the dedicated bulk workspace. s.mux.Handle("GET /static/", http.StripPrefix("/static/", cacheStatic(fileServer))) // Operational and machine-readable endpoints are available in every mode. s.mux.HandleFunc("POST /v1/validate", s.handleValidate) s.mux.HandleFunc("GET /v1/capabilities", s.handleCapabilities) s.mux.HandleFunc("GET /v1/article50-assessment.json", s.handleArticle50Assessment) 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) s.mux.HandleFunc("GET /metrics", s.handleMetrics) // Licensed batch processing is intentionally exposed in API and bulk modes. s.mux.HandleFunc("POST /v1/bulk/declarations", s.handleBulkDeclarations) if s.cfg.ServiceMode != "api" { s.mux.HandleFunc("GET /legal", s.handleOperatorPage("legal")) s.mux.HandleFunc("GET /imprint", s.handleOperatorPage("legal")) s.mux.HandleFunc("GET /privacy", s.handleOperatorPage("privacy")) s.mux.HandleFunc("GET /accessibility", s.handleOperatorPage("accessibility")) } if s.cfg.ServiceMode == "bulk" { s.mux.HandleFunc("GET /", s.handleBulkWorkspace) s.mux.HandleFunc("GET /bulk", s.handleBulkWorkspace) return } 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/declaration.bundle.json", s.handleDeclarationBundle) if s.cfg.ServiceMode == "api" { return } s.mux.HandleFunc("GET /", s.handleIndex) s.mux.HandleFunc("GET /bulk", s.handleBulkWorkspace) s.mux.HandleFunc("GET /product", s.handleMarketing) s.mux.HandleFunc("GET /background", s.handleBackground) s.mux.HandleFunc("GET /install", s.handleMarketingAlias) } func (s *Server) effectiveBulkMaxItems() int { limit := s.cfg.BulkMaxItems if licensedLimit, ok := s.licenses.Limit("bulk_items"); ok && licensedLimit > 0 && int64(limit) > licensedLimit { limit = int(licensedLimit) } return limit } func (s *Server) handleBulkWorkspace(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" && r.URL.Path != "/bulk" { http.NotFound(w, r) return } lang := s.language(r) locale := i18n.Get(lang) cfg := bulkClientConfig{ Endpoint: "/v1/bulk/declarations", MaxItems: s.effectiveBulkMaxItems(), RequireAPIKey: s.cfg.BulkRequireAPIKey, Texts: map[string]string{ "error_no_items": locale.Text["bulk_error_no_items"], "error_too_many_items": locale.Text["bulk_error_too_many_items"], "error_api_key": locale.Text["bulk_error_api_key"], "status_running": locale.Text["bulk_status_running"], "status_done": locale.Text["bulk_status_done"], "status_error": locale.Text["bulk_status_error"], "summary_processed": locale.Text["bulk_summary_processed"], "summary_successful": locale.Text["bulk_summary_successful"], "summary_failed": locale.Text["bulk_summary_failed"], "result_ok": locale.Text["bulk_result_ok"], "result_error": locale.Text["bulk_result_error"], "link_declaration": locale.Text["bulk_link_declaration"], "link_manifest": locale.Text["bulk_link_manifest"], "link_badge": locale.Text["bulk_link_badge"], }, } 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(), Extents: orderedOptions(locale.Extents, []string{"none", "assisted", "partial", "mostly", "full"}), Reviews: orderedOptions(locale.Reviews, []string{"none", "basic", "editorial", "expert"}), Assurances: orderedOptions(locale.Assurances, []string{"selfDeclared", "technicallyRecorded", "signed", "verified"}), AppConfig: template.JS(appJSON), License: s.licenses.Status(), BulkAPI: s.licenses.Has(FeatureBulkAPI), BulkMaxItems: s.effectiveBulkMaxItems(), BulkRequireAPIKey: s.cfg.BulkRequireAPIKey, WhiteLabel: s.cfg.WhiteLabel && s.licenses.Has(FeatureWhiteLabel), } s.renderHTML(w, "bulk.html", data) } 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(), Capabilities: map[string]bool{ FeatureCustomText: s.licenses.Has(FeatureCustomText), FeatureCustomBadge: s.licenses.Has(FeatureCustomBadge), FeatureBulkAPI: s.licenses.Has(FeatureBulkAPI), FeatureExportBundle: s.licenses.Has(FeatureExportBundle), }, } 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), License: s.licenses.Status(), CustomText: s.licenses.Has(FeatureCustomText), CustomBadge: s.licenses.Has(FeatureCustomBadge), BulkAPI: s.licenses.Has(FeatureBulkAPI), ExportBundle: s.licenses.Has(FeatureExportBundle), WhiteLabel: s.cfg.WhiteLabel && s.licenses.Has(FeatureWhiteLabel), } 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, WhiteLabel: s.cfg.WhiteLabel && s.licenses.Has(FeatureWhiteLabel), } s.renderHTML(w, "background.html", data) } 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.ContactURL) 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(), WhiteLabel: s.cfg.WhiteLabel && s.licenses.Has(FeatureWhiteLabel), } 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, "licensed_feature_required", "Custom badge labels require the licensed capability custom_badge.") return } if customTextRequested(q, "leftColor", "rightColor") && !s.licenses.Has(FeatureCustomBadge) { s.problem(w, http.StatusForbidden, "licensed_feature_required", "Custom badge colours require the licensed capability 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" if isArticle { if d, err := s.declarationFromQuery(q); err == nil { extent = overallExtent(d) } } 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"] } if d, err := s.declarationFromQuery(q); err == nil && d.RegulatoryContext != nil { assessment := declaration.AssessArticle50(d) if d.RegulatoryContext.Deepfake { message = locale.Text["badge_article50_deepfake"] } else if d.RegulatoryContext.PublicInterestText && assessment.Code == "public_interest_text_disclosure_relevant" { message = locale.Text["badge_article50_text"] } if assessment.Applicable && rightColor == "" { rightColor = "#b45309" } } if isArticle && rightColor == "" { // Article-level declarations use calm violet unless a regulatory context // warrants an attention-oriented amber. Explicit licensed colours win. 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 } 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, 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) if s.cfg.APIAllowedOrigin != "" { w.Header().Set("Access-Control-Allow-Origin", s.cfg.APIAllowedOrigin) } w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; 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, manifestURL, badgeURL := s.generatedURLs(q) bundleURL := s.cfg.BaseURL + "/v1/declaration.bundle.json?" + q.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, BundleURL: bundleURL, CanonicalURL: canonicalURL, JSONLD: template.JS(jsonLD), Facts: declarationFacts(d, locale), ComponentRows: declarationComponentRows(d, locale), Summary: declarationSummary(d, locale), IsArticle: isArticle, License: s.licenses.Status(), LanguageLinks: languageLinks, DefaultLanguageURL: defaultLanguageURL, Assessment: article50AssessmentView(declaration.AssessArticle50(d), locale), RegulatoryFacts: regulatoryFacts(d, locale), ExportBundle: s.licenses.Has(FeatureExportBundle), WhiteLabel: s.cfg.WhiteLabel && s.licenses.Has(FeatureWhiteLabel), } 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", "*") _ = json.NewEncoder(w).Encode(d) } 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, "licensed_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) 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) 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) handleCapabilities(w http.ResponseWriter, _ *http.Request) { s.writeJSON(w, http.StatusOK, map[string]any{ "product": ProductID, "productVersion": ProductVersion, "version": ProductVersion, "schemaVersion": declaration.SchemaVersion, "serviceMode": s.cfg.ServiceMode, "license": s.licenses.Status(), "supportedLanguages": languageCodes(), "licensedCapabilities": map[string]bool{FeatureCustomText: s.licenses.Has(FeatureCustomText), FeatureCustomBadge: s.licenses.Has(FeatureCustomBadge), FeatureBulkAPI: s.licenses.Has(FeatureBulkAPI), FeatureExportBundle: s.licenses.Has(FeatureExportBundle), FeatureWhiteLabel: s.licenses.Has(FeatureWhiteLabel)}, }) } 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") if s.cfg.APIAllowedOrigin != "" { w.Header().Set("Access-Control-Allow-Origin", s.cfg.APIAllowedOrigin) } _, _ = 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") if s.cfg.APIAllowedOrigin != "" { w.Header().Set("Access-Control-Allow-Origin", s.cfg.APIAllowedOrigin) } _, _ = w.Write([]byte(strings.ReplaceAll(jsonLDContext, "__BASE_URL__", s.cfg.BaseURL))) } func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request) { if !s.cfg.MetricsEnabled { http.NotFound(w, r) return } expected := strings.TrimSpace(s.cfg.MetricsToken) auth := strings.TrimSpace(r.Header.Get("Authorization")) provided := "" if strings.HasPrefix(strings.ToLower(auth), "bearer ") { provided = strings.TrimSpace(auth[len("Bearer "):]) } if len(provided) != len(expected) || subtle.ConstantTimeCompare([]byte(provided), []byte(expected)) != 1 { w.Header().Set("WWW-Authenticate", `Bearer realm="metrics"`) http.Error(w, "unauthorized", http.StatusUnauthorized) return } s.metrics.serveHTTP(w, r) } 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") status := s.licenses.Status() if s.cfg.ServiceMode == "bulk" && !s.licenses.Has(FeatureBulkAPI) { w.WriteHeader(http.StatusServiceUnavailable) _, _ = io.WriteString(w, "bulk_api capability is not available\n") return } if s.cfg.ServiceMode == "bulk" && s.cfg.BulkRequireAPIKey && strings.TrimSpace(s.cfg.BulkAPIKey) == "" { w.WriteHeader(http.StatusServiceUnavailable) _, _ = io.WriteString(w, "bulk API key is required but not configured\n") return } if s.cfg.RequireLicense && !status.Licensed { w.WriteHeader(http.StatusServiceUnavailable) _, _ = io.WriteString(w, "a valid license is required for readiness\n") return } _, _ = io.WriteString(w, "ready\n") } func (s *Server) renderHTML(w http.ResponseWriter, name string, data pageData) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "no-store") w.Header().Set("Content-Security-Policy", "default-src 'self'; img-src 'self' data:; style-src 'self'; script-src 'self' 'unsafe-inline'; connect-src 'self'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'") if err := s.templates.ExecuteTemplate(w, name, data); err != nil { s.logger.Error("template render failed", "template", name, "error", err) } } 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 := r.Header.Get("X-Request-ID") if 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", "cross-origin") if s.cfg.EnableHSTS { w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") } if strings.HasPrefix(r.URL.Path, "/v1/") || strings.HasPrefix(r.URL.Path, "/schema/") || strings.HasPrefix(r.URL.Path, "/context/") { if s.cfg.APIAllowedOrigin != "" { w.Header().Set("Access-Control-Allow-Origin", s.cfg.APIAllowedOrigin) } w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key, X-Request-ID") 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} if s.cfg.RequireLicense && !licenseExemptPath(r.URL.Path) && !s.licenses.Status().Licensed { s.problem(rw, http.StatusServiceUnavailable, "license_required", "This deployment requires a valid runtime license.") return } 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) } args := []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 { args = append(args, "remote", clientIP(r, s.cfg.TrustProxy, s.cfg.TrustedProxies)) } s.logger.Info("request", args...) }() next.ServeHTTP(rw, r) }) } func licenseExemptPath(path string) bool { switch path { case "/healthz", "/readyz", "/metrics", "/v1/capabilities", "/legal", "/imprint", "/privacy", "/accessibility": return true } return strings.HasPrefix(path, "/static/") } func (s *Server) bulkAuthorized(r *http.Request) bool { if !s.cfg.BulkRequireAPIKey { return true } expected := strings.TrimSpace(s.cfg.BulkAPIKey) if expected == "" { return false } provided := strings.TrimSpace(r.Header.Get("X-API-Key")) if auth := strings.TrimSpace(r.Header.Get("Authorization")); strings.HasPrefix(strings.ToLower(auth), "bearer ") { provided = strings.TrimSpace(auth[len("Bearer "):]) } if len(provided) != len(expected) { return false } return subtle.ConstantTimeCompare([]byte(provided), []byte(expected)) == 1 } 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.EditorialResponsibility != nil && d.EditorialResponsibility.Name != "" { facts = append(facts, fact{Label: locale.Text["fact_responsibility"], Value: d.EditorialResponsibility.Name, Link: d.EditorialResponsibility.URL}) } if d.DeclaredAt != "" { facts = append(facts, fact{Label: locale.Text["fact_declared_at"], Value: d.DeclaredAt}) } return facts } func regulatoryFacts(d declaration.Declaration, locale i18n.Locale) []fact { rc := d.RegulatoryContext if rc == nil { return nil } yes, no := locale.Text["yes_value"], locale.Text["no_value"] if yes == "" { yes = "Yes" } if no == "" { no = "No" } boolValue := func(v bool) string { if v { return yes } return no } return []fact{ {Label: locale.Text["field_public_interest_text"], Value: boolValue(rc.PublicInterestText)}, {Label: locale.Text["field_deepfake"], Value: boolValue(rc.Deepfake)}, {Label: locale.Text["field_artistic_context"], Value: boolValue(rc.ArtisticCreativeSatiricalFictional)}, {Label: locale.Text["field_substantial_review"], Value: boolValue(rc.SubstantialHumanReview)}, {Label: locale.Text["field_editorial_responsibility_confirmed"], Value: boolValue(rc.EditorialResponsibilityConfirmed)}, {Label: locale.Text["field_first_exposure"], Value: boolValue(rc.FirstExposureDisclosure)}, {Label: locale.Text["field_accessibility"], Value: boolValue(rc.AccessibilityConsidered)}, } } func article50AssessmentView(a declaration.Article50Assessment, locale i18n.Locale) assessmentView { key := map[string]string{ "not_assessed": "not_assessed", "voluntary_transparency": "voluntary", "deepfake_disclosure_relevant": "deepfake", "public_interest_text_disclosure_relevant": "public_text", "public_interest_text_possible_exemption": "possible_exemption", "multiple_article50_contexts": "multiple", }[a.Code] if key == "" { key = "not_assessed" } details := make([]string, 0, len(a.Findings)) for _, finding := range a.Findings { findingKey := map[string]string{ "deepfake_disclosure_relevant": "deepfake", "public_interest_text_disclosure_relevant": "public_text", "public_interest_text_possible_exemption": "possible_exemption", }[finding.Code] if text := locale.Text["assessment_"+findingKey+"_text"]; text != "" { details = append(details, text) } } warnings := make([]string, 0, len(a.Warnings)) for _, warning := range a.Warnings { translationKey := map[string]string{ "first_exposure_not_confirmed": "warning_first_exposure", "accessibility_not_confirmed": "warning_accessibility", "editorial_responsibility_not_confirmed": "warning_editorial_responsibility", "artistic_context_disclosure_manner": "warning_artistic_context", }[warning] if text := locale.Text[translationKey]; text != "" { warnings = append(warnings, text) } } return assessmentView{ Code: a.Code, Severity: a.Severity, Title: locale.Text["assessment_"+key+"_title"], Summary: locale.Text["assessment_"+key+"_text"], Details: details, Warnings: warnings, } } 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 UCNG Declaration beschreibt den angegebenen Entstehungs- und Transformationsprozess des deklarierten Inhalts. Sie dokumentiert, in welchen Bereichen KI beteiligt war, ohne Qualität oder Wahrheit des Inhalts zu bewerten.", "en": "This UCNG Declaration describes the stated creation and transformation process of the declared content. It records where AI was involved without judging the quality or truth of the content.", "fr": "Cette UCNG Declaration décrit le processus déclaré de création et de transformation du contenu et documente les domaines dans lesquels l’intelligence artificielle a été impliquée, sans juger la qualité ni la véracité du contenu.", "es": "Esta UCNG Declaration describe el proceso declarado de creación y transformación del contenido y documenta dónde intervino la inteligencia artificial, sin juzgar la calidad ni la veracidad del contenido.", "it": "Questa UCNG Declaration descrive il processo dichiarato di creazione e trasformazione del contenuto e documenta dove è intervenuta l’intelligenza artificiale, senza giudicare qualità o veridicità del contenuto.", "nl": "Deze UCNG Declaration beschrijft het gedeclareerde creatie- en transformatieproces en legt vast waar AI betrokken was, zonder de kwaliteit of waarheid van de inhoud te beoordelen.", "pt": "Esta UCNG Declaration descreve o processo declarado de criação e transformação do conteúdo e documenta onde a inteligência artificial esteve envolvida, sem avaliar a qualidade ou a veracidade do conteúdo.", "pl": "Ta UCNG Declaration opisuje zadeklarowany proces tworzenia i przekształcania treści oraz dokumentuje, gdzie uczestniczyła AI, bez oceniania jakości ani prawdziwości 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": { "Die Angaben werden unter einem gemeinsamen Link veröffentlicht und stehen zusätzlich als maschinenlesbares JSON-LD zur Verfügung.", "Die UCNG Declaration gibt bereitgestellte Angaben wieder. Sie bedeutet nicht automatisch verified, zertifiziert, authentisch, korrekt, wahr oder rechtskonform.", }, "en": { "The declaration is published under a single link and is also available as machine-readable JSON-LD.", "The UCNG Declaration reflects supplied information. It does not automatically mean verified, certified, authentic, accurate, true or legally compliant.", }, "fr": { "Toutes les informations sont publiées sous un lien unique et sont également disponibles au format JSON-LD lisible par machine.", "La déclaration reprend les informations fournies par la personne ou l’organisation éditrice; elle ne constitue pas en elle-même une qualification juridique.", }, "es": { "Toda la información se publica mediante un único enlace y también está disponible como JSON-LD legible por máquina.", "La declaración refleja la información facilitada por la persona u organización editora; no constituye por sí misma una calificación jurídica.", }, "it": { "Tutte le informazioni sono pubblicate tramite un unico collegamento e sono disponibili anche come JSON-LD leggibile automaticamente.", "La dichiarazione riporta le informazioni fornite dalla persona o organizzazione che pubblica il contenuto; non costituisce di per sé una valutazione giuridica.", }, "nl": { "Alle gegevens worden via één gezamenlijke link gepubliceerd en zijn daarnaast beschikbaar als machineleesbare JSON-LD.", "De verklaring geeft de informatie weer die door de publicerende persoon of organisatie is verstrekt; zij vormt op zichzelf geen juridische kwalificatie.", }, "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 declaração reproduz as informações fornecidas pela pessoa ou organização responsável pela publicação; não constitui, por si só, uma qualificação jurídica.", }, "pl": { "Wszystkie informacje są publikowane pod jednym wspólnym odsyłaczem i są również dostępne jako maszynowo czytelny JSON-LD.", "Deklaracja odzwierciedla informacje podane przez publikującą osobę lub organizację; sama w sobie nie stanowi kwalifikacji prawnej.", }, } end := closing[lang] end = append(end, assuranceStatement(d.Assurance, locale)) return []string{strings.Join(statements, " "), strings.Join(end, " ")} } 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 "AI ASSISTED (unterstützende KI-Nutzung)" case "partial": return "MIXED (gemischte Entstehung mit teilweise KI-generierten Anteilen)" case "mostly": return "AI GENERATED (überwiegend KI-generierte Erstellung)" case "full": return "AI GENERATED (KI-generierte Erstellung)" default: return "eine dokumentierte KI-Rolle" } } 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 { direct := remoteIP(r.RemoteAddr) if trustProxy && direct.IsValid() && addressInPrefixes(direct, trusted) { if x := strings.TrimSpace(strings.Split(r.Header.Get("X-Forwarded-For"), ",")[0]); x != "" { if forwarded, err := netip.ParseAddr(x); err == nil { return forwarded.String() } } if x := strings.TrimSpace(r.Header.Get("X-Real-IP")); x != "" { if forwarded, err := netip.ParseAddr(x); err == nil { return forwarded.String() } } } if direct.IsValid() { return direct.String() } return r.RemoteAddr } func remoteIP(remote string) netip.Addr { host, _, err := net.SplitHostPort(remote) if err != nil { host = remote } addr, _ := netip.ParseAddr(strings.Trim(host, "[]")) return addr } func addressInPrefixes(addr netip.Addr, prefixes []netip.Prefix) bool { for _, prefix := range prefixes { if prefix.Contains(addr) { return true } } return false } 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.2" }, "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" } }, "editorialResponsibility": { "type": "object", "additionalProperties": false, "properties": { "name": { "type": "string" }, "url": { "type": "string", "format": "uri" } } }, "regulatoryContext": { "type": "object", "additionalProperties": false, "properties": { "framework": { "enum": [ "EU-AI-Act-Article-50" ] }, "publicInterestText": { "type": "boolean" }, "deepfake": { "type": "boolean" }, "artisticCreativeSatiricalFictional": { "type": "boolean" }, "substantialHumanReview": { "type": "boolean" }, "editorialResponsibilityConfirmed": { "type": "boolean" }, "firstExposureDisclosure": { "type": "boolean" }, "accessibilityConsidered": { "type": "boolean" } } }, "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}$" } } } }, "$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", "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", "regulatoryContext": "__BASE_URL__/vocab/regulatoryContext", "framework": "__BASE_URL__/vocab/framework", "publicInterestText": "__BASE_URL__/vocab/publicInterestText", "deepfake": "__BASE_URL__/vocab/deepfake", "artisticCreativeSatiricalFictional": "__BASE_URL__/vocab/artisticCreativeSatiricalFictional", "substantialHumanReview": "__BASE_URL__/vocab/substantialHumanReview", "editorialResponsibilityConfirmed": "__BASE_URL__/vocab/editorialResponsibilityConfirmed", "firstExposureDisclosure": "__BASE_URL__/vocab/firstExposureDisclosure", "accessibilityConsidered": "__BASE_URL__/vocab/accessibilityConsidered" } }`