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
-
Slice B verwendet
pdfkit@0.20.1als 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. -
Der einzige HTTP-Zugang ist
POST /api/internal/pdf-ua-shadow/[tier]mit exakt einem der Wertefree,standard,premiumoderplaner. Die Route läuft auf der Node.js-Runtime und antwortet nur beiVERCEL_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 leere404-Antwort. Andere framework- oder proxyseitig behandelte Methoden können bereits vor dem Handler mit einem abweichenden4xxenden. Die Route ist kein öffentlicher API-Vertrag. -
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.
-
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.
-
Die Tiermatrix übernimmt ausschließlich die bereits implementierten Produktgates:
Tier Szenario-Narrative synthetischer Google-Solar-Baustein freenein nein standardja, nur nicht rote Szenarien nein premiumja, nur nicht rote Szenarien ja planernein, tabellarisch ja Die Texte und Google-Solar-Werte sind feste synthetische Fixture-Inhalte; weder AI Gateway noch Google Solar werden aufgerufen.
-
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. -
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 bleibtmaxDuration=60die ä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ält429mitRetry-After, der kooperative Timeout und andere Renderfehler503. Erfolgreiche Antworten sindprivate, no-store,noindex,nosniffund 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. -
Der öffentliche Produktionspfad
GET /api/v1/report/{id}/pdfbleibt unverändert bei@react-pdf/rendererundpdf-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. -
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
| Alternative | Vorteil | Nachteil | Entscheidung |
|---|---|---|---|
Stock @sparticuz/chromium | vertrautes Serverless-Paket | Stock-Build ohne belastbaren Tagged-PDF-Pfad; Slice-A-Full-Chrome-Erfolg nicht übertragbar | verworfen |
| eigener Chromium-Build | Browser-HTML könnte weiterverwendet werden | großes eigenes Binary, Build-/Supply-Chain-Pflege und höheres Kaltstart-/Speicherrisiko | für Slice B verworfen |
| öffentliche Shadow-Route mit Secret | einfacher externer Aufruf | neue Credential- und Leckagefläche; unnötig bei geschützter Preview | verworfen |
| echte gespeicherte Reports im Shadow | produktnäher | DB-, PII-, Provider- und Retention-Risiko vor Produktfreigabe | verworfen |
| reiner Node.js-Shadow mit fixer Fixture | kleine Runtime, enge Daten- und Zugriffsgrenze | reale Reportintegration und Screenreader-Verhalten bleiben offen | angenommen |
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-
404ist auf Merge-SHAf9807311e2d9b68ce16067564be166c16a5996dffü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.