redac---n 0.0.4 → 0.0.5
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/README.md +11 -10
- package/dist/calls.d.ts +8 -0
- package/dist/calls.js +60 -3
- package/dist/checksums.js +4 -3
- package/dist/commands.d.ts +5 -0
- package/dist/commands.js +93 -14
- package/dist/context.d.ts +5 -2
- package/dist/context.js +36 -9
- package/dist/encoded.js +13 -6
- package/dist/environment.d.ts +5 -3
- package/dist/environment.js +38 -14
- package/dist/formats.js +12 -0
- package/dist/index.js +10 -4
- package/dist/json.js +29 -5
- package/dist/known.d.ts +7 -7
- package/dist/known.js +33 -25
- package/dist/named.d.ts +6 -0
- package/dist/named.js +421 -125
- package/dist/names.d.ts +5 -1
- package/dist/names.js +143 -18
- package/dist/normalize.d.ts +4 -1
- package/dist/normalize.js +37 -11
- package/dist/pem.d.ts +2 -0
- package/dist/pem.js +12 -0
- package/dist/policy.js +12 -5
- package/dist/public.js +35 -13
- package/dist/secretfile.d.ts +5 -0
- package/dist/secretfile.js +49 -20
- package/dist/shell.d.ts +12 -3
- package/dist/shell.js +51 -7
- package/dist/spans.d.ts +4 -3
- package/dist/spans.js +79 -8
- package/dist/sql.d.ts +12 -8
- package/dist/sql.js +94 -16
- package/dist/stream.d.ts +2 -1
- package/dist/stream.js +15 -3
- package/dist/structure.js +210 -61
- package/dist/syntax.d.ts +5 -2
- package/dist/syntax.js +7 -4
- package/dist/transcript.js +116 -28
- package/dist/unnamed.js +128 -20
- package/dist/urls.d.ts +8 -3
- package/dist/urls.js +16 -7
- package/dist/values.d.ts +2 -1
- package/dist/values.js +70 -25
- package/docs/holdout.svg +8 -8
- package/docs/layers.md +1 -1
- package/package.json +1 -1
package/dist/environment.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The secrets in an environment, as known values: variables whose name has a credential word (`*_API_KEY`, `*_TOKEN`,
|
|
3
|
-
* `BW_SESSION`, read widely), a vendor's token under any name, and
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* `BW_SESSION`, read widely), a vendor's token under any name, and the passwords and keys inside any variable that
|
|
4
|
+
* the detectors read there (a URL's, a connection string's: `DATABASE_URL`, `password=` in a DSN). Not what the
|
|
5
|
+
* decision stage keeps on purpose: a key public by design, or under a browser's prefix (`NEXT_PUBLIC_STRIPE_KEY=pk_live_…`).
|
|
6
|
+
* Only values a Redactor accepts as known values (the rule `addKnown` applies) are returned; a short or plain-word value
|
|
7
|
+
* is left out. `new Redactor({ known: fromEnvironment(process.env) })`.
|
|
6
8
|
*/
|
|
7
9
|
export declare function fromEnvironment(env: Record<string, string | undefined>): {
|
|
8
10
|
value: string;
|
package/dist/environment.js
CHANGED
|
@@ -1,21 +1,38 @@
|
|
|
1
1
|
import { findFormats } from "./formats.js";
|
|
2
2
|
import { isReplaceable } from "./known.js";
|
|
3
3
|
import { hasCredentialWord, isCredentialName, isSettingName, namesPasswordOrPin } from "./names.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { findNamed } from "./named.js";
|
|
5
|
+
import { keepReason } from "./policy.js";
|
|
6
|
+
import { findStructure, isLiteralPassword } from "./structure.js";
|
|
6
7
|
import { naturalness } from "./charlm.js";
|
|
7
8
|
import { hasThreeClasses, isInert, isRandomToken } from "./values.js";
|
|
8
9
|
/**
|
|
9
10
|
* The secrets in an environment, as known values: variables whose name has a credential word (`*_API_KEY`, `*_TOKEN`,
|
|
10
|
-
* `BW_SESSION`, read widely), a vendor's token under any name, and
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* `BW_SESSION`, read widely), a vendor's token under any name, and the passwords and keys inside any variable that
|
|
12
|
+
* the detectors read there (a URL's, a connection string's: `DATABASE_URL`, `password=` in a DSN). Not what the
|
|
13
|
+
* decision stage keeps on purpose: a key public by design, or under a browser's prefix (`NEXT_PUBLIC_STRIPE_KEY=pk_live_…`).
|
|
14
|
+
* Only values a Redactor accepts as known values (the rule `addKnown` applies) are returned; a short or plain-word value
|
|
15
|
+
* is left out. `new Redactor({ known: fromEnvironment(process.env) })`.
|
|
13
16
|
*/
|
|
14
17
|
export function fromEnvironment(env) {
|
|
15
18
|
const out = [];
|
|
19
|
+
const add = (value, label) => {
|
|
20
|
+
if (!out.some((o) => o.value === value))
|
|
21
|
+
out.push({ value, label });
|
|
22
|
+
};
|
|
16
23
|
for (const [name, value] of Object.entries(env)) {
|
|
17
24
|
if (!value || value.length < 6 || value.length > 16_384)
|
|
18
25
|
continue;
|
|
26
|
+
// (Judged as the decision stage judges the same value on a `NAME=value` line, with the rule that read it.)
|
|
27
|
+
const line = `${name}=${value}`;
|
|
28
|
+
const kept = (f) => keepReason(line, {
|
|
29
|
+
...f,
|
|
30
|
+
start: f.start + name.length + 1,
|
|
31
|
+
end: f.end + name.length + 1,
|
|
32
|
+
kind: "secret",
|
|
33
|
+
name,
|
|
34
|
+
confidence: 1,
|
|
35
|
+
}) !== undefined;
|
|
19
36
|
// (Read widely: the caller's own environment, where a credential missed goes to a provider. A credential word anywhere
|
|
20
37
|
// in the name or a session's at its end, `BW_SESSION`, `TF_TOKEN_app_terraform_io`, `ACTIONS_ID_TOKEN_REQUEST_TOKEN`,
|
|
21
38
|
// unless the name ends as a setting's or a generator didn't make the value: `TOKEN_ENDPOINT`, `DESKTOP_SESSION=ubuntu-wayland`.
|
|
@@ -24,17 +41,24 @@ export function fromEnvironment(env) {
|
|
|
24
41
|
((namesPasswordOrPin(name) && !isInert(value)) ||
|
|
25
42
|
((hasCredentialWord(name) || /(?:^|_)(?:SESSION|PEPPER)$/i.test(name)) && isGenerated(value)));
|
|
26
43
|
const secretName = isCredentialName(name) || /WEBHOOK/i.test(name) || widened;
|
|
27
|
-
if (secretName && !isInert(value))
|
|
28
|
-
|
|
29
|
-
// (A vendor's token under any name
|
|
44
|
+
if (secretName && !isInert(value) && !kept({ start: 0, end: value.length, rule: "named:assign" }))
|
|
45
|
+
add(value, `$${name}`);
|
|
46
|
+
// (A vendor's token under any name, `CI_DEPLOY=ghp_…`, a Slack webhook URL; one public by design is kept, a
|
|
47
|
+
// publishable key or a browser's Google key.)
|
|
30
48
|
else
|
|
31
49
|
for (const f of findFormats(value))
|
|
32
|
-
if (!
|
|
33
|
-
|
|
34
|
-
// (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
50
|
+
if (!kept(f))
|
|
51
|
+
add(value.slice(f.start, f.end), `${f.kind} in $${name}`);
|
|
52
|
+
// (What the structure and name readers find inside the value: a URL's password, a connection string's, a DSN's
|
|
53
|
+
// `password=`. A literal one, as the URL readers take it: not `${DB_PASSWORD}`, `<password>`, `%PASSWORD%`.)
|
|
54
|
+
if (!/[=:]/.test(value))
|
|
55
|
+
continue;
|
|
56
|
+
for (const f of [...findStructure(value), ...findNamed(value)]) {
|
|
57
|
+
const secret = value.slice(f.start, f.end);
|
|
58
|
+
if (f.confidence < 0.9 || !isLiteralPassword(secret) || kept(f))
|
|
59
|
+
continue;
|
|
60
|
+
add(/^structure:url/.test(f.rule) ? decodeSafe(secret) : secret, `${f.kind} in $${name}`);
|
|
61
|
+
}
|
|
38
62
|
}
|
|
39
63
|
// (Only what a Redactor takes as a known value, so the caller isn't told a `JWT_SECRET=devsecret` is protected when it isn't.)
|
|
40
64
|
return out.filter((o) => isReplaceable(o.value.trim()));
|
package/dist/formats.js
CHANGED
|
@@ -252,6 +252,18 @@ export const CORE_FORMATS = [
|
|
|
252
252
|
["aeaaqwvhqxbps2v5"],
|
|
253
253
|
],
|
|
254
254
|
["Amazon Bedrock API key", /\b(bedrock-api-key-[A-Za-z0-9+/_-]{40,2000}={0,2})/, ["bedrock-api-key-"]],
|
|
255
|
+
// The long-term key: `ABSK` and the base64 of `BedrockAPIKey-<id>-at-<account>:<secret>`.
|
|
256
|
+
["Amazon Bedrock API key (long-term)", /\b(ABSKQmVkcm9ja0FQSUtleS[A-Za-z0-9+/]{40,2000}={0,2})/, ["absk"]],
|
|
257
|
+
// Cloudflare Tunnel's token: the base64 of `{"a":<account>,"t":<tunnel>,"s":<secret>}`.
|
|
258
|
+
[
|
|
259
|
+
"Cloudflare Tunnel token",
|
|
260
|
+
/(?<![A-Za-z0-9+/])(eyJhIjoi[A-Za-z0-9+/]{90,600}={0,2})(?![A-Za-z0-9+/=])/,
|
|
261
|
+
["eyjhijoi"],
|
|
262
|
+
],
|
|
263
|
+
["Inngest signing key", /\b(signkey-(?:prod|test|branch)-[0-9a-f]{64})\b/, ["signkey-"]],
|
|
264
|
+
["Firecrawl API key", /(?<![\w-])(fc-[0-9a-f]{32})\b/, ["fc-"]],
|
|
265
|
+
["Context7 API key", /\b(ctx7sk-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\b/, ["ctx7sk-"]],
|
|
266
|
+
["Axiom token", /\b(xa[ap]t-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\b/, ["xaat-", "xapt-"]],
|
|
255
267
|
[
|
|
256
268
|
"Heroku API key",
|
|
257
269
|
/\b(HRKU-(?:[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}|AA[0-9A-Za-z_-]{58}))\b/,
|
package/dist/index.js
CHANGED
|
@@ -226,7 +226,7 @@ export class Redactor {
|
|
|
226
226
|
...f,
|
|
227
227
|
start: n.map[f.start],
|
|
228
228
|
end: f.end > f.start ? n.ends[f.end - 1] : n.map[f.start],
|
|
229
|
-
value:
|
|
229
|
+
value: secretValue(n.text, f),
|
|
230
230
|
}));
|
|
231
231
|
// An escaped value in markup (`<password>Vq7m<Zp2</password>`, `value="a"b"`): decoded, its `<` or `"`
|
|
232
232
|
// ends what the XML and attribute readers read, so the names are also read where it's written. (Escaped code in
|
|
@@ -282,8 +282,14 @@ export class Redactor {
|
|
|
282
282
|
!(f.name !== undefined && f.start === l.map[0] && !l.decoded.includes("\n")));
|
|
283
283
|
};
|
|
284
284
|
const decoded = merge(extra);
|
|
285
|
+
// (Where both passes read a value from the same place, the decoded one says where it ends: read raw, Go's
|
|
286
|
+
// `client_secret=…\u0026grant_type=…` runs on through the escaped `&`.)
|
|
287
|
+
const decodedStarts = new Set(extra.map((f) => f.start));
|
|
285
288
|
return merge([
|
|
286
|
-
...found.filter((f) => isVouched(f.rule) ||
|
|
289
|
+
...found.filter((f) => isVouched(f.rule) ||
|
|
290
|
+
layerOf(f.rule) === "repeat" ||
|
|
291
|
+
!insideLiteral(f) ||
|
|
292
|
+
(overlaps(decoded, f.start, f.end) && !decodedStarts.has(f.start))),
|
|
287
293
|
...extra,
|
|
288
294
|
]);
|
|
289
295
|
}
|
|
@@ -297,7 +303,7 @@ export class Redactor {
|
|
|
297
303
|
const out = [];
|
|
298
304
|
for (const { decoded, map } of decodings) {
|
|
299
305
|
for (const f of this.detectNormalized(decoded, { ...options, hint: {} }, depth + 1))
|
|
300
|
-
out.push({ ...f, start: map[f.start], end: map[f.end], value:
|
|
306
|
+
out.push({ ...f, start: map[f.start], end: map[f.end], value: secretValue(decoded, f) });
|
|
301
307
|
}
|
|
302
308
|
return out;
|
|
303
309
|
}
|
|
@@ -376,7 +382,7 @@ export class Redactor {
|
|
|
376
382
|
: this.detectNormalized(decoded, { guessing: "off", encoded: encoded + 1 }, 0).some((f) => !isGuess(f.rule));
|
|
377
383
|
found.push(...findEncoded(text, inner), ...findHexDumps(text, inner));
|
|
378
384
|
}
|
|
379
|
-
found = merge(wrapped(text, found));
|
|
385
|
+
found = merge(wrapped(text, found), text);
|
|
380
386
|
if (on.has("random")) {
|
|
381
387
|
// (Guessing as asked, but in a secret file, the hint's or one written inside the text, always: there the model
|
|
382
388
|
// isn't guessing whether secrets are present, it tells the file's generated values from its settings.)
|
package/dist/json.js
CHANGED
|
@@ -8,7 +8,16 @@
|
|
|
8
8
|
* (transcript.ts): the shell command a tool runs, the path it reads or writes, and the text it writes there.
|
|
9
9
|
*/
|
|
10
10
|
export const TOOL_COMMAND_KEYS = ["command", "cmd", "script", "shell_command", "run"];
|
|
11
|
-
export const TOOL_PATH_KEYS = [
|
|
11
|
+
export const TOOL_PATH_KEYS = [
|
|
12
|
+
"file_path",
|
|
13
|
+
"path",
|
|
14
|
+
"filePath",
|
|
15
|
+
"absolute_path",
|
|
16
|
+
"filename",
|
|
17
|
+
"file",
|
|
18
|
+
"target_file",
|
|
19
|
+
"notebook_path",
|
|
20
|
+
];
|
|
12
21
|
export const TOOL_CONTENT_KEYS = [
|
|
13
22
|
"content",
|
|
14
23
|
"contents",
|
|
@@ -27,13 +36,19 @@ export const TOOL_CONTENT_KEYS = [
|
|
|
27
36
|
"code",
|
|
28
37
|
"edits",
|
|
29
38
|
];
|
|
39
|
+
/**
|
|
40
|
+
* A key that names a command line by its last word, in any case and joining (`CommandLine`, Windsurf's run_command
|
|
41
|
+
* input; `commandLine`, `bash_command`, `COMMAND_LINE`, `shellCmd`; vercel.json's `buildCommand`), or one of the tool
|
|
42
|
+
* keys.
|
|
43
|
+
*/
|
|
44
|
+
const COMMAND_KEY = String.raw `(?:(?:[A-Za-z\d]*[a-z\d](?=[A-Z])|[\w-]*[_-])?(?:command|Command|COMMAND|cmd|Cmd|CMD)(?:[_-]?(?:line|Line|LINE))?|${TOOL_COMMAND_KEYS.join("|")})`;
|
|
30
45
|
/** The keys tools take text under: an editor's `new_string`, a writer's `content`, a shell's `command`, a tool's `input`. */
|
|
31
|
-
const TOOL_TEXT_KEY = String.raw `(?:${[...TOOL_CONTENT_KEYS,
|
|
46
|
+
const TOOL_TEXT_KEY = String.raw `(?:${[...TOOL_CONTENT_KEYS, COMMAND_KEY, "input"].join("|")})`;
|
|
32
47
|
const TOOL_TEXT_ARGUMENT = new RegExp(String.raw `"${TOOL_TEXT_KEY}"[ \t]*:[ \t]*"`);
|
|
33
48
|
/** A literal's opening quote right after a tool's text key (`"new_string": "`, escaped any number of times). */
|
|
34
49
|
const AFTER_TOOL_TEXT_KEY = new RegExp(String.raw `(?<![\w$-])${TOOL_TEXT_KEY}\\*"[ \t]*:[ \t]*$`);
|
|
35
50
|
/** A literal's opening quote right after a shell command's key (`"command": "`, `"cmd": "`): a command line, read as one. */
|
|
36
|
-
const AFTER_COMMAND_KEY = new RegExp(String.raw `(?<![\w$-])
|
|
51
|
+
const AFTER_COMMAND_KEY = new RegExp(String.raw `(?<![\w$-])${COMMAND_KEY}\\*"[ \t]*:[ \t]*$`);
|
|
37
52
|
/**
|
|
38
53
|
* Whether a text may hold JSON string literals worth decoding: escapes a document's literals carry (a line break,
|
|
39
54
|
* a tab, a quote, a slash, a Unicode escape), or a tool's text argument, decoded even without escapes (a one-line
|
|
@@ -173,6 +188,11 @@ export function jsonStringLiterals(text) {
|
|
|
173
188
|
decoded.length >= 20 &&
|
|
174
189
|
(isOutputText(decoded) ||
|
|
175
190
|
decoded.includes('"') ||
|
|
191
|
+
// (A character escaped as a code or a doubled backslash, Go's `\u0026` for `&`, Python's `\u00e9`, a path's
|
|
192
|
+
// `\\`: read raw, `-pQm7v\u0026Tx2…` or `postgres://app:Qm7v\u0026Tx2…@db` isn't the value the program saw.
|
|
193
|
+
// Only where JSON's syntax stands around it, as for the slash: a shell's `"s/\\(token=\\).*/\\1…/"` is no JSON.)
|
|
194
|
+
(/\\(?:u[0-9a-fA-F]{4}|\\)/.test(text.slice(map[0], map[map.length - 1])) &&
|
|
195
|
+
isJsonPlaced(text, open, map[map.length - 1])) ||
|
|
176
196
|
// (An escaped slash, PHP's `"https:\/\/…"`, only where JSON's syntax stands around the literal: in a sed
|
|
177
197
|
// script in a shell's string, `"s/^DB_PASSWORD=.*/DB_PASSWORD=a\/b/"`, it escapes the script's delimiter.)
|
|
178
198
|
(decoded.includes("/") &&
|
|
@@ -338,8 +358,12 @@ export function* jsonDocuments(text) {
|
|
|
338
358
|
yield doc.node;
|
|
339
359
|
}
|
|
340
360
|
}
|
|
341
|
-
/**
|
|
342
|
-
|
|
361
|
+
/**
|
|
362
|
+
* The `{…}` object around [start, end), braces balanced (braces inside its strings too), within `max` characters on
|
|
363
|
+
* each side; "" if none. (Enough for an RSA-8192 private JWK, about 6,500 characters, whose last members sit far from
|
|
364
|
+
* its `{`.)
|
|
365
|
+
*/
|
|
366
|
+
function enclosingObject(text, start, end, max = 8000) {
|
|
343
367
|
let depth = 0;
|
|
344
368
|
let open = -1;
|
|
345
369
|
for (let i = start - 1; i >= Math.max(0, start - max); i--) {
|
package/dist/known.d.ts
CHANGED
|
@@ -7,10 +7,9 @@ export declare class KnownValues {
|
|
|
7
7
|
private shortB64;
|
|
8
8
|
/** Every short form in the order added; `word` if it's matched as a whole word (`short`), else anywhere (`shortB64`). */
|
|
9
9
|
private shortForms;
|
|
10
|
-
/** The short forms indexed
|
|
11
|
-
private
|
|
12
|
-
private
|
|
13
|
-
private recent?;
|
|
10
|
+
/** The short forms indexed, in indexes of decreasing size (see rebuild), and how many they hold. */
|
|
11
|
+
private levels;
|
|
12
|
+
private indexed;
|
|
14
13
|
private labels;
|
|
15
14
|
/** The value each form was made from: a form found is the value written another way, and tagged as the value. */
|
|
16
15
|
private origins;
|
|
@@ -23,9 +22,10 @@ export declare class KnownValues {
|
|
|
23
22
|
/** Adds a secret value. Values too short or too ordinary to replace everywhere are ignored. (One already held keeps its first label.) */
|
|
24
23
|
add(value: string, label?: string): boolean;
|
|
25
24
|
/**
|
|
26
|
-
* Indexes the short forms added since the last find. They go in
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* Indexes the short forms added since the last find. They go in an index of their own, merged with the indexes
|
|
26
|
+
* no bigger than it, so each holds at least twice what the next one holds: a session that learns a value and then
|
|
27
|
+
* finds, over and over, indexes each form a logarithmic number of times, not every form at every few values, and
|
|
28
|
+
* a find reads a logarithmic number of indexes.
|
|
29
29
|
*/
|
|
30
30
|
private rebuild;
|
|
31
31
|
find(text: string): Finding[];
|
package/dist/known.js
CHANGED
|
@@ -52,13 +52,25 @@ function variants(value) {
|
|
|
52
52
|
out.add(json.replace(/\//g, "\\/"));
|
|
53
53
|
out.add(json.replace(/[&<>']/g, (c) => `\\u00${c.charCodeAt(0).toString(16).padStart(2, "0")}`));
|
|
54
54
|
out.add(JSON.stringify(json).slice(1, -1)); // twice-escaped: JSON inside a JSON string
|
|
55
|
+
// Every character past ASCII as a code, as Python's json.dumps and PHP's json_encode (with `\/`) write it, and .NET's
|
|
56
|
+
// in capitals.
|
|
57
|
+
const ascii = json.replace(/[^\x00-\x7f]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
58
|
+
out.add(ascii);
|
|
59
|
+
out.add(ascii.replace(/\//g, "\\/"));
|
|
60
|
+
out.add(ascii.replace(/\\u[0-9a-f]{4}/g, (m) => `\\u${m.slice(2).toUpperCase()}`));
|
|
55
61
|
let uri = value;
|
|
56
62
|
try {
|
|
57
63
|
uri = encodeURIComponent(value);
|
|
58
64
|
}
|
|
59
65
|
catch { } // lone surrogates
|
|
60
66
|
out.add(uri);
|
|
61
|
-
|
|
67
|
+
// RFC 3986's unreserved characters only (Go's QueryEscape, Python's quote_plus with `+`, quote, which also leaves `/`),
|
|
68
|
+
// and a form body's (URLSearchParams, Java's URLEncoder: `*` left, `~` escaped, a space as `+`).
|
|
69
|
+
const strict = uri.replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
70
|
+
out.add(strict);
|
|
71
|
+
out.add(strict.replace(/%20/g, "+"));
|
|
72
|
+
out.add(strict.replace(/%2F/g, "/"));
|
|
73
|
+
out.add(strict.replace(/%2A/g, "*").replace(/~/g, "%7E").replace(/%20/g, "+"));
|
|
62
74
|
out.add(uri.toLowerCase() === uri ? uri : uri.replace(/%[0-9A-F]{2}/g, (m) => m.toLowerCase()));
|
|
63
75
|
out.add(value.replace(/([^\w\-.,:/@+=%])/g, "\\$1")); // printf %q
|
|
64
76
|
out.add(value.replace(/(["\\$`])/g, "\\$1")); // inside double quotes
|
|
@@ -178,8 +190,6 @@ class LiteralIndex {
|
|
|
178
190
|
}
|
|
179
191
|
}
|
|
180
192
|
const TOKEN_CHAR = /[A-Za-z0-9+/_-]/;
|
|
181
|
-
/** How many short forms are indexed apart from the rest before all are indexed in one again. */
|
|
182
|
-
const RECENT_FORMS = 256;
|
|
183
193
|
export class KnownValues {
|
|
184
194
|
long = new LiteralIndex();
|
|
185
195
|
ends = new LiteralIndex();
|
|
@@ -188,10 +198,9 @@ export class KnownValues {
|
|
|
188
198
|
shortB64 = new Map();
|
|
189
199
|
/** Every short form in the order added; `word` if it's matched as a whole word (`short`), else anywhere (`shortB64`). */
|
|
190
200
|
shortForms = [];
|
|
191
|
-
/** The short forms indexed
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
recent;
|
|
201
|
+
/** The short forms indexed, in indexes of decreasing size (see rebuild), and how many they hold. */
|
|
202
|
+
levels = [];
|
|
203
|
+
indexed = 0;
|
|
195
204
|
labels = new Map();
|
|
196
205
|
/** The value each form was made from: a form found is the value written another way, and tagged as the value. */
|
|
197
206
|
origins = new Map();
|
|
@@ -248,25 +257,22 @@ export class KnownValues {
|
|
|
248
257
|
return true;
|
|
249
258
|
}
|
|
250
259
|
/**
|
|
251
|
-
* Indexes the short forms added since the last find. They go in
|
|
252
|
-
*
|
|
253
|
-
*
|
|
260
|
+
* Indexes the short forms added since the last find. They go in an index of their own, merged with the indexes
|
|
261
|
+
* no bigger than it, so each holds at least twice what the next one holds: a session that learns a value and then
|
|
262
|
+
* finds, over and over, indexes each form a logarithmic number of times, not every form at every few values, and
|
|
263
|
+
* a find reads a logarithmic number of indexes.
|
|
254
264
|
*/
|
|
255
265
|
rebuild() {
|
|
256
|
-
|
|
257
|
-
if (indexed === this.shortForms.length)
|
|
266
|
+
if (this.indexed === this.shortForms.length)
|
|
258
267
|
return;
|
|
259
|
-
|
|
268
|
+
let forms = this.shortForms.slice(this.indexed);
|
|
269
|
+
while (this.levels.length && this.levels[this.levels.length - 1].forms.length <= forms.length)
|
|
270
|
+
forms = [...this.levels.pop().forms, ...forms];
|
|
271
|
+
this.levels.push({
|
|
260
272
|
set: new KeywordSet(forms.map((f) => f.form), true),
|
|
261
273
|
forms,
|
|
262
274
|
});
|
|
263
|
-
|
|
264
|
-
this.main = index(this.shortForms.slice());
|
|
265
|
-
this.mainCount = this.shortForms.length;
|
|
266
|
-
this.recent = undefined;
|
|
267
|
-
}
|
|
268
|
-
else
|
|
269
|
-
this.recent = index(this.shortForms.slice(this.mainCount));
|
|
275
|
+
this.indexed = this.shortForms.length;
|
|
270
276
|
}
|
|
271
277
|
find(text) {
|
|
272
278
|
this.rebuild();
|
|
@@ -281,9 +287,7 @@ export class KnownValues {
|
|
|
281
287
|
value: this.origins.get(v),
|
|
282
288
|
});
|
|
283
289
|
// Short values whole only (or straight after a flag like `-p`); short base64 pieces anywhere.
|
|
284
|
-
for (const index of
|
|
285
|
-
if (!index)
|
|
286
|
-
continue;
|
|
290
|
+
for (const index of this.levels) {
|
|
287
291
|
for (const [at, k] of index.set.matches(text)) {
|
|
288
292
|
const { form: v, word } = index.forms[k];
|
|
289
293
|
const dash = v.includes("-");
|
|
@@ -312,11 +316,15 @@ export class KnownValues {
|
|
|
312
316
|
fullEnd = Math.max(fullEnd, full[fi++].end);
|
|
313
317
|
if (fullEnd >= at + W || at < lastEnd)
|
|
314
318
|
continue;
|
|
319
|
+
// (Over the characters the value has: a token with no `/` cut short after a path, `…/repos/o/r/ghp_Zq8v…`,
|
|
320
|
+
// takes none of the path with it.)
|
|
321
|
+
const value = this.origins.get(v) ?? "";
|
|
322
|
+
const inValue = (c) => TOKEN_CHAR.test(c) && (/[A-Za-z0-9]/.test(c) || value.includes(c));
|
|
315
323
|
let start = at;
|
|
316
324
|
let end = at + W;
|
|
317
|
-
while (start > 0 &&
|
|
325
|
+
while (start > 0 && inValue(text[start - 1]))
|
|
318
326
|
start--;
|
|
319
|
-
while (end < text.length &&
|
|
327
|
+
while (end < text.length && inValue(text[end]))
|
|
320
328
|
end++;
|
|
321
329
|
lastEnd = end;
|
|
322
330
|
out.push({
|
package/dist/named.d.ts
CHANGED
|
@@ -12,6 +12,12 @@ export declare function isPairLabel(name: string, label: string): boolean;
|
|
|
12
12
|
export declare function isNamedSecret(name: string, raw: string, literal?: boolean): boolean;
|
|
13
13
|
/** Values under a credential name. (Values public by design, client-side Google keys and DocSearch keys among them, are policy.ts's.) */
|
|
14
14
|
export declare function findNamed(text: string): Finding[];
|
|
15
|
+
/**
|
|
16
|
+
* The key of the object or mapping the entry at `at` sits in: before the nearest `{` it is inside (`mixpanel: {`,
|
|
17
|
+
* `"mixpanel": {`), or a YAML line indented less with nothing after its key (`mixpanel:`). Read within 2,000
|
|
18
|
+
* characters; undefined if none.
|
|
19
|
+
*/
|
|
20
|
+
export declare function enclosingKey(text: string, at: number): string | undefined;
|
|
15
21
|
/**
|
|
16
22
|
* A word a sentence has and a passphrase doesn't: a function word, or a verb's or an adverb's form (`hashed`, `using`,
|
|
17
23
|
* `effectively`) that isn't a word of BIP39's list, which passphrases are drawn from (`jelly`, `bring`, `speed`).
|