UNPKG

resume-anonymizer

Version:

A flexible NPM package for anonymizing resumes to reduce bias in hiring

159 lines (158 loc) 6.63 kB
"use strict"; 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 } }