@botbuddy/cli 1.13.1 → 1.13.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.
- package/package.json +1 -1
- package/src/commands.mjs +34 -2
- package/src/update-check.mjs +50 -10
package/package.json
CHANGED
package/src/commands.mjs
CHANGED
|
@@ -296,6 +296,15 @@ async function cmdStart(args) {
|
|
|
296
296
|
// (BOT-1566, Codex P2).
|
|
297
297
|
const STATUS_PROBE_TIMEOUT_MS = 4000;
|
|
298
298
|
|
|
299
|
+
// The OAuth server issues OPAQUE access tokens, not JWTs: `mcp_at_` + hex
|
|
300
|
+
// (oauth_tokens.access_token default = 'mcp_at_' + two dash-stripped uuids = 64
|
|
301
|
+
// hex chars; handleToken returns that verbatim). We use this only to refuse
|
|
302
|
+
// shipping a legacy typed-password "owner token" (the pre-1.13 Keychain bug) to
|
|
303
|
+
// the server in an Authorization header — which would disclose the secret just
|
|
304
|
+
// to learn it is invalid (BOT-1566, Codex P1). Prefix + hex body, length-lenient
|
|
305
|
+
// so a future opaque-token size change can't reintroduce the false rejection.
|
|
306
|
+
const looksLikeOwnerToken = (t) => typeof t === "string" && /^mcp_at_[0-9a-f]{32,}$/.test(t);
|
|
307
|
+
|
|
299
308
|
async function logServerStatus(call, auth, log) {
|
|
300
309
|
let res;
|
|
301
310
|
try {
|
|
@@ -331,10 +340,20 @@ export async function cmdStatus({
|
|
|
331
340
|
const cfg = getCfg();
|
|
332
341
|
const owner = await resolveOwner({ getConfig: getCfg });
|
|
333
342
|
if (owner) {
|
|
334
|
-
|
|
343
|
+
// BOT-1566 (Codex P1): a pre-1.13 login could have stored a typed password
|
|
344
|
+
// where the OAuth token belongs. Such a value is NOT healthy — resolveCallAuth
|
|
345
|
+
// / authHeader still prefer this entry, so every `botbuddy call` keeps sending
|
|
346
|
+
// the bad secret until `botbuddy login` overwrites it. Never present it as
|
|
347
|
+
// authenticated, and never put it on the wire.
|
|
348
|
+
const ownerValid = looksLikeOwnerToken(owner.token);
|
|
349
|
+
if (ownerValid) {
|
|
350
|
+
log(`${green("✓")} Authenticated via OAuth ${dim("(Keychain)")}`);
|
|
351
|
+
} else {
|
|
352
|
+
log(`${red("✗")} Stored OAuth token is invalid — not a sign-in token (pre-1.13 login bug). Run ${cyan("botbuddy login")} to replace it.`);
|
|
353
|
+
}
|
|
335
354
|
if (cfg.agent_name) log(` Agent: ${cyan(cfg.agent_name)}`);
|
|
336
355
|
if (cfg.client_id) log(` Client: ${dim(cfg.client_id)}`);
|
|
337
|
-
if (owner.expiresAt) {
|
|
356
|
+
if (ownerValid && owner.expiresAt) {
|
|
338
357
|
const remaining = owner.expiresAt - now();
|
|
339
358
|
if (remaining <= 0) {
|
|
340
359
|
log(` Token: ${red("EXPIRED")} — run ${cyan("botbuddy start")}`);
|
|
@@ -345,6 +364,19 @@ export async function cmdStatus({
|
|
|
345
364
|
}
|
|
346
365
|
}
|
|
347
366
|
log(` Config: ${dim(getConfigPath())}`);
|
|
367
|
+
if (!ownerValid) {
|
|
368
|
+
// Never send the non-token secret to the server. An agent key can still be
|
|
369
|
+
// health-checked, but the broken owner credential is what `botbuddy call`
|
|
370
|
+
// will use, so report it either way rather than looking healthy.
|
|
371
|
+
const key = await resolveAgent();
|
|
372
|
+
if (key) {
|
|
373
|
+
await logServerStatus(call, { "x-agent-api-key": key }, log);
|
|
374
|
+
log(` ${red("!")} the stored OAuth token is invalid — botbuddy commands will keep using it until you run ${cyan("botbuddy login")}`);
|
|
375
|
+
} else {
|
|
376
|
+
log(` server: ${red("not checked")} — stored OAuth token is invalid; run ${cyan("botbuddy login")}`);
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
348
380
|
// BOT-1566 (Codex P2): match resolveCallAuth — an expired owner token defers
|
|
349
381
|
// to a valid profile agent key, so probe the credential real commands would
|
|
350
382
|
// actually use instead of reporting a false `server: rejected` on the dead
|
package/src/update-check.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// off to a detached, unref'd `npm install -g` and exits 0.
|
|
14
14
|
|
|
15
15
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
16
|
-
import { spawn } from "node:child_process";
|
|
16
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
17
17
|
import { homedir } from "node:os";
|
|
18
18
|
import { dirname, join } from "node:path";
|
|
19
19
|
|
|
@@ -114,14 +114,54 @@ export async function maybeWarnStale({
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
// `botbuddy update` — hand off to npm and get out of the way.
|
|
117
|
-
export function cmdUpdate({ version = VERSION, spawnImpl = spawn, log = (line) => console.log(line), platform = process.platform } = {}) {
|
|
117
|
+
export function cmdUpdate({ version = VERSION, spawnImpl = spawn, probeImpl = spawnSync, log = (line) => console.log(line), platform = process.platform, stderr = process.stderr } = {}) {
|
|
118
118
|
log(`current: ${version}`);
|
|
119
|
-
// BOT-1566 (Codex P2): on Windows npm is the `npm.cmd` shim, and
|
|
120
|
-
// spawn
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
119
|
+
// BOT-1566 (Codex P1/P2): on Windows npm is the `npm.cmd` shim, and Node
|
|
120
|
+
// cannot spawn a .cmd file shell-free (docs) — run it through a shell on
|
|
121
|
+
// win32 (args are all static constants, so no injection surface) and select
|
|
122
|
+
// the shim name; elsewhere a plain shell-free `npm` is correct.
|
|
123
|
+
const isWin = platform === "win32";
|
|
124
|
+
const npmBin = isWin ? "npm.cmd" : "npm";
|
|
125
|
+
const shellOpt = isWin ? { shell: true } : {};
|
|
126
|
+
// Resolve npm BEFORE the detached handoff. With `shell: true` the async
|
|
127
|
+
// `spawn` event only confirms the shell (cmd.exe) launched, not that npm
|
|
128
|
+
// resolved — a missing `npm.cmd` would otherwise be reported as a successful
|
|
129
|
+
// background update (Codex P2). A synchronous `npm --version` probe confirms
|
|
130
|
+
// the executable exists; `botbuddy update` is an explicit command, not a hot
|
|
131
|
+
// path, so the extra call is acceptable.
|
|
132
|
+
const probe = probeImpl(npmBin, ["--version"], { stdio: "ignore", ...shellOpt });
|
|
133
|
+
if (probe?.error || (typeof probe?.status === "number" && probe.status !== 0)) {
|
|
134
|
+
try { stderr.write(`botbuddy update: npm was not found — install Node.js/npm, then retry (looked for ${npmBin})\n`); } catch { /* ignore */ }
|
|
135
|
+
return Promise.resolve(1);
|
|
136
|
+
}
|
|
137
|
+
const child = spawnImpl(npmBin, ["install", "-g", "@botbuddy/cli@latest"], {
|
|
138
|
+
stdio: "inherit",
|
|
139
|
+
detached: true,
|
|
140
|
+
...shellOpt,
|
|
141
|
+
});
|
|
142
|
+
// Node reports a launch failure asynchronously via the `error` event, so we
|
|
143
|
+
// must NOT announce success until the child has actually spawned (Codex P2).
|
|
144
|
+
// Race spawn vs error: print + unref only on `spawn`, exit nonzero on `error`.
|
|
145
|
+
return new Promise((resolve) => {
|
|
146
|
+
if (!child || typeof child.once !== "function") {
|
|
147
|
+
log("updating in background — re-run botbuddy --version to confirm");
|
|
148
|
+
if (child && typeof child.unref === "function") child.unref();
|
|
149
|
+
resolve(0);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
let settled = false;
|
|
153
|
+
child.once("spawn", () => {
|
|
154
|
+
if (settled) return;
|
|
155
|
+
settled = true;
|
|
156
|
+
if (typeof child.unref === "function") child.unref();
|
|
157
|
+
log("updating in background — re-run botbuddy --version to confirm");
|
|
158
|
+
resolve(0);
|
|
159
|
+
});
|
|
160
|
+
child.once("error", (err) => {
|
|
161
|
+
if (settled) return;
|
|
162
|
+
settled = true;
|
|
163
|
+
try { stderr.write(`botbuddy update: failed to launch npm (${err?.message ?? err})\n`); } catch { /* ignore */ }
|
|
164
|
+
resolve(1);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
127
167
|
}
|