ADR-0060: PDF/UA-1-Shadow-Renderer ausschließlich in Vercel Preview

  • Status: angenommen; Slice B lokal, in CI, geschützter Preview und Produktion belegt
  • Datum: 26. August 2026
  • Issue: #333 bleibt offen
  • Vorgänger: ADR-0059

Kontext

Slice A hat mit einer synthetischen HTML-Fixture belegt, dass Chromium 148 einen unabhängig validierbaren PDF/UA-1-Kandidaten erzeugen kann. Dieser Machbarkeitsnachweis ist aber noch kein geeigneter Vercel-Produktionspfad. Die unveränderte öffentliche Ausgabe verwendet @react-pdf/renderer und pdf-lib; ihre Viewer-, Text-, Link- und Metadatenqualität ist belegt, ein vollständiger PDF/UA-Tag-Baum jedoch nicht.

Für Slice B wird deshalb ein enger Runtime-Shadow benötigt: alle vier Produkttiers sollen mit festen, nicht personenbezogenen Daten denselben semantischen Fünfseitenvertrag durchlaufen, ohne den öffentlichen Report-Endpunkt, die Datenbank, Provider, Accounts oder reale Reportdaten zu berühren.

Der zunächst naheliegende Serverless-Browser @sparticuz/chromium wird für diesen Slice nicht verwendet. Der unveränderte Stock-Build stellt den für getaggte PDFs erforderlichen Chromium-Buildpfad nicht bereit; ein eigener Build mit aktivierter PDF- und Tagged-PDF-Unterstützung wäre nötig (enable_pdf=true und enable_tagged_pdf=true). Das wäre ein zusätzlicher, großer und dauerhaft zu pflegender Runtime-Artefaktpfad. Die erfolgreiche Full-Chrome-Fixture aus Slice A darf deshalb nicht als Beleg für einen Stock-Sparticuz-Produktionspfad gelesen werden.

Entscheidung

  1. Slice B verwendet pdfkit@0.20.1 als reinen Node.js-Renderer. Die semantische Finalisierung bleibt Teil desselben serverseitigen Pfads. Der Shadow benötigt weder Chromium noch Playwright, Java oder veraPDF zur Laufzeit; Java, veraPDF und Poppler sind ausschließlich Prüfwerkzeuge.

  2. Der einzige HTTP-Zugang ist POST /api/internal/pdf-ua-shadow/[tier] mit exakt einem der Werte free, standard, premium oder planer. Die Route läuft auf der Node.js-Runtime und antwortet nur bei VERCEL_ENV=preview. Produktion, Development, unbekannte oder fehlende Umgebungen, die von Next.js unterstützten Nicht-POST-Methoden, unbekannte Tiers, Queryparameter und ein Request-Body erhalten absichtlich dieselbe leere 404-Antwort. Andere framework- oder proxyseitig behandelte Methoden können bereits vor dem Handler mit einem abweichenden 4xx enden. Die Route ist kein öffentlicher API-Vertrag.

  3. Die Vercel-Preview-Deployment-Protection bildet die äußere Zugriffsschranke. Für Slice B wird kein zusätzlicher anwendungsseitiger Shadow-Token eingeführt. Ein automatisierter Zugriff auf eine geschützte Preview darf nur mit einem bereits freigegebenen Vercel-Zugang erfolgen; die Einführung eines neuen CI-Credentials ist nicht Bestandteil dieser Entscheidung. Die Route ist aus dem globalen Auth.js-Proxy-Matcher ausgenommen: Sie liest keine App-Sitzung und setzt auf der PDF-Antwort keine Auth.js-CSRF- oder Callback-Cookies.

  4. Die Route akzeptiert keinen request-abgeleiteten Inhalt. Sie liest keine Datenbank, keinen gespeicherten Report, keine Session, keinen Provider und keine Umgebungsvariable mit fachlichen Daten. Die Fixture ist vollständig im Repository definiert, benutzt erfundene Standort- und Kostenwerte und enthält weder Adresse, Person, Organisation, Koordinate, echte Parzelle noch sonstige personenbezogene Daten. Der Renderpfad führt keine Netzwerkanfrage aus.

  5. Die Tiermatrix übernimmt ausschließlich die bereits implementierten Produktgates:

    TierSzenario-Narrativesynthetischer Google-Solar-Baustein
    freeneinnein
    standardja, nur nicht rote Szenariennein
    premiumja, nur nicht rote Szenarienja
    planernein, tabellarischja

    Die Texte und Google-Solar-Werte sind feste synthetische Fixture-Inhalte; weder AI Gateway noch Google Solar werden aufgerufen.

  6. Jeder Tier-Output hat exakt fünf physische Seiten, zwei informative Figuren mit Alternativtext und zwei URI-Links. Darüber hinaus prüft das Harness unter anderem Sprache de-AT, Titel, Metadaten, PDF/UA-1-XMP, MarkInfo/Marked, Strukturbaum, Parent-Tree-/OBJR-Bindung, Lesereihenfolge, Seiten-Chrome als Pagination-Artefakt und die physische Textschicht über alle Seiten. Abweichungen von Seiten-, Figuren- oder Linkanzahl sind fail-closed. Die Roh-PDF trägt pro Figure eine Bindung aus stabiler ID und Digest des Alternativtexts; die Finalisierung ersetzt diese nur positionsgleich. Ein vertauschtes oder manipuliertes Figure-Manifest wird abgewiesen.

  7. Die Vercel Function hat maxDuration=60. Der Renderer verwirft Läufe über 4.000.000 Bytes. Ein kooperativer Timer beendet das Warten auf die HTTP-Antwort nach 50 Sekunden, sofern der Node.js-Event-Loop weiterläuft. Er kann einen synchron blockierten PDF-Schritt nicht hart abbrechen; dafür bleibt maxDuration=60 die äußere Plattformgrenze. Gewinnt der Timer, bleibt der Instanz-Lock bis zum tatsächlichen Ende der Operation gesetzt; eine Fortsetzung nach der Antwort ist jedoch keine Vercel-Dauerhaftigkeitszusage. Pro Function-Instanz läuft höchstens ein Render gleichzeitig; ein zweiter Lauf erhält 429 mit Retry-After, der kooperative Timeout und andere Renderfehler 503. Erfolgreiche Antworten sind private, no-store, noindex, nosniff und enthalten nur das erzeugte PDF sowie grobe Renderzeiten. PDFs werden weder persistiert noch als CI-Artefakt veröffentlicht. Temporäre Prüfverzeichnisse müssen auch bei Fehlern entfernt werden. Raw- und finaler PDF-Buffer können während der Finalisierung gleichzeitig im Speicher liegen; die Ein-Render-Sperre begrenzt diese Last pro Instanz. Der geschützte Preview-Readback misst Antwortgrößen und grobe Renderzeiten; ein exakter Peak-RSS-Wert bleibt ohne zusätzliche Runtime-Instrumentierung offen.

  8. Der öffentliche Produktionspfad GET /api/v1/report/{id}/pdf bleibt unverändert bei @react-pdf/renderer und pdf-lib. Kein Produktpfad, Produkt-UI-Element, OpenAPI- oder MCP-Consumer ruft den Shadow auf oder bewirbt ihn; die öffentliche technische Dokumentation beschreibt ihn bewusst. Ein bestandener Shadow ist keine Produktfreigabe.

  9. Issue #333 bleibt offen, bis die echte Reportstruktur aller vier Tiers in einen freigegebenen PDF/UA-Pfad integriert, gegen die vorhandenen Viewergates und reale Umbrüche geprüft, kontrolliert ausgerollt, produktiv rückgelesen und mit mindestens einem Screenreader fachlich abgenommen wurde.

Erwogene Alternativen

AlternativeVorteilNachteilEntscheidung
Stock @sparticuz/chromiumvertrautes Serverless-PaketStock-Build ohne belastbaren Tagged-PDF-Pfad; Slice-A-Full-Chrome-Erfolg nicht übertragbarverworfen
eigener Chromium-BuildBrowser-HTML könnte weiterverwendet werdengroßes eigenes Binary, Build-/Supply-Chain-Pflege und höheres Kaltstart-/Speicherrisikofür Slice B verworfen
öffentliche Shadow-Route mit Secreteinfacher externer Aufrufneue Credential- und Leckagefläche; unnötig bei geschützter Previewverworfen
echte gespeicherte Reports im ShadowproduktnäherDB-, PII-, Provider- und Retention-Risiko vor Produktfreigabeverworfen
reiner Node.js-Shadow mit fixer Fixturekleine Runtime, enge Daten- und Zugriffsgrenzereale Reportintegration und Screenreader-Verhalten bleiben offenangenommen

Konsequenzen

  • Slice B kann Serverless-Laufzeit, Tiergates und den semantischen Fünfseitenvertrag prüfen, ohne den öffentlichen Reportrenderer zu ändern.
  • Ein grüner veraPDF-/Poppler-Lauf belegt nur den festen synthetischen Shadow. Er macht weder den React-PDF-Bestandsoutput konform noch ersetzt er eine Screenreader-Abnahme.
  • Die PDF-Erzeugung ist zwar mit 4 MB Antwortlimit und Instanz-Lock begrenzt, aber der 50-s-Timer ist kooperativ und kein CPU-Abbruch. Der Preview- Readback belegt 539 bis 903 ms Gesamtdauer für die vier Tiers; Peak-RSS ist nicht instrumentiert.
  • Deployment Protection, Vier-Tier-Responses und negative Preview-Pfade sind commitgenau belegt. Gleichzeitige Requests können auf mehrere Function- Instanzen verteilt werden; die Ein-Render-Sperre ist deshalb bewusst keine globale Drossel.
  • Die absichtliche Produktions-404 ist auf Merge-SHA f9807311e2d9b68ce16067564be166c16a5996df für alle vier Tier-POSTs sowie einen GET belegt. Der öffentliche React-PDF-Pfad blieb auf demselben SHA unverändert und weiterhin nicht als PDF/UA getaggt.

Referenzen