@wirecat/cli-core 0.443.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 CHANGED
@@ -7,6 +7,20 @@ 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
+
10
24
  ## 0.443.0 — 26.10.2026
11
25
 
12
26
  ### Added
@@ -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` or `name: value`, and a
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;
@@ -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
- "apikey",
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 === "api" && words[at + 1] === "key"));
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
- const NAMED_PAIR = /(?<![A-Za-z0-9])([A-Za-z0-9_.-]*(?:token|password|passphrase|secret|credential|cookie|authorization|api[_-]?key)[A-Za-z0-9_.-]*)(["']?\s*[:=]\s*["']?)((?:Bearer\s+|Basic\s+)?)([^\s"'&,;]+)/gi;
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` or `name: value`, and a
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) => (looksRandom(run) ? mark : 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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.443.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",