@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.
Files changed (2) hide show
  1. package/dist/index.js +83 -1
  2. 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
- for (const redaction of redactions) candidate = performReplacementInText(candidate, redaction.redacted, redaction.start, redaction.end);
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.11.0",
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.11.0"
51
+ "@arcjet/redact-wasm": "1.12.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/node": "22.20.1",