@arcjet/redact
Version:
Arcjet sensitive information redaction library
136 lines (133 loc) • 4.52 kB
JavaScript
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 };