@ouro.bot/cli 0.1.0-alpha.555 → 0.1.0-alpha.557
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.json
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.557",
|
|
6
|
+
"changes": [
|
|
7
|
+
"`ouro vault unlock` now validates the typed unlock secret against the agent vault before replacing this machine's saved local unlock material, so a failed unlock attempt cannot poison a previously working Keychain/DPAPI/Secret Service/plaintext entry.",
|
|
8
|
+
"The Bitwarden adapter now rebuilds stale per-agent local `bw` profiles when they point at the wrong vault account/server or reject a saved unlock during unlock, reducing restart-time false locked-vault failures while keeping wrong saved secrets explicit.",
|
|
9
|
+
"`@ouro.bot/cli` and the `ouro.bot` wrapper are version-synced for the vault unlock hardening release."
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"version": "0.1.0-alpha.556",
|
|
14
|
+
"changes": [
|
|
15
|
+
"`ouro up` runtime replacement now captures the daemon boot timestamp before starting the replacement process, so a fast healthy daemon is not rejected for publishing health before the monitor begins polling."
|
|
16
|
+
]
|
|
17
|
+
},
|
|
4
18
|
{
|
|
5
19
|
"version": "0.1.0-alpha.555",
|
|
6
20
|
"changes": [
|
|
@@ -889,9 +889,10 @@ async function ensureDaemonRunning(deps, options = {}) {
|
|
|
889
889
|
startDaemonProcess: deps.startDaemonProcess,
|
|
890
890
|
checkSocketAlive: deps.checkSocketAlive,
|
|
891
891
|
onProgress: deps.reportDaemonStartupPhase,
|
|
892
|
-
|
|
892
|
+
now: deps.now,
|
|
893
|
+
waitForDaemonStartup: async ({ pid, bootStartedAtMs }) => {
|
|
893
894
|
const startupFailure = await waitForDaemonStartup(deps, {
|
|
894
|
-
bootStartedAtMs
|
|
895
|
+
bootStartedAtMs,
|
|
895
896
|
pid,
|
|
896
897
|
serviceLabel: "replacement background service",
|
|
897
898
|
readLatestDaemonEvent: readLatestDaemonStartupEvent,
|
|
@@ -1452,15 +1453,13 @@ async function executeVaultUnlock(command, deps) {
|
|
|
1452
1453
|
const progress = createHumanCommandProgress(deps, "vault unlock");
|
|
1453
1454
|
let store;
|
|
1454
1455
|
try {
|
|
1456
|
+
await runCommandProgressPhase(progress, "checking vault access", () => (0, credential_access_1.probeCredentialVaultAccess)(command.agent, unlockSecret, { homeDir: deps.homeDir }), () => "ok");
|
|
1455
1457
|
store = await runCommandProgressPhase(progress, "saving local unlock", () => (0, vault_unlock_1.storeVaultUnlockSecret)({
|
|
1456
1458
|
agentName: command.agent,
|
|
1457
1459
|
email: vault.email,
|
|
1458
1460
|
serverUrl: vault.serverUrl,
|
|
1459
1461
|
}, unlockSecret, { homeDir: deps.homeDir, store: command.store }), (saved) => saved.kind);
|
|
1460
|
-
|
|
1461
|
-
(0, credential_access_1.resetCredentialStore)();
|
|
1462
|
-
await (0, credential_access_1.getCredentialStore)(command.agent).get("__ouro_vault_probe__");
|
|
1463
|
-
}, () => "ok");
|
|
1462
|
+
(0, credential_access_1.resetCredentialStore)();
|
|
1464
1463
|
}
|
|
1465
1464
|
finally {
|
|
1466
1465
|
progress.end();
|
|
@@ -164,10 +164,11 @@ async function ensureCurrentDaemonRuntime(deps) {
|
|
|
164
164
|
}
|
|
165
165
|
deps.cleanupStaleSocket(deps.socketPath);
|
|
166
166
|
deps.onProgress?.("starting the replacement background service");
|
|
167
|
+
const bootStartedAtMs = (deps.now ?? Date.now)();
|
|
167
168
|
const started = await deps.startDaemonProcess(deps.socketPath);
|
|
168
169
|
const pid = started.pid ?? "unknown";
|
|
169
170
|
const startupCheck = deps.waitForDaemonStartup
|
|
170
|
-
? await deps.waitForDaemonStartup({ pid: started.pid ?? null })
|
|
171
|
+
? await deps.waitForDaemonStartup({ pid: started.pid ?? null, bootStartedAtMs })
|
|
171
172
|
: { ok: await verifyDaemonStarted(deps) };
|
|
172
173
|
const verified = startupCheck.ok;
|
|
173
174
|
/* v8 ignore next -- daemon liveness failure: requires real daemon crash timing @preserve */
|
|
@@ -88,7 +88,9 @@ function isBwSessionUnavailableMessage(message) {
|
|
|
88
88
|
/local bitwarden session/i.test(message));
|
|
89
89
|
}
|
|
90
90
|
function isBwInvalidUnlockSecretMessage(message) {
|
|
91
|
-
return /invalid master password/i.test(message) ||
|
|
91
|
+
return (/invalid master password/i.test(message) ||
|
|
92
|
+
/saved vault unlock secret/i.test(message) ||
|
|
93
|
+
/username or password is incorrect/i.test(message));
|
|
92
94
|
}
|
|
93
95
|
function isBwTimeoutError(err) {
|
|
94
96
|
const timeoutErr = err;
|
|
@@ -135,6 +137,13 @@ function isBwConfigLogoutRequired(err) {
|
|
|
135
137
|
function isBwAlreadyLoggedInError(err) {
|
|
136
138
|
return err.message.toLowerCase().includes("already logged in");
|
|
137
139
|
}
|
|
140
|
+
function isBwLoggedOutOrUnauthenticatedError(err) {
|
|
141
|
+
const message = err.message.toLowerCase();
|
|
142
|
+
return (message.includes("not logged in") ||
|
|
143
|
+
message.includes("not authenticated") ||
|
|
144
|
+
message.includes("unauthenticated") ||
|
|
145
|
+
message.includes("local bitwarden session because it is locked, missing, or expired"));
|
|
146
|
+
}
|
|
138
147
|
function shouldUseStructuredItemLookup(domain) {
|
|
139
148
|
return domain.includes("/");
|
|
140
149
|
}
|
|
@@ -393,6 +402,7 @@ class BitwardenCredentialStore {
|
|
|
393
402
|
masterPassword;
|
|
394
403
|
appDataDir;
|
|
395
404
|
sessionToken = null;
|
|
405
|
+
terminalLoginError = null;
|
|
396
406
|
bwBinaryPath = "bw";
|
|
397
407
|
structuredItemCache = null;
|
|
398
408
|
constructor(serverUrl, email, masterPassword, options = {}) {
|
|
@@ -416,6 +426,9 @@ class BitwardenCredentialStore {
|
|
|
416
426
|
* Retries transient failures (network/timeout) up to MAX_RETRIES with exponential backoff.
|
|
417
427
|
*/
|
|
418
428
|
async login() {
|
|
429
|
+
if (this.terminalLoginError) {
|
|
430
|
+
throw this.terminalLoginError;
|
|
431
|
+
}
|
|
419
432
|
// Ensure bw CLI is installed before any bw commands
|
|
420
433
|
this.bwBinaryPath = await (0, bw_installer_1.ensureBwCli)();
|
|
421
434
|
if (this.appDataDir) {
|
|
@@ -432,6 +445,7 @@ class BitwardenCredentialStore {
|
|
|
432
445
|
lastError = err instanceof Error ? err : new Error(String(err));
|
|
433
446
|
// Don't retry non-transient errors (auth failures, bw not installed)
|
|
434
447
|
if (!isTransientError(lastError)) {
|
|
448
|
+
this.terminalLoginError = lastError;
|
|
435
449
|
throw lastError;
|
|
436
450
|
}
|
|
437
451
|
// Don't retry after final attempt
|
|
@@ -447,22 +461,15 @@ class BitwardenCredentialStore {
|
|
|
447
461
|
await delay(backoffMs);
|
|
448
462
|
}
|
|
449
463
|
}
|
|
464
|
+
this.terminalLoginError = lastError;
|
|
450
465
|
throw lastError;
|
|
451
466
|
}
|
|
452
467
|
/** Single login attempt — called by login() retry loop. */
|
|
453
468
|
async loginAttempt() {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
status = JSON.parse(raw);
|
|
459
|
-
}
|
|
460
|
-
catch (err) {
|
|
461
|
-
// If bw CLI is not installed or a transient error, propagate it for retry
|
|
462
|
-
if (err instanceof Error && (isBwNotInstalled(err) || isTransientError(err))) {
|
|
463
|
-
throw err;
|
|
464
|
-
}
|
|
465
|
-
// CLI not configured or broken — proceed with full setup
|
|
469
|
+
let status = await this.readStatus();
|
|
470
|
+
if (this.shouldRebuildLocalProfile(status)) {
|
|
471
|
+
await this.rebuildLocalProfile("local bw profile did not match requested vault", status);
|
|
472
|
+
status = { status: "unlocked", serverUrl: this.serverUrl, userEmail: this.email };
|
|
466
473
|
}
|
|
467
474
|
// Configure server URL if needed (only works when logged out)
|
|
468
475
|
if (status.status === "unauthenticated" || !status.serverUrl) {
|
|
@@ -471,43 +478,28 @@ class BitwardenCredentialStore {
|
|
|
471
478
|
}
|
|
472
479
|
catch (error) {
|
|
473
480
|
const err = error;
|
|
474
|
-
|
|
475
|
-
if (!isBwConfigLogoutRequired(err)) {
|
|
481
|
+
if (!isBwConfigLogoutRequired(err))
|
|
476
482
|
throw err;
|
|
477
|
-
|
|
483
|
+
// "Logout required" means bw already has local auth state; keep the
|
|
484
|
+
// existing behavior and proceed to login/unlock below.
|
|
478
485
|
}
|
|
479
486
|
}
|
|
480
|
-
if (
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
else if (status.status === "unauthenticated" || !status.status) {
|
|
486
|
-
// Not logged in — full login
|
|
487
|
-
let loginOutput;
|
|
488
|
-
try {
|
|
489
|
-
loginOutput = await this.execBwWithPasswordEnv(["login", this.email, "--raw"]);
|
|
490
|
-
}
|
|
491
|
-
catch (error) {
|
|
492
|
-
const err = error;
|
|
493
|
-
if (!isBwAlreadyLoggedInError(err)) {
|
|
494
|
-
throw err;
|
|
495
|
-
}
|
|
496
|
-
loginOutput = await this.execBwWithPasswordEnv(["unlock", "--raw"]);
|
|
487
|
+
if (!this.sessionToken) {
|
|
488
|
+
if (status.status === "locked") {
|
|
489
|
+
// Already logged in, just needs unlock.
|
|
490
|
+
const unlockOutput = await this.execWithLocalProfileRebuild("unlock rejected saved local unlock material", status, () => this.execBwWithPasswordEnv(["unlock", "--raw"]));
|
|
491
|
+
this.sessionToken = unlockOutput.trim();
|
|
497
492
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
this.sessionToken =
|
|
493
|
+
else if (status.status === "unauthenticated" || !status.status) {
|
|
494
|
+
// Not logged in -- full login.
|
|
495
|
+
this.sessionToken = this.sessionTokenFromLoginOutput(await this.loginWithPassword());
|
|
501
496
|
}
|
|
502
|
-
|
|
503
|
-
|
|
497
|
+
else {
|
|
498
|
+
// Status is "unlocked" -- already good, just need the session token.
|
|
499
|
+
const unlockOutput = await this.execWithLocalProfileRebuild("unlocked bw profile rejected saved local unlock material", status, () => this.execBwWithPasswordEnv(["unlock", "--raw"]));
|
|
500
|
+
this.sessionToken = unlockOutput.trim();
|
|
504
501
|
}
|
|
505
502
|
}
|
|
506
|
-
else {
|
|
507
|
-
// Status is "unlocked" — already good, just need the session token
|
|
508
|
-
const unlockOutput = await this.execBwWithPasswordEnv(["unlock", "--raw"]);
|
|
509
|
-
this.sessionToken = unlockOutput.trim();
|
|
510
|
-
}
|
|
511
503
|
if (this.shouldSyncVaultAfterSession(status)) {
|
|
512
504
|
/* v8 ignore next -- defensive: loginAttempt always sets sessionToken before sync @preserve */
|
|
513
505
|
await this.execBw(["sync"], this.sessionToken ?? undefined);
|
|
@@ -521,6 +513,128 @@ class BitwardenCredentialStore {
|
|
|
521
513
|
meta: { email: this.email, serverUrl: this.serverUrl, freshnessWindowMs: BW_SYNC_FRESH_MS },
|
|
522
514
|
});
|
|
523
515
|
}
|
|
516
|
+
this.terminalLoginError = null;
|
|
517
|
+
}
|
|
518
|
+
async readStatus() {
|
|
519
|
+
try {
|
|
520
|
+
const raw = await this.execBw(["status"]);
|
|
521
|
+
const parsed = JSON.parse(raw);
|
|
522
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
523
|
+
return {};
|
|
524
|
+
const record = parsed;
|
|
525
|
+
return {
|
|
526
|
+
...(typeof record.status === "string" ? { status: record.status } : {}),
|
|
527
|
+
...(typeof record.serverUrl === "string" ? { serverUrl: record.serverUrl } : {}),
|
|
528
|
+
...(typeof record.userEmail === "string" ? { userEmail: record.userEmail } : {}),
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
catch (err) {
|
|
532
|
+
// If bw CLI is not installed or a transient error, propagate it for retry.
|
|
533
|
+
if (err instanceof Error && (isBwNotInstalled(err) || isTransientError(err))) {
|
|
534
|
+
throw err;
|
|
535
|
+
}
|
|
536
|
+
// CLI not configured or broken -- proceed with full setup.
|
|
537
|
+
return {};
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
normalizedServerUrl(value) {
|
|
541
|
+
return value.trim().replace(/\/+$/, "").toLowerCase();
|
|
542
|
+
}
|
|
543
|
+
shouldRebuildLocalProfile(status) {
|
|
544
|
+
if (status.status !== "locked" && status.status !== "unlocked")
|
|
545
|
+
return false;
|
|
546
|
+
if (status.serverUrl && this.normalizedServerUrl(status.serverUrl) !== this.normalizedServerUrl(this.serverUrl))
|
|
547
|
+
return true;
|
|
548
|
+
if (status.userEmail && status.userEmail.trim().toLowerCase() !== this.email.trim().toLowerCase())
|
|
549
|
+
return true;
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
sessionTokenFromLoginOutput(loginOutput) {
|
|
553
|
+
try {
|
|
554
|
+
const parsed = JSON.parse(loginOutput);
|
|
555
|
+
return typeof parsed.access_token === "string" && parsed.access_token.trim()
|
|
556
|
+
? parsed.access_token.trim()
|
|
557
|
+
: loginOutput.trim();
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
return loginOutput.trim();
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
async loginWithPassword() {
|
|
564
|
+
try {
|
|
565
|
+
return await this.execBwWithPasswordEnv(["login", this.email, "--raw"]);
|
|
566
|
+
}
|
|
567
|
+
catch (error) {
|
|
568
|
+
const err = error;
|
|
569
|
+
if (!isBwAlreadyLoggedInError(err))
|
|
570
|
+
throw err;
|
|
571
|
+
return this.execBwWithPasswordEnv(["unlock", "--raw"]);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
forgetLocalSyncMarker() {
|
|
575
|
+
if (!this.appDataDir)
|
|
576
|
+
return;
|
|
577
|
+
try {
|
|
578
|
+
fs.rmSync(path.join(this.appDataDir, BW_SYNC_MARKER_FILENAME), { force: true });
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
// A stale sync marker is only a cache hint; failure to remove it should not block auth repair.
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
async logoutLocalProfile(reason, status, cause) {
|
|
585
|
+
this.sessionToken = null;
|
|
586
|
+
this.structuredItemCache = null;
|
|
587
|
+
this.forgetLocalSyncMarker();
|
|
588
|
+
(0, runtime_1.emitNervesEvent)({
|
|
589
|
+
level: "warn",
|
|
590
|
+
event: "repertoire.bw_local_profile_rebuild",
|
|
591
|
+
component: "repertoire",
|
|
592
|
+
message: "rebuilding local bw profile for agent vault",
|
|
593
|
+
meta: {
|
|
594
|
+
email: this.email,
|
|
595
|
+
serverUrl: this.serverUrl,
|
|
596
|
+
reason,
|
|
597
|
+
/* v8 ignore next -- defensive: every profile rebuild path starts from a locked/unlocked bw status @preserve */
|
|
598
|
+
previousStatus: status.status ?? "unknown",
|
|
599
|
+
previousServerUrl: status.serverUrl ?? null,
|
|
600
|
+
previousUserEmail: status.userEmail ?? null,
|
|
601
|
+
/* v8 ignore next -- defensive: internal rebuild callers pass Error or undefined causes @preserve */
|
|
602
|
+
error: cause instanceof Error ? cause.message : cause ? String(cause) : undefined,
|
|
603
|
+
},
|
|
604
|
+
});
|
|
605
|
+
try {
|
|
606
|
+
await this.execBw(["logout"]);
|
|
607
|
+
}
|
|
608
|
+
catch (error) {
|
|
609
|
+
const err = error;
|
|
610
|
+
if (isBwLoggedOutOrUnauthenticatedError(err))
|
|
611
|
+
return;
|
|
612
|
+
(0, runtime_1.emitNervesEvent)({
|
|
613
|
+
level: "warn",
|
|
614
|
+
event: "repertoire.bw_local_profile_logout_failed",
|
|
615
|
+
component: "repertoire",
|
|
616
|
+
message: "failed to logout local bw profile before rebuild",
|
|
617
|
+
meta: { email: this.email, serverUrl: this.serverUrl, error: err.message },
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
async rebuildLocalProfile(reason, status, cause) {
|
|
622
|
+
await this.logoutLocalProfile(reason, status, cause);
|
|
623
|
+
await this.execBw(["config", "server", this.serverUrl]);
|
|
624
|
+
this.sessionToken = this.sessionTokenFromLoginOutput(await this.loginWithPassword());
|
|
625
|
+
}
|
|
626
|
+
async execWithLocalProfileRebuild(reason, status, operation) {
|
|
627
|
+
try {
|
|
628
|
+
return await operation();
|
|
629
|
+
}
|
|
630
|
+
catch (error) {
|
|
631
|
+
const err = error;
|
|
632
|
+
if (!isBwInvalidUnlockSecretMessage(err.message))
|
|
633
|
+
throw err;
|
|
634
|
+
await this.rebuildLocalProfile(reason, status, err);
|
|
635
|
+
/* v8 ignore next -- defensive: rebuildLocalProfile always stores a string session token on success @preserve */
|
|
636
|
+
return this.sessionToken ?? "";
|
|
637
|
+
}
|
|
524
638
|
}
|
|
525
639
|
async ensureSession() {
|
|
526
640
|
if (!this.sessionToken) {
|
|
@@ -44,6 +44,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
44
44
|
})();
|
|
45
45
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
46
|
exports.getCredentialStore = getCredentialStore;
|
|
47
|
+
exports.probeCredentialVaultAccess = probeCredentialVaultAccess;
|
|
47
48
|
exports.resetCredentialStore = resetCredentialStore;
|
|
48
49
|
const crypto = __importStar(require("node:crypto"));
|
|
49
50
|
const fs = __importStar(require("node:fs"));
|
|
@@ -64,13 +65,13 @@ function loadVaultSectionForAgent(agentName) {
|
|
|
64
65
|
return { configPath };
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
|
-
function bitwardenAppDataDir(agentName, vaultConfig) {
|
|
68
|
+
function bitwardenAppDataDir(agentName, vaultConfig, homeDir = os.homedir()) {
|
|
68
69
|
const digest = crypto
|
|
69
70
|
.createHash("sha256")
|
|
70
71
|
.update(`${agentName}:${vaultConfig.serverUrl}:${vaultConfig.email}`)
|
|
71
72
|
.digest("hex")
|
|
72
73
|
.slice(0, 24);
|
|
73
|
-
return path.join(
|
|
74
|
+
return path.join(homeDir, ".ouro-cli", "bitwarden", digest);
|
|
74
75
|
}
|
|
75
76
|
function getCredentialStore(agentNameInput) {
|
|
76
77
|
const agentName = agentNameInput ?? identity.getAgentName();
|
|
@@ -106,6 +107,16 @@ function getCredentialStore(agentNameInput) {
|
|
|
106
107
|
});
|
|
107
108
|
return store;
|
|
108
109
|
}
|
|
110
|
+
async function probeCredentialVaultAccess(agentNameInput, unlockSecret, options = {}) {
|
|
111
|
+
const agentName = agentNameInput;
|
|
112
|
+
const { configPath, vault } = loadVaultSectionForAgent(agentName);
|
|
113
|
+
if (!vault || typeof vault.email !== "string" || vault.email.trim().length === 0) {
|
|
114
|
+
throw new Error((0, vault_unlock_1.credentialVaultNotConfiguredError)(agentName, configPath));
|
|
115
|
+
}
|
|
116
|
+
const vaultConfig = identity.resolveVaultConfig(agentName, vault);
|
|
117
|
+
const store = new bitwarden_store_1.BitwardenCredentialStore(vaultConfig.serverUrl, vaultConfig.email, unlockSecret, { appDataDir: bitwardenAppDataDir(agentName, vaultConfig, options.homeDir) });
|
|
118
|
+
await store.get("__ouro_vault_probe__");
|
|
119
|
+
}
|
|
109
120
|
function resetCredentialStore() {
|
|
110
121
|
stores = new Map();
|
|
111
122
|
}
|