purgo
Version:
Zero-config PHI-scrubber for browser and Node.js
229 lines (225 loc) • 6.39 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/node.ts
var node_exports = {};
__export(node_exports, {
initNodePatches: () => initNodePatches,
pinoRedactor: () => pinoRedactor,
redact: () => redact
});
module.exports = __toCommonJS(node_exports);
// src/redact.ts
var BUILT_IN_PATTERNS = {
// Email addresses
email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
// Phone numbers (various formats)
phone: /(\+\d{1,2}\s?)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}/g,
// Social Security Numbers
ssn: /\b\d{3}[-]?\d{2}[-]?\d{4}\b/g,
// Medical Record Numbers (generic pattern, can be customized)
mrn: /\b\d{6,10}\b/g,
// ICD-10 codes
icd10: /\b[A-Z]\d{2}(\.[A-Z0-9]{1,4})?\b/g
};
var DEFAULT_CENSOR = () => "***";
var globalConfig = {
patterns: [],
censor: DEFAULT_CENSOR,
hashMode: false
};
function initRedactionEngine(options = {}) {
const patterns = options.patterns || ["email", "phone", "ssn", "mrn", "icd10"];
const regexPatterns = patterns.map((pattern) => {
if (typeof pattern === "string") {
return BUILT_IN_PATTERNS[pattern] || new RegExp(pattern, "g");
}
return pattern;
});
globalConfig = {
patterns: regexPatterns,
censor: options.censor || DEFAULT_CENSOR,
hashMode: options.hashMode || false
};
}
function redactString(value) {
if (typeof value !== "string")
return value;
let result = value;
try {
for (const pattern of globalConfig.patterns) {
const patternCopy = new RegExp(pattern.source, pattern.flags);
result = result.replace(patternCopy, (match) => {
if (globalConfig.hashMode) {
const hash = match.split("").reduce((a, b) => {
const hashCode = a.charCodeAt(0) ^ b.charCodeAt(0);
return String.fromCharCode(hashCode);
}, "\0");
return `[HASH:${hash}]`;
}
return globalConfig.censor(match);
});
}
} catch (e) {
console.error("Purgo: Error during string redaction, returning original value", e);
return value;
}
return result;
}
function isDOMNode(obj) {
return typeof Node === "object" ? obj instanceof Node : obj && typeof obj === "object" && typeof obj.nodeType === "number";
}
function redact(value, seen = /* @__PURE__ */ new WeakSet()) {
if (value === null || value === void 0)
return value;
if (typeof value !== "object") {
return typeof value === "string" ? redactString(value) : value;
}
if (isDOMNode(value))
return value;
if (seen.has(value))
return value;
seen.add(value);
if (Array.isArray(value)) {
let hasCircularRefs2 = false;
const length = value.length;
const result2 = new Array(length);
for (let i = 0; i < length; i++) {
const item = value[i];
if (item === value) {
result2[i] = null;
} else {
result2[i] = redact(item, seen);
}
}
if (hasCircularRefs2) {
for (let i = 0; i < length; i++) {
if (value[i] === value) {
result2[i] = result2;
}
}
}
return result2;
}
let hasPrimitiveOnly = true;
let hasCircularRefs = false;
let hasPHI = false;
for (const key in value) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
const propValue = value[key];
if (propValue === value) {
hasCircularRefs = true;
hasPrimitiveOnly = false;
break;
}
if (propValue !== null && typeof propValue === "object") {
hasPrimitiveOnly = false;
break;
}
if (typeof propValue === "string" && containsPHI(propValue)) {
hasPHI = true;
break;
}
}
}
if (hasPrimitiveOnly && !hasPHI) {
return value;
}
const result = {};
const circularKeys = [];
for (const key in value) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
const propValue = value[key];
if (propValue === value) {
hasCircularRefs = true;
circularKeys.push(key);
result[key] = null;
} else {
result[key] = redact(propValue, seen);
}
}
}
if (hasCircularRefs) {
for (const key of circularKeys) {
result[key] = result;
}
}
return result;
}
function containsPHI(value) {
try {
for (const pattern of globalConfig.patterns) {
const patternCopy = new RegExp(pattern.source, pattern.flags);
if (patternCopy.test(value)) {
return true;
}
}
return false;
} catch (e) {
console.error("Purgo: Error checking for PHI, assuming it might contain PHI", e);
return true;
}
}
initRedactionEngine();
// src/node.ts
var originalStdoutWrite = null;
function patchStdout() {
if (typeof process === "undefined" || !process.stdout)
return;
originalStdoutWrite = process.stdout.write;
process.stdout.write = function(buffer, encoding, callback) {
if (typeof encoding === "function") {
callback = encoding;
encoding = void 0;
}
let redactedBuffer = buffer;
if (typeof buffer === "string") {
redactedBuffer = redact(buffer);
}
return originalStdoutWrite.call(
process.stdout,
redactedBuffer,
encoding,
callback
);
};
}
function pinoRedactor(options) {
const { paths, additionalPatterns = [], censor } = options;
const allPatterns = [
"email",
"phone",
"ssn",
"mrn",
"icd10",
...additionalPatterns
];
initRedactionEngine({
patterns: allPatterns,
censor
});
return {
paths,
censor: (value) => redact(value)
};
}
function initNodePatches(options = {}) {
initRedactionEngine(options);
patchStdout();
}
initNodePatches();