redac---n 0.0.0-stage → 0.0.2

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 (102) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +323 -2
  3. package/THIRD_PARTY_NOTICES.md +59 -0
  4. package/dist/aho.d.ts +25 -0
  5. package/dist/aho.js +116 -0
  6. package/dist/base64.d.ts +10 -0
  7. package/dist/base64.js +29 -0
  8. package/dist/bip39-data.d.ts +2 -0
  9. package/dist/bip39-data.js +2 -0
  10. package/dist/cache.d.ts +14 -0
  11. package/dist/cache.js +40 -0
  12. package/dist/calls.d.ts +64 -0
  13. package/dist/calls.js +269 -0
  14. package/dist/charlm-data.d.ts +2 -0
  15. package/dist/charlm-data.js +3 -0
  16. package/dist/charlm.d.ts +7 -0
  17. package/dist/charlm.js +65 -0
  18. package/dist/checksums.d.ts +10 -0
  19. package/dist/checksums.js +98 -0
  20. package/dist/commands.d.ts +72 -0
  21. package/dist/commands.js +787 -0
  22. package/dist/context.d.ts +98 -0
  23. package/dist/context.js +469 -0
  24. package/dist/encoded.d.ts +11 -0
  25. package/dist/encoded.js +336 -0
  26. package/dist/environment.d.ts +10 -0
  27. package/dist/environment.js +62 -0
  28. package/dist/formats-data.d.ts +2 -0
  29. package/dist/formats-data.js +405 -0
  30. package/dist/formats.d.ts +5 -0
  31. package/dist/formats.js +470 -0
  32. package/dist/hmac.d.ts +3 -0
  33. package/dist/hmac.js +87 -0
  34. package/dist/index.d.ts +101 -0
  35. package/dist/index.js +488 -0
  36. package/dist/json.d.ts +82 -0
  37. package/dist/json.js +392 -0
  38. package/dist/known.d.ts +34 -0
  39. package/dist/known.js +340 -0
  40. package/dist/lines.d.ts +28 -0
  41. package/dist/lines.js +69 -0
  42. package/dist/lookup.d.ts +5 -0
  43. package/dist/lookup.js +7 -0
  44. package/dist/model-data.d.ts +3 -0
  45. package/dist/model-data.js +4 -0
  46. package/dist/model.d.ts +2 -0
  47. package/dist/model.js +19 -0
  48. package/dist/named.d.ts +20 -0
  49. package/dist/named.js +3257 -0
  50. package/dist/names.d.ts +106 -0
  51. package/dist/names.js +1517 -0
  52. package/dist/normalize.d.ts +25 -0
  53. package/dist/normalize.js +529 -0
  54. package/dist/pem.d.ts +32 -0
  55. package/dist/pem.js +269 -0
  56. package/dist/policy.d.ts +3 -0
  57. package/dist/policy.js +470 -0
  58. package/dist/public.d.ts +44 -0
  59. package/dist/public.js +362 -0
  60. package/dist/publickeys.d.ts +41 -0
  61. package/dist/publickeys.js +232 -0
  62. package/dist/random.d.ts +29 -0
  63. package/dist/random.js +165 -0
  64. package/dist/repeats.d.ts +21 -0
  65. package/dist/repeats.js +80 -0
  66. package/dist/rules.d.ts +42 -0
  67. package/dist/rules.js +58 -0
  68. package/dist/runs.d.ts +10 -0
  69. package/dist/runs.js +36 -0
  70. package/dist/secretfile.d.ts +52 -0
  71. package/dist/secretfile.js +722 -0
  72. package/dist/shell.d.ts +127 -0
  73. package/dist/shell.js +703 -0
  74. package/dist/spans.d.ts +61 -0
  75. package/dist/spans.js +287 -0
  76. package/dist/sql.d.ts +20 -0
  77. package/dist/sql.js +66 -0
  78. package/dist/stream.d.ts +46 -0
  79. package/dist/stream.js +150 -0
  80. package/dist/structure.d.ts +24 -0
  81. package/dist/structure.js +1625 -0
  82. package/dist/syntax.d.ts +156 -0
  83. package/dist/syntax.js +446 -0
  84. package/dist/transcript.d.ts +30 -0
  85. package/dist/transcript.js +713 -0
  86. package/dist/types.d.ts +81 -0
  87. package/dist/types.js +1 -0
  88. package/dist/unnamed.d.ts +59 -0
  89. package/dist/unnamed.js +939 -0
  90. package/dist/urls.d.ts +70 -0
  91. package/dist/urls.js +303 -0
  92. package/dist/values.d.ts +178 -0
  93. package/dist/values.js +1621 -0
  94. package/dist/yaml.d.ts +40 -0
  95. package/dist/yaml.js +174 -0
  96. package/docs/creddata.svg +318 -0
  97. package/docs/guessing.md +139 -0
  98. package/docs/holdout.svg +85 -0
  99. package/docs/layers.md +80 -0
  100. package/docs/models.svg +81 -0
  101. package/docs/real-text.svg +112 -0
  102. package/package.json +57 -5
@@ -0,0 +1,156 @@
1
+ /** Text and where it is. */
2
+ export interface Span {
3
+ value: string;
4
+ start: number;
5
+ end: number;
6
+ }
7
+ /** A `d`-flag match's group `k` as a span. */
8
+ export declare function group(m: RegExpMatchArray, k: number): Span;
9
+ /**
10
+ * How a string literal escapes its own quote: with a backslash (`"a\"b"`: JSON, C, JavaScript, Python, YAML's double
11
+ * quotes), by doubling it (`'it''s'`: YAML's and SQL's single quotes, C#'s `@"…"`), or not at all (a shell's `'…'`,
12
+ * Rust's `r#"…"#`).
13
+ */
14
+ export type Escapes = "backslash" | "doubled" | "none";
15
+ /**
16
+ * The longest value a reader reads where its syntax bounds it (a quote, a call's bracket): a guard against a quote
17
+ * that never closes, not a length a secret is expected to stay under.
18
+ */
19
+ export declare const MAX_VALUE = 65536;
20
+ /**
21
+ * How far a reader reads a value that only its characters bound (an unquoted word, `[^\s;]{1,4096}`, the rest of a
22
+ * line): further, a reader that matches from many places on one long line would scan it once from each. A longer value
23
+ * runs on from where its reader stopped to its word's end (spans.ts toWordEnd).
24
+ */
25
+ export declare const UNQUOTED_READ = 4096;
26
+ /**
27
+ * An unquoted value as a regex source for a rule that needs `end` (a lookahead: a blank, a quote, the line's end) right
28
+ * after it: one capture group of `chars` (a character class), at least `min` of them, read as far as UNQUOTED_READ. A
29
+ * longer value is cut there without the check, instead of failing the whole match (`end` follows a cut one only by
30
+ * chance, and toWordEnd runs it on to its word's end).
31
+ */
32
+ export declare function unquotedValue(chars: string, min: number, end: string): string;
33
+ /**
34
+ * The string literal scanner, for readers that walk text (calls.ts, yaml.ts, named.ts's values, attributes; `quoted`
35
+ * is its form for rules that match a whole pattern): where the literal whose opening quote is at `at` closes (the index of
36
+ * its closing quote), or -1 when it doesn't close within `max` characters, or on its line unless `multiline`. `from`
37
+ * starts the scan inside the literal (a value in the middle of `"NAME=value"`).
38
+ */
39
+ export declare function stringEnd(text: string, at: number, { escapes, multiline, max, from, }?: {
40
+ escapes?: Escapes;
41
+ multiline?: boolean;
42
+ max?: number;
43
+ from?: number;
44
+ }): number;
45
+ /**
46
+ * The items of a list of strings that `value` is, each without its quotes (where in `value`, and its text): a flow
47
+ * list on one line or over several, `["a", "b"]`, `['a','b']` (JSON, Python, TOML, YAML), its commas optional as HCL
48
+ * and Vault's listings write it (`["a" "b"]`); or a YAML block list, `- a` lines, plain or quoted. Undefined when it
49
+ * is no such list or anything else is in it: a number, an object, a name (code), a mapping under a `-`.
50
+ */
51
+ export declare function stringListItems(value: string): Span[] | undefined;
52
+ /**
53
+ * `value` without the closing brackets at its end that it didn't open (of `closers`, among `)`, `]`, `}`): the end of
54
+ * a call, a link or an object around it (`{user: alice, password: foo}`, `[link](https://x.io/?a=b)`), not a value's
55
+ * own (`Kx7(mQ2)`, `p)q(r)`). One pass, matching each closer to an opener of its kind before it: linear in the value.
56
+ */
57
+ export declare function withoutUnopenedClosers(value: string, closers?: string): string;
58
+ /**
59
+ * Where a value that a reader cut (`start`–`end`) inside a bracket it opened ends: at that bracket's close on the same
60
+ * line, read over the strings and blanks within it (`0x#{key.unpack('H*')[0]}`, `Buffer.from(pw, "base64")`) and a
61
+ * quote that closes nothing (Rust's lifetime, `Option<&'static str>`; a `<` opens a bracket after a name, a generic's).
62
+ * Then the value is judged whole, as the code it is, not as the fragment before the cut; and a password with a
63
+ * bracket, `Xy(z, 9)`, is hidden whole. `end` when the value opened no bracket it didn't close, or nothing closes it on
64
+ * the line within BRACKET_REACH of the cut (a line of brackets left open would have each value read to the bound).
65
+ */
66
+ export declare function throughBrackets(text: string, start: number, end: number): number;
67
+ /**
68
+ * Whether the character at `i` would continue a token beside a value, so the value there is part of a longer one: a
69
+ * letter or a digit of any script, an identifier's `_` and `$`, and a kebab-case name's `-` where the value has one
70
+ * (`set_guest_wifi_password` inside `_async_set_guest_wifi_password` is no whole token). Not the last character of an
71
+ * escape (`\n`, `\t`, `\u0027`, `\x27`): in an escaped string that is where a line or a quote is.
72
+ */
73
+ export declare function continuesToken(text: string, i: number, dash?: boolean): boolean;
74
+ /**
75
+ * A string literal's prefix: Python's `b r u f` (two of them: `rb`), C#'s `@` and `$`, Rust's `r#`. The readers of
76
+ * calls', assignments' and names' literals find where one opens with it and a quote.
77
+ */
78
+ export declare const STRING_PREFIX: string;
79
+ /** A contraction's apostrophe (`Here's`, `don't`, `you'll`), which prose writes and no quote is: test its source, or replace it. */
80
+ export declare const CONTRACTION: RegExp;
81
+ /**
82
+ * Whether the `'` at `i` is a contraction's apostrophe (CONTRACTION) rather than a quote. (Not after a word that is a
83
+ * string's prefix: `r'd+'`, `f'd={d}'`.)
84
+ */
85
+ export declare function isContraction(text: string, i: number): boolean;
86
+ /** How many backslashes run right before `at`: an odd count escapes the character at `at`. */
87
+ export declare function backslashesBefore(text: string, at: number): number;
88
+ /** The length of the escape sequence whose backslash is at `at`: `\u0041` 6, `\x41` 4, any other (`\n`, `\"`) 2. */
89
+ export declare function escapeLength(text: string, at: number): number;
90
+ /**
91
+ * Whether a `\n` escape starts at `at` (a backslash no other escapes, then `n`) and breaks a line of escaped text: not
92
+ * where a real break follows it, as in a notebook's list of lines (`"…\n",`). (`C:\nginx` reads as one too: there is
93
+ * no telling a path's `\n` from a break.)
94
+ */
95
+ export declare function isEscapedBreak(text: string, at: number): boolean;
96
+ /** Where the line `at` is on starts, counting an escaped break (isEscapedBreak) as one, looking back no further than `from`. */
97
+ export declare function escapedLineStart(text: string, at: number, from?: number): number;
98
+ /**
99
+ * A quoted string on one line with backslash escapes (stringEnd's default), as a regex source for rules that match
100
+ * a whole pattern: the opening quote (one of `quotes`) is capture group `n`, the content group `n + 1`, at least `min`
101
+ * characters. (Its closing quote bounds the read: no cap on its length.)
102
+ */
103
+ export declare function quoted(n: number, min?: number, quotes?: string): string;
104
+ /** The attribute that names a pair's value (`{"name": "API_KEY", "value": …}`, `- name: DB_PASSWORD`), and the one that holds it. */
105
+ export declare const PAIR_NAME: RegExp;
106
+ export declare const PAIR_VALUE: RegExp;
107
+ /** An attribute (see `attributes`): its key, its operator, and its value with where it is. */
108
+ export interface Attribute {
109
+ /** Where the attribute starts (its key, or the quote around its key) and ends (after its value and blanks). */
110
+ start: number;
111
+ end: number;
112
+ key: string;
113
+ operator: string;
114
+ value: Span;
115
+ quoted: boolean;
116
+ }
117
+ /**
118
+ * The attributes of a text, in any of the syntaxes pairs come in: `name: x`, `name = "x"`, `"Name": "x"`, `name="x"` in
119
+ * a tag. A quoted value runs to its closing quote; a bare one to a `,` `;` `}` or line break, a tag's `>`, or blanks
120
+ * before another word or a comment, and holds no `{`; either is followed by what can follow a value (followsValue).
121
+ * Each value is read from where it starts to the first character that can end it (one table per text), so a long line
122
+ * of keys is read once.
123
+ */
124
+ export declare function attributes(text: string): Generator<Attribute>;
125
+ /**
126
+ * The name a sibling attribute gives a value: in the text before it on its line, the last name attribute with a quoted
127
+ * value and no brace after it (`{"name": "api_key", "value": "`), or, with `alone`, a line that is a name attribute and
128
+ * nothing else but a comment (`- name: API_KEY # from vault` above ` value: …`). Not a word in a sentence: `Missing environment variable: X`.
129
+ */
130
+ export declare function siblingName(text: string, { alone }?: {
131
+ alone?: boolean | undefined;
132
+ }): string | undefined;
133
+ /** A table cell's text (trimmed, without the backticks of inline code) and where it is. */
134
+ export interface Cell {
135
+ text: string;
136
+ start: number;
137
+ end: number;
138
+ }
139
+ /**
140
+ * The rule under a `|` table's header or around a MySQL client's table: `|---|:--:|`, `+------+------+`, `===|===`; and
141
+ * the start of one, to look ahead for it. (Each run of blanks belongs to one quantifier, after a separator or after
142
+ * dashes, so a long run of blanks is read once.)
143
+ */
144
+ export declare const PIPE_RULE: RegExp;
145
+ export declare const PIPE_RULE_START: RegExp;
146
+ /**
147
+ * A table row's cells between `|` bars, read as GitHub's tables are: `\|` is a pipe inside a cell, and a code span
148
+ * (`` `a|b` ``) isn't split. Each cell's text (trimmed, a code span's backticks off) and where it is (`offset` added).
149
+ */
150
+ export declare function pipeCells(line: string, offset?: number): Cell[];
151
+ /**
152
+ * A delimited line's cells (CSV, TSV, `mysql --batch`), as RFC 4180 reads them on one line: a cell in double quotes
153
+ * may hold the delimiter and `""` for a quote (not a line break: a quote still open at the line's end ends there). Each
154
+ * cell's text (unquoted) and where that text starts and ends in the line.
155
+ */
156
+ export declare function delimitedCells(line: string, delim: string, offset?: number): Cell[];
package/dist/syntax.js ADDED
@@ -0,0 +1,446 @@
1
+ /**
2
+ * Pieces of the syntaxes values are written in, shared by the readers of the named layer and the unnamed layer's
3
+ * context, so both read a value's name the same way: quoted strings, spans of matched groups, and name/value pairs
4
+ * written as sibling attributes.
5
+ */
6
+ import { lastText } from "./lines.js";
7
+ /** A `d`-flag match's group `k` as a span. */
8
+ export function group(m, k) {
9
+ const [start, end] = m.indices[k];
10
+ return { value: m[k], start, end };
11
+ }
12
+ /**
13
+ * The longest value a reader reads where its syntax bounds it (a quote, a call's bracket): a guard against a quote
14
+ * that never closes, not a length a secret is expected to stay under.
15
+ */
16
+ export const MAX_VALUE = 65_536;
17
+ /**
18
+ * How far a reader reads a value that only its characters bound (an unquoted word, `[^\s;]{1,4096}`, the rest of a
19
+ * line): further, a reader that matches from many places on one long line would scan it once from each. A longer value
20
+ * runs on from where its reader stopped to its word's end (spans.ts toWordEnd).
21
+ */
22
+ export const UNQUOTED_READ = 4096;
23
+ /**
24
+ * An unquoted value as a regex source for a rule that needs `end` (a lookahead: a blank, a quote, the line's end) right
25
+ * after it: one capture group of `chars` (a character class), at least `min` of them, read as far as UNQUOTED_READ. A
26
+ * longer value is cut there without the check, instead of failing the whole match (`end` follows a cut one only by
27
+ * chance, and toWordEnd runs it on to its word's end).
28
+ */
29
+ export function unquotedValue(chars, min, end) {
30
+ return `(${chars}{${min},${UNQUOTED_READ - 1}}?(?=${end})|${chars}{${UNQUOTED_READ}})`;
31
+ }
32
+ /**
33
+ * The string literal scanner, for readers that walk text (calls.ts, yaml.ts, named.ts's values, attributes; `quoted`
34
+ * is its form for rules that match a whole pattern): where the literal whose opening quote is at `at` closes (the index of
35
+ * its closing quote), or -1 when it doesn't close within `max` characters, or on its line unless `multiline`. `from`
36
+ * starts the scan inside the literal (a value in the middle of `"NAME=value"`).
37
+ */
38
+ export function stringEnd(text, at, { escapes = "backslash", multiline = false, max = MAX_VALUE, from = at + 1, } = {}) {
39
+ const quote = text[at];
40
+ const end = Math.min(text.length, from + max);
41
+ for (let i = from; i < end; i++) {
42
+ const c = text[i];
43
+ if (c === "\\" && escapes === "backslash")
44
+ i++;
45
+ else if (c === quote) {
46
+ if (escapes !== "doubled" || text[i + 1] !== quote)
47
+ return i;
48
+ i++;
49
+ }
50
+ else if (c === "\n" && !multiline)
51
+ return -1;
52
+ }
53
+ return -1;
54
+ }
55
+ /** A quoted item of a flow list and the blanks after it, read from where the last one ended (`y`): its content is group 2. */
56
+ const FLOW_ITEM = /[ \t\r\n]*(["'])((?:(?!\1)[^\\\n]|\\.)*)\1[ \t\r\n]*/y;
57
+ /** The bracket that closes a flow list, and nothing after it but blanks. */
58
+ const FLOW_CLOSE = /[ \t\r\n]*\][ \t\r\n]*$/y;
59
+ /**
60
+ * The items of a list of strings that `value` is, each without its quotes (where in `value`, and its text): a flow
61
+ * list on one line or over several, `["a", "b"]`, `['a','b']` (JSON, Python, TOML, YAML), its commas optional as HCL
62
+ * and Vault's listings write it (`["a" "b"]`); or a YAML block list, `- a` lines, plain or quoted. Undefined when it
63
+ * is no such list or anything else is in it: a number, an object, a name (code), a mapping under a `-`.
64
+ */
65
+ export function stringListItems(value) {
66
+ const items = [];
67
+ const open = /^[ \t\r\n]*\[/.exec(value);
68
+ if (open) {
69
+ let at = open[0].length;
70
+ for (;;) {
71
+ FLOW_ITEM.lastIndex = at;
72
+ const m = FLOW_ITEM.exec(value);
73
+ if (!m)
74
+ return undefined;
75
+ const start = m.index + m[0].indexOf(m[1]) + 1;
76
+ items.push({ value: m[2], start, end: start + m[2].length });
77
+ at = m.index + m[0].length;
78
+ if (value[at] === ",")
79
+ at++;
80
+ FLOW_CLOSE.lastIndex = at;
81
+ if (FLOW_CLOSE.test(value))
82
+ return items;
83
+ }
84
+ }
85
+ let at = 0;
86
+ for (const line of value.split("\n")) {
87
+ const m = /^([ \t]*-[ \t]+)(.*?)[ \t]*\r?$/.exec(line);
88
+ if (/^[ \t]*(?:#.*)?\r?$/.test(line)) {
89
+ at += line.length + 1;
90
+ continue;
91
+ }
92
+ if (!m)
93
+ return undefined;
94
+ let item = m[2];
95
+ let start = at + m[1].length;
96
+ const quote = /^(["'])(.*)\1$/.exec(item);
97
+ if (quote) {
98
+ item = quote[2];
99
+ start++;
100
+ }
101
+ else {
102
+ item = item.replace(/[ \t]+#.*$/, "");
103
+ // (A mapping, `- name: x`, or a collection, `- [a]`, is no string.)
104
+ if (!item || /:(?:[ \t]|$)|^[[{&*!|>]/.test(item))
105
+ return undefined;
106
+ }
107
+ items.push({ value: item, start, end: start + item.length });
108
+ at += line.length + 1;
109
+ }
110
+ return items.length ? items : undefined;
111
+ }
112
+ /** The opening bracket of each closing one. */
113
+ const OPENER = { ")": "(", "]": "[", "}": "{" };
114
+ /**
115
+ * `value` without the closing brackets at its end that it didn't open (of `closers`, among `)`, `]`, `}`): the end of
116
+ * a call, a link or an object around it (`{user: alice, password: foo}`, `[link](https://x.io/?a=b)`), not a value's
117
+ * own (`Kx7(mQ2)`, `p)q(r)`). One pass, matching each closer to an opener of its kind before it: linear in the value.
118
+ */
119
+ export function withoutUnopenedClosers(value, closers = ")]}") {
120
+ const open = { "(": 0, "[": 0, "{": 0 };
121
+ // (Where the run of unopened closers that ends the value so far starts.)
122
+ let run = -1;
123
+ for (let i = 0; i < value.length; i++) {
124
+ const c = value[i];
125
+ const opener = OPENER[c];
126
+ if (opener === undefined) {
127
+ if (c in open)
128
+ open[c]++;
129
+ run = -1;
130
+ }
131
+ else if (open[opener] > 0) {
132
+ open[opener]--;
133
+ run = -1;
134
+ }
135
+ else if (closers.includes(c))
136
+ run = run < 0 ? i : run;
137
+ else
138
+ run = -1;
139
+ }
140
+ return run < 0 ? value : value.slice(0, run);
141
+ }
142
+ /** How far past a cut value a bracket it opened may close: a call's arguments, an expression's index. */
143
+ const BRACKET_REACH = 256;
144
+ /**
145
+ * Where a value that a reader cut (`start`–`end`) inside a bracket it opened ends: at that bracket's close on the same
146
+ * line, read over the strings and blanks within it (`0x#{key.unpack('H*')[0]}`, `Buffer.from(pw, "base64")`) and a
147
+ * quote that closes nothing (Rust's lifetime, `Option<&'static str>`; a `<` opens a bracket after a name, a generic's).
148
+ * Then the value is judged whole, as the code it is, not as the fragment before the cut; and a password with a
149
+ * bracket, `Xy(z, 9)`, is hidden whole. `end` when the value opened no bracket it didn't close, or nothing closes it on
150
+ * the line within BRACKET_REACH of the cut (a line of brackets left open would have each value read to the bound).
151
+ */
152
+ export function throughBrackets(text, start, end) {
153
+ const close = { "(": ")", "[": "]", "{": "}", "<": ">" };
154
+ const open = [];
155
+ const lineEnd = text.indexOf("\n", end);
156
+ const stop = Math.min(lineEnd < 0 ? text.length : lineEnd, start + UNQUOTED_READ, end + BRACKET_REACH);
157
+ for (let i = start; i < stop; i++) {
158
+ const c = text[i];
159
+ if (i >= end && open.length === 0)
160
+ return end;
161
+ if (c in close && (c !== "<" || /\w/.test(text[i - 1] ?? "")))
162
+ open.push(close[c]);
163
+ else if (c === open.at(-1) && !(c === ">" && /[-=]/.test(text[i - 1]))) {
164
+ open.pop();
165
+ if (open.length === 0 && i >= end)
166
+ return i + 1;
167
+ }
168
+ else if (i >= end && /["'`]/.test(c)) {
169
+ const q = stringEnd(text, i, { max: stop - i });
170
+ if (q >= 0)
171
+ i = q;
172
+ }
173
+ }
174
+ return end;
175
+ }
176
+ /**
177
+ * Whether the character at `i` would continue a token beside a value, so the value there is part of a longer one: a
178
+ * letter or a digit of any script, an identifier's `_` and `$`, and a kebab-case name's `-` where the value has one
179
+ * (`set_guest_wifi_password` inside `_async_set_guest_wifi_password` is no whole token). Not the last character of an
180
+ * escape (`\n`, `\t`, `\u0027`, `\x27`): in an escaped string that is where a line or a quote is.
181
+ */
182
+ export function continuesToken(text, i, dash = false) {
183
+ // (A letter outside the basic plane is two UTF-16 units: the pair, whichever half `i` is.)
184
+ const pair = /[\uDC00-\uDFFF]/.test(text[i] ?? "") && /[\uD800-\uDBFF]/.test(text[i - 1] ?? "");
185
+ const c = pair ? text.slice(i - 1, i + 1) : text.slice(i, i + 2);
186
+ if (!(/^[\p{L}\p{N}_$]/u.test(c) || (dash && c[0] === "-")))
187
+ return false;
188
+ const escape = /\\(?:[nrtbf]|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2})$/.exec(text.slice(Math.max(0, i - 5), i + 1));
189
+ return !escape || backslashesBefore(text, i + 1 - escape[0].length) % 2 === 1;
190
+ }
191
+ /**
192
+ * A string literal's prefix: Python's `b r u f` (two of them: `rb`), C#'s `@` and `$`, Rust's `r#`. The readers of
193
+ * calls', assignments' and names' literals find where one opens with it and a quote.
194
+ */
195
+ export const STRING_PREFIX = String.raw `r#+|[rRbBuUfF]{1,2}|@\$?|\$@?`;
196
+ /** A contraction's apostrophe (`Here's`, `don't`, `you'll`), which prose writes and no quote is: test its source, or replace it. */
197
+ export const CONTRACTION = /(?<=[A-Za-z])['’](?=(?:s|t|ll|re|ve|d|m)\b)/gi;
198
+ /**
199
+ * Whether the `'` at `i` is a contraction's apostrophe (CONTRACTION) rather than a quote. (Not after a word that is a
200
+ * string's prefix: `r'd+'`, `f'd={d}'`.)
201
+ */
202
+ export function isContraction(text, i) {
203
+ const word = /[A-Za-z]+$/.exec(text.slice(Math.max(0, i - 20), i))?.[0];
204
+ return !!word && !/^[rRbBuUfF]{1,2}$/.test(word) && /^(?:s|t|ll|re|ve|d|m)\b/i.test(text.slice(i + 1, i + 4));
205
+ }
206
+ /** How many backslashes run right before `at`: an odd count escapes the character at `at`. */
207
+ export function backslashesBefore(text, at) {
208
+ let n = 0;
209
+ while (at - n - 1 >= 0 && text.charCodeAt(at - n - 1) === 92)
210
+ n++;
211
+ return n;
212
+ }
213
+ /** The length of the escape sequence whose backslash is at `at`: `\u0041` 6, `\x41` 4, any other (`\n`, `\"`) 2. */
214
+ export function escapeLength(text, at) {
215
+ return /^u[0-9a-fA-F]{4}/.test(text.slice(at + 1, at + 6))
216
+ ? 6
217
+ : /^x[0-9a-fA-F]{2}/.test(text.slice(at + 1, at + 4))
218
+ ? 4
219
+ : 2;
220
+ }
221
+ /**
222
+ * Whether a `\n` escape starts at `at` (a backslash no other escapes, then `n`) and breaks a line of escaped text: not
223
+ * where a real break follows it, as in a notebook's list of lines (`"…\n",`). (`C:\nginx` reads as one too: there is
224
+ * no telling a path's `\n` from a break.)
225
+ */
226
+ export function isEscapedBreak(text, at) {
227
+ return (text.charCodeAt(at) === 92 &&
228
+ text.charCodeAt(at + 1) === 110 &&
229
+ backslashesBefore(text, at) % 2 === 0 &&
230
+ !/^[",\] \t]{0,8}\r?\n/.test(text.slice(at + 2, at + 13)));
231
+ }
232
+ /** Where the line `at` is on starts, counting an escaped break (isEscapedBreak) as one, looking back no further than `from`. */
233
+ export function escapedLineStart(text, at, from = 0) {
234
+ let floor = at;
235
+ while (floor > from && text.charCodeAt(floor - 1) !== 10)
236
+ floor--;
237
+ for (let b = at - 2; b >= floor; b--)
238
+ if (isEscapedBreak(text, b))
239
+ return b + 2;
240
+ return floor;
241
+ }
242
+ /**
243
+ * A quoted string on one line with backslash escapes (stringEnd's default), as a regex source for rules that match
244
+ * a whole pattern: the opening quote (one of `quotes`) is capture group `n`, the content group `n + 1`, at least `min`
245
+ * characters. (Its closing quote bounds the read: no cap on its length.)
246
+ */
247
+ export function quoted(n, min = 1, quotes = `"'`) {
248
+ const ref = `\\${n}`;
249
+ return `([${quotes}])((?:(?!${ref})[^\\\\\\n]|\\\\.){${min},})${ref}`;
250
+ }
251
+ /** The attribute that names a pair's value (`{"name": "API_KEY", "value": …}`, `- name: DB_PASSWORD`), and the one that holds it. */
252
+ export const PAIR_NAME = /^(?:name|key|keyName|id|label|ParameterKey|ParameterName|OptionName|OutputKey|variable|env|field)$/i;
253
+ export const PAIR_VALUE = /^(?:value|val|v|data|content|result|secret|secret_value|ParameterValue|OutputValue|SecretString)$/i;
254
+ /** An attribute's key and operator: `name: `, `name = `, `"Name": `, `name=` in a tag. */
255
+ const ATTRIBUTE_HEAD = /(?<![\w-])(["']?)([A-Za-z]\w{0,80})\1[ \t]*([:=])[ \t]*/g;
256
+ /**
257
+ * The attributes of a text, in any of the syntaxes pairs come in: `name: x`, `name = "x"`, `"Name": "x"`, `name="x"` in
258
+ * a tag. A quoted value runs to its closing quote; a bare one to a `,` `;` `}` or line break, a tag's `>`, or blanks
259
+ * before another word or a comment, and holds no `{`; either is followed by what can follow a value (followsValue).
260
+ * Each value is read from where it starts to the first character that can end it (one table per text), so a long line
261
+ * of keys is read once.
262
+ */
263
+ export function* attributes(text) {
264
+ const stops = stopsOf(text);
265
+ // (Each read after the one before it: a value's text, `arn:aws:…:secret:x`, holds no attributes.)
266
+ const head = new RegExp(ATTRIBUTE_HEAD.source, "g");
267
+ for (let m = head.exec(text); m; m = head.exec(text)) {
268
+ const at = m.index + m[0].length;
269
+ let value;
270
+ let end;
271
+ const quote = text[at];
272
+ if (quote === '"' || quote === "'") {
273
+ const close = stringEnd(text, at);
274
+ if (close < 0)
275
+ continue;
276
+ value = { value: text.slice(at + 1, close), start: at + 1, end: close };
277
+ end = close + 1;
278
+ }
279
+ else {
280
+ if (at >= text.length || /[\s,;}{"'<>]/.test(quote))
281
+ continue;
282
+ const stop = bareEnd(text, at, stops);
283
+ if (stop < 0)
284
+ continue;
285
+ value = { value: text.slice(at, stop), start: at, end: stop };
286
+ end = stop;
287
+ }
288
+ const after = end;
289
+ while (text[end] === " " || text[end] === "\t")
290
+ end++;
291
+ if (!followsValue(text, end, end > after))
292
+ continue;
293
+ head.lastIndex = Math.max(head.lastIndex, end);
294
+ yield { start: m.index, end, key: m[2], operator: m[3], value, quoted: quote === '"' || quote === "'" };
295
+ }
296
+ }
297
+ /**
298
+ * Whether what is at `i` can follow an attribute's value (with `blank`, after blanks): the text's end, a separator, a
299
+ * tag's end, a comment (`"x"// prod`, `"x" # set by CI`), or, after a blank, another word (`name="a" value="b"`). Not an
300
+ * operator: `"abc" + suffix` is no value.
301
+ */
302
+ function followsValue(text, i, blank) {
303
+ const c = text[i];
304
+ return (i >= text.length ||
305
+ /[,;}\r\n>]/.test(c) ||
306
+ (c === "/" && /[/*>]/.test(text[i + 1] ?? "")) ||
307
+ (blank && /[\w"'#]/.test(c)));
308
+ }
309
+ /** For each position of a text, the next one (at or after it) that can end a bare attribute value: `, ; } { \r \n > /` or a blank. */
310
+ const stopsOf = lastText((text) => {
311
+ const next = new Int32Array(text.length + 1);
312
+ next[text.length] = text.length;
313
+ for (let i = text.length - 1; i >= 0; i--)
314
+ next[i] = /[,;}{\r\n>/ \t]/.test(text[i]) ? i : next[i + 1];
315
+ return next;
316
+ });
317
+ /**
318
+ * Where a bare attribute value that starts at `at` ends (see `attributes`), within 4,097 characters; -1 if it doesn't.
319
+ * (At a `{` too, which can't follow a value: the attribute is none.)
320
+ */
321
+ function bareEnd(text, at, stops) {
322
+ for (let p = stops[at + 1] ?? text.length; p - at <= 4097; p = stops[p + 1]) {
323
+ const c = text[p];
324
+ // (A path's `/` is the value's.)
325
+ if (c === "/" && text[p + 1] !== ">")
326
+ continue;
327
+ // (The text's end, where `c` is undefined, too.)
328
+ if (c !== " " && c !== "\t")
329
+ return p;
330
+ // Blanks end the value before what can follow one; before a symbol (`Kx9 - 2mQ`'s `-`) they don't.
331
+ let q = p;
332
+ while (text[q] === " " || text[q] === "\t")
333
+ q++;
334
+ if (followsValue(text, q, true))
335
+ return p;
336
+ p = q - 1;
337
+ }
338
+ return -1;
339
+ }
340
+ /**
341
+ * The name a sibling attribute gives a value: in the text before it on its line, the last name attribute with a quoted
342
+ * value and no brace after it (`{"name": "api_key", "value": "`), or, with `alone`, a line that is a name attribute and
343
+ * nothing else but a comment (`- name: API_KEY # from vault` above ` value: …`). Not a word in a sentence: `Missing environment variable: X`.
344
+ */
345
+ export function siblingName(text, { alone = false } = {}) {
346
+ if (!/(?:name|key|id|label|variable|env|field)["']?[ \t]*[:=]/i.test(text))
347
+ return undefined;
348
+ let name;
349
+ let after = 0;
350
+ for (const a of attributes(text)) {
351
+ const value = a.quoted || alone ? a.value.value : "";
352
+ if (!PAIR_NAME.test(a.key) || !/^[\w.-]{1,80}$/.test(value))
353
+ continue;
354
+ if (alone && !(/^[ \t-]*$/.test(text.slice(0, a.start)) && /^[ \t,]*(?:#.*)?\r?$/.test(text.slice(a.end))))
355
+ continue;
356
+ name = value;
357
+ after = a.end;
358
+ }
359
+ return name !== undefined && !/[{}]/.test(text.slice(after)) ? name : undefined;
360
+ }
361
+ /**
362
+ * The rule under a `|` table's header or around a MySQL client's table: `|---|:--:|`, `+------+------+`, `===|===`; and
363
+ * the start of one, to look ahead for it. (Each run of blanks belongs to one quantifier, after a separator or after
364
+ * dashes, so a long run of blanks is read once.)
365
+ */
366
+ export const PIPE_RULE = /^[ \t]*(?:[|+][ \t]*)?(?::?[-=]+:?[ \t]*[|+][ \t]*)+(?::?[-=]+:?[ \t]*)?\r?$/;
367
+ export const PIPE_RULE_START = /^[ \t]*(?:[|+][ \t]*)?(?::?[-=]+:?[ \t]*[|+][ \t]*)+/;
368
+ /**
369
+ * A table row's cells between `|` bars, read as GitHub's tables are: `\|` is a pipe inside a cell, and a code span
370
+ * (`` `a|b` ``) isn't split. Each cell's text (trimmed, a code span's backticks off) and where it is (`offset` added).
371
+ */
372
+ export function pipeCells(line, offset = 0) {
373
+ const body = line.replace(/\r$/, "");
374
+ const bounds = [];
375
+ // (The lengths of the backtick runs with no closing run after them: a long line of backticks is read once.)
376
+ const unclosed = new Set();
377
+ for (let i = 0; i < body.length; i++) {
378
+ const c = body[i];
379
+ if (c === "\\")
380
+ i++;
381
+ else if (c === "`") {
382
+ // A code span runs to the next run of as many backticks; a run that finds none is text.
383
+ let run = 1;
384
+ while (body[i + run] === "`")
385
+ run++;
386
+ const close = unclosed.has(run) ? -1 : body.indexOf("`".repeat(run), i + run);
387
+ if (close < 0)
388
+ unclosed.add(run);
389
+ i = (close < 0 ? i : close) + run - 1;
390
+ }
391
+ else if (c === "|")
392
+ bounds.push(i);
393
+ }
394
+ // The cells between the bars; a row may leave out its outer bars.
395
+ const lead = /^[ \t]*$/.test(body.slice(0, bounds[0] ?? 0)) && bounds.length ? bounds.shift() : -1;
396
+ const trail = bounds.length && /^[ \t]*$/.test(body.slice(bounds[bounds.length - 1] + 1)) ? bounds.pop() : body.length;
397
+ const cells = [];
398
+ for (const [from, to] of [lead, ...bounds].map((b, k, all) => [b + 1, all[k + 1] ?? trail])) {
399
+ const raw = body.slice(from, to);
400
+ let text = raw.trim();
401
+ let start = offset + from + raw.indexOf(text);
402
+ // (A cell is judged as what it holds: a code span's or emphasis' content, `` `x` ``, `**x**`, `_x_`; not of two
403
+ // emphasised words, `**Name** and **Value**`.)
404
+ const wrapped = /^(`+|\*{1,3}|_{1,3})([^`*_](?:(?:(?!\1).)*[^`*_])?)\1$/.exec(text);
405
+ if (wrapped) {
406
+ start += wrapped[1].length;
407
+ text = wrapped[2];
408
+ }
409
+ cells.push({ text, start, end: start + text.length });
410
+ }
411
+ return cells;
412
+ }
413
+ /**
414
+ * A delimited line's cells (CSV, TSV, `mysql --batch`), as RFC 4180 reads them on one line: a cell in double quotes
415
+ * may hold the delimiter and `""` for a quote (not a line break: a quote still open at the line's end ends there). Each
416
+ * cell's text (unquoted) and where that text starts and ends in the line.
417
+ */
418
+ export function delimitedCells(line, delim, offset = 0) {
419
+ const cells = [];
420
+ let i = 0;
421
+ for (;;) {
422
+ if (line[i] === '"') {
423
+ let j = i + 1;
424
+ let text = "";
425
+ for (; j < line.length; j++) {
426
+ if (line[j] !== '"')
427
+ text += line[j];
428
+ else if (line[j + 1] === '"')
429
+ text += line[j++];
430
+ else
431
+ break;
432
+ }
433
+ cells.push({ text, start: offset + i + 1, end: offset + j });
434
+ i = line.indexOf(delim, j);
435
+ }
436
+ else {
437
+ const next = line.indexOf(delim, i);
438
+ const end = next < 0 ? line.length : next;
439
+ cells.push({ text: line.slice(i, end), start: offset + i, end: offset + end });
440
+ i = next;
441
+ }
442
+ if (i < 0)
443
+ return cells;
444
+ i += delim.length;
445
+ }
446
+ }
@@ -0,0 +1,30 @@
1
+ import type { Redactor } from "./index.ts";
2
+ import type { Finding, Guessing } from "./types.ts";
3
+ export interface TranscriptFinding extends Finding {
4
+ /** JSON path of the string the finding is in. */
5
+ path: (string | number)[];
6
+ /** The text the finding covered, the secret itself: strip it before logging. */
7
+ value: string;
8
+ /** Set when the finding is in the object key that `path` ends with, not in its value. */
9
+ inKey?: boolean;
10
+ }
11
+ export interface TranscriptOptions {
12
+ /** How hard to guess at unnamed random-looking tokens, for this call (RedactOptions.guessing). */
13
+ guessing?: Guessing;
14
+ /**
15
+ * true (default): confident findings are learned as known values, hidden throughout this value and in what this
16
+ * Redactor redacts later. false: hidden throughout this value only.
17
+ */
18
+ learn?: boolean;
19
+ /**
20
+ * Anthropic `thinking` blocks carry a signature over their text, so a redacted one no longer verifies
21
+ * and the API rejects a request whose turn it continues. "redact" (default) redacts them like any text; "keep" leaves them alone
22
+ * (for a provider you trust with them, or strip them yourself before sending).
23
+ */
24
+ thinking?: "redact" | "keep";
25
+ }
26
+ /** A copy of `value` with every secret in every string and key replaced, and the findings by JSON path. */
27
+ export declare function redactValue<T>(redactor: Redactor, value: T, options?: TranscriptOptions): {
28
+ value: T;
29
+ findings: TranscriptFinding[];
30
+ };