ccdeck 3.29.11 → 3.30.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/README.md +1 -1
- package/bin/agent-dag.js +67 -118
- package/bin/cli/help.js +67 -0
- package/bin/cli/login-item.js +182 -0
- package/bin/cli/one-shot.js +254 -0
- package/bin/cli/package.js +39 -0
- package/bin/cli/pulse.js +98 -0
- package/bin/cli/restart.js +179 -0
- package/bin/cli/screen.js +166 -0
- package/bin/cli/second-start.js +120 -0
- package/bin/cli/startup.js +459 -0
- package/bin/cli/uninstall.js +169 -0
- package/bin/deck.js +112 -1559
- package/dist/web/assets/BrowserWatchModal-DP3YMtEi.js +1 -0
- package/dist/web/assets/UsageHistoryModal-CcdPVXAw.js +1 -0
- package/dist/web/assets/index-3vS6A0vt.css +1 -0
- package/dist/web/assets/index-BVTWHDta.js +298 -0
- package/dist/web/index.html +2 -2
- package/hook/hook.js +15 -14
- package/package.json +1 -1
- package/release-notes.json +22 -0
- package/src/server/account-projects.mjs +25 -11
- package/src/server/account-routes.mjs +254 -0
- package/src/server/activity.mjs +3 -3
- package/src/server/append-budget.mjs +185 -0
- package/src/server/append-failures.mjs +147 -0
- package/src/server/args.mjs +37 -5
- package/src/server/atomic-write.mjs +300 -0
- package/src/server/block-notify.mjs +0 -5
- package/src/server/boot-lock.mjs +1 -1
- package/src/server/brand.mjs +7 -6
- package/src/server/browser-react.mjs +10 -2
- package/src/server/browser-watch-decks.mjs +146 -0
- package/src/server/browser-watch-feed.mjs +69 -0
- package/src/server/browser-watch-log.mjs +173 -0
- package/src/server/browser-watch-mtime.mjs +18 -0
- package/src/server/browser-watch-relay.mjs +112 -0
- package/src/server/browser-watch-routes.mjs +172 -0
- package/src/server/browser-watch-seen.mjs +127 -0
- package/src/server/browser-watch-store.mjs +14 -143
- package/src/server/browser-watch.mjs +278 -626
- package/src/server/canonical-path.mjs +150 -0
- package/src/server/ccusage-failure.mjs +64 -0
- package/src/server/ccusage-install.mjs +459 -0
- package/src/server/ccusage-runner.mjs +271 -0
- package/src/server/ccusage.mjs +21 -756
- package/src/server/claude-accounts.mjs +220 -464
- package/src/server/claude-collector.mjs +189 -0
- package/src/server/claude-dir.mjs +2 -2
- package/src/server/claude-identity.mjs +5 -4
- package/src/server/claude-reset-credits.mjs +130 -0
- package/src/server/claude-verdicts.mjs +174 -0
- package/src/server/clear-route.mjs +191 -0
- package/src/server/codex-auth.mjs +4 -4
- package/src/server/codex-dir.mjs +52 -8
- package/src/server/codex-enrichment.mjs +310 -0
- package/src/server/codex-quota.mjs +5 -3
- package/src/server/codex-translate.mjs +407 -0
- package/src/server/codex-usage.mjs +8 -5
- package/src/server/codex-watch.mjs +496 -0
- package/src/server/cswap-auto-loop.mjs +342 -0
- package/src/server/cswap-auto-readers.mjs +303 -0
- package/src/server/cswap-auto.mjs +51 -609
- package/src/server/cswap-install.mjs +69 -265
- package/src/server/cswap-layout.mjs +242 -0
- package/src/server/deck-home.mjs +1 -1
- package/src/server/deck-prefs.mjs +51 -184
- package/src/server/deck-scope.mjs +52 -0
- package/src/server/desktop-update-routes.mjs +75 -0
- package/src/server/detach.mjs +78 -0
- package/src/server/discovery.mjs +298 -0
- package/src/server/event-log.mjs +328 -0
- package/src/server/event-pipeline.mjs +235 -0
- package/src/server/event-ring.mjs +214 -0
- package/src/server/event-routes.mjs +352 -0
- package/src/server/event-sink.mjs +26 -0
- package/src/server/exec-children.mjs +128 -0
- package/src/server/exec-not-found.mjs +213 -0
- package/src/server/exec-output.mjs +23 -0
- package/src/server/exec-spec.mjs +341 -0
- package/src/server/exec.mjs +74 -661
- package/src/server/health-route.mjs +42 -0
- package/src/server/hook-challenge.mjs +37 -0
- package/src/server/http-io.mjs +104 -0
- package/src/server/index.mjs +172 -7620
- package/src/server/install-layout.mjs +506 -0
- package/src/server/installer.mjs +79 -608
- package/src/server/jsonl-chunks.mjs +139 -0
- package/src/server/lan-accounts.mjs +27 -0
- package/src/server/lan-beacon.mjs +457 -0
- package/src/server/lan-call.mjs +239 -0
- package/src/server/lan-copies.mjs +270 -0
- package/src/server/lan-deck.mjs +583 -0
- package/src/server/lan-dials.mjs +161 -0
- package/src/server/lan-engine.mjs +475 -1024
- package/src/server/lan-hearing.mjs +49 -0
- package/src/server/lan-inbound.mjs +100 -0
- package/src/server/lan-invite-offer.mjs +77 -0
- package/src/server/lan-invite.mjs +207 -0
- package/src/server/lan-lines.mjs +73 -0
- package/src/server/lan-manifest.mjs +74 -0
- package/src/server/lan-reach.mjs +82 -69
- package/src/server/lan-requests.mjs +249 -0
- package/src/server/lan-round-record.mjs +45 -0
- package/src/server/lan-round-timer.mjs +62 -0
- package/src/server/lan-routes.mjs +143 -0
- package/src/server/lan-serve.mjs +112 -0
- package/src/server/lan-socket.mjs +243 -925
- package/src/server/lan-sync.mjs +58 -1026
- package/src/server/lan-tailnet-poll.mjs +57 -0
- package/src/server/lan-turns.mjs +80 -0
- package/src/server/lan-wire.mjs +620 -0
- package/src/server/lifecycle.mjs +421 -0
- package/src/server/listen.mjs +110 -0
- package/src/server/live-decks.mjs +138 -0
- package/src/server/load-average.mjs +32 -0
- package/src/server/log-election.mjs +235 -0
- package/src/server/log-replay.mjs +374 -0
- package/src/server/log-writer.mjs +72 -557
- package/src/server/login-service.mjs +3 -2
- package/src/server/macmon.mjs +1 -1
- package/src/server/memory-files.mjs +163 -0
- package/src/server/memory-metrics.mjs +178 -0
- package/src/server/metrics-history.mjs +94 -0
- package/src/server/metrics-run.mjs +100 -0
- package/src/server/music-routes.mjs +134 -0
- package/src/server/network-metrics.mjs +5 -3
- package/src/server/network-sampler.mjs +256 -0
- package/src/server/npm-latest.mjs +385 -0
- package/src/server/npm-upgrade.mjs +352 -0
- package/src/server/npx.mjs +41 -7
- package/src/server/output-watch.mjs +5 -4
- package/src/server/pinned-build.mjs +108 -0
- package/src/server/prefs-path.mjs +14 -0
- package/src/server/prefs-refusal.mjs +77 -0
- package/src/server/prefs-routes.mjs +76 -0
- package/src/server/prefs-set-aside.mjs +86 -0
- package/src/server/prefs-state.mjs +51 -0
- package/src/server/process-command.mjs +104 -0
- package/src/server/process-list.mjs +547 -0
- package/src/server/pulse-line.mjs +132 -0
- package/src/server/purge-key.mjs +7 -7
- package/src/server/quota-cli.mjs +174 -0
- package/src/server/quota-oauth.mjs +190 -0
- package/src/server/quota-shape.mjs +229 -0
- package/src/server/quota-store-resets.mjs +206 -0
- package/src/server/quota.mjs +123 -509
- package/src/server/relay-guard.mjs +3 -3
- package/src/server/request-gates.mjs +441 -0
- package/src/server/restart-note.mjs +180 -0
- package/src/server/retire-sound-hook.mjs +3 -3
- package/src/server/ring-bounds.mjs +215 -0
- package/src/server/running-deck.mjs +1 -1
- package/src/server/running-version.mjs +24 -0
- package/src/server/self-update.mjs +39 -1371
- package/src/server/session-enrichment.mjs +622 -0
- package/src/server/session-read-gate.mjs +49 -0
- package/src/server/session-tracking.mjs +242 -0
- package/src/server/sse-clients.mjs +222 -0
- package/src/server/stale-discovery.mjs +30 -0
- package/src/server/static-serve.mjs +81 -0
- package/src/server/supervisor.mjs +73 -12
- package/src/server/system-metrics.mjs +169 -1935
- package/src/server/term.mjs +11 -249
- package/src/server/thermal-metrics.mjs +520 -0
- package/src/server/thermal-sampler.mjs +204 -0
- package/src/server/token-redact.mjs +123 -0
- package/src/server/transcript-gate.mjs +107 -0
- package/src/server/transcript-scan.mjs +563 -0
- package/src/server/usage-routes.mjs +93 -0
- package/src/server/uv-bootstrap.mjs +1 -1
- package/src/server/wordmark.mjs +127 -0
- package/dist/web/assets/BrowserWatchModal-CyAFTvAY.js +0 -1
- package/dist/web/assets/UsageHistoryModal-KJfli0oa.js +0 -1
- package/dist/web/assets/index-Ck2RAla3.css +0 -1
- package/dist/web/assets/index-jmrcEsCj.js +0 -298
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// The deck's settings routes: GET /api/prefs, which answers what a page may see
|
|
2
|
+
// of them and what the machine allows, and POST /api/prefs, which writes a
|
|
3
|
+
// patch and hands the result to the LAN engine.
|
|
4
|
+
//
|
|
5
|
+
// These lived in src/server/index.mjs, beside the desktop notifier that reads
|
|
6
|
+
// the same settings, and moved once the settings had (prefs-state.mjs) and the
|
|
7
|
+
// engine had (lan-deck.mjs): what is left of them is a body read, a write
|
|
8
|
+
// through the one, a push to the other, and an answer. The notifier stays in
|
|
9
|
+
// index.mjs, beside the desktop-app connections it notifies through.
|
|
10
|
+
import { notificationsOn, notificationsVetoed, prefsRefusalDetail, prefsWriteRefusal, publicPrefs } from "./deck-prefs.mjs";
|
|
11
|
+
import { PRODUCT } from "./brand.mjs";
|
|
12
|
+
import { heldPrefs } from "./prefs-state.mjs";
|
|
13
|
+
import { applyLanPrefs, forgetReach } from "./lan-deck.mjs";
|
|
14
|
+
import { readBody, send } from "./http-io.mjs";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* GET the deck's settings, and what the machine is allowing.
|
|
18
|
+
*
|
|
19
|
+
* THREE fields, not two, and the third is the one a first attempt got wrong.
|
|
20
|
+
* `notificationsAllowed` is the effective answer, after the environment
|
|
21
|
+
* variable has had its say — but "false" there means either "the user switched
|
|
22
|
+
* it off" or "the machine forbids it", and the menu has to say a different
|
|
23
|
+
* sentence for each. Deriving the second from the first made the switch read
|
|
24
|
+
* "off — set at launch" the moment anybody turned it off on a deck launched
|
|
25
|
+
* with no variable at all, which is the deck telling the user their own press
|
|
26
|
+
* was somebody else's doing. `notificationsVetoed` answers only the machine's
|
|
27
|
+
* half.
|
|
28
|
+
*/
|
|
29
|
+
function prefsPayload() {
|
|
30
|
+
const prefs = heldPrefs.current();
|
|
31
|
+
return {
|
|
32
|
+
ok: true,
|
|
33
|
+
// `publicPrefs`, never the held prefs whole. This route answers anything
|
|
34
|
+
// that can reach the loopback port, and the LAN group passphrase is in the
|
|
35
|
+
// stored object — one field, and sending it here would put it in front of
|
|
36
|
+
// every page on the machine. What goes out is whether one is set.
|
|
37
|
+
prefs: publicPrefs(prefs),
|
|
38
|
+
notificationsAllowed: notificationsOn(prefs),
|
|
39
|
+
notificationsVetoed: notificationsVetoed(),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function handlePrefsRead(req, res) {
|
|
44
|
+
return send(res, 200, prefsPayload());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** POST a patch. Fields nobody sent keep their value — see writePrefs. */
|
|
48
|
+
export async function handlePrefsWrite(req, res) {
|
|
49
|
+
const raw = await readBody(req, res).catch(() => null);
|
|
50
|
+
let body = null;
|
|
51
|
+
try { body = JSON.parse(raw ?? ""); } catch { /* handled below */ }
|
|
52
|
+
if (!body || typeof body !== "object") return send(res, 400, { ok: false, reason: "bad_request" });
|
|
53
|
+
try {
|
|
54
|
+
await heldPrefs.write(body);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
// A settings file or folder this user cannot reach is the machine's to fix,
|
|
57
|
+
// not the deck's, and `guard`'s bare 500 left the panel with nothing to say
|
|
58
|
+
// about it (#1335). Anything else is still a bug and still goes to `guard`.
|
|
59
|
+
const reason = prefsWriteRefusal(err);
|
|
60
|
+
if (!reason) throw err;
|
|
61
|
+
console.error(`${PRODUCT}: settings were not saved:`, err?.message ?? err);
|
|
62
|
+
// What blocked it, from closed sets and without the path, so a screenshot
|
|
63
|
+
// of the panel is enough to tell the cases apart (#1335).
|
|
64
|
+
const detail = prefsRefusalDetail(err);
|
|
65
|
+
return send(res, 500, detail ? { ok: false, reason, detail } : { ok: false, reason });
|
|
66
|
+
}
|
|
67
|
+
// The engine reads its settings from here rather than holding its own copy,
|
|
68
|
+
// so turning the switch off in the panel really does stop the sockets rather
|
|
69
|
+
// than only changing what the panel says.
|
|
70
|
+
await applyLanPrefs();
|
|
71
|
+
// Somebody has just switched this on and is watching the panel for the answer
|
|
72
|
+
// to one question: is anything going to turn up. Whatever the probe said
|
|
73
|
+
// before is about a deck whose sockets were down — see forgetReach.
|
|
74
|
+
if (body.lan?.enabled === true) forgetReach();
|
|
75
|
+
return send(res, 200, prefsPayload());
|
|
76
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Moving a prefs.json nobody can use out of the way, before anything can write
|
|
2
|
+
// over it: the bytes of a file that is not JSON (#1002), and a file another
|
|
3
|
+
// user owns (#1335). Moved out of deck-prefs.mjs unchanged; loadPrefs decides
|
|
4
|
+
// when, and this is how.
|
|
5
|
+
import { stat } from "node:fs/promises";
|
|
6
|
+
import { renameWithRetry } from "./atomic-write.mjs";
|
|
7
|
+
import { deckDataDir } from "./deck-home.mjs";
|
|
8
|
+
import { prefsDir, prefsPath } from "./prefs-path.mjs";
|
|
9
|
+
|
|
10
|
+
/** Where the bytes of a prefs.json nothing could parse are put.
|
|
11
|
+
*
|
|
12
|
+
* `Date.now()` rather than an ISO timestamp because a colon is not a legal
|
|
13
|
+
* filename character on Windows, and a quarantine that cannot be created on
|
|
14
|
+
* the platform it is protecting is not a quarantine. */
|
|
15
|
+
export const quarantinePath = (home = deckDataDir(), at = Date.now()) =>
|
|
16
|
+
`${prefsPath(home)}.corrupt-${at}`;
|
|
17
|
+
|
|
18
|
+
/** The read errors that can mean "not yours" rather than "broken". Only these
|
|
19
|
+
* send a read to setAsideForeign; EISDIR, EIO and EMFILE never do. */
|
|
20
|
+
export const DENIED = new Set(["EACCES", "EPERM"]);
|
|
21
|
+
|
|
22
|
+
/** Where a prefs.json another user owns is put. Beside the file, like
|
|
23
|
+
* quarantinePath, and stamped in milliseconds for the same Windows reason —
|
|
24
|
+
* but a name of its own, because nothing is wrong with these bytes. */
|
|
25
|
+
export const foreignPath = (home = deckDataDir(), at = Date.now()) =>
|
|
26
|
+
`${prefsPath(home)}.foreign-${at}`;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Move a prefs.json that ANOTHER USER OWNS out of the way, or say why not.
|
|
30
|
+
*
|
|
31
|
+
* #1335. A `sudo ccdeck` that kept the user's HOME writes a root-owned 0600
|
|
32
|
+
* prefs.json into the user's own folder. From then on every
|
|
33
|
+
* deck that user starts is refused the read, refuses every settings write on
|
|
34
|
+
* top of it — rightly — and so can never keep anything: a new LAN identity on
|
|
35
|
+
* every start, a share tick that says "Could not share that account", and no
|
|
36
|
+
* way out from inside the deck.
|
|
37
|
+
*
|
|
38
|
+
* WHY MOVING IT IS SAFE HERE WHEN IT IS NOT FOR "unreadable". The refusal
|
|
39
|
+
* exists so the deck never loses a key it could still have read. A file owned
|
|
40
|
+
* by somebody else is not one this user's deck will ever read — waiting does
|
|
41
|
+
* not help, and the deck is already running on defaults. A rename reads and
|
|
42
|
+
* changes nothing: owner and mode travel with the file, so the key in it stays
|
|
43
|
+
* private and whole, and `sudo chown` plus a move puts it back.
|
|
44
|
+
*
|
|
45
|
+
* Only when the owner is KNOWN to be somebody else. A file this user owns and
|
|
46
|
+
* still cannot read (a mode bit, a lock, a sandbox) is left exactly where it
|
|
47
|
+
* is, and so is anything on Windows, where there is no uid to compare and
|
|
48
|
+
* EPERM is usually another program holding the file.
|
|
49
|
+
*
|
|
50
|
+
* Answers `{ moved, uid }`, `{ gone }` when the file vanished under it, or,
|
|
51
|
+
* when it stayed, `{ why, owner, on }`: a sentence for the log, possibly empty,
|
|
52
|
+
* and who owns what blocked the read — "you", "other" or "unknown" — and
|
|
53
|
+
* whether that is the "file" or its "folder". The last two are what the panel
|
|
54
|
+
* is told (see prefsRefusalDetail); the sentence, which has the path, is not.
|
|
55
|
+
*/
|
|
56
|
+
export async function setAsideForeign(path, home, deps) {
|
|
57
|
+
const uid = deps.getuid ? deps.getuid() : process.getuid?.();
|
|
58
|
+
if (uid === undefined) return { why: "", owner: "unknown", on: "file" };
|
|
59
|
+
const look = deps.stat ?? stat;
|
|
60
|
+
let st;
|
|
61
|
+
try {
|
|
62
|
+
st = await look(path);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
if (err?.code === "ENOENT") return { gone: true };
|
|
65
|
+
// The folder itself is out of reach, so nothing in it can be looked at, let
|
|
66
|
+
// alone moved. Name the folder and its owner, which is what a person needs.
|
|
67
|
+
const folder = prefsDir(home);
|
|
68
|
+
const owner = await look(folder).then(s => s.uid, () => undefined);
|
|
69
|
+
if (owner === undefined) return { why: "", owner: "unknown", on: "folder" };
|
|
70
|
+
if (owner === uid) return { why: "", owner: "you", on: "folder" };
|
|
71
|
+
return {
|
|
72
|
+
why: ` The folder ${folder} belongs to another user (uid ${owner}), most likely from a run with sudo; sudo chown -R "$(id -un)" "${folder}" gives it back.`,
|
|
73
|
+
owner: "other",
|
|
74
|
+
on: "folder",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (st.uid === uid) return { why: "", owner: "you", on: "file" };
|
|
78
|
+
const to = foreignPath(home);
|
|
79
|
+
try {
|
|
80
|
+
await (deps.rename ?? renameWithRetry)(path, to);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (err?.code === "ENOENT") return { gone: true };
|
|
83
|
+
return { why: ` It belongs to another user (uid ${st.uid}) and could not be moved aside either: ${err?.message ?? err}.`, owner: "other", on: "file" };
|
|
84
|
+
}
|
|
85
|
+
return { moved: to, uid: st.uid };
|
|
86
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// The deck's settings as this process holds them: the one in-memory copy every
|
|
2
|
+
// reader asks, and the one place a write's answer is kept.
|
|
3
|
+
//
|
|
4
|
+
// This was `let _prefs` in src/server/index.mjs, and it is the reason the LAN
|
|
5
|
+
// section could not leave that file. The notifier and the away-update read it;
|
|
6
|
+
// the settings route, the LAN peer route and six of the LAN engine's callbacks
|
|
7
|
+
// each assigned it back with `_prefs = await writePrefs(...)`; and a
|
|
8
|
+
// module-level `let` is one no other module can assign. So the variable moved
|
|
9
|
+
// first, behind the three things its callers actually do with it: read it,
|
|
10
|
+
// write a patch, and compute a patch from the file. Every write now goes
|
|
11
|
+
// through here, which is also what keeps the promise the comment below has
|
|
12
|
+
// always made — that the copy and the file cannot drift within one process —
|
|
13
|
+
// true by construction rather than by every caller remembering the assignment.
|
|
14
|
+
//
|
|
15
|
+
// The disk half is deck-prefs.mjs, unchanged: the queue, the merge and the
|
|
16
|
+
// refusal to write over a file it could not read all live there. This holds
|
|
17
|
+
// what they return and nothing else.
|
|
18
|
+
import { DEFAULTS, readPrefs, updatePrefs, writePrefs } from "./deck-prefs.mjs";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The deck's own settings, in memory, refreshed whenever they are written.
|
|
22
|
+
*
|
|
23
|
+
* Read once at boot and then kept here rather than read per event: `consider`
|
|
24
|
+
* is on the ingest path every hook event goes through, and a file read there
|
|
25
|
+
* would be a syscall per event to answer a question that changes when somebody
|
|
26
|
+
* presses a switch. The write path below updates this, so the in-memory copy
|
|
27
|
+
* and the file cannot drift within one process — and a second deck writing the
|
|
28
|
+
* file is picked up on ITS next write or this one's next boot, which is the
|
|
29
|
+
* same freshness every other cross-deck setting has.
|
|
30
|
+
*/
|
|
31
|
+
let _prefs = { ...DEFAULTS };
|
|
32
|
+
|
|
33
|
+
// Read at import, so `consider` has its answer from the first event. LAN sync
|
|
34
|
+
// used to start from this same read, and it must not: bin/deck.js imports the
|
|
35
|
+
// server module to ASK whether a deck is already up, and a launcher that then
|
|
36
|
+
// attaches and exits had already bound the beacon and the sync port on the way
|
|
37
|
+
// in — which is the `lan sync (listen): EADDRINUSE` line a second `ccdeck`
|
|
38
|
+
// printed above `deck already running`. The engine starts from the listen that
|
|
39
|
+
// succeeds, in startServer, which is the only process that may hold a port.
|
|
40
|
+
export const prefsRead = readPrefs().then(p => { _prefs = p; }).catch(() => {});
|
|
41
|
+
|
|
42
|
+
export const heldPrefs = Object.freeze({
|
|
43
|
+
/** What the last read or write left here. Never a disk read — see above. */
|
|
44
|
+
current: () => _prefs,
|
|
45
|
+
/** `writePrefs(patch)`, and keep what it wrote. A write that throws keeps
|
|
46
|
+
* nothing, so the copy is never ahead of the file. */
|
|
47
|
+
write: async patch => (_prefs = await writePrefs(patch)),
|
|
48
|
+
/** `updatePrefs(mutate)`, and keep what it wrote: for a patch that has to be
|
|
49
|
+
* computed from the file rather than from `current()` — see updatePrefs. */
|
|
50
|
+
update: async mutate => (_prefs = await updatePrefs(mutate)),
|
|
51
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// What of a process's command line the Machine panel may show: the arguments,
|
|
2
|
+
// with the executable that carries them taken off the front (commandTail), and
|
|
3
|
+
// then with the shapes a secret takes removed and the rest capped
|
|
4
|
+
// (redactCommand).
|
|
5
|
+
//
|
|
6
|
+
// Moved out of process-list.mjs unchanged. attachDetail there runs both on
|
|
7
|
+
// every candidate's argument vector before it leaves that module, because
|
|
8
|
+
// /api/system/processes answers anything that can reach the loopback port;
|
|
9
|
+
// process-list.mjs re-exports CMD_MAX, redactCommand and commandTail, which is
|
|
10
|
+
// where the suite reaches them.
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* An argument vector with the parts that must not be looked at taken out, and
|
|
14
|
+
* the parts nobody reads cut off.
|
|
15
|
+
*
|
|
16
|
+
* THIS IS A BLOCKLIST AND A BLOCKLIST LEAKS. It is written down here rather
|
|
17
|
+
* than implied, because the alternative — `comm`, which is what this replaced —
|
|
18
|
+
* leaked nothing and told the reader nothing either. What it removes is the
|
|
19
|
+
* shapes a secret actually takes on a command line; what it cannot remove is a
|
|
20
|
+
* secret that looks like an ordinary word, and no rule here pretends otherwise.
|
|
21
|
+
*
|
|
22
|
+
* The cap is the other half, and it is not a nicety. Measured on this machine:
|
|
23
|
+
* one Chrome helper's argv is 906 characters of `--field-trial-handle`,
|
|
24
|
+
* base64 `--gpu-preferences` and shared-memory handles. Forty of those is 36 KB
|
|
25
|
+
* on the wire every four seconds to render a column nobody can read. What
|
|
26
|
+
* identifies a process is its first few arguments — `ng serve admin-portal
|
|
27
|
+
* --port 44440` — and that is what fits.
|
|
28
|
+
*/
|
|
29
|
+
export const CMD_MAX = 180;
|
|
30
|
+
|
|
31
|
+
const SECRET_FLAG =
|
|
32
|
+
/(-{1,2}[\w.-]*(?:token|password|passwd|secret|api[-_]?key|apikey|auth|credential|bearer|cookie|session[-_]?id|private[-_]?key)[\w.-]*)(=|\s+)(\S+)/gi;
|
|
33
|
+
// The shapes that are a secret on their own, with no flag in front of them.
|
|
34
|
+
const BARE_SECRET =
|
|
35
|
+
/\b(?:sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{16,}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{12,}|eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,})/g;
|
|
36
|
+
// user:password@host, in any URL. The password goes; the rest is a location.
|
|
37
|
+
const URL_CREDENTIALS = /\b([a-z][a-z0-9+.-]*:\/\/[^\s:@/]+):[^\s@/]+@/gi;
|
|
38
|
+
|
|
39
|
+
export function redactCommand(argv, home = "") {
|
|
40
|
+
let s = String(argv ?? "").trim();
|
|
41
|
+
if (!s) return "";
|
|
42
|
+
s = s.replace(SECRET_FLAG, (_, flag, sep, value) => `${flag}${sep === "=" ? "=" : " "}\u2022\u2022\u2022`);
|
|
43
|
+
s = s.replace(URL_CREDENTIALS, "$1:\u2022\u2022\u2022@");
|
|
44
|
+
s = s.replace(BARE_SECRET, "\u2022\u2022\u2022");
|
|
45
|
+
// `~` last, so a home path inside a redacted value is already gone. Only a
|
|
46
|
+
// whole path segment, so a user called `con` cannot rewrite the middle of an
|
|
47
|
+
// unrelated word.
|
|
48
|
+
if (home && home.length > 1) s = s.split(home + "/").join("~/").split(home + " ").join("~ ");
|
|
49
|
+
if (s.length > CMD_MAX) s = s.slice(0, CMD_MAX - 1).trimEnd() + "\u2026";
|
|
50
|
+
return s;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The ARGUMENTS, with the executable that carries them taken off the front.
|
|
55
|
+
*
|
|
56
|
+
* The name column already says `Google Chrome Helper`; repeating
|
|
57
|
+
* `/Applications/Google Chrome.app/Contents/Frameworks/Google Chrome
|
|
58
|
+
* Framework.framework/Versions/152.0.7977.76/Helpers/Google Chrome
|
|
59
|
+
* Helper.app/Contents/MacOS/Google Chrome Helper` beside it spends the entire
|
|
60
|
+
* 180-character budget on a path the reader can already see the end of, and
|
|
61
|
+
* cuts off `--type=renderer`, which is the part that says WHICH helper this is.
|
|
62
|
+
*
|
|
63
|
+
* argv[0] cannot be found by splitting on whitespace — on macOS the executable
|
|
64
|
+
* path routinely contains spaces, and `/Applications/Google Chrome.app/…` would
|
|
65
|
+
* be cut at "Google". The name from the first `ps` call is what resolves it:
|
|
66
|
+
* argv begins with a path whose last segment IS that name.
|
|
67
|
+
*
|
|
68
|
+
* "Last segment" is the whole of the rule, and the first version of this got it
|
|
69
|
+
* wrong in a way only the render showed. A plain `indexOf` cuts at the FIRST
|
|
70
|
+
* occurrence, and a macOS bundle contains the name twice — `…/Helpers/Google
|
|
71
|
+
* Chrome Helper.app/Contents/MacOS/Google Chrome Helper` — so every browser row
|
|
72
|
+
* read `.app/Contents/MacOS/Google Chrome Helper --type=gpu-process`, having
|
|
73
|
+
* spent the budget on the half of the path it was supposed to remove. A plain
|
|
74
|
+
* `lastIndexOf` is wrong the other way: `node /srv/node_modules/x` would cut
|
|
75
|
+
* inside `node_modules`.
|
|
76
|
+
*
|
|
77
|
+
* So: the first occurrence that a path segment actually ends at — preceded by a
|
|
78
|
+
* separator or nothing, followed by whitespace or nothing. `node /srv/app.js
|
|
79
|
+
* --port 3000` leaves `/srv/app.js --port 3000`; `/usr/local/bin/dotnet exec
|
|
80
|
+
* --runtimeconfig …` leaves `exec --runtimeconfig …`, which is what identifies
|
|
81
|
+
* it.
|
|
82
|
+
*
|
|
83
|
+
* When the name is not in argv at all — a process that rewrote argv[0], or a
|
|
84
|
+
* `comm` that Linux truncated at 15 characters — the first whitespace token
|
|
85
|
+
* goes only if it looks like a path, because dropping the first word of
|
|
86
|
+
* something already short would take the only word there was.
|
|
87
|
+
*/
|
|
88
|
+
export function commandTail(argv, name) {
|
|
89
|
+
const s = String(argv ?? "").trim();
|
|
90
|
+
if (!s) return "";
|
|
91
|
+
const n = String(name ?? "").trim();
|
|
92
|
+
if (n) {
|
|
93
|
+
for (let at = s.indexOf(n); at >= 0; at = s.indexOf(n, at + 1)) {
|
|
94
|
+
const before = at === 0 ? "" : s[at - 1];
|
|
95
|
+
const after = s[at + n.length] ?? "";
|
|
96
|
+
if ((before === "" || before === "/" || before === "\\" || /\s/.test(before))
|
|
97
|
+
&& (after === "" || /\s/.test(after))) {
|
|
98
|
+
return s.slice(at + n.length).trim();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const first = s.split(/\s+/)[0] ?? "";
|
|
103
|
+
return first.includes("/") ? s.slice(first.length).trim() : s;
|
|
104
|
+
}
|