@arcjet/redact 1.11.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +83 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -35,6 +35,86 @@ function wasmEntitiesToString(entity) {
|
|
|
35
35
|
function performReplacementInText(text, replacement, start, end) {
|
|
36
36
|
return text.substring(0, start) + replacement + text.substring(end);
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Number of bytes a code point occupies in UTF-8.
|
|
40
|
+
*
|
|
41
|
+
* @param codePoint
|
|
42
|
+
* Code point.
|
|
43
|
+
* @returns
|
|
44
|
+
* Byte length.
|
|
45
|
+
*/
|
|
46
|
+
function utf8Length(codePoint) {
|
|
47
|
+
if (codePoint < 128) return 1;
|
|
48
|
+
if (codePoint < 2048) return 2;
|
|
49
|
+
if (codePoint < 65536) return 3;
|
|
50
|
+
return 4;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the UTF-8 byte offsets reported by Wasm to UTF-16 code unit indices.
|
|
54
|
+
*
|
|
55
|
+
* The detector counts bytes; `String.prototype.substring` counts UTF-16 code
|
|
56
|
+
* units. The two coincide for ASCII, so applying one as the other looks correct
|
|
57
|
+
* until the text contains a multi-byte character *before* an entity — at which
|
|
58
|
+
* point every replacement lands short by the number of excess bytes and part of
|
|
59
|
+
* the entity survives in the "redacted" output.
|
|
60
|
+
*
|
|
61
|
+
* An offset that falls inside a character is snapped outward — starts back to
|
|
62
|
+
* the beginning of that character, ends on to the end of it — so a span always
|
|
63
|
+
* covers at least the bytes the detector reported. Over-redacting is the safe
|
|
64
|
+
* direction here; under-redacting is the bug.
|
|
65
|
+
*
|
|
66
|
+
* @param text
|
|
67
|
+
* Text the offsets refer to.
|
|
68
|
+
* @param redactions
|
|
69
|
+
* Detected entities, carrying byte offsets.
|
|
70
|
+
* @returns
|
|
71
|
+
* Spans in UTF-16 code unit indices, parallel to `redactions`.
|
|
72
|
+
*/
|
|
73
|
+
function resolveByteOffsets(text, redactions) {
|
|
74
|
+
const wanted = /* @__PURE__ */ new Set();
|
|
75
|
+
for (const redaction of redactions) {
|
|
76
|
+
wanted.add(redaction.start);
|
|
77
|
+
wanted.add(redaction.end);
|
|
78
|
+
}
|
|
79
|
+
const ascending = Array.from(wanted).sort((a, b) => a - b);
|
|
80
|
+
const floors = /* @__PURE__ */ new Map();
|
|
81
|
+
const ceilings = /* @__PURE__ */ new Map();
|
|
82
|
+
let cursor = 0;
|
|
83
|
+
let byteOffset = 0;
|
|
84
|
+
let stringIndex = 0;
|
|
85
|
+
while (cursor < ascending.length && ascending[cursor] <= 0) {
|
|
86
|
+
floors.set(ascending[cursor], 0);
|
|
87
|
+
ceilings.set(ascending[cursor], 0);
|
|
88
|
+
cursor++;
|
|
89
|
+
}
|
|
90
|
+
for (const character of text) {
|
|
91
|
+
if (cursor >= ascending.length) break;
|
|
92
|
+
const nextByteOffset = byteOffset + utf8Length(character.codePointAt(0));
|
|
93
|
+
const nextStringIndex = stringIndex + character.length;
|
|
94
|
+
while (cursor < ascending.length && ascending[cursor] <= nextByteOffset) {
|
|
95
|
+
const offset = ascending[cursor];
|
|
96
|
+
const onBoundary = offset === nextByteOffset;
|
|
97
|
+
floors.set(offset, onBoundary ? nextStringIndex : stringIndex);
|
|
98
|
+
ceilings.set(offset, nextStringIndex);
|
|
99
|
+
cursor++;
|
|
100
|
+
}
|
|
101
|
+
byteOffset = nextByteOffset;
|
|
102
|
+
stringIndex = nextStringIndex;
|
|
103
|
+
}
|
|
104
|
+
while (cursor < ascending.length) {
|
|
105
|
+
floors.set(ascending[cursor], text.length);
|
|
106
|
+
ceilings.set(ascending[cursor], text.length);
|
|
107
|
+
cursor++;
|
|
108
|
+
}
|
|
109
|
+
return redactions.map((redaction) => {
|
|
110
|
+
const start = floors.get(redaction.start) ?? 0;
|
|
111
|
+
const end = ceilings.get(redaction.end) ?? text.length;
|
|
112
|
+
return {
|
|
113
|
+
start,
|
|
114
|
+
end: Math.max(start, end)
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
}
|
|
38
118
|
/* c8 ignore start */
|
|
39
119
|
function noOpDetect(_tokens) {
|
|
40
120
|
return [];
|
|
@@ -105,8 +185,10 @@ async function callRedactWasm(candidate, options) {
|
|
|
105
185
|
*/
|
|
106
186
|
async function redact(candidate, options) {
|
|
107
187
|
const redactions = await callRedactWasm(candidate, options);
|
|
188
|
+
const spans = resolveByteOffsets(candidate, redactions);
|
|
108
189
|
redactions.reverse();
|
|
109
|
-
|
|
190
|
+
spans.reverse();
|
|
191
|
+
for (const [index, redaction] of redactions.entries()) candidate = performReplacementInText(candidate, redaction.redacted, spans[index].start, spans[index].end);
|
|
110
192
|
function unredact(input) {
|
|
111
193
|
for (const redaction of redactions) {
|
|
112
194
|
let position;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcjet/redact",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"description": "Arcjet sensitive information redaction library",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"arcjet",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"test": "npm run build && npm run test-coverage"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@arcjet/redact-wasm": "1.
|
|
51
|
+
"@arcjet/redact-wasm": "1.12.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/node": "22.20.1",
|