Files
velum-raycast/src/velum.ts

213 lines
6.3 KiB
TypeScript

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<string>();
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(
`(?<![<\\w])${escapeRegExp(stripped)}(?![\\w>])`,
"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<T>(response: Response): Promise<T> {
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<EntityType[]> {
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<string, string[]>,
): Promise<PseudonymizeResponse> {
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<PseudonymizeResponse>(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<void> {
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<Record<string, boolean>>(response);
if (!capabilities.request_force_masks || !capabilities.document_hit_review
|| !capabilities.document_restoration || !capabilities.comment_author_review
|| !capabilities.revision_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<string, string[]>,
acceptedHitIds?: string[],
entityTypes?: EntityType[],
): Promise<DocumentResult> {
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<DocumentResult>(response);
}
export async function depseudonymizeDocument(
filename: string,
data: Buffer,
mapping: PlaceholderMapping,
): Promise<Buffer> {
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<DepseudonymizeResponse> {
const response = await fetchWithAuth(apiUrl("/api/depseudonymize"), {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
text,
mapping,
}),
});
return parseJsonResponse<DepseudonymizeResponse>(response);
}