Files
jbergner c212155f61
All checks were successful
release-tag / release-image (push) Successful in 1m52s
Neues Branding und Text-Konzept
2026-07-26 20:59:38 +02:00

1473 lines
56 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 lintelligence 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 lintelligenza 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 lorganisation é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 na é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 na é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 lIA; è comunque documentata una revisione umana %s.", componentName, strings.ToLower(review))
}
return fmt.Sprintf("Secondo le informazioni fornite, per «%s» non è stata utilizzata lIA.", 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 », lutilisation déclarée est %s, avec pour finalité %s ; aucune vérification humaine des résultats liés à lIA nest indiquée.", componentName, strings.ToLower(extent), purpose)
}
return fmt.Sprintf("Pour « %s », lutilisation déclarée est %s, avec pour finalité %s ; les résultats liés à lIA ont fait lobjet dune 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 allIA.", componentName, strings.ToLower(extent), purpose)
}
return fmt.Sprintf("Per «%s» è dichiarato %s, con finalità %s; i risultati legati allIA 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"
}
}`