Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

49 lines · 1.5 KBRaw History
1/**
2 * Values shorter than this are not masked: masking e.g. a 1-character secret
3 * would replace that character everywhere and make logs unreadable.
4 */
5export const MIN_MASK_LENGTH = 3;
6
7/** Replaces registered secret values with `***` in log output. */
8export class Masker {
9 private values = new Set<string>();
10 private sorted: string[] = [];
11
12 add(value: string | undefined | null): void {
13 if (value === undefined || value === null) return;
14 const v = String(value);
15 if (v.trim().length < MIN_MASK_LENGTH) return;
16 const before = this.values.size;
17 this.values.add(v);
18 this.values.add(v.trim());
19 // multi-line secrets: mask each line individually, since logs are line based
20 if (/\r?\n/.test(v)) {
21 for (const line of v.split(/\r?\n/)) {
22 const t = line.trim();
23 if (t.length >= MIN_MASK_LENGTH) this.values.add(t);
24 }
25 }
26 if (this.values.size !== before) {
27 this.sorted = [...this.values].sort((a, b) => b.length - a.length);
28 }
29 }
30
31 has(value: string): boolean {
32 return this.values.has(value);
33 }
34
35 /** True when the text contains any masked value. */
36 containsSecret(text: string): boolean {
37 for (const v of this.sorted) if (text.includes(v)) return true;
38 return false;
39 }
40
41 mask(text: string): string {
42 if (this.sorted.length === 0 || text === "") return text;
43 let out = text;
44 for (const v of this.sorted) {
45 if (out.includes(v)) out = out.split(v).join("***");
46 }
47 return out;
48 }
49}