@wirecat/cli-core 0.442.0 → 0.444.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.
- package/CHANGELOG.md +26 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/runs/secrets.d.ts +3 -2
- package/dist/runs/secrets.js +25 -6
- package/dist/terminal/sign-in-steps.d.ts +38 -0
- package/dist/terminal/sign-in-steps.js +87 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,32 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.444.0 — 26.10.2026
|
|
11
|
+
|
|
12
|
+
### Security
|
|
13
|
+
|
|
14
|
+
- **Run records hide more secrets: hex keys, and a password written after its name with a space.** A run of 20 or
|
|
15
|
+
more lower-case hex characters (`0-9a-f`) is hidden when it looks random (3 bits per character or more, the limit
|
|
16
|
+
Yelp's detect-secrets uses). This hides hex keys and also full commit hashes; a short hash (7–12 characters), a
|
|
17
|
+
UUID and an upper-case WhatsApp message id stay. The list of secret names grows by detect-secrets' keyword list
|
|
18
|
+
(`passwd`, `pwd`, `auth_key`, `private_key`, `db_pass`, `contraseña` and others) and by `pass`, `passcode`, `pin`,
|
|
19
|
+
`otp`, `2fa`, `clave`, `пароль`, `пин`, `код`, in any letter case. The value after such a name is hidden when it
|
|
20
|
+
follows `=`, `:`, `=>` or a space (`password hunter2`, `pin 1234`, `код 123456`); after a space only a value that
|
|
21
|
+
is not a plain word, so `pin this message` stays. A flag or argument with such a name (`--pin <chat>`) now has its
|
|
22
|
+
value hidden in a run record that keeps what was typed.
|
|
23
|
+
|
|
24
|
+
## 0.443.0 — 26.10.2026
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **`signInSteps` and `answerStdinOption` (`@wirecat/cli-core/cli`): a sign-in a window can drive.** With `--json`, a
|
|
29
|
+
tool prints each step of its sign-in as one JSON line on stderr: `open_url` (a page to open), `pairing_code` (a code
|
|
30
|
+
to type into the phone), `progress`, and `needs` (`field`, `secret`, `prompt`, `hint`), whose answer is one line on
|
|
31
|
+
stdin. A QR code stays `loginQr`'s `qr` line. Reading answers is opt-in, `--answer-stdin`, and needs `--json`:
|
|
32
|
+
without it a run with no terminal still refuses, so an agent's shell never waits on a question. One reader serves
|
|
33
|
+
the whole run, a question can be withdrawn when the answer came another way (a browser callback), stdin is let go
|
|
34
|
+
between questions so the tool ends with its work, and an answer is never printed or kept in a run record.
|
|
35
|
+
|
|
10
36
|
## 0.442.0 — 26.10.2026
|
|
11
37
|
|
|
12
38
|
### Security
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export { maskedAccount } from "../services/people.js";
|
|
|
37
37
|
export { unanswered } from "../services/questions.js";
|
|
38
38
|
export { openRequestTask } from "../services/task-rules.js";
|
|
39
39
|
export { TASK_PAIR_QUERY, TASK_QUERY } from "../services/tasks.js";
|
|
40
|
+
export { answerStdinOption, type Need, type SignInSteps, type SignInStepsOptions, signInSteps, } from "../terminal/sign-in-steps.js";
|
|
40
41
|
export { type AgentPolicy, type AgentTask, type EvaluationResult, evaluateAgent, type Observation, } from "./agent-evaluation.js";
|
|
41
42
|
export { answerEntity } from "./answer-entity.js";
|
|
42
43
|
export type { ApiCommandInput, ApiCommands } from "./bot/api.js";
|
package/dist/cli/index.js
CHANGED
|
@@ -32,6 +32,7 @@ export { maskedAccount } from "../services/people.js";
|
|
|
32
32
|
export { unanswered } from "../services/questions.js";
|
|
33
33
|
export { openRequestTask } from "../services/task-rules.js";
|
|
34
34
|
export { TASK_PAIR_QUERY, TASK_QUERY } from "../services/tasks.js";
|
|
35
|
+
export { answerStdinOption, signInSteps, } from "../terminal/sign-in-steps.js";
|
|
35
36
|
export { evaluateAgent, } from "./agent-evaluation.js";
|
|
36
37
|
export { answerEntity } from "./answer-entity.js";
|
|
37
38
|
export { apiFlagOf, apiJson, apiOptionKey, apiPlainJson, checkApiBody, checkApiParameter, parseApiJson, readApiBody, } from "./bot/api-input.js";
|
package/dist/runs/secrets.d.ts
CHANGED
|
@@ -6,7 +6,8 @@ export declare const secretNamed: (name: string) => boolean;
|
|
|
6
6
|
export declare const looksRandom: (run: string) => boolean;
|
|
7
7
|
/**
|
|
8
8
|
* Text with each secret inside it replaced by `mark` and the words around it kept: a token of a known vendor format,
|
|
9
|
-
* a private key, a `Bearer`/`Basic` credential, the value of a secret-named `name=value
|
|
10
|
-
* random-looking string of 20 or more printable characters (`looksRandom`)
|
|
9
|
+
* a private key, a `Bearer`/`Basic` credential, the value of a secret-named `name=value`, `name: value`, `name => value`
|
|
10
|
+
* or `name value`, a random-looking string of 20 or more printable characters (`looksRandom`), and a run of 20 or more
|
|
11
|
+
* lower-case hex characters with a Shannon entropy of 3 bits per character or more.
|
|
11
12
|
*/
|
|
12
13
|
export declare const withoutSecrets: (text: string, mark?: string) => string;
|
package/dist/runs/secrets.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// Single words, and pairs written together (`apikey`) or apart (`api_key`); after Yelp detect-secrets' keyword list.
|
|
1
2
|
const SECRET_WORDS = new Set([
|
|
2
3
|
"authorization",
|
|
3
4
|
"cookie",
|
|
@@ -5,9 +6,23 @@ const SECRET_WORDS = new Set([
|
|
|
5
6
|
"credentials",
|
|
6
7
|
"passphrase",
|
|
7
8
|
"password",
|
|
9
|
+
"passwd",
|
|
10
|
+
"pwd",
|
|
11
|
+
"pass",
|
|
12
|
+
"passcode",
|
|
13
|
+
"pin",
|
|
14
|
+
"otp",
|
|
15
|
+
"2fa",
|
|
8
16
|
"secret",
|
|
9
17
|
"token",
|
|
10
|
-
"
|
|
18
|
+
"contraseña",
|
|
19
|
+
"contrasena",
|
|
20
|
+
"clave",
|
|
21
|
+
"пароль",
|
|
22
|
+
"пин",
|
|
23
|
+
"код",
|
|
24
|
+
...["api", "auth", "service", "account", "db", "database", "priv", "private", "client"].map((word) => `${word}key`),
|
|
25
|
+
...["db", "database", "key"].map((word) => `${word}pass`),
|
|
11
26
|
]);
|
|
12
27
|
// Undeclared flags carry no mark, so the name is the only signal; CLIs take secrets at a prompt (WC-SECR-5) and this
|
|
13
28
|
// catches the one typed anyway.
|
|
@@ -16,7 +31,7 @@ export const secretNamed = (name) => {
|
|
|
16
31
|
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
17
32
|
.split(/[\s_.-]+/)
|
|
18
33
|
.map((word) => word.toLowerCase());
|
|
19
|
-
return words.some((word, at) => SECRET_WORDS.has(word) || (word
|
|
34
|
+
return words.some((word, at) => SECRET_WORDS.has(word) || SECRET_WORDS.has(word + (words[at + 1] ?? "")));
|
|
20
35
|
};
|
|
21
36
|
const START = "(?<![A-Za-z0-9_-])";
|
|
22
37
|
const END = "(?![A-Za-z0-9_-])";
|
|
@@ -37,7 +52,8 @@ const KNOWN_TOKENS = new RegExp(`${[
|
|
|
37
52
|
const PRIVATE_KEY = /-----BEGIN[A-Z ]* PRIVATE KEY-----[\s\S]*?(?:-----END[A-Z ]* PRIVATE KEY-----|$)/g;
|
|
38
53
|
// Capitalised, as in a header, and not followed by a plain word: "the Basic plan" is somebody's words.
|
|
39
54
|
const SCHEME = /(?<![A-Za-z])(Bearer|Basic)(\s+)(?![A-Z]?[a-z]+(?![A-Za-z0-9]))[A-Za-z0-9._~+/=-]{8,}/g;
|
|
40
|
-
|
|
55
|
+
// After a bare space only a value that is not a plain word: `pin 1234` is a secret, `pin this message` is not.
|
|
56
|
+
const NAMED_PAIR = /(?<![\p{L}\p{N}])([\p{L}\p{N}_.-]*(?:token|pass|pwd|pin|otp|2fa|secret|credential|cookie|authorization|key|contrase|clave|пароль|пин|код)[\p{L}\p{N}_.-]*)(["']?\s*(?:=>|[:=])\s*["']?|\s+(?![\p{L}]+(?![\p{L}\p{N}])))((?:Bearer\s+|Basic\s+)?)([^\s"'&,;]+)/giu;
|
|
41
57
|
/**
|
|
42
58
|
* Whether a run of token characters is a random string rather than words: long, mixed case, high Shannon entropy,
|
|
43
59
|
* cut into short pieces (`Kz9vQ2mX`) where a name is cut into words (`InlineQueryResultMpeg4Gif`).
|
|
@@ -72,8 +88,9 @@ const entropy = (text) => {
|
|
|
72
88
|
};
|
|
73
89
|
/**
|
|
74
90
|
* Text with each secret inside it replaced by `mark` and the words around it kept: a token of a known vendor format,
|
|
75
|
-
* a private key, a `Bearer`/`Basic` credential, the value of a secret-named `name=value
|
|
76
|
-
* random-looking string of 20 or more printable characters (`looksRandom`)
|
|
91
|
+
* a private key, a `Bearer`/`Basic` credential, the value of a secret-named `name=value`, `name: value`, `name => value`
|
|
92
|
+
* or `name value`, a random-looking string of 20 or more printable characters (`looksRandom`), and a run of 20 or more
|
|
93
|
+
* lower-case hex characters with a Shannon entropy of 3 bits per character or more.
|
|
77
94
|
*/
|
|
78
95
|
export const withoutSecrets = (text, mark = "[redacted]") => text
|
|
79
96
|
.replace(PRIVATE_KEY, mark)
|
|
@@ -84,5 +101,7 @@ export const withoutSecrets = (text, mark = "[redacted]") => text
|
|
|
84
101
|
// A URL or a path is somebody's address, not a key; a credential in one is caught by the rules above.
|
|
85
102
|
token.includes("://") || /^(?:\/|~\/|\.\.?\/)/.test(token)
|
|
86
103
|
? token
|
|
87
|
-
: token.replace(RUN, (run) =>
|
|
104
|
+
: token.replace(RUN, (run) => looksRandom(run) ? mark : run.replace(HEX, (hex) => (entropy(hex) >= 3 ? mark : hex))));
|
|
88
105
|
const RUN = /(?<![A-Za-z0-9_+/-])[A-Za-z0-9_+/-]{20,}/g;
|
|
106
|
+
// Lower case only: WhatsApp message ids are upper-case hex of 20–32 characters. A dash ends a run, so a UUID stays.
|
|
107
|
+
const HEX = /(?<![A-Za-z0-9])(?=[0-9]*[a-f])[0-9a-f]{20,}(?![A-Za-z0-9])/g;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Option } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* Opt-in, because a read nobody answers hangs: an agent running `session start` in a shell must get the refusal it
|
|
4
|
+
* gets today, and only a caller that holds stdin open and answers each `needs` line (WireCat Desktop) passes it.
|
|
5
|
+
*/
|
|
6
|
+
export declare const answerStdinOption: () => Option;
|
|
7
|
+
export interface Need {
|
|
8
|
+
/** What is asked, for the caller to pick a field: `phone`, `code`, `password`, `api_id`, `api_hash`, `token`, `redirect`… */
|
|
9
|
+
field: string;
|
|
10
|
+
/** Shown masked and never echoed. */
|
|
11
|
+
secret: boolean;
|
|
12
|
+
prompt: string;
|
|
13
|
+
hint?: string | undefined;
|
|
14
|
+
}
|
|
15
|
+
export interface SignInStepsOptions {
|
|
16
|
+
/** Writes one whole line to the diagnostic stream; stdout keeps only the answer. */
|
|
17
|
+
diagnostic: (line: string) => void;
|
|
18
|
+
/** Machine output (`--json`, `--jsonl`): only then are the steps printed. */
|
|
19
|
+
json: boolean;
|
|
20
|
+
/** `--answer-stdin`. */
|
|
21
|
+
answers: boolean;
|
|
22
|
+
input?: NodeJS.ReadableStream;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A sign-in told as JSON lines on the diagnostic stream, for a window to draw instead of a terminal: `open_url`,
|
|
26
|
+
* `pairing_code`, `progress`, and `needs`, whose answer is one line on stdin. A QR code is `loginQr`'s `qr` line.
|
|
27
|
+
*/
|
|
28
|
+
export interface SignInSteps {
|
|
29
|
+
readonly json: boolean;
|
|
30
|
+
/** Whether `ask` reads answers from stdin; tools keep their own terminal prompt otherwise. */
|
|
31
|
+
readonly answers: boolean;
|
|
32
|
+
openUrl: (url: string, purpose: string) => void;
|
|
33
|
+
pairingCode: (code: string, where: string, expiresAt?: Date) => void;
|
|
34
|
+
progress: (step: string, message: string) => void;
|
|
35
|
+
/** A `signal` that aborts withdraws the question: the caller got its answer another way (a browser callback). */
|
|
36
|
+
ask: (need: Need, signal?: AbortSignal) => Promise<string>;
|
|
37
|
+
}
|
|
38
|
+
export declare const signInSteps: ({ diagnostic, json, answers, input }: SignInStepsOptions) => SignInSteps;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { StringDecoder } from "node:string_decoder";
|
|
2
|
+
import { CliError } from "@wirecat/cli-base";
|
|
3
|
+
import { Option } from "commander";
|
|
4
|
+
import { MAX_SECRET_INPUT } from "../cli/input-policy.js";
|
|
5
|
+
/**
|
|
6
|
+
* Opt-in, because a read nobody answers hangs: an agent running `session start` in a shell must get the refusal it
|
|
7
|
+
* gets today, and only a caller that holds stdin open and answers each `needs` line (WireCat Desktop) passes it.
|
|
8
|
+
*/
|
|
9
|
+
export const answerStdinOption = () => new Option("--answer-stdin", "with --json and no terminal: print each question as a `needs` line on stderr and read its answer, one line, from stdin");
|
|
10
|
+
export const signInSteps = ({ diagnostic, json, answers, input = process.stdin }) => {
|
|
11
|
+
if (answers && !json)
|
|
12
|
+
throw new CliError("validation_error", "--answer-stdin needs --json");
|
|
13
|
+
const emit = (event) => {
|
|
14
|
+
if (json)
|
|
15
|
+
diagnostic(JSON.stringify(event));
|
|
16
|
+
};
|
|
17
|
+
const next = answers ? lineReader(input) : undefined;
|
|
18
|
+
return {
|
|
19
|
+
json,
|
|
20
|
+
answers,
|
|
21
|
+
openUrl: (url, purpose) => emit({ event: "open_url", url, purpose }),
|
|
22
|
+
pairingCode: (code, where, expiresAt) => emit({ event: "pairing_code", code, where, expires_at: expiresAt?.toISOString() ?? null }),
|
|
23
|
+
progress: (step, message) => emit({ event: "progress", step, message }),
|
|
24
|
+
ask: async ({ field, secret, prompt, hint }, signal) => {
|
|
25
|
+
if (!next)
|
|
26
|
+
throw new CliError("validation_error", `the ${prompt} must be typed at a terminal`, {
|
|
27
|
+
reason: "input.required",
|
|
28
|
+
});
|
|
29
|
+
signal?.throwIfAborted();
|
|
30
|
+
emit({ event: "needs", field, secret, prompt, hint: hint ?? null });
|
|
31
|
+
return await next(signal);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* One reader for the whole run, so a second answer that arrived with the first is not lost. It reads only while a
|
|
37
|
+
* question waits and pauses stdin between them, so a pipe the caller keeps open does not hold the process after the
|
|
38
|
+
* last answer.
|
|
39
|
+
*/
|
|
40
|
+
const lineReader = (input) => {
|
|
41
|
+
const decoder = new StringDecoder("utf8");
|
|
42
|
+
let buffered = "";
|
|
43
|
+
let ended = false;
|
|
44
|
+
return (signal) => new Promise((resolve, reject) => {
|
|
45
|
+
const cleanup = () => {
|
|
46
|
+
input.off("data", data).off("end", end).off("error", fail);
|
|
47
|
+
signal?.removeEventListener("abort", aborted);
|
|
48
|
+
input.pause();
|
|
49
|
+
// A paused pipe still holds the event loop; the tool must end once its work is done.
|
|
50
|
+
input.unref?.();
|
|
51
|
+
};
|
|
52
|
+
const fail = (error) => {
|
|
53
|
+
cleanup();
|
|
54
|
+
reject(error);
|
|
55
|
+
};
|
|
56
|
+
const take = () => {
|
|
57
|
+
const at = buffered.indexOf("\n");
|
|
58
|
+
if (at < 0)
|
|
59
|
+
return false;
|
|
60
|
+
const line = buffered.slice(0, at).replace(/\r$/, "");
|
|
61
|
+
buffered = buffered.slice(at + 1);
|
|
62
|
+
cleanup();
|
|
63
|
+
resolve(line.trim());
|
|
64
|
+
return true;
|
|
65
|
+
};
|
|
66
|
+
const data = (chunk) => {
|
|
67
|
+
buffered += typeof chunk === "string" ? chunk : decoder.write(chunk);
|
|
68
|
+
if (take())
|
|
69
|
+
return;
|
|
70
|
+
if (buffered.length > MAX_SECRET_INPUT)
|
|
71
|
+
fail(new CliError("validation_error", "the answer exceeds its byte limit", { reason: "input.too-large" }));
|
|
72
|
+
};
|
|
73
|
+
const end = () => {
|
|
74
|
+
ended = true;
|
|
75
|
+
fail(new CliError("cancelled", "the input closed before the answer came", { reason: "input.closed" }));
|
|
76
|
+
};
|
|
77
|
+
const aborted = () => fail(signal?.reason ?? new CliError("cancelled", "cancelled"));
|
|
78
|
+
if (take())
|
|
79
|
+
return;
|
|
80
|
+
if (ended)
|
|
81
|
+
return end();
|
|
82
|
+
input.on("data", data).once("end", end).once("error", fail);
|
|
83
|
+
signal?.addEventListener("abort", aborted, { once: true });
|
|
84
|
+
input.ref?.();
|
|
85
|
+
input.resume();
|
|
86
|
+
});
|
|
87
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.444.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|