From f63fcb8a2d16b71855e6208a81f1107634d47972 Mon Sep 17 00:00:00 2001
From: patrick
Date: Fri, 16 Oct 2026 06:51:29 +0000
Subject: [PATCH] UX-03: anhang-vorschau
Frontend (web/mail-search): Anhang-Vorschau fuer PDF (Canvas-Rendering
via pdfjs-dist, gleiches Muster wie DOC-05s web/pdf-viewer/PdfViewer.tsx
-- kein aequivalentes PdfCanvasPreview im Repo gefunden, daher PdfViewer
als Vorlage uebernommen statt neu erfunden) und Bilder ( auf einem
authentifiziert geladenen Blob). Die Vorschauart haengt ausschliesslich
vom serverseitig per Magic-Bytes bestimmten contentType ab, nie von
einem Dateinamen (den es an der Schnittstelle gar nicht gibt); nicht
unterstuetzte Typen werden nur als Hinweistext angezeigt, nie
eingebettet. Kein iframe//dangerouslySetInnerHTML -- durch
automatisierten Sicherheitsreview-Test abgesichert. Neue lib/http.ts
(authentifizierter Zugriff ueber @nexarch/shl, identisch zu
web/pdf-viewer/lib/http.ts) und lib/attachments.ts (Anhangsliste/
-inhalt der Mail-API). Pruefprotokoll in mail/docs/UX-03-PRUEFPROTOKOLL.md.
Co-Authored-By: Claude Sonnet 5
---
.gitignore | 1 +
mail/docs/UX-03-PRUEFPROTOKOLL.md | 227 ++++++++++++++
.../mail/[messageId]/AttachmentList.test.tsx | 60 ++++
.../app/mail/[messageId]/AttachmentList.tsx | 50 ++++
.../[messageId]/AttachmentPreview.test.tsx | 63 ++++
.../mail/[messageId]/AttachmentPreview.tsx | 33 +++
.../app/mail/[messageId]/ImagePreview.tsx | 54 ++++
.../app/mail/[messageId]/PdfCanvasPreview.tsx | 106 +++++++
.../attachment-preview.security.test.ts | 33 +++
web/mail-search/app/mail/[messageId]/page.tsx | 2 +
web/mail-search/lib/attachmentKind.test.ts | 32 ++
web/mail-search/lib/attachmentKind.ts | 22 ++
web/mail-search/lib/attachments.test.ts | 62 ++++
web/mail-search/lib/attachments.ts | 58 ++++
web/mail-search/lib/http.ts | 33 +++
web/mail-search/package-lock.json | 278 ++++++++++++++++++
web/mail-search/package.json | 4 +
web/mail-search/vitest.config.ts | 5 +
18 files changed, 1123 insertions(+)
create mode 100644 mail/docs/UX-03-PRUEFPROTOKOLL.md
create mode 100644 web/mail-search/app/mail/[messageId]/AttachmentList.test.tsx
create mode 100644 web/mail-search/app/mail/[messageId]/AttachmentList.tsx
create mode 100644 web/mail-search/app/mail/[messageId]/AttachmentPreview.test.tsx
create mode 100644 web/mail-search/app/mail/[messageId]/AttachmentPreview.tsx
create mode 100644 web/mail-search/app/mail/[messageId]/ImagePreview.tsx
create mode 100644 web/mail-search/app/mail/[messageId]/PdfCanvasPreview.tsx
create mode 100644 web/mail-search/app/mail/[messageId]/attachment-preview.security.test.ts
create mode 100644 web/mail-search/lib/attachmentKind.test.ts
create mode 100644 web/mail-search/lib/attachmentKind.ts
create mode 100644 web/mail-search/lib/attachments.test.ts
create mode 100644 web/mail-search/lib/attachments.ts
create mode 100644 web/mail-search/lib/http.ts
diff --git a/.gitignore b/.gitignore
index b543ce14..5a08459e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,5 @@ web/*/.next/
web/*/next-env.d.ts
web/*/tsconfig.tsbuildinfo
web/pdf-viewer/public/pdf.worker.min.mjs
+web/mail-search/public/pdf.worker.min.mjs
__pycache__/
diff --git a/mail/docs/UX-03-PRUEFPROTOKOLL.md b/mail/docs/UX-03-PRUEFPROTOKOLL.md
new file mode 100644
index 00000000..b03cde6d
--- /dev/null
+++ b/mail/docs/UX-03-PRUEFPROTOKOLL.md
@@ -0,0 +1,227 @@
+# UX-03 — Anhang-Vorschau: Prüfprotokoll
+
+Datum: 2026-10-16
+Host: 192.168.1.204 (Build/Test), frischer Klon in eigenem Arbeitsverzeichnis
+Pakete: `mail/internal/mailapi` (geändert), `mail/internal/attachments` (Test ergänzt), `web/mail-search` (neu: Anhang-Vorschau)
+
+## Umsetzung
+
+### Backend (`mail/internal/mailapi`)
+
+- Neue Route `GET /api/v1/mail/messages/{messageID}/attachments` —
+ liefert die Anhangsliste als `{index, contentType, size}[]`. Es gibt
+ keinen eigenen Anhang-Katalog (keine DB-Tabelle mit Dateinamen); wie
+ `mailexport.fetchAttachments` tastet der Handler die Indizes ab 0 ab,
+ bis `storage.ErrNotFound` den ersten fehlenden Index meldet
+ (`maxAttachmentProbe = 200`, dieselbe Schutzgrenze wie in
+ `mailexport`).
+- `contentType` wird für JEDEN Anhang mit `net/http.DetectContentType`
+ auf den tatsächlich gelesenen Bytes bestimmt (Akzeptanzkriterium 2).
+ Es gibt an dieser Stelle gar keinen Dateinamen, der stattdessen
+ herangezogen werden könnte — `attachmentListItem` hat bewusst kein
+ Filename-Feld.
+- `handleGetAttachment` (bestehende Route, INT-01) ist geändert: der
+ bisher hartkodierte `Content-Type: application/octet-stream` wird
+ durch `http.DetectContentType(content)` ersetzt; zusätzlich
+ `Content-Disposition: inline` (vorher kein Header) statt eines
+ erzwungenen Downloads — Grundlage für Akzeptanzkriterium 1 ("ohne
+ externen Download").
+- `openapi.yaml` um den neuen Pfad `/messages/{messageID}/attachments`
+ samt Schema `AttachmentListResponse`/`AttachmentListItem` ergänzt;
+ `openapi_test.go`s Endpunkt-Abgleich (`TestOpenAPIDocument_MatchesActualEndpoints`)
+ und der Auth-Pflichtprüfungstest (`TestRoutes_RejectMissingOrInvalidToken`)
+ um die neue Route erweitert.
+
+### Backend (`mail/internal/attachments`, IMP-02)
+
+- Kein Verhaltens-, nur ein Testzugang: neuer Test
+ `TestExtract_WrongExtensionRealPDFContentDetectedByMagicBytes` belegt,
+ dass `VerifiedContentType` bereits vor UX-03 korrekt per Magic-Bytes
+ bestimmt wurde (`http.DetectContentType` auf den echten Bytes) und
+ dabei sowohl eine falsche Dateiendung (`.txt`) als auch einen falsch
+ deklarierten MIME-Typ (`text/plain`) ignoriert.
+
+### Frontend (`web/mail-search`)
+
+- `lib/http.ts` (neu): authentifizierter Zugriff auf Core-Modul-Token-
+ geschützte APIs über `@nexarch/shl` (`createSession`/`apiFetch`) —
+ identisches Muster wie `web/pdf-viewer/lib/http.ts` (DOC-05/UX-10),
+ nicht neu erfunden. `NEXT_PUBLIC_AUTH_MODE`, `NEXT_PUBLIC_CORE_API_BASE`,
+ `NEXT_PUBLIC_LOGIN_URL` (alles Umgebungsvariablen, keine Zugangsdaten
+ im Code).
+- `lib/attachments.ts` (neu): dünner Client für die Anhangsliste/den
+ Anhanginhalt der Mail-API (`NEXT_PUBLIC_MAIL_API_URL`). Direkter
+ authentifizierter Aufruf wie bei `web/pdf-viewer/lib/api.ts`
+ (`documentContentUrl`) — kein Backend-for-Frontend-Proxy, weil die
+ Mail-API ein Bearer-Modul-Token verlangt, das ein Next.js-Server-Route-
+ Handler nicht eigenständig ausstellen kann. Liefert bei Ausfall
+ `{status:"unavailable"}` statt zu werfen (gleiche Konvention wie
+ `lib/caseContext.ts`).
+- `lib/attachmentKind.ts` (neu): `classifyAttachment(contentType)` ist
+ die EINZIGE Stelle, die über die Vorschauart entscheidet — Eingabe
+ ausschließlich der serverseitig bestimmte `contentType`.
+- `app/mail/[messageId]/PdfCanvasPreview.tsx` (neu): PDF-Vorschau per
+ Canvas-Rendering (pdfjs-dist, wie DOC-05s `PdfViewer.tsx` — gleiches
+ Grundmuster, hier auf Laden + Seitennavigation reduziert, ohne
+ Suche/Zoom). **Kein äquivalentes, bereits existierendes
+ `PdfCanvasPreview` wurde im Repository gefunden** (durchsucht:
+ gesamtes Repo inkl. aller Branches, auch DMS/dms-browser) — DOC-05s
+ `web/pdf-viewer/components/PdfViewer.tsx` ist das einzige vorhandene
+ Canvas-PDF-Muster und wurde als Vorlage übernommen (`workerSrc` aus
+ `public/pdf.worker.min.mjs`, `copy-pdf-worker`-Skript in
+ `package.json`, identisch zu pdf-viewer).
+- `app/mail/[messageId]/ImagePreview.tsx` (neu): Bild-Vorschau per
+ ` ` auf einem via `apiFetch` geladenen Blob (nativ sicheres
+ Bild-Decoding, kein Skript-/HTML-Rendering wie bei iframe — anders
+ als PDF ist hier kein Canvas-Zwang nötig, das Ticket verlangt Canvas
+ ausdrücklich nur für PDF).
+- `app/mail/[messageId]/AttachmentPreview.tsx` (neu): wählt anhand
+ `classifyAttachment` zwischen PDF-/Bild-Vorschau oder einem neutralen
+ Hinweis ("Für diesen Dateityp ist keine Vorschau verfügbar.") — nie
+ iframe/``/`dangerouslySetInnerHTML`.
+- `app/mail/[messageId]/AttachmentList.tsx` (neu): lädt die Anhangsliste
+ und rendert je Anhang eine `AttachmentPreview`; in `page.tsx` unter
+ `CaseContextPanel` eingebunden.
+- `.gitignore`: `web/mail-search/public/pdf.worker.min.mjs` ergänzt
+ (gleiche Ausnahme wie für `web/pdf-viewer`).
+
+## Prüfung 1: Test — Anhang mit falscher Dateiendung, aber echtem PDF-Inhalt wird korrekt als PDF erkannt und angezeigt
+
+- Backend, Erkennung bei Extraktion (IMP-02):
+ `TestExtract_WrongExtensionRealPDFContentDetectedByMagicBytes`
+ (`mail/internal/attachments/attachments_test.go`) — Anhang mit Endung
+ `.txt`, deklariertem MIME-Typ `text/plain`, echtem PDF-Inhalt.
+ `VerifiedContentType` == `application/pdf`. **BESTANDEN.**
+- Backend, Erkennung beim Abruf (UX-03):
+ `TestGetAttachment_ContentTypeFromMagicBytesIgnoresDeclaredType` und
+ `TestListAttachments_ContentTypesDetectedFromMagicBytes`
+ (`mail/internal/mailapi/mailapi_test.go`) — Anhang mit real im
+ Speicher abgelegtem PDF-Inhalt, aber als `text/plain` bzw.
+ `application/octet-stream` deklariert abgelegt. `GET .../attachments/0`
+ liefert `Content-Type: application/pdf`; `GET .../attachments` listet
+ `contentType: "application/pdf"`. **BESTANDEN.**
+- Frontend: `lib/attachmentKind.test.ts` — `classifyAttachment` liefert
+ `"pdf"` für `application/pdf`, unabhängig von jeder Namensinformation
+ (`AttachmentInfo` trägt ohnehin keinen Dateinamen). `AttachmentPreview.test.tsx`
+ zeigt, dass ein Anhang mit `contentType: "application/pdf"` tatsächlich
+ die Canvas-PDF-Vorschau rendert. **BESTANDEN.**
+
+## Prüfung 2: Sicherheitsreview bestätigt: keine iframe-Einbindung von Anhangsinhalt
+
+- Automatisiert statt nur behauptet:
+ `app/mail/[messageId]/attachment-preview.security.test.ts` liest den
+ Quelltext von `PdfCanvasPreview.tsx`, `ImagePreview.tsx`,
+ `AttachmentPreview.tsx`, `AttachmentList.tsx` (Kommentare vor dem
+ Abgleich entfernt) und prüft die Abwesenheit von `
)}
+
);
}
diff --git a/web/mail-search/lib/attachmentKind.test.ts b/web/mail-search/lib/attachmentKind.test.ts
new file mode 100644
index 00000000..e8029100
--- /dev/null
+++ b/web/mail-search/lib/attachmentKind.test.ts
@@ -0,0 +1,32 @@
+import { describe, expect, it } from "vitest";
+import { classifyAttachment, describeAttachmentKind } from "./attachmentKind";
+
+// UX-03 Akzeptanzkriterium 2/3: classifyAttachment ist die EINZIGE
+// Stelle, die ueber die Vorschauart entscheidet, und entscheidet
+// ausschliesslich anhand des (serverseitig per Magic-Bytes bestimmten)
+// Content-Type -- nie anhand eines Dateinamens/einer Endung.
+describe("classifyAttachment", () => {
+ it("erkennt PDF ausschliesslich am Content-Type 'application/pdf'", () => {
+ expect(classifyAttachment("application/pdf")).toBe("pdf");
+ });
+
+ it("erkennt Bilder an jedem 'image/*'-Content-Type", () => {
+ expect(classifyAttachment("image/png")).toBe("image");
+ expect(classifyAttachment("image/jpeg")).toBe("image");
+ expect(classifyAttachment("image/gif")).toBe("image");
+ });
+
+ it("stuft jeden anderen Content-Type als nicht unterstützt ein, auch potenziell gefährliche", () => {
+ expect(classifyAttachment("text/html")).toBe("unsupported");
+ expect(classifyAttachment("application/octet-stream")).toBe("unsupported");
+ expect(classifyAttachment("application/x-msdownload")).toBe("unsupported");
+ });
+});
+
+describe("describeAttachmentKind", () => {
+ it("liefert deutschsprachige Bezeichnungen je Art", () => {
+ expect(describeAttachmentKind("application/pdf")).toBe("PDF-Dokument");
+ expect(describeAttachmentKind("image/png")).toBe("Bild (PNG)");
+ expect(describeAttachmentKind("text/html")).toContain("Nicht unterstützter Dateityp");
+ });
+});
diff --git a/web/mail-search/lib/attachmentKind.ts b/web/mail-search/lib/attachmentKind.ts
new file mode 100644
index 00000000..1182b128
--- /dev/null
+++ b/web/mail-search/lib/attachmentKind.ts
@@ -0,0 +1,22 @@
+// UX-03 Akzeptanzkriterium 2/3: Diese Zuordnung ist die EINZIGE Stelle,
+// die ueber die Vorschauart entscheidet -- Eingabe ist ausschliesslich
+// der serverseitig per Magic-Bytes bestimmte contentType (nie ein
+// Dateiname/eine Endung, die es an dieser Schnittstelle gar nicht gibt).
+export type AttachmentKind = "pdf" | "image" | "unsupported";
+
+export function classifyAttachment(contentType: string): AttachmentKind {
+ if (contentType === "application/pdf") return "pdf";
+ if (contentType.startsWith("image/")) return "image";
+ return "unsupported";
+}
+
+export function describeAttachmentKind(contentType: string): string {
+ switch (classifyAttachment(contentType)) {
+ case "pdf":
+ return "PDF-Dokument";
+ case "image":
+ return `Bild (${contentType.slice("image/".length).toUpperCase()})`;
+ default:
+ return `Nicht unterstützter Dateityp (${contentType})`;
+ }
+}
diff --git a/web/mail-search/lib/attachments.test.ts b/web/mail-search/lib/attachments.test.ts
new file mode 100644
index 00000000..272e9d40
--- /dev/null
+++ b/web/mail-search/lib/attachments.test.ts
@@ -0,0 +1,62 @@
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { attachmentContentUrl, attachmentsUrl, fetchAttachmentList } from "./attachments";
+
+const ORIGINAL_ENV = process.env;
+
+afterEach(() => {
+ vi.restoreAllMocks();
+ process.env = ORIGINAL_ENV;
+});
+
+describe("attachmentsUrl / attachmentContentUrl", () => {
+ it("baut die URLs aus NEXT_PUBLIC_MAIL_API_URL ohne doppelten Schrägstrich", () => {
+ process.env = { ...ORIGINAL_ENV, NEXT_PUBLIC_MAIL_API_URL: "http://mail-api.test/" };
+ expect(attachmentsUrl("m-1")).toBe("http://mail-api.test/messages/m-1/attachments");
+ expect(attachmentContentUrl("m-1", 2)).toBe("http://mail-api.test/messages/m-1/attachments/2");
+ });
+
+ it("wirft einen klaren Fehler ohne gesetzte Umgebungsvariable", () => {
+ process.env = { ...ORIGINAL_ENV, NEXT_PUBLIC_MAIL_API_URL: undefined };
+ expect(() => attachmentsUrl("m-1")).toThrow(/NEXT_PUBLIC_MAIL_API_URL/);
+ });
+});
+
+describe("fetchAttachmentList", () => {
+ // Akzeptanzkriterium 1/2: liefert die vom Server per Magic-Bytes
+ // bestimmten Content-Types unveraendert durch.
+ it("liefert die Anhangsliste bei Erfolg", async () => {
+ process.env = { ...ORIGINAL_ENV, NEXT_PUBLIC_MAIL_API_URL: "http://mail-api.test" };
+ global.fetch = vi.fn().mockResolvedValue({
+ ok: true,
+ status: 200,
+ json: async () => ({ attachments: [{ index: 0, contentType: "application/pdf", size: 1234 }] }),
+ });
+
+ const result = await fetchAttachmentList("m-1");
+
+ expect(result.status).toBe("ok");
+ if (result.status === "ok") {
+ expect(result.attachments).toEqual([{ index: 0, contentType: "application/pdf", size: 1234 }]);
+ }
+ });
+
+ // Akzeptanzkriterium 1: ein Ausfall der Mail-API blockiert die uebrige
+ // Mailanzeige nicht -- liefert "unavailable" statt zu werfen.
+ it("liefert 'unavailable' statt zu werfen, wenn die Mail-API nicht erreichbar ist", async () => {
+ process.env = { ...ORIGINAL_ENV, NEXT_PUBLIC_MAIL_API_URL: "http://mail-api.test" };
+ global.fetch = vi.fn().mockRejectedValue(new Error("network down"));
+
+ const result = await fetchAttachmentList("m-1");
+
+ expect(result.status).toBe("unavailable");
+ });
+
+ it("liefert 'unavailable' bei einer Fehlerantwort (z. B. 404/502)", async () => {
+ process.env = { ...ORIGINAL_ENV, NEXT_PUBLIC_MAIL_API_URL: "http://mail-api.test" };
+ global.fetch = vi.fn().mockResolvedValue({ ok: false, status: 404, json: async () => ({ error: "nicht gefunden" }) });
+
+ const result = await fetchAttachmentList("m-1");
+
+ expect(result.status).toBe("unavailable");
+ });
+});
diff --git a/web/mail-search/lib/attachments.ts b/web/mail-search/lib/attachments.ts
new file mode 100644
index 00000000..6bc015a0
--- /dev/null
+++ b/web/mail-search/lib/attachments.ts
@@ -0,0 +1,58 @@
+// UX-03: duenner Client fuer die Anhangsliste/den Anhanginhalt der
+// Mail-API (mailapi, INT-01). Direkter authentifizierter Aufruf ueber
+// apiFetch (dasselbe Muster wie web/pdf-viewer/lib/api.ts fuer die
+// DMS-Dokumentinhalts-URL) -- keine eigene Kopie der Backend-Logik.
+import { apiFetch } from "./http";
+
+function apiBase(): string {
+ const base = process.env.NEXT_PUBLIC_MAIL_API_URL;
+ if (!base) {
+ throw new Error("NEXT_PUBLIC_MAIL_API_URL ist nicht gesetzt (Umgebungsvariable erforderlich)");
+ }
+ return base.replace(/\/$/, "");
+}
+
+// AttachmentInfo hat BEWUSST keinen Dateinamen -- die Mail-API liefert
+// keinen (siehe mailapi.attachmentListItem) und die Vorschau darf sich
+// nur auf contentType stuetzen (Akzeptanzkriterium 2/3).
+export interface AttachmentInfo {
+ index: number;
+ contentType: string;
+ size: number;
+}
+
+interface AttachmentListResponseBody {
+ attachments: AttachmentInfo[];
+}
+
+export function attachmentsUrl(messageId: string): string {
+ return `${apiBase()}/messages/${encodeURIComponent(messageId)}/attachments`;
+}
+
+export function attachmentContentUrl(messageId: string, index: number): string {
+ return `${apiBase()}/messages/${encodeURIComponent(messageId)}/attachments/${index}`;
+}
+
+export type AttachmentListResult =
+ | { status: "ok"; attachments: AttachmentInfo[] }
+ | { status: "unavailable" };
+
+// Akzeptanzkriterium 1: ein Ausfall der Mail-API darf die uebrige
+// Mailanzeige nicht blockieren (gleiche Konvention wie
+// lib/caseContext.ts#fetchCaseContext) -- daher wirft diese Funktion nie.
+export async function fetchAttachmentList(messageId: string): Promise {
+ let res: Response;
+ try {
+ res = await apiFetch(attachmentsUrl(messageId));
+ } catch {
+ return { status: "unavailable" };
+ }
+ if (!res.ok) {
+ return { status: "unavailable" };
+ }
+ const body: AttachmentListResponseBody | null = await res.json().catch(() => null);
+ if (!body || !Array.isArray(body.attachments)) {
+ return { status: "unavailable" };
+ }
+ return { status: "ok", attachments: body.attachments };
+}
diff --git a/web/mail-search/lib/http.ts b/web/mail-search/lib/http.ts
new file mode 100644
index 00000000..7b54b3eb
--- /dev/null
+++ b/web/mail-search/lib/http.ts
@@ -0,0 +1,33 @@
+// UX-03: Zugriff auf die Mail-API (mailapi, Bearer/Core-Modul-Token) mit
+// demselben Muster wie DOC-05/UX-10 im pdf-viewer (web/pdf-viewer/lib/http.ts)
+// -- zentral ueber @nexarch/shl (SHL-01), nicht neu erfunden.
+import { createSession, redirectToLogin, type Session } from "@nexarch/shl";
+
+export function authMode(): "token" | "caller" {
+ return process.env.NEXT_PUBLIC_AUTH_MODE === "caller" ? "caller" : "token";
+}
+
+let session: Session | undefined;
+
+export function getSession(): Session {
+ if (!session) {
+ const coreBase = process.env.NEXT_PUBLIC_CORE_API_BASE;
+ if (!coreBase) {
+ throw new Error("NEXT_PUBLIC_CORE_API_BASE ist nicht gesetzt (Umgebungsvariable erforderlich)");
+ }
+ session = createSession({
+ coreBase,
+ onLoginRequired: () => redirectToLogin(process.env.NEXT_PUBLIC_LOGIN_URL),
+ });
+ }
+ return session;
+}
+
+export function resetSessionForTests(): void {
+ session = undefined;
+}
+
+export function apiFetch(input: string, init?: RequestInit): Promise {
+ if (authMode() === "caller") return fetch(input, init);
+ return getSession().fetch(input, init);
+}
diff --git a/web/mail-search/package-lock.json b/web/mail-search/package-lock.json
index 383720ce..4d4cd76f 100644
--- a/web/mail-search/package-lock.json
+++ b/web/mail-search/package-lock.json
@@ -8,6 +8,7 @@
"dependencies": {
"@nexarch/shl": "file:../shl",
"next": "16.3.5",
+ "pdfjs-dist": "^4.10.38",
"react": "19.3.0",
"react-dom": "19.3.0"
},
@@ -735,6 +736,271 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@napi-rs/canvas": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.100.tgz",
+ "integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==",
+ "license": "MIT",
+ "optional": true,
+ "workspaces": [
+ "e2e/*"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "optionalDependencies": {
+ "@napi-rs/canvas-android-arm64": "0.1.100",
+ "@napi-rs/canvas-darwin-arm64": "0.1.100",
+ "@napi-rs/canvas-darwin-x64": "0.1.100",
+ "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100",
+ "@napi-rs/canvas-linux-arm64-gnu": "0.1.100",
+ "@napi-rs/canvas-linux-arm64-musl": "0.1.100",
+ "@napi-rs/canvas-linux-riscv64-gnu": "0.1.100",
+ "@napi-rs/canvas-linux-x64-gnu": "0.1.100",
+ "@napi-rs/canvas-linux-x64-musl": "0.1.100",
+ "@napi-rs/canvas-win32-arm64-msvc": "0.1.100",
+ "@napi-rs/canvas-win32-x64-msvc": "0.1.100"
+ }
+ },
+ "node_modules/@napi-rs/canvas-android-arm64": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz",
+ "integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-darwin-arm64": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz",
+ "integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-darwin-x64": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz",
+ "integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz",
+ "integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-arm64-gnu": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz",
+ "integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-arm64-musl": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz",
+ "integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.100.tgz",
+ "integrity": "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-x64-gnu": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz",
+ "integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-x64-musl": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz",
+ "integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-win32-arm64-msvc": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz",
+ "integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-win32-x64-msvc": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz",
+ "integrity": "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
"node_modules/@nexarch/shl": {
"version": "0.1.0",
"resolved": "file:../shl",
@@ -2445,6 +2711,18 @@
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
+ "node_modules/pdfjs-dist": {
+ "version": "4.10.38",
+ "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.10.38.tgz",
+ "integrity": "sha512-/Y3fcFrXEAsMjJXeL9J8+ZG9U01LbuWaYypvDW2ycW1jL269L3js3DVBjDJ0Up9Np1uqDXsDrRihHANhZOlwdQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=20"
+ },
+ "optionalDependencies": {
+ "@napi-rs/canvas": "^0.1.65"
+ }
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
diff --git a/web/mail-search/package.json b/web/mail-search/package.json
index 34263062..620daeed 100644
--- a/web/mail-search/package.json
+++ b/web/mail-search/package.json
@@ -2,6 +2,9 @@
"name": "nexarch-mail-search",
"private": true,
"scripts": {
+ "copy-pdf-worker": "mkdir -p public && cp node_modules/pdfjs-dist/build/pdf.worker.min.mjs public/pdf.worker.min.mjs",
+ "predev": "npm run copy-pdf-worker",
+ "prebuild": "npm run copy-pdf-worker",
"dev": "next dev",
"build": "next build",
"start": "next start",
@@ -10,6 +13,7 @@
"dependencies": {
"@nexarch/shl": "file:../shl",
"next": "16.3.5",
+ "pdfjs-dist": "^4.10.38",
"react": "19.3.0",
"react-dom": "19.3.0"
},
diff --git a/web/mail-search/vitest.config.ts b/web/mail-search/vitest.config.ts
index 262024c2..6b7e5b51 100644
--- a/web/mail-search/vitest.config.ts
+++ b/web/mail-search/vitest.config.ts
@@ -4,5 +4,10 @@ export default defineConfig({
test: {
environment: "jsdom",
setupFiles: ["./vitest.setup.ts"],
+ // UX-03: Tests mocken global.fetch direkt (wie die uebrigen lib/*.test.ts
+ // dieser App) -- "caller" ueberspringt die @nexarch/shl-Session, die ein
+ // echtes NEXT_PUBLIC_CORE_API_BASE braeuchte (gleiches Muster wie
+ // web/pdf-viewer/vitest.config.ts).
+ env: { NEXT_PUBLIC_AUTH_MODE: "caller" },
},
});