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[], ): Promise { const response = await fetchWithAuth(apiUrl("/api/pseudonymize"), { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ text, mapping, entity_types: entityTypes && entityTypes.length > 0 ? entityTypes : undefined, }), }); return parseJsonResponse(response); } 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); }