/** * Anonymize payload (folder_context, question) by replacing PII with placeholders. * Returns anonymized payload and a mapping for recontextualization. * Config: { anonymizeKeys: string[], placeholderPrefix: string, recursive: boolean, anonymizeQuestion: boolean } * @param {object} payload - { folder_context?: object, question?: string, ... } * @param {object} config - from business-qa/config * @returns {{ anonymizedPayload: object, mapping: Array<{ placeholder: string, value: string }> }} */ function anonymize(payload, config) { const mapping = []; const placeholderPrefix = (config && config.placeholderPrefix) || "PII"; const anonymizeKeys = (config && config.anonymizeKeys) || []; const recursive = config && config.recursive !== false; const anonymizeQuestion = config && config.anonymizeQuestion !== false; function nextPlaceholder() { return `${placeholderPrefix}_${mapping.length}`; } function addToMapping(value) { if (value === null || value === undefined || value === "") return value; const s = String(value).trim(); if (!s) return value; const existing = mapping.find((m) => m.value === s); if (existing) return existing.placeholder; const placeholder = nextPlaceholder(); mapping.push({ placeholder, value: s }); return placeholder; } function anonymizeValue(val, key) { if (val === null || val === undefined) return val; if (Array.isArray(val)) { return val.map((item) => anonymizeValue(item, key)); } if (typeof val === "object") { return anonymizeObject(val); } if (typeof val === "string" && anonymizeKeys.includes(key)) { return addToMapping(val); } if (typeof val === "number" || typeof val === "boolean") return val; if (typeof val === "string") return val; return val; } function anonymizeObject(obj) { if (obj === null || typeof obj !== "object") return obj; const out = {}; for (const [k, v] of Object.entries(obj)) { const key = k; if (recursive && (Array.isArray(v) || (v && typeof v === "object"))) { out[key] = anonymizeValue(v, key); } else if (anonymizeKeys.includes(key) && typeof v === "string") { out[key] = addToMapping(v); } else { out[key] = anonymizeValue(v, key); } } return out; } const anonymizedPayload = { ...payload }; if (payload.folder_context && typeof payload.folder_context === "object") { anonymizedPayload.folder_context = anonymizeObject(payload.folder_context); } if (anonymizeQuestion && payload.question && typeof payload.question === "string") { let q = payload.question; for (const m of mapping) { const re = new RegExp(escapeRegex(m.value), "g"); q = q.replace(re, m.placeholder); } anonymizedPayload.question = q; } return { anonymizedPayload, mapping }; } function escapeRegex(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } module.exports = { anonymize };