86 lines
2.9 KiB
JavaScript
86 lines
2.9 KiB
JavaScript
/**
|
|
* 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 };
|