feat(bug-reports): Herkunft der Fehlermeldung im Betreff-Kuerzel und als Zeile Herkunft ausweisen
WebView2 (Windows) sieht im User-Agent aus wie Edge, WebKitGTK (Linux) wie Safari — im Postfach war eine Client-Meldung von einer Browser-Meldung nicht zu unterscheiden. Neuer reiner Helfer origin.ts leitet aus vier optionalen DTO-Feldern (Desktop-App) bzw. dem User-Agent (Browser) ein Betreff-Kuerzel und eine Zeile "Herkunft: ..." ab; rein informativ, laengenbegrenzt, nichts wird gespeichert (T-GZA-01). Browser-Pfad ist damit Ende-zu-Ende fertig. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016g2npLxzH5gZpg8s2S6vKh
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* origin.ts — Herkunft einer Fehlermeldung ausweisen (quick-260918-gza).
|
||||
*
|
||||
* Zweck: WebView2 (Windows-Desktop-App) sieht im User-Agent aus wie Edge,
|
||||
* WebKitGTK (Linux-Desktop-App) wie Safari — im Postfach des Betreibers ist
|
||||
* damit nicht erkennbar, ob eine Meldung aus einem Client oder einem
|
||||
* echten Browser stammt. `describeOrigin()` leitet aus den vier optionalen
|
||||
* Client-Feldern (gesetzt durch die Desktop-App ueber das Cookie
|
||||
* `tessera_desktop_client`) und, im Browser-Fall, aus dem User-Agent ein
|
||||
* kurzes Betreff-Kuerzel und eine Textzeile ab.
|
||||
*
|
||||
* Trust-Modell (T-GZA-01): alle Eingaben stammen unbeglaubigt vom Client,
|
||||
* wie der bestehende User-Agent auch. Die Ableitung ist rein informativ —
|
||||
* sie entscheidet nie ueber Routing, Berechtigung oder Speicherung, und
|
||||
* nichts davon wird in der Datenbank abgelegt. `clean()` laesst nur ein
|
||||
* begrenztes Alphabet und hoechstens 40 Zeichen zu, damit weder ein
|
||||
* Zeilenumbruch noch Markup in die E-Mail gelangt; das Betriebssystem wird
|
||||
* ausserdem auf eine feste Menge von Labels abgebildet.
|
||||
*
|
||||
* Warum Regex statt Bibliothek: fuenf Browser und fuenf Betriebssysteme
|
||||
* reichen aus, um ein Postfach sinnvoll zu sortieren — ein neues Paket
|
||||
* (etwa ua-parser-js) waere fuer diesen Umfang unverhaeltnismaessig.
|
||||
*/
|
||||
|
||||
export type ClientKind = 'desktop' | 'browser';
|
||||
|
||||
export interface OriginInput {
|
||||
clientKind?: string;
|
||||
clientOs?: string;
|
||||
clientVersion?: string;
|
||||
clientCommit?: string;
|
||||
userAgent?: string;
|
||||
}
|
||||
|
||||
export interface Origin {
|
||||
/** Betreff-Kuerzel in eckigen Klammern, z. B. `[Browser]`, `[Desktop/Windows]`. */
|
||||
tag: string;
|
||||
/** Text NACH dem Label `Herkunft: ` — der Dienst setzt das Label davor. */
|
||||
line: string;
|
||||
}
|
||||
|
||||
export interface ParsedUserAgent {
|
||||
browser: string;
|
||||
os: string;
|
||||
}
|
||||
|
||||
const UNKNOWN = 'unbekannt';
|
||||
|
||||
/**
|
||||
* Entfernt alles ausser `[A-Za-z0-9.+_-]` und kuerzt auf `max` Zeichen
|
||||
* (Vorgabe 40) — kein Zeilenumbruch, kein Markup in Betreff oder Text
|
||||
* (T-GZA-01).
|
||||
*/
|
||||
function clean(value: unknown, max = 40): string {
|
||||
return String(value ?? '')
|
||||
.replace(/[^A-Za-z0-9.+_-]/g, '')
|
||||
.slice(0, max);
|
||||
}
|
||||
|
||||
/**
|
||||
* Leitet Browsername samt Hauptversion und Betriebssystem aus einem
|
||||
* User-Agent ab. Reihenfolge ist entscheidend: Android-UAs enthalten
|
||||
* `Linux`, iPad-UAs enthalten `like Mac OS X`, Edge- und Opera-UAs
|
||||
* enthalten sowohl `Chrome/` als auch `Safari/` — die spezifischeren
|
||||
* Muster muessen deshalb zuerst gewinnen.
|
||||
*/
|
||||
export function parseUserAgent(ua: string): ParsedUserAgent {
|
||||
const value = ua ?? '';
|
||||
|
||||
let browser = UNKNOWN;
|
||||
const edg = value.match(/Edg\/(\d+)/);
|
||||
const opr = value.match(/OPR\/(\d+)/);
|
||||
const firefox = value.match(/Firefox\/(\d+)/);
|
||||
const chrome = value.match(/(?:Chrome|CriOS)\/(\d+)/);
|
||||
const hasSafariToken = /Safari\//.test(value);
|
||||
const hasChromeToken = /Chrome\//.test(value);
|
||||
|
||||
if (edg) {
|
||||
browser = `Edge ${edg[1]}`;
|
||||
} else if (opr) {
|
||||
browser = `Opera ${opr[1]}`;
|
||||
} else if (firefox) {
|
||||
browser = `Firefox ${firefox[1]}`;
|
||||
} else if (chrome) {
|
||||
browser = `Chrome ${chrome[1]}`;
|
||||
} else if (hasSafariToken && !hasChromeToken) {
|
||||
const version = value.match(/Version\/(\d+)/);
|
||||
browser = version ? `Safari ${version[1]}` : 'Safari';
|
||||
}
|
||||
|
||||
let os = UNKNOWN;
|
||||
if (/Windows NT/.test(value)) {
|
||||
os = 'Windows';
|
||||
} else if (/Android/.test(value)) {
|
||||
os = 'Android';
|
||||
} else if (/iPhone|iPad|iPod/.test(value)) {
|
||||
os = 'iOS';
|
||||
} else if (/Mac OS X|Macintosh/.test(value)) {
|
||||
os = 'macOS';
|
||||
} else if (/Linux|X11/.test(value)) {
|
||||
os = 'Linux';
|
||||
}
|
||||
|
||||
return { browser, os };
|
||||
}
|
||||
|
||||
/** Bildet `clean(clientOs)` auf eines von drei festen Labels ab. */
|
||||
function desktopOsLabel(clientOs: string | undefined): string {
|
||||
switch (clean(clientOs).toLowerCase()) {
|
||||
case 'windows':
|
||||
return 'Windows';
|
||||
case 'linux':
|
||||
return 'Linux';
|
||||
case 'macos':
|
||||
return 'macOS';
|
||||
default:
|
||||
return UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Leitet Betreff-Kuerzel und Herkunftszeile ab. Desktop-Fall: der
|
||||
* User-Agent spielt keine Rolle, nur die vier Client-Felder zaehlen
|
||||
* (dieselbe Regel wie `client_info_label` im Rust-Client — Version und
|
||||
* Commit werden nur zusammen mit " · Stand " verbunden, ein leerer
|
||||
* Commit faellt weg). Alles andere (auch fehlende Felder, alter
|
||||
* Web-Bau) faellt auf den Browser-Pfad mit `parseUserAgent` zurueck.
|
||||
*/
|
||||
export function describeOrigin(input: OriginInput): Origin {
|
||||
if (input.clientKind === 'desktop') {
|
||||
const osLabel = desktopOsLabel(input.clientOs);
|
||||
const version = clean(input.clientVersion);
|
||||
const commit = clean(input.clientCommit);
|
||||
|
||||
let appLabel = '';
|
||||
if (version && commit) {
|
||||
appLabel = `Tessera-App ${version} · Stand ${commit}`;
|
||||
} else if (version) {
|
||||
appLabel = `Tessera-App ${version}`;
|
||||
}
|
||||
|
||||
const line = appLabel ? `Desktop-App (${osLabel}), ${appLabel}` : `Desktop-App (${osLabel})`;
|
||||
const tag = osLabel === UNKNOWN ? '[Desktop]' : `[Desktop/${osLabel}]`;
|
||||
return { tag, line };
|
||||
}
|
||||
|
||||
const { browser, os } = parseUserAgent(input.userAgent ?? '');
|
||||
return { tag: '[Browser]', line: `Browser — ${browser} auf ${os}` };
|
||||
}
|
||||
Reference in New Issue
Block a user