File size: 2,182 Bytes
76289e7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// Fleet container stream redaction preserves chunked output without leaking split secrets.
import { StringDecoder } from "node:string_decoder";

// Longest suffix of `text` that is a proper prefix of any secret. Retaining it
// across emissions guarantees no complete secret is ever split between two
// emitted chunks, and emitted text can never grow into a match later.
function secretPrefixSuffixLength(text: string, redactValues: readonly string[]): number {
  let longest = 0;
  for (const value of redactValues) {
    const max = Math.min(value.length - 1, text.length);
    for (let length = max; length > longest; length -= 1) {
      if (value.startsWith(text.slice(text.length - length))) {
        longest = length;
        break;
      }
    }
  }
  return longest;
}

export function createRedactingStreamWriter(
  target: NodeJS.WriteStream,
  redactValues: readonly string[],
): { write: (chunk: Buffer) => boolean; flush: () => void } {
  const decoder = new StringDecoder("utf8");
  let pending = "";
  const redact = (text: string): string => {
    let redacted = text;
    for (const value of redactValues) {
      if (value) {
        redacted = redacted.replaceAll(value, "<redacted>");
      }
    }
    return redacted;
  };
  // Returns the raw target.write() backpressure signal so callers can pause
  // the child stream instead of buffering a noisy follow stream without bound.
  const emit = (text: string): boolean => {
    if (!text) {
      return true;
    }
    return target.write(redact(text));
  };
  return {
    // Emit everything except a possible secret prefix at the tail on every
    // chunk, so unterminated output (progress lines, prompts) streams live
    // instead of stalling until a newline arrives.
    write: (chunk) => {
      pending += decoder.write(chunk);
      const keep = secretPrefixSuffixLength(pending, redactValues);
      const cut = pending.length - keep;
      if (cut <= 0) {
        return true;
      }
      const writable = emit(pending.slice(0, cut));
      pending = pending.slice(cut);
      return writable;
    },
    flush: () => {
      emit(pending + decoder.end());
      pending = "";
    },
  };
}