Zum Hauptinhalt springen

Middleware

Postbox verwendet sechzehn Custom Middleware neben den Laravel-Standard-Middleware. Die Registrierung erfolgt in bootstrap/app.php (globale Middleware) und routes/api.php (API-Middleware).

Uebersicht

MiddlewareScopeZweck
HandleRedirectsGlobal (prepend)URL-Redirects vor Routing abfangen
SecurityHeadersGlobal (Web + API)Sicherheits-Header auf jeder Response
TrackErrorPagesGlobal (append)HTTP-Fehler (4xx/5xx) fuer Monitoring loggen
CheckBlockedUserWeb (global)Gesperrte User sofort ausloggen
EnsureLegalConsentWeb (global)Datenschutz + AGB Zustimmung erzwingen
TrackPageViewWeb (global)Server-seitiges Matomo-Tracking
VerifyHealthTokenEinzelrouteHealth-Endpoint Authentifizierung
EnsureCurrentWorkspaceWatcher-RoutenWorkspace in Session pruefen
EnsureWorkspaceFromRouteWorkspace-ShowWorkspace aus URL laden und Session synchronisieren
RejectRevokedTokensAPI (Collector)Revozierte und IP-gesperrte Sanctum-Tokens ablehnen
TrackApiTokenUsageAPI (Collector)Stuendliche API-Token-Nutzung aggregieren
IncreaseMemoryForLogViewerEinzelrouteMemory-Limit fuer Log Viewer temporaer erhoehen
ContentFreshnessExplorer-RoutenHTTP-Caching-Header (ETag, Last-Modified) fuer oeffentliche Seiten
RedirectToLocalizedUrlGlobal (prepend)Legacy-URLs ohne Locale-Prefix auf lokalisierte Version umleiten
SetLocaleFromUrlOeffentliche RoutenApp-Locale aus URL-Prefix ({locale}) setzen
SetLocaleFromUserApp-Routen (auth)App-Locale aus User-DB-Praeferenz setzen

HandleRedirects

Location: app/Http/Middleware/HandleRedirects.php

Before-Middleware, die URL-Redirects (Legacy-URLs, Marketing-Redirects, Error-Monitor-Redirects) vor dem eigentlichen Routing prueft. Laeuft als erste Middleware im Stack (prepended).

Logik

  1. Pfad gegen Ausschluss-Liste pruefen (/api/, /livewire/, /_debugbar/, /up, statische Assets)
  2. RedirectManager::findRedirect() sucht exakten oder Regex-Match in url_redirects
  3. Falls Match: Query-Parameter weiterleiten, Hit-Counter inkrementieren, Redirect mit konfiguriertem Status-Code
  4. Falls kein Match: Request normal weiterleiten

Fehlerverhalten

SituationResponse
Exakter Redirect-Match301/302 Redirect zum Ziel-URL
Regex-Match301/302 Redirect mit Capture-Group-Replacement
Kein MatchRequest wird weitergeleitet
Hit-Tracking-FehlerWird via report() geloggt, Redirect laeuft trotzdem

Ausgeschlossene Pfade

/api/, /livewire/, /_debugbar/, /_ignition/, /up, /up_system, statische Assets (.css, .js, .png, .jpg, etc.)

Abhaengigkeiten

  • App\Services\ErrorMonitor\RedirectManager — Redirect-Lookup und Regex-Aufloesung
  • App\Models\UrlRedirect — Redirect-Konfigurationen

SecurityHeaders

Location: app/Http/Middleware/SecurityHeaders.php

Setzt sicherheitsrelevante HTTP-Header auf jeder Response. Wird global fuer Web- und API-Requests angehaengt.

Gesetzte Header

HeaderWertZweck
X-Content-Type-OptionsnosniffVerhindert MIME-Type-Sniffing (XSS-Schutz)
X-Frame-OptionsDENY / SAMEORIGINClickjacking-Schutz. SAMEORIGIN fuer Pulse, Vantage, Log Viewer (iFrame-Einbettung im Admin-Layout).
Referrer-Policystrict-origin-when-cross-originBegrenzt Referrer-Informationen an Dritte
Permissions-Policycamera=(), microphone=(), geolocation=(), fullscreen=(), payment=()Deaktiviert nicht benoetigte Browser-APIs
Strict-Transport-Securitymax-age=31536000; includeSubDomainsHSTS (nur bei HTTPS-Requests, um lokale HTTP-Umgebungen nicht zu sperren)
Content-Security-PolicyDynamisch (siehe unten)Schutz gegen XSS, Clickjacking, Data-Injection

Content-Security-Policy (CSP)

Die CSP wird dynamisch aus der Config generiert — keine hardcodierten Domains. Alle externen Origins werden zur Laufzeit aus .env/Config-Werten abgeleitet.

DirektiveWertHerkunft
default-src'self'Statisch
script-src'self' 'unsafe-inline' 'unsafe-eval' + Turnstile + jsdelivr CDN + Matomoconfig('services.matomo.url')
style-src'self' 'unsafe-inline' + Bunny FontsStatisch
img-src'self' data: blob: https: + CDNconfig('filesystems.disks.public.url')
font-src'self' data: + Bunny FontsStatisch
connect-src'self' + Matomo + WebSocket + CDNconfig('reverb.apps.apps.0.options.host/port'), s.o.
worker-src'self' blob:Statisch
media-src'self' blob:Statisch
frame-src'self' + Turnstile + YouTubeStatisch
object-src'none'Statisch
base-uri'self'Statisch
form-action'self'Statisch

Dynamische Origins:

  • Matomo: URL aus config('services.matomo.url') (z.B. https://ana.lyse.io)
  • WebSocket: Host + Port aus config('reverb.apps.apps.0.options') (z.B. wss://app.postbox.so:443)
  • CDN: URL aus config('filesystems.disks.public.url') (z.B. https://cdn.postbox.so) — nur wenn CDN-Host != App-Host

Hinweise:

  • 'unsafe-eval' ist fuer Alpine.js erforderlich (nutzt new Function() fuer Expression-Evaluation)
  • 'unsafe-inline' ist fuer Livewire/Flux UI Inline-Styles erforderlich
  • https://cdn.jsdelivr.net fuer ApexCharts (Score-Charts, Google API Usage, Publishing Analytics etc.)
  • Same-Origin iFrames (Pulse, Vantage, Log Viewer) werden ueber 'self' abgedeckt

Registrierung

// bootstrap/app.php
$middleware->append(SecurityHeaders::class);

$middleware->web(append: [
SecurityHeaders::class,
// ...
]);

$middleware->api(append: [
SecurityHeaders::class,
]);

Fehlerverhalten

Kein Fehlerverhalten -- die Middleware fuegt Header hinzu, blockiert aber keine Requests.


CheckBlockedUser

Location: app/Http/Middleware/CheckBlockedUser.php

Prueft bei jedem authentifizierten Web-Request, ob der User gesperrt ist (blocked_at IS NOT NULL). Gesperrte User werden sofort ausgeloggt und auf die Login-Seite weitergeleitet.

Logik

  1. Request hat keinen authentifizierten User? → Weiterleiten (kein Check noetig)
  2. $user->isBlocked() prueft blocked_at IS NOT NULL
  3. Gesperrt: Session invalidieren, ausloggen, Redirect zu /login
  4. Nicht gesperrt: Request weiterleiten

Fehlerverhalten

SituationResponse
User gesperrtSession invalidiert, 302 Redirect zu /login
User nicht gesperrtRequest wird weitergeleitet
Kein User authentifiziertRequest wird weitergeleitet

Integration mit Fortify

Zusaetzlich zur Middleware wird der Login-Versuch gesperrter User direkt in FortifyServiceProvider abgefangen:

// app/Providers/FortifyServiceProvider.php
Fortify::authenticateUsing(function (Request $request) {
$user = User::where('email', $request->email)->first();
if ($user?->isBlocked()) {
throw ValidationException::withMessages([
'email' => ['Diese Zugangsdaten sind ungültig.'],
]);
}
// ... normale Authentifizierung
});

Die generische Fehlermeldung verraet nicht, dass der Account gesperrt ist.

Registrierung

// bootstrap/app.php
$middleware->web(append: [
CheckBlockedUser::class,
// ...
]);

VerifyHealthToken

Location: app/Http/Middleware/VerifyHealthToken.php

Schuetzt den erweiterten Health-Endpoint (/up_system) vor unbefugtem Zugriff. Das Token wird gegen config('postbox.health.token') (Env: HEALTH_TOKEN) geprueft.

Akzeptierte Token-Quellen

QuelleFormatTypischer Nutzer
HTTP-HeaderX-Health-Token: <token>UptimeRobot, Monitoring-Tools
Query-Parameter?token=<token>Browser-Zugriff

Fehlerverhalten

SituationResponse
Kein Token konfiguriert (HEALTH_TOKEN leer)503 Service Unavailable (Plain Text)
Token fehlt oder falsch401 Unauthorized mit WWW-Authenticate: X-Health-Token Header

Der Token-Vergleich nutzt hash_equals() zum Schutz gegen Timing-Angriffe.

Beispiel

# UptimeRobot-Style
curl -H "X-Health-Token: my-secret" https://app.postbox.so/up_system

# Browser-Style
curl "https://app.postbox.so/up_system?token=my-secret"

EnsureCurrentWorkspace

Location: app/Http/Middleware/EnsureCurrentWorkspace.php

Stellt sicher, dass ein Workspace in der Session gesetzt ist. Wird auf allen Watcher-bezogenen Routen verwendet (z.B. /watchers, /watcher/{watcher}).

Logik

  1. Liest den aktuellen Workspace aus CurrentWorkspace::get() (Session-basiert).
  2. Falls kein Workspace gesetzt ist: Redirect zu /workspaces (Workspace-Auswahl).
  3. Falls vorhanden: Request wird weitergeleitet.

Fehlerverhalten

SituationResponse
Kein Workspace in Session302 Redirect zu workspaces.index

Verwendung in Routen

// routes/web.php
Route::middleware(['auth', EnsureCurrentWorkspace::class])
->group(function () {
Route::get('/watchers', Watchers\Index::class);
Route::get('/watcher/{watcher}', Watchers\Show::class);
// ...
});

EnsureWorkspaceFromRoute

Location: app/Http/Middleware/EnsureWorkspaceFromRoute.php

Laedt den Workspace aus dem Route-Parameter ({workspace}) und synchronisiert ihn mit der Session. Unterstuetzt den Admin-Workspace (ID 999999999999).

Logik

  1. Liest {workspace} aus dem Route-Parameter.
  2. Admin-Check: Falls der User Admin ist und die Workspace-ID dem Admin-Workspace entspricht, wird der Admin-Workspace via AdminWorkspaceManager sichergestellt.
  3. Normal: Prueft, ob der User Mitglied des Workspace ist ($user->workspaces()->whereKey(...)).
  4. Setzt den Workspace in der Session via CurrentWorkspace::set().

Fehlerverhalten

SituationResponse
Kein User authentifiziert403 Forbidden
Workspace-ID fehlt404 Not Found
User ist kein Mitglied des Workspace (und kein Admin mit Admin-Workspace)403 Forbidden

Verwendung in Routen

// routes/web.php
Route::get('/workspace/{workspace}', Watchers\Index::class)
->middleware(['auth', EnsureWorkspaceFromRoute::class])
->name('workspaces.show');

TrackPageView

Location: app/Http/Middleware/TrackPageView.php

Server-seitiges Matomo-Tracking fuer Page Views. Nutzt den MatomoTrackingService und dispatcht asynchrone Tracking-Requests.

Logik

Trackt nur wenn alle Bedingungen erfuellt sind:

BedingungGrund
GET-RequestNur Seitenaufrufe, keine Form-Submissions
Kein X-Livewire-HeaderNur initiale Page Loads, keine Livewire-Updates
Status Code 200Keine Fehler, Redirects oder JSON-Responses
User ist kein AdminAdmin-Traffic verfaelscht Analytics

Erfasste Daten

  • URL, Seitentitel (aus <title>-Tag oder Route-Name), User-ID (optional), IP-Adresse, User-Agent, Referrer, Accept-Language

Konfiguration

Die Middleware ist nur aktiv, wenn Matomo konfiguriert ist (MATOMO_ENABLED=true).

MATOMO_URL=https://analytics.example.com
MATOMO_SITE_ID=1
MATOMO_TOKEN=your-token
MATOMO_ENABLED=true

EnsureLegalConsent

Location: app/Http/Middleware/EnsureLegalConsent.php

Erzwingt Zustimmung zu Datenschutz und AGB fuer alle authentifizierten User. Bestandsuser (registriert vor Einfuehrung der Legal-Checkboxen) haben NULL in privacy_accepted_at/terms_accepted_at und werden auf die Consent-Seite umgeleitet.

Logik

  1. Kein authentifizierter User? → Weiterleiten
  2. privacy_accepted_at UND terms_accepted_at vorhanden? → Weiterleiten
  3. Bereits auf Consent-Seite oder Logout? → Weiterleiten (Loop-Schutz)
  4. Sonst → Redirect zu legal-consent Route

Fehlerverhalten

SituationResponse
User ohne Consent302 Redirect zu /legal-consent
User mit ConsentRequest wird weitergeleitet
UnauthentifiziertRequest wird weitergeleitet

Verbundene Components

  • app/Livewire/LegalConsent.php — Consent-Seite mit Checkboxen, CMS-Modal-Vorschau und Logout-Option

TrackErrorPages

Location: app/Http/Middleware/TrackErrorPages.php

After-Middleware, die HTTP-Fehler-Responses (Status 400–599) fuer das Error-Monitoring loggt. Laeuft nach der Response-Generierung (appended).

Logik

  1. Response-Status < 400 oder > 599? → Kein Tracking, sofort zurueckgeben
  2. Pfad gegen Ausschluss-Liste pruefen (isExcludedPath())
  3. Statische Assets ueberspringen
  4. ErrorMonitorService::recordErrorSimple() — PostgreSQL UPSERT (1 Zeile pro Pfad + Status-Code + Stunde)
  5. Tracking-Fehler werden via report() geloggt, die Response wird nie beeinflusst

Fehlerverhalten

SituationResponse
Error-Status (4xx/5xx)Response + asynchrones Logging
Success-Status (2xx/3xx)Response ohne Tracking
Logging-FehlerWird geloggt, Response bleibt unberuehrt

Erfasste Daten

  • Pfad (lowercase), Status-Code, Referer, User-Agent
  • Aggregation per UPSERT: hit_count wird pro Stunde hochgezaehlt

Abhaengigkeiten

  • App\Services\ErrorMonitor\ErrorMonitorService

RejectRevokedTokens

Location: app/Http/Middleware/RejectRevokedTokens.php

Prueft API-Requests mit Sanctum-Token auf Soft-Revocation (revoked_at) und IP-Whitelist (allowed_ips). Laeuft nach auth:sanctum in der API-Middleware-Chain.

Logik

  1. Kein Token vorhanden? → Request weiterleiten (wird von auth:sanctum behandelt)
  2. $token->revoked_at !== null? → 401 Unauthorized
  3. $token->isIpAllowed($request->ip()) gibt false zurueck? → 403 Forbidden
  4. Sonst → Request weiterleiten

Fehlerverhalten

SituationResponse
Token revoziert401 Unauthorized — "Token has been revoked."
IP nicht in Whitelist403 Forbidden — "Request IP not allowed for this token."
Keine Whitelist konfiguriertRequest wird weitergeleitet (alle IPs erlaubt)
Kein TokenRequest wird weitergeleitet

Registrierung

// routes/api.php
Route::middleware(['auth:sanctum', RejectRevokedTokens::class, 'throttle:collector', TrackApiTokenUsage::class])
->group(/* Collector-Routes */);

TrackApiTokenUsage

Location: app/Http/Middleware/TrackApiTokenUsage.php

After-Middleware, die API-Request-Counts pro Token und Stunde aggregiert. Nutzt PostgreSQL UPSERT fuer atomare Zaehler-Inkremente.

Logik

  1. Kein Sanctum-Token? → Kein Tracking
  2. Aktuelle Stunde als Bucket: now()->startOfHour()
  3. UPSERT in api_token_usage_logs: Falls Eintrag existiert → request_count + 1, sonst neuer Eintrag mit request_count = 1

Daten-Modell

TabelleSpalteTypBeschreibung
api_token_usage_logspersonal_access_token_idFKToken-Referenz
hourtimestampStunden-Bucket
request_countuintAkkumulierter Zaehler

Fehlerverhalten

Tracking-Fehler werden via report() geloggt. Die API-Response wird nie beeinflusst.

Retention

Alte Usage-Logs werden via model:prune bereinigt. Retention: config('postbox.api_tokens.usage_retention_days', 90) Tage.


IncreaseMemoryForLogViewer

Location: app/Http/Middleware/IncreaseMemoryForLogViewer.php

Temporaere Memory-Limit-Erhoehung fuer den Log Viewer (/admin/logs). Der Log Viewer benoetigt mehr als die Standard-128MB, da grosse Log-Dateien geladen werden.

Logik

  1. Aktuelles memory_limit merken
  2. Auf 256M erhoehen
  3. Request an Log Viewer weiterleiten
  4. Sicheres Restaurieren: Memory-Limit nur zuruecksetzen, wenn der aktuelle Speicherverbrauch (memory_get_usage(true)) unter dem vorherigen Limit liegt. Verhindert Crash wenn PHP waehrend des Requests bereits mehr als 128MB allokiert hat.

parseMemoryLimit() Helper

Parst PHP Memory-Strings (128M, 1G, -1) in Bytes fuer den Vergleich. Unterstuetzt K/M/G-Suffixe und den Spezialwert -1 (unlimited).

Fehlerverhalten

SituationResponse
Speicherverbrauch < vorheriges LimitLimit wird restauriert
Speicherverbrauch >= vorheriges LimitLimit bleibt auf 256M (kein Crash)
Vorheriges Limit ist -1 (unlimited)Limit wird immer restauriert

Registrierung

Die Middleware wird direkt an der Log-Viewer-Route angehaengt:

// routes/web.php
Route::middleware(['auth', IncreaseMemoryForLogViewer::class])
->get('/admin/logs', ...);

ContentFreshness

Location: app/Http/Middleware/ContentFreshness.php

Fuegt HTTP-Caching-Header (Last-Modified, ETag) zu oeffentlichen Seiten-Responses hinzu fuer effizientes Client- und CDN-Caching. Reduziert Bandbreite, indem AI-Crawler, Suchmaschinen und Browser unveraenderten Content nicht erneut herunterladen muessen.

Logik

  1. Response generieren (next Middleware)
  2. Nur bei Status 200 fortfahren — alle anderen Status uebergehen
  3. ETag aus MD5 des Response-Contents generieren
  4. Last-Modified Header auf aktuelle GMT-Zeit setzen (falls nicht vorhanden)
  5. Client-If-None-Match Header pruefen: Falls ETag uebereinstimmt → 304 Not Modified zurueckgeben
  6. Sonst: Cache-Control: public, max-age=300, must-revalidate setzen

Fehlerverhalten

SituationResponse
Status ≠ 200Skip, Response unveraendert
Kein Response-ContentSkip, Response unveraendert
ETag Match304 Not Modified
Kein ETag MatchResponse mit Caching-Headern

Registrierung

Route-Middleware — nur auf /explorer/* oeffentliche Routen angewandt.


RedirectToLocalizedUrl

Location: app/Http/Middleware/RedirectToLocalizedUrl.php

Faengt Legacy-URLs ohne Locale-Prefix (z.B. /login, /explorer) ab und leitet sie per 301-Redirect auf lokalisierte Versionen um (z.B. /de/login, /en/explorer). Erkennt die bevorzugte Locale aus: Query-Parameter (?locale=), User-DB-Praeferenz, Cookie oder Default (de).

Logik

  1. Nur GET-Requests verarbeiten (POST/PUT/DELETE durchlassen fuer Fortify-Actions)
  2. Pruefen ob Pfad oeffentlich ist UND kein Locale-Prefix hat (/de/ oder /en/)
  3. Falls ja: Bevorzugte Locale ermitteln (Prioritaet: ?locale= → User-DB → Cookie → de)
  4. ?locale= aus Query-String entfernen (wird konsumiert)
  5. Redirect-Pfad bauen: /{locale}/{path} mit verbleibenden Query-Parametern
  6. 301-Redirect mit locale-Cookie (30 Tage) zurueckgeben

Fehlerverhalten

SituationResponse
GET-Request, oeffentlich, kein Locale-Prefix301 Redirect zu /{locale}/{path} + Cookie
POST/PUT/DELETERequest wird weitergeleitet
Pfad hat bereits Locale-PrefixRequest wird weitergeleitet
Ungueltige ?locale=Naechste Prioritaet verwenden

Registrierung

// bootstrap/app.php
$middleware->prepend(RedirectToLocalizedUrl::class);

Laeuft als zweite globale Middleware nach HandleRedirects.


SetLocaleFromUrl

Location: app/Http/Middleware/SetLocaleFromUrl.php

Setzt die App-Locale aus dem URL-Prefix ({locale}) auf oeffentlichen Seiten. Liest den Route-Parameter und konfiguriert Laravel entsprechend. Persistiert die Gast-Locale-Wahl per Cookie (30 Tage).

Logik

  1. locale Route-Parameter lesen (Default: de)
  2. Gegen User::SUPPORTED_LOCALES validieren; ungueltig → de
  3. app()->setLocale($locale) setzen
  4. Date::setLocale($locale) fuer locale-aware Datumsformatierung
  5. URL::defaults(['locale' => $locale]) damit route()-Calls den {locale}-Parameter automatisch fuellen
  6. Response mit locale-Cookie (30 Tage TTL) zurueckgeben

Fehlerverhalten

SituationResponse
Gueltiges Locale (de/en)Locale gesetzt, Cookie gesetzt
Ungueltiges oder fehlendes LocaleFallback auf de

Registrierung

// bootstrap/app.php — Route-Alias
'set-locale-from-url' => SetLocaleFromUrl::class,

Angewandt auf oeffentliche Route-Gruppen mit {locale}-Parameter.


SetLocaleFromUser

Location: app/Http/Middleware/SetLocaleFromUser.php

Setzt die App-Locale aus der DB-Praeferenz des authentifizierten Users (users.locale-Spalte) fuer den App-Bereich (Dashboard, Settings, Admin). Die Locale wird NICHT aus der URL gelesen, sondern aus der gespeicherten User-Praeferenz.

Logik

  1. Default-Locale de initialisieren
  2. Authentifizierten User pruefen: Falls vorhanden, locale gegen User::SUPPORTED_LOCALES validieren
  3. app()->setLocale($locale) setzen
  4. Date::setLocale($locale) fuer locale-aware Datumsformatierung
  5. URL::defaults(['locale' => $locale]) fuer locale-aware Route-Generierung
  6. Kein Cookie gesetzt (User-DB-Praeferenz ist persistent)

Fehlerverhalten

SituationResponse
User authentifiziert, Locale gueltigUser-Locale gesetzt
User authentifiziert, Locale ungueltigFallback auf de
Nicht authentifiziertDefault de

Registrierung

// bootstrap/app.php — Route-Alias
'set-locale-from-user' => SetLocaleFromUser::class,

Angewandt auf geschuetzte App-Routen (Dashboard, Settings, Admin).


Middleware-Registrierung (bootstrap/app.php)

Die vollstaendige Middleware-Konfiguration in bootstrap/app.php:

->withMiddleware(function (Middleware $middleware): void {
// Inline comment: HandleRedirects runs BEFORE all other middleware to redirect early.
$middleware->prepend(HandleRedirects::class);

// Inline comment: RedirectToLocalizedUrl catches legacy URLs without locale prefix.
$middleware->prepend(RedirectToLocalizedUrl::class);

// Apply security headers to every response, including health checks.
$middleware->append(SecurityHeaders::class);

// Inline comment: TrackErrorPages runs AFTER all other middleware to capture error responses.
$middleware->append(TrackErrorPages::class);

// Web-Middleware-Stack
$middleware->web(append: [
\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class,
SecurityHeaders::class,
CheckBlockedUser::class,
TrackPageView::class,
]);

// API-Middleware-Stack
$middleware->api(append: [
SecurityHeaders::class,
]);

// Route-Middleware-Aliases (Plan 33: i18n)
$middleware->alias([
'set-locale-from-url' => SetLocaleFromUrl::class,
'set-locale-from-user' => SetLocaleFromUser::class,
'redirect-to-localized' => RedirectToLocalizedUrl::class,
]);
})

Ausfuehrungsreihenfolge:

  1. HandleRedirects (prepended) — Redirects vor Routing
  2. RedirectToLocalizedUrl (prepended) — Legacy-URLs auf lokalisierte Variante umleiten
  3. Routing, Controller, Livewire etc.
  4. Route-spezifische Aliases (set-locale-from-url, set-locale-from-user) je nach Route-Gruppe
  5. SecurityHeaders, TrackErrorPages (appended) — Header + Error-Tracking

VerifyHealthToken, EnsureCurrentWorkspace, EnsureWorkspaceFromRoute und ContentFreshness werden nicht global registriert, sondern direkt an den jeweiligen Routen angehaengt.