import { fetchWithAuth } from "./auth"; import { getPreferences } from "./preferences"; import type { DepseudonymizeResponse, EntityType, PlaceholderMapping, PseudonymizeResponse, } from "./types"; function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } export function normalizePseudonymizeResponse( response: PseudonymizeResponse, ): PseudonymizeResponse { const normalizedMapping: PlaceholderMapping = {}; const strippedKeys = new Set(); for (const [key, entry] of Object.entries(response.mapping)) { const stripped = key.replace(/[<>]/g, ""); normalizedMapping[`<${stripped}>`] = entry; strippedKeys.add(stripped); } let text = response.pseudonymized_text; const ordered = Array.from(strippedKeys).sort((a, b) => b.length - a.length); for (const stripped of ordered) { const bracketed = `<${stripped}>`; const bareRegex = new RegExp( `(?])`, "g", ); text = text.replace(bareRegex, bracketed); } return { ...response, pseudonymized_text: text, mapping: normalizedMapping, }; } export function localDepseudonymize( text: string, mapping: PlaceholderMapping, ): DepseudonymizeResponse { let result = text; let replacementsMade = 0; const entries = Object.entries(mapping) .map(([key, entry]) => ({ stripped: key.replace(/[<>]/g, ""), entry, })) .sort((a, b) => b.stripped.length - a.stripped.length); for (const { stripped, entry } of entries) { const bracketed = `<${stripped}>`; const bracketedRegex = new RegExp(escapeRegExp(bracketed), "g"); const bracketedMatches = result.match(bracketedRegex); if (bracketedMatches) { replacementsMade += bracketedMatches.length; result = result.replace(bracketedRegex, entry.original); } const bareRegex = new RegExp(`\\b${escapeRegExp(stripped)}\\b`, "g"); const bareMatches = result.match(bareRegex); if (bareMatches) { replacementsMade += bareMatches.length; result = result.replace(bareRegex, entry.original); } } return { original_text: result, replacements_made: replacementsMade }; } function apiUrl(path: string): string { return `${getPreferences().velumBaseUrl}${path}`; } async function parseJsonResponse(response: Response): Promise { const contentType = response.headers.get("content-type") ?? ""; if (!response.ok) { const details = await response.text().catch(() => response.statusText); throw new Error( `Velum API request failed: ${response.status} ${response.statusText}: ${details}`, ); } if (!contentType.includes("application/json")) { const details = await response.text().catch(() => ""); throw new Error( `Velum API returned non-JSON response: ${details.slice(0, 200)}`, ); } return (await response.json()) as T; } export async function getEntityTypes(): Promise { const response = await fetchWithAuth(apiUrl("/api/entity-types")); const payload = await parseJsonResponse<{ entity_types: EntityType[] }>( response, ); return payload.entity_types; } export async function pseudonymize( text: string, mapping: PlaceholderMapping, entityTypes?: EntityType[], forceMasks?: Record, ): Promise { const response = await fetchWithAuth(apiUrl("/api/pseudonymize"), { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ text, mapping, entity_types: entityTypes, force_masks: forceMasks, }), }); return parseJsonResponse(response); } export type DocumentHit = { id: string; source: string; context: string; start: number; end: number; type: string; score: number; placeholder: string; original: string; }; export async function requireProjectCapabilities(): Promise { const response = await fetchWithAuth(apiUrl("/api/project-capabilities")); if (!response.ok) throw new Error("Die Velum-API muss für die Projektfunktionen aktualisiert werden."); const capabilities = await parseJsonResponse>(response); if (!capabilities.request_force_masks || !capabilities.document_hit_review || !capabilities.document_restoration || !capabilities.comment_author_review) { throw new Error("Die Velum-API unterstützt die Projektfunktionen noch nicht vollständig."); } } export type DocumentResult = { filename: string; content_type: string; file_base64: string; mapping: PlaceholderMapping; active_placeholders: string[]; hits: DocumentHit[]; }; export async function pseudonymizeDocument( filename: string, data: Buffer, mapping: PlaceholderMapping, forceMasks: Record, acceptedHitIds?: string[], entityTypes?: EntityType[], ): Promise { const form = new FormData(); form.set("file", new Blob([new Uint8Array(data)]), filename); form.set("mapping", JSON.stringify(mapping)); form.set("force_masks", JSON.stringify(forceMasks)); if (entityTypes !== undefined) form.set("entity_types", JSON.stringify(entityTypes)); if (acceptedHitIds) form.set("accepted_hit_ids", JSON.stringify(acceptedHitIds)); const response = await fetchWithAuth(apiUrl("/api/pseudonymize-document"), { method: "POST", body: form, }); return parseJsonResponse(response); } export async function depseudonymizeDocument( filename: string, data: Buffer, mapping: PlaceholderMapping, ): Promise { const form = new FormData(); form.set("file", new Blob([new Uint8Array(data)]), filename); form.set("mapping", JSON.stringify(mapping)); const response = await fetchWithAuth(apiUrl("/api/depseudonymize-document"), { method: "POST", body: form, }); const result = await parseJsonResponse<{ file_base64: string }>(response); return Buffer.from(result.file_base64, "base64"); } export async function depseudonymize( text: string, mapping: PlaceholderMapping, ): Promise { const response = await fetchWithAuth(apiUrl("/api/depseudonymize"), { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ text, mapping, }), }); return parseJsonResponse(response); }