beehaven 0.8.16
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/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/__tests__/_cli-switch.js +105 -0
- package/dist/__tests__/agent-wrap-probe.js +136 -0
- package/dist/agent-attribution.js +192 -0
- package/dist/agent-identity-daemon.js +135 -0
- package/dist/agent-onboard.js +156 -0
- package/dist/agent-scope.js +79 -0
- package/dist/agent-sessions.js +370 -0
- package/dist/agent-state.js +482 -0
- package/dist/app-manifest.js +221 -0
- package/dist/app-signer.js +54 -0
- package/dist/app-signing.js +127 -0
- package/dist/audio-play.js +98 -0
- package/dist/avatar-protocol.js +25 -0
- package/dist/avatar-route.js +79 -0
- package/dist/bee/cloud-proxy.js +130 -0
- package/dist/blocked-actions.js +120 -0
- package/dist/build-channel.js +101 -0
- package/dist/buildings/daemon-workspace.js +475 -0
- package/dist/claude-bridge.js +310 -0
- package/dist/claude-credential-store.js +633 -0
- package/dist/clearly-sync.js +518 -0
- package/dist/clearly.js +616 -0
- package/dist/cli-state.js +334 -0
- package/dist/cli.js +6973 -0
- package/dist/commands/agent-skills.js +302 -0
- package/dist/commands/agent.js +1310 -0
- package/dist/commands/app.js +1123 -0
- package/dist/commands/auth.js +171 -0
- package/dist/commands/building-shell.js +392 -0
- package/dist/commands/canvas.js +932 -0
- package/dist/commands/claude.js +218 -0
- package/dist/commands/compliance.js +387 -0
- package/dist/commands/compose.js +507 -0
- package/dist/commands/composition.js +95 -0
- package/dist/commands/connect-session.js +409 -0
- package/dist/commands/conversations.js +160 -0
- package/dist/commands/costs.js +443 -0
- package/dist/commands/d1.js +253 -0
- package/dist/commands/daemon-cmd.js +162 -0
- package/dist/commands/deploy.js +164 -0
- package/dist/commands/design.js +617 -0
- package/dist/commands/dev-generate.js +125 -0
- package/dist/commands/dev.js +109 -0
- package/dist/commands/devices.js +135 -0
- package/dist/commands/explain-router.js +24 -0
- package/dist/commands/explain.js +882 -0
- package/dist/commands/explore.js +264 -0
- package/dist/commands/help.js +2418 -0
- package/dist/commands/init.js +408 -0
- package/dist/commands/install.js +129 -0
- package/dist/commands/library.js +229 -0
- package/dist/commands/marketplace.js +138 -0
- package/dist/commands/mcp-install.js +476 -0
- package/dist/commands/mention.js +257 -0
- package/dist/commands/omni.js +529 -0
- package/dist/commands/secrets.js +323 -0
- package/dist/commands/self-update.js +206 -0
- package/dist/commands/sheet.js +264 -0
- package/dist/commands/shell-output.js +309 -0
- package/dist/commands/shell.js +1048 -0
- package/dist/commands/shop.js +368 -0
- package/dist/commands/skills.js +311 -0
- package/dist/commands/slides.js +418 -0
- package/dist/commands/swarm.js +341 -0
- package/dist/commands/ticket.js +1301 -0
- package/dist/commands/tool.js +312 -0
- package/dist/commands/trash.js +106 -0
- package/dist/commands/treehouse.js +120 -0
- package/dist/commands/user.js +240 -0
- package/dist/commands/voice-tts.js +319 -0
- package/dist/commands/voice.js +166 -0
- package/dist/commands/watch-git.js +401 -0
- package/dist/commands/webhook.js +159 -0
- package/dist/commands/workspace.js +3354 -0
- package/dist/composition-repos.js +91 -0
- package/dist/daemon-avatar.js +130 -0
- package/dist/daemon-claude-code-auto-install.js +52 -0
- package/dist/daemon-client.js +363 -0
- package/dist/daemon-codex.js +349 -0
- package/dist/daemon-entry.js +101 -0
- package/dist/daemon-fetch-allowlist.js +194 -0
- package/dist/daemon-gemini.js +224 -0
- package/dist/daemon-mcp-clearly.js +353 -0
- package/dist/daemon-mcp-sandbox.js +127 -0
- package/dist/daemon-omni-company.js +338 -0
- package/dist/daemon-omni-hooks.js +375 -0
- package/dist/daemon-omni-identity.js +229 -0
- package/dist/daemon-omni-pipeline.js +251 -0
- package/dist/daemon-omni-policies.js +346 -0
- package/dist/daemon-omni-principal.js +580 -0
- package/dist/daemon-omni-tool-gate.js +116 -0
- package/dist/daemon-omni.js +1442 -0
- package/dist/daemon-path-transform.js +120 -0
- package/dist/daemon-write-sandbox.js +191 -0
- package/dist/daemon.js +4471 -0
- package/dist/dashboard.js +846 -0
- package/dist/dep-installer.js +286 -0
- package/dist/dev-mode.js +67 -0
- package/dist/doc-indexer.js +169 -0
- package/dist/e2e-crypto.js +94 -0
- package/dist/feature-flags.js +154 -0
- package/dist/file-browser.js +88 -0
- package/dist/first-party-hives.js +211 -0
- package/dist/folder-sync.js +226 -0
- package/dist/health.js +56 -0
- package/dist/ipc-auth.js +299 -0
- package/dist/ipc-client.js +495 -0
- package/dist/ipc-server.js +684 -0
- package/dist/keychain.js +224 -0
- package/dist/lib/admin-client.js +73 -0
- package/dist/lib/mobile-build-parser.js +156 -0
- package/dist/local-app-manager.js +660 -0
- package/dist/log-redaction.js +89 -0
- package/dist/logger.js +108 -0
- package/dist/manifest.js +143 -0
- package/dist/marketplace.js +793 -0
- package/dist/min-version-copy.js +51 -0
- package/dist/min-version.js +214 -0
- package/dist/public-surface.js +23 -0
- package/dist/relay-presets.js +89 -0
- package/dist/relay.js +2723 -0
- package/dist/repl.js +619 -0
- package/dist/setup-relay.js +363 -0
- package/dist/setup-service.js +536 -0
- package/dist/shared/bee-memory/index.js +6 -0
- package/dist/shared/bee-memory/types.js +682 -0
- package/dist/shared/claude-code/daemon-types.js +112 -0
- package/dist/shared/claude-code/index.js +2 -0
- package/dist/shared/claude-code/types.js +1 -0
- package/dist/shared/status/schema.js +133 -0
- package/dist/shell-session.js +198 -0
- package/dist/speak-lock.js +97 -0
- package/dist/spinner.js +57 -0
- package/dist/telemetry.js +166 -0
- package/dist/terminal/block-parser.js +207 -0
- package/dist/terminal/shell-integration.js +108 -0
- package/dist/transcript-tailer.js +215 -0
- package/dist/tts-qwen3.js +547 -0
- package/dist/types.js +4 -0
- package/dist/utm.js +38 -0
- package/dist/verb-surface.js +331 -0
- package/dist/version-utils.js +85 -0
- package/dist/watcher.js +207 -0
- package/package.json +88 -0
- package/python/qwen3_tts.py +230 -0
- package/scripts/stamp-build-channel.mjs +115 -0
- package/scripts/sync-installed.mjs +159 -0
- package/scripts/sync-skills.mjs +112 -0
- package/skills/README.md +24 -0
- package/skills/agent-doodle-space/SKILL.md +118 -0
- package/skills/agent-home/SKILL.md +98 -0
- package/skills/beehaven-cli/SKILL.md +144 -0
- package/skills/claude-shopper/README.md +90 -0
- package/skills/claude-shopper/SKILL.md +151 -0
- package/skills/clearly-agent/SKILL.md +183 -0
- package/skills/clearly-canvas/SKILL.md +222 -0
- package/skills/clearly-canvas/authoring.md +160 -0
- package/skills/clearly-canvas/communications.md +32 -0
- package/skills/clearly-canvas/composition-patterns.md +84 -0
- package/skills/clearly-canvas/example.mjs +78 -0
- package/skills/clearly-canvas/project-plans.md +156 -0
- package/skills/clearly-canvas/review.mjs +68 -0
- package/skills/clearly-workspace/SKILL.md +79 -0
- package/skills/clearly-workspace/survey.mjs +101 -0
- package/skills/codebase-map/SKILL.md +142 -0
- package/skills/design-craft/SKILL.md +300 -0
- package/skills/index.json +79 -0
- package/skills/layout-systems/SKILL.md +184 -0
- package/skills/pair-on-canvas/SKILL.md +92 -0
package/dist/health.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Beehaven Daemon — Health Monitor
|
|
3
|
+
// Tracks uptime, connectivity, and activity. Exposes /health on the watcher socket.
|
|
4
|
+
// ============================================================================
|
|
5
|
+
import { logger } from './logger.js';
|
|
6
|
+
export class HealthMonitor {
|
|
7
|
+
getConnected;
|
|
8
|
+
getPendingCount;
|
|
9
|
+
version;
|
|
10
|
+
startedAt = Date.now();
|
|
11
|
+
lastEventAt = null;
|
|
12
|
+
watchdogTimer = null;
|
|
13
|
+
watchdogLastPing = Date.now();
|
|
14
|
+
constructor(getConnected, getPendingCount, version) {
|
|
15
|
+
this.getConnected = getConnected;
|
|
16
|
+
this.getPendingCount = getPendingCount;
|
|
17
|
+
this.version = version;
|
|
18
|
+
}
|
|
19
|
+
recordEvent() {
|
|
20
|
+
this.lastEventAt = Date.now();
|
|
21
|
+
this.watchdogLastPing = Date.now();
|
|
22
|
+
}
|
|
23
|
+
getStatus() {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
const connected = this.getConnected();
|
|
26
|
+
const lastEventAge = this.lastEventAt ? Math.round((now - this.lastEventAt) / 1000) : null;
|
|
27
|
+
let status = 'healthy';
|
|
28
|
+
if (!connected)
|
|
29
|
+
status = 'degraded';
|
|
30
|
+
if (!connected && lastEventAge !== null && lastEventAge > 300)
|
|
31
|
+
status = 'unhealthy';
|
|
32
|
+
return {
|
|
33
|
+
status,
|
|
34
|
+
uptime: Math.round((now - this.startedAt) / 1000),
|
|
35
|
+
connected,
|
|
36
|
+
lastEventAge,
|
|
37
|
+
pendingRequests: this.getPendingCount(),
|
|
38
|
+
version: this.version,
|
|
39
|
+
pid: process.pid,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Start watchdog — logs warning if no activity for 60s */
|
|
43
|
+
startWatchdog() {
|
|
44
|
+
this.watchdogTimer = setInterval(() => {
|
|
45
|
+
const elapsed = Date.now() - this.watchdogLastPing;
|
|
46
|
+
if (elapsed > 60000) {
|
|
47
|
+
logger.warn('health', `Watchdog: no activity for ${Math.round(elapsed / 1000)}s`);
|
|
48
|
+
}
|
|
49
|
+
this.watchdogLastPing = Date.now(); // reset so we don't spam
|
|
50
|
+
}, 30000);
|
|
51
|
+
}
|
|
52
|
+
stop() {
|
|
53
|
+
if (this.watchdogTimer)
|
|
54
|
+
clearInterval(this.watchdogTimer);
|
|
55
|
+
}
|
|
56
|
+
}
|
package/dist/ipc-auth.js
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPC auth — scope-aware shared secrets between daemon and local clients.
|
|
3
|
+
*
|
|
4
|
+
* Threat model: any process running as the same user can connect to
|
|
5
|
+
* /tmp/beehaven-ipc.sock (it's chmod 0600, which only restricts other users).
|
|
6
|
+
* Without per-scope auth, any same-user process can invoke privileged
|
|
7
|
+
* verbs — extract the Firebase ID token via `get-token`, run arbitrary
|
|
8
|
+
* SQL, install marketplace packages with code execution.
|
|
9
|
+
*
|
|
10
|
+
* Defense layers:
|
|
11
|
+
* 1. Socket file mode 0600 + parent dir 0700 (kernel-enforced same-user).
|
|
12
|
+
* 2. Per-launch token rotation (fresh tokens every daemon start).
|
|
13
|
+
* 3. Constant-time compare (timingSafeEqual).
|
|
14
|
+
* 4. *Two-token scope split* — see "Scope model" below.
|
|
15
|
+
*
|
|
16
|
+
* ── Scope model ──────────────────────────────────────────────────────
|
|
17
|
+
* Two distinct tokens, two distinct files:
|
|
18
|
+
*
|
|
19
|
+
* ~/.beehaven/ipc-auth → consumer scope (always exists)
|
|
20
|
+
* ~/.beehaven/ipc-auth-dev → developer scope (only if HIVE_DEV_MODE=1
|
|
21
|
+
* or the Mac app's Settings → Developer
|
|
22
|
+
* Mode toggle is on)
|
|
23
|
+
*
|
|
24
|
+
* Mwah CLI reads the consumer token only. Beehaven CLI reads the
|
|
25
|
+
* developer token (falling back to consumer when dev mode is off, in
|
|
26
|
+
* which case privileged verbs fail closed).
|
|
27
|
+
*
|
|
28
|
+
* The daemon stores both (when minted) and tags each authenticated
|
|
29
|
+
* client with its scope on the auth handshake. The IPC server gates
|
|
30
|
+
* verbs by scope:
|
|
31
|
+
*
|
|
32
|
+
* consumer → ping, auth, app-rpc (local buildings), rpc to home,
|
|
33
|
+
* session:announce, subscribe, send
|
|
34
|
+
* developer → everything consumer plus get-token, daemon-fetch,
|
|
35
|
+
* shell, sql, marketplace:install, admin:* RPC actions
|
|
36
|
+
*
|
|
37
|
+
* Even if a malicious script in consumer mode reads ~/.beehaven/ipc-auth
|
|
38
|
+
* and authenticates, it cannot exfiltrate the Firebase ID token, run
|
|
39
|
+
* SQL, install packages, or call admin verbs. The blast radius is
|
|
40
|
+
* scoped to what mwah itself does.
|
|
41
|
+
*
|
|
42
|
+
* ── Backward compat ──────────────────────────────────────────────────
|
|
43
|
+
* The legacy single-token shape (one file, full access) is gone. Old
|
|
44
|
+
* CLIs that pre-date this change get *consumer* scope automatically —
|
|
45
|
+
* they don't request a scope on auth, and the daemon defaults missing
|
|
46
|
+
* scopes to consumer. Any script that relied on `beehaven rpc get-token`
|
|
47
|
+
* working in a non-dev daemon will now get UNAUTHORIZED — that's the
|
|
48
|
+
* intended security improvement.
|
|
49
|
+
*/
|
|
50
|
+
import { randomBytes, timingSafeEqual } from 'crypto';
|
|
51
|
+
import { readFileSync, writeFileSync, existsSync, chmodSync, mkdirSync, unlinkSync } from 'fs';
|
|
52
|
+
import { join } from 'path';
|
|
53
|
+
import { homedir } from 'os';
|
|
54
|
+
import { keychain } from './keychain.js';
|
|
55
|
+
const AUTH_DIR = join(homedir(), '.beehaven');
|
|
56
|
+
const AUTH_FILE_CONSUMER = join(AUTH_DIR, 'ipc-auth');
|
|
57
|
+
const AUTH_FILE_DEVELOPER = join(AUTH_DIR, 'ipc-auth-dev');
|
|
58
|
+
const KEYCHAIN_ACCOUNT_CONSUMER = 'ipc-auth';
|
|
59
|
+
const KEYCHAIN_ACCOUNT_DEVELOPER = 'ipc-auth-dev';
|
|
60
|
+
const FILE_FOR_SCOPE = {
|
|
61
|
+
consumer: AUTH_FILE_CONSUMER,
|
|
62
|
+
developer: AUTH_FILE_DEVELOPER,
|
|
63
|
+
};
|
|
64
|
+
const KEYCHAIN_ACCOUNT_FOR_SCOPE = {
|
|
65
|
+
consumer: KEYCHAIN_ACCOUNT_CONSUMER,
|
|
66
|
+
developer: KEYCHAIN_ACCOUNT_DEVELOPER,
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Mint fresh IPC auth tokens on daemon startup.
|
|
70
|
+
*
|
|
71
|
+
* Always mints a consumer token. Mints a developer token only when
|
|
72
|
+
* `developerMode` is true — i.e. the user explicitly opted into dev
|
|
73
|
+
* mode (HIVE_DEV_MODE=1 env var, or the Mac app's tray toggle).
|
|
74
|
+
* Without dev mode, no developer token exists on disk and beehaven CLI
|
|
75
|
+
* commands that need it fail with UNAUTHORIZED. That is the intended
|
|
76
|
+
* security default.
|
|
77
|
+
*/
|
|
78
|
+
export function mintIpcAuthTokens(opts) {
|
|
79
|
+
try {
|
|
80
|
+
mkdirSync(AUTH_DIR, { recursive: true, mode: 0o700 });
|
|
81
|
+
}
|
|
82
|
+
catch { }
|
|
83
|
+
const consumer = writeScopedToken('consumer');
|
|
84
|
+
if (opts.developerMode) {
|
|
85
|
+
const developer = writeScopedToken('developer');
|
|
86
|
+
return { consumer, developer };
|
|
87
|
+
}
|
|
88
|
+
// Dev mode is off — actively *clear* any stale developer token left
|
|
89
|
+
// from a previous launch. Otherwise a user who toggled dev mode off
|
|
90
|
+
// would still have a usable developer token on disk.
|
|
91
|
+
clearScopedToken('developer');
|
|
92
|
+
return { consumer, developer: null };
|
|
93
|
+
}
|
|
94
|
+
function writeScopedToken(scope) {
|
|
95
|
+
const token = randomBytes(32).toString('hex');
|
|
96
|
+
const file = FILE_FOR_SCOPE[scope];
|
|
97
|
+
// Atomic-ish write: unlink first to drop any stale perms, then create
|
|
98
|
+
// with mode 0600 from the start. The chmod is belt + braces because
|
|
99
|
+
// mode is advisory in writeFileSync on some platforms.
|
|
100
|
+
try {
|
|
101
|
+
if (existsSync(file))
|
|
102
|
+
unlinkSync(file);
|
|
103
|
+
}
|
|
104
|
+
catch { }
|
|
105
|
+
writeFileSync(file, token, { mode: 0o600 });
|
|
106
|
+
try {
|
|
107
|
+
chmodSync(file, 0o600);
|
|
108
|
+
}
|
|
109
|
+
catch { }
|
|
110
|
+
/**
|
|
111
|
+
* ⚠⚠ THE IPC TOKEN IS NOT MIRRORED TO THE KEYCHAIN, AND THAT IS THE FIX FOR A MODAL DIALOG.
|
|
112
|
+
*
|
|
113
|
+
* The daemon is spawned DETACHED, so it has no Aqua security session — and `security
|
|
114
|
+
* add-generic-password` in that context cannot resolve a default keychain. macOS answers with a
|
|
115
|
+
* BLOCKING system window:
|
|
116
|
+
*
|
|
117
|
+
* A keychain cannot be found to store "ipc auth"
|
|
118
|
+
*
|
|
119
|
+
* Reported from a real machine, and dismissing it opened TWO MORE — because the app spawns the
|
|
120
|
+
* daemon three times per launch (boot, then `daemon.install()`'s deliberate restart after
|
|
121
|
+
* sign-in). Three mints, three dialogs. The machine's keychain is fine; the SESSION is not.
|
|
122
|
+
*
|
|
123
|
+
* ⚠ AND IT BLOCKS THE MINT. `spawnSync` waits on that window, so the token is not written while
|
|
124
|
+
* it is open — which is how a daemon ends up alive, owning the socket, with no token. Measured
|
|
125
|
+
* separately at 153 SECONDS on one keychain call.
|
|
126
|
+
*
|
|
127
|
+
* ⚠ THE MIRROR BOUGHT ALMOST NOTHING. This token is EPHEMERAL — minted per launch, scrubbed on
|
|
128
|
+
* shutdown, and stored in a 0600 file that is the primary source. `readScopedToken`'s keychain
|
|
129
|
+
* fallback only matters when the file is absent, and by design that means no daemon is running,
|
|
130
|
+
* so there is nothing for a stale keychain copy to authenticate against.
|
|
131
|
+
*
|
|
132
|
+
* ⚠ THIS IS NOT A CLAIM THAT THE KEYCHAIN IS UNSAFE. Long-lived secrets (the relay refresh
|
|
133
|
+
* token, Claude credentials) still belong there. A per-launch socket token does not.
|
|
134
|
+
*/
|
|
135
|
+
void keychain; // still imported for `clearScopedToken`, which reaps any legacy mirror
|
|
136
|
+
return token;
|
|
137
|
+
}
|
|
138
|
+
function clearScopedToken(scope, expected) {
|
|
139
|
+
/**
|
|
140
|
+
* ⚠ A MISMATCH MEANS A NEWER DAEMON OWNS IT — leave it alone. See `clearIpcAuthToken`.
|
|
141
|
+
* An unreadable file is treated as ours: failing to scrub is the weaker outcome of the two.
|
|
142
|
+
*/
|
|
143
|
+
if (expected) {
|
|
144
|
+
let current = '';
|
|
145
|
+
try {
|
|
146
|
+
current = readFileSync(FILE_FOR_SCOPE[scope], 'utf8').trim();
|
|
147
|
+
}
|
|
148
|
+
catch { /* treat as ours */ }
|
|
149
|
+
if (current && current !== expected)
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
if (existsSync(FILE_FOR_SCOPE[scope]))
|
|
154
|
+
unlinkSync(FILE_FOR_SCOPE[scope]);
|
|
155
|
+
}
|
|
156
|
+
catch { }
|
|
157
|
+
try {
|
|
158
|
+
keychain.delete(KEYCHAIN_ACCOUNT_FOR_SCOPE[scope]);
|
|
159
|
+
}
|
|
160
|
+
catch { }
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Read the consumer IPC token. Used by mwah and any other consumer-
|
|
164
|
+
* scope client.
|
|
165
|
+
*/
|
|
166
|
+
export function readIpcAuthToken() {
|
|
167
|
+
return readScopedToken('consumer');
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Read the scoped IPC token. Falls back to keychain if file is missing
|
|
171
|
+
* (e.g. /tmp was cleared but daemon still running).
|
|
172
|
+
*/
|
|
173
|
+
export function readScopedToken(scope) {
|
|
174
|
+
const file = FILE_FOR_SCOPE[scope];
|
|
175
|
+
try {
|
|
176
|
+
if (existsSync(file)) {
|
|
177
|
+
const token = readFileSync(file, 'utf-8').trim();
|
|
178
|
+
if (token)
|
|
179
|
+
return token;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch { }
|
|
183
|
+
try {
|
|
184
|
+
return keychain.get(KEYCHAIN_ACCOUNT_FOR_SCOPE[scope]);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Remove all auth tokens on daemon shutdown so leftover file contents
|
|
192
|
+
* can't authenticate a connection to a newly-launched daemon. (The
|
|
193
|
+
* new daemon mints fresh tokens anyway — belt + suspenders.)
|
|
194
|
+
*/
|
|
195
|
+
export function clearIpcAuthToken(mine) {
|
|
196
|
+
/**
|
|
197
|
+
* ⚠⚠ ONLY SCRUB WHAT THIS DAEMON MINTED — an unconditional clear DELETES A LIVE DAEMON'S TOKEN.
|
|
198
|
+
*
|
|
199
|
+
* Shutdown used to clear both scopes outright. That is correct for the last daemon standing and
|
|
200
|
+
* WRONG during a restart, which is the common case: `daemon.install()` deliberately restarts
|
|
201
|
+
* after sign-in, so two daemons overlap. The order that bites is
|
|
202
|
+
*
|
|
203
|
+
* A mints T1 -> B starts and mints T2 (overwriting the file) -> A finishes shutting
|
|
204
|
+
* down and deletes T2
|
|
205
|
+
*
|
|
206
|
+
* leaving B alive, owning the socket, with NO token on disk. Every later call then answers "the
|
|
207
|
+
* daemon is still starting — it has not minted an IPC token yet" FOREVER, and the tray sits on
|
|
208
|
+
* "Checking…" because `workspace:list` can never succeed. Reproduced on this machine: one
|
|
209
|
+
* daemon, pid file matching, socket owned, `~/.beehaven/ipc-auth` absent.
|
|
210
|
+
*
|
|
211
|
+
* ⚠ COMPARING THE VALUE IS THE FIX, not skipping the clear. The security reason for scrubbing is
|
|
212
|
+
* real (it shortens the window in which a captured token pairs with a live socket); what was
|
|
213
|
+
* missing is that a token this daemon did not write is somebody else's.
|
|
214
|
+
*
|
|
215
|
+
* ⚠ NO ARGUMENT MEANS "CLEAR REGARDLESS" and that is deliberate — an explicit `beehaven stop`
|
|
216
|
+
* wants everything gone, and a caller that cannot say what it minted must not silently leave a
|
|
217
|
+
* token behind.
|
|
218
|
+
*/
|
|
219
|
+
clearScopedToken('consumer', mine?.consumer);
|
|
220
|
+
clearScopedToken('developer', mine?.developer);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Constant-time comparison. Defensive checks: rejects empty/short
|
|
224
|
+
* tokens even if the caller misconfigured the daemon to accept one.
|
|
225
|
+
* Real tokens are 64-char hex.
|
|
226
|
+
*/
|
|
227
|
+
export function verifyIpcAuthToken(presented, expected) {
|
|
228
|
+
if (typeof presented !== 'string' || typeof expected !== 'string')
|
|
229
|
+
return false;
|
|
230
|
+
if (presented.length < 32 || expected.length < 32)
|
|
231
|
+
return false;
|
|
232
|
+
if (presented.length !== expected.length)
|
|
233
|
+
return false;
|
|
234
|
+
try {
|
|
235
|
+
return timingSafeEqual(Buffer.from(presented, 'utf-8'), Buffer.from(expected, 'utf-8'));
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Match a presented token against the daemon's known consumer +
|
|
243
|
+
* developer tokens, returning the scope it authenticates as (or null
|
|
244
|
+
* if neither matches). Used by ipc-server.ts on the auth handshake.
|
|
245
|
+
*
|
|
246
|
+
* Order: developer first so a successful developer-token match returns
|
|
247
|
+
* the higher scope. Consumer scope is the strict subset.
|
|
248
|
+
*/
|
|
249
|
+
export function resolveAuthScope(presented, tokens) {
|
|
250
|
+
if (tokens.developer && verifyIpcAuthToken(presented, tokens.developer)) {
|
|
251
|
+
return 'developer';
|
|
252
|
+
}
|
|
253
|
+
if (tokens.consumer && verifyIpcAuthToken(presented, tokens.consumer)) {
|
|
254
|
+
return 'consumer';
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Resolve the best auth token available for the calling process.
|
|
260
|
+
*
|
|
261
|
+
* Resolution order:
|
|
262
|
+
* 1. BEEHAVEN_IPC_AUTH env var (used by the Electron app's spawned
|
|
263
|
+
* child processes — they're in the daemon's trust domain).
|
|
264
|
+
* 2. If BEEHAVEN_IPC_SCOPE=developer is set OR `preferDeveloper` is
|
|
265
|
+
* true, try the developer token first. Falls back to consumer
|
|
266
|
+
* if dev mode isn't on.
|
|
267
|
+
* 3. The consumer token.
|
|
268
|
+
*
|
|
269
|
+
* The beehaven CLI sets BEEHAVEN_IPC_SCOPE=developer at startup (in
|
|
270
|
+
* its entry script) so calls automatically attempt developer scope.
|
|
271
|
+
* Mwah and other consumer-only callers don't set the env var and get
|
|
272
|
+
* consumer scope. When dev mode is off, even beehaven gracefully
|
|
273
|
+
* downgrades to consumer scope; privileged verbs then fail with
|
|
274
|
+
* FORBIDDEN_SCOPE rather than the connection breaking entirely.
|
|
275
|
+
*/
|
|
276
|
+
export function resolveClientAuthToken(opts) {
|
|
277
|
+
const fromEnv = process.env.BEEHAVEN_IPC_AUTH;
|
|
278
|
+
if (fromEnv && fromEnv.length >= 32)
|
|
279
|
+
return fromEnv;
|
|
280
|
+
const preferDeveloper = opts?.preferDeveloper === true || process.env.BEEHAVEN_IPC_SCOPE === 'developer';
|
|
281
|
+
if (preferDeveloper) {
|
|
282
|
+
const dev = readScopedToken('developer');
|
|
283
|
+
if (dev)
|
|
284
|
+
return dev;
|
|
285
|
+
}
|
|
286
|
+
return readScopedToken('consumer');
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Read the developer-scope token specifically. Returns null when dev
|
|
290
|
+
* mode is off (no developer token on disk). Useful when a caller
|
|
291
|
+
* needs to explicitly check scope availability before proceeding.
|
|
292
|
+
*/
|
|
293
|
+
export function resolveDeveloperAuthToken() {
|
|
294
|
+
const fromEnv = process.env.BEEHAVEN_IPC_AUTH;
|
|
295
|
+
const scopeHint = process.env.BEEHAVEN_IPC_SCOPE;
|
|
296
|
+
if (fromEnv && fromEnv.length >= 32 && scopeHint === 'developer')
|
|
297
|
+
return fromEnv;
|
|
298
|
+
return readScopedToken('developer');
|
|
299
|
+
}
|