UNPKG

@arcjet/redact

Version:

Arcjet sensitive information redaction library

136 lines (133 loc) 4.52 kB
import { initializeWasm } from '@arcjet/redact-wasm'; function userEntitiesToWasm(entity) { if (typeof entity !== "string") { throw new Error("redaction entities must be strings"); } if (entity === "email") { return { tag: "email" }; } if (entity === "phone-number") { return { tag: "phone-number" }; } if (entity === "ip-address") { return { tag: "ip-address" }; } if (entity === "credit-card-number") { return { tag: "credit-card-number" }; } return { tag: "custom", val: entity, }; } function wasmEntitiesToString(entity) { if (entity.tag === "email") { return "email"; } if (entity.tag === "ip-address") { return "ip-address"; } if (entity.tag === "credit-card-number") { return "credit-card-number"; } if (entity.tag === "phone-number") { return "phone-number"; } return entity.val; } function performReplacementInText(text, replacement, start, end) { return text.substring(0, start) + replacement + text.substring(end); } /* c8 ignore start */ // Coverage is ignored on these no-op functions because they are never executed // due to the `skipCustomDetect` and `skipCustomReplace` options. function noOpDetect(_tokens) { return []; } function noOpReplace(_input, _plaintext) { return undefined; } function getWasmOptions(options) { if (typeof options === "object" && options !== null) { if (typeof options.entities !== "undefined") { if (Array.isArray(options.entities)) { if (options.entities.length < 1) { throw new Error("no entities configured for redaction"); } } else { throw new Error("entities must be an array"); } } return { entities: options.entities?.map(userEntitiesToWasm), contextWindowSize: options.contextWindowSize || 1, skipCustomDetect: typeof options.detect !== "function", skipCustomRedact: typeof options.replace !== "function", }; } else { return { entities: undefined, contextWindowSize: 1, skipCustomDetect: true, skipCustomRedact: true, }; } } async function callRedactWasm(candidate, options) { let convertedDetect = noOpDetect; if (typeof options?.detect === "function") { const detect = options.detect; convertedDetect = (tokens) => { return detect(tokens) .filter((e) => typeof e !== "undefined") .map((e) => userEntitiesToWasm(e)); }; } let convertedReplace = noOpReplace; if (typeof options?.replace === "function") { const replace = options.replace; convertedReplace = (identifiedType, plaintext) => { return replace( // @ts-ignore because we know this is coming from Wasm wasmEntitiesToString(identifiedType), plaintext); }; } const wasm = await initializeWasm(convertedDetect, convertedReplace); if (typeof wasm !== "undefined") { const config = getWasmOptions(options); return wasm.redact(candidate, config).map((e) => { return { ...e, identifiedType: wasmEntitiesToString(e.identifiedType), }; }); } else { throw new Error("redact failed to run because Wasm is not supported in this environment"); } } async function redact(candidate, options) { const redactions = await callRedactWasm(candidate, options); // Need to apply the redactions in reverse order so that the offsets aren't changed // when we redact with strings that are longer/shorter than the original. redactions.reverse(); for (const redaction of redactions) { candidate = performReplacementInText(candidate, redaction.redacted, redaction.start, redaction.end); } function unredact(input) { for (const redaction of redactions) { let position; while (position !== -1) { position = input.indexOf(redaction.redacted); if (position !== -1) { input = performReplacementInText(input, redaction.original, position, position + redaction.redacted.length); } } } return input; } return [candidate, unredact]; } export { redact };