@arcjet/redact
Version:
Arcjet sensitive information redaction library
143 lines (140 loc) • 4.98 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 === "credit-card-number" ||
entity === "email" ||
entity === "ip-address" ||
entity === "phone-number") {
return { tag: entity };
}
return {
tag: "custom",
val: entity,
};
}
function wasmEntitiesToString(entity) {
if (entity.tag === "credit-card-number" ||
entity.tag === "email" ||
entity.tag === "ip-address" ||
entity.tag === "phone-number") {
return entity.tag;
}
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) {
const entities = options.entities;
if (entities !== undefined) {
if (!Array.isArray(entities)) {
throw new Error("entities must be an array");
}
if (entities.length < 1) {
throw new Error("no entities configured for redaction");
}
}
// `entities` is an optional field but not allowed to be `undefined`.
return entities
? {
entities: entities.map(userEntitiesToWasm),
contextWindowSize: options.contextWindowSize || 1,
skipCustomDetect: typeof options.detect !== "function",
skipCustomRedact: typeof options.replace !== "function",
}
: {
contextWindowSize: options.contextWindowSize || 1,
skipCustomDetect: typeof options.detect !== "function",
skipCustomRedact: typeof options.replace !== "function",
};
}
else {
return {
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");
}
}
/**
* Redact sensitive info.
*
* @template DetectedEntities
* Custom entity names that are returned from `detect` and optionally listed in `entities`.
* @template ListedEntities
* Entity names that can be listed in the `entities` option.
* @param candidate
* Value to redact.
* @param options
* Configuration.
* @returns
* Promise to a tuple with the redacted string and a function to unredact it.
*/
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 };