resume-anonymizer
Version:
A flexible NPM package for anonymizing resumes to reduce bias in hiring
159 lines (158 loc) • 6.63 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.anonymizeCandidate = anonymizeCandidate;
const constants_1 = require("./constants");
const replacer_1 = require("./utils/replacer");
const pii_detector_1 = require("./utils/pii-detector");
const ai_anonymizer_1 = require("./ai-anonymizer");
async function anonymizeCandidate(candidate, options = {}) {
// Check if AI is available (API key exists)
const hasAI = options.aiProvider?.apiKey || process.env.OPENAI_API_KEY;
// If AI is available and not explicitly disabled, use AI-powered anonymization
if (hasAI && options.useAI !== false) {
return anonymizeWithAI(candidate, options);
}
// Otherwise, fall back to rule-based anonymization
return anonymizeWithRules(candidate, options);
}
async function anonymizeWithAI(candidate, options = {}) {
try {
// Prepare AI options
const aiOptions = {
apiKey: options.aiProvider?.apiKey || process.env.OPENAI_API_KEY,
model: options.aiProvider?.model || 'gpt-4o-mini',
rewordingLevel: options.rewordingLevel || 'medium',
preserveStructure: true
};
// Analyze the data with AI
const analysis = await (0, ai_anonymizer_1.analyzeForAnonymization)(candidate, aiOptions);
// Apply the AI-suggested anonymization
const { anonymizedData, changes } = await (0, ai_anonymizer_1.applyAIAnonymization)(candidate, analysis, aiOptions);
// Apply any additional rule-based processing if needed
const effectiveOptions = options.mode
? { ...options, hide: { ...constants_1.PRESET_MODES[options.mode].hide, ...options.hide } }
: options;
// Verify anonymization if requested
if (effectiveOptions.verify) {
await verifyAnonymization(anonymizedData, candidate);
}
return {
anonymizedResume: anonymizedData,
changes,
};
}
catch {
// AI anonymization failed, fall back to rule-based anonymization
return anonymizeWithRules(candidate, options);
}
}
async function anonymizeWithRules(candidate, options = {}) {
// Apply preset mode if specified
const effectiveOptions = options.mode
? { ...options, hide: { ...constants_1.PRESET_MODES[options.mode].hide, ...options.hide } }
: options;
const hide = effectiveOptions.hide || {};
const pseudonym = {
...constants_1.DEFAULT_PSEUDONYMS,
...effectiveOptions.pseudonym,
};
const replacer = new replacer_1.Replacer(pseudonym);
let anonymized = JSON.parse(JSON.stringify(candidate)); // Deep clone
// Step 1: Anonymize structured fields
if (hide.name && anonymized.name) {
const replacement = replacer.getOrCreateReplacement(anonymized.name, 'name');
anonymized.name = replacement;
}
if (hide.contact) {
anonymized = (0, pii_detector_1.removeContactFromObject)(anonymized, replacer.changeLog);
}
if (hide.photo && anonymized.photo) {
delete anonymized.photo;
}
// Step 2: Process work experience
if (anonymized.work && Array.isArray(anonymized.work)) {
anonymized.work = anonymized.work.map((job) => {
const newJob = { ...job };
if (hide?.companyNames && job.company) {
newJob.company = replacer.getOrCreateReplacement(job.company, 'company');
}
if (hide?.locations && job.location) {
delete newJob.location;
}
// Process text fields for PII and replacements
if (job.summary) {
newJob.summary = processTextField(job.summary, hide, replacer);
}
if (job.highlights && Array.isArray(job.highlights)) {
newJob.highlights = job.highlights.map((highlight) => processTextField(highlight, hide, replacer));
}
return newJob;
});
}
// Step 3: Process education
if (anonymized.education && Array.isArray(anonymized.education)) {
anonymized.education = anonymized.education.map((edu) => {
const newEdu = { ...edu };
if (hide?.education && edu.institution) {
newEdu.institution = replacer.getOrCreateReplacement(edu.institution, 'education');
}
if (hide?.locations) {
delete newEdu.location;
delete newEdu.area;
}
return newEdu;
});
}
// Step 4: Process references
if (hide?.references && anonymized.references && Array.isArray(anonymized.references)) {
anonymized.references = anonymized.references.map((ref) => {
const newRef = { ...ref };
if (ref.name) {
newRef.name = replacer.getOrCreateReplacement(ref.name, 'person');
}
if (ref.reference) {
newRef.reference = processTextField(ref.reference, hide, replacer);
}
return newRef;
});
}
// Step 5: Process summary and other text fields
if (anonymized.summary) {
anonymized.summary = processTextField(anonymized.summary, hide, replacer);
}
// Step 6: Verification
if (effectiveOptions.verify) {
await verifyAnonymization(anonymized, candidate);
}
return {
anonymizedResume: anonymized,
changes: replacer.changeLog,
};
}
function processTextField(text, hide, replacer) {
let processed = text;
// Remove contact information
if (hide?.contact) {
const pii = (0, pii_detector_1.detectPII)(text);
processed = (0, pii_detector_1.removePII)(processed, pii);
}
// Apply all replacements
for (const [key, replacement] of replacer.getAllReplacements()) {
const [, original] = key.split(':');
processed = replacer.replaceInText(processed, original, replacement);
}
return processed;
}
async function verifyAnonymization(anonymized, _original) {
// This is a placeholder for verification functionality
// In a real implementation, you would:
// 1. Check for any remaining PII patterns
// 2. Optionally use AI to scan for missed information
// 3. Compare against the original to ensure all targeted data was removed
// For now, just perform basic regex checks
const jsonString = JSON.stringify(anonymized);
const pii = (0, pii_detector_1.detectPII)(jsonString);
if (pii.emails.length > 0 || pii.phones.length > 0) {
// Warning: Potential PII detected in anonymized resume
}
}