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
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPC Server — bidirectional Unix socket for local clients (Electron, CLI tools).
|
|
3
|
+
*
|
|
4
|
+
* Provides request-response RPCs and broadcast subscriptions over a line-based
|
|
5
|
+
* JSON protocol on /tmp/beehaven-ipc.sock.
|
|
6
|
+
*
|
|
7
|
+
* Local clients send messages to the daemon, which forwards them to the DO
|
|
8
|
+
* via the relay WebSocket. DO responses and broadcasts are forwarded back
|
|
9
|
+
* to connected local clients.
|
|
10
|
+
*
|
|
11
|
+
* Protocol: newline-delimited JSON. Each message is a single JSON object.
|
|
12
|
+
*
|
|
13
|
+
* Client → Daemon:
|
|
14
|
+
* { "type": "rpc", "action": "chat", "input": {...}, "rid": "req_1" }
|
|
15
|
+
* { "type": "send", "msg": { "type": "avatar-presence", ... } }
|
|
16
|
+
* { "type": "subscribe", "patterns": ["agent:*", "avatar-*"] }
|
|
17
|
+
* { "type": "ping" }
|
|
18
|
+
*
|
|
19
|
+
* Daemon → Client:
|
|
20
|
+
* { "type": "rpc-result", "rid": "req_1", "result": {...} }
|
|
21
|
+
* { "type": "rpc-error", "rid": "req_1", "error": "..." }
|
|
22
|
+
* { "type": "broadcast", "msg": { "type": "agent:text", ... } }
|
|
23
|
+
* { "type": "pong" }
|
|
24
|
+
* { "type": "status", "connected": true, "daemonPid": 1234 }
|
|
25
|
+
*/
|
|
26
|
+
import { createConnection, createServer } from 'net';
|
|
27
|
+
import { existsSync, unlinkSync, chmodSync, statSync } from 'fs';
|
|
28
|
+
import { EventEmitter } from 'events';
|
|
29
|
+
import { resolveAuthScope } from './ipc-auth.js';
|
|
30
|
+
const IPC_SOCKET_PATH = '/tmp/beehaven-ipc.sock';
|
|
31
|
+
/**
|
|
32
|
+
* Message types the daemon will process without authentication.
|
|
33
|
+
*
|
|
34
|
+
* `ping` is side-effect-free and used by liveness probes; `auth` is the
|
|
35
|
+
* scope-handshake itself. Everything else requires the client to first
|
|
36
|
+
* present a valid token via `auth` and be tagged with a scope.
|
|
37
|
+
*/
|
|
38
|
+
const UNAUTH_ALLOWED_TYPES = new Set(['ping', 'auth']);
|
|
39
|
+
/**
|
|
40
|
+
* Verbs that require *developer* scope. A client authenticated with the
|
|
41
|
+
* consumer token (the default — what mwah and other consumer-facing
|
|
42
|
+
* tools use) cannot invoke any of these.
|
|
43
|
+
*
|
|
44
|
+
* Why these specifically:
|
|
45
|
+
* - get-token leaks the Firebase ID token to the calling process
|
|
46
|
+
* - daemon-fetch arbitrary HTTP egress, can hit admin worker routes
|
|
47
|
+
* - shell / sql raw exec on a connected DO
|
|
48
|
+
*
|
|
49
|
+
* RPCs with admin:* / marketplace:install action names are gated
|
|
50
|
+
* separately inside the `rpc` case (see requireDeveloperForRpc below).
|
|
51
|
+
*/
|
|
52
|
+
const DEVELOPER_ONLY_VERBS = new Set(['get-token', 'daemon-fetch', 'shell', 'sql']);
|
|
53
|
+
/**
|
|
54
|
+
* RPC action prefixes that require developer scope. These are admin-side
|
|
55
|
+
* verbs (rejected by the worker anyway, but failing early here avoids
|
|
56
|
+
* leaking a Firebase token into the request envelope) and the marketplace
|
|
57
|
+
* installer (executes shell commands on the user's Mac).
|
|
58
|
+
*/
|
|
59
|
+
// SECURITY: `shell:`/`sql:` actions are arbitrary command / raw-SQL execution
|
|
60
|
+
// on the target DO. The verb-type gate (DEVELOPER_ONLY_VERBS) blocks
|
|
61
|
+
// {type:'shell'|'sql'}, but the real transport is {type:'rpc',
|
|
62
|
+
// action:'shell:exec'} — so these prefixes MUST be developer-only here too, or
|
|
63
|
+
// a consumer-scope client (mwah / any process that read ~/.beehaven/ipc-auth)
|
|
64
|
+
// bypasses the scope split and runs raw SQL/shell on the user's home DO.
|
|
65
|
+
const DEVELOPER_ONLY_RPC_PREFIXES = ['admin:', 'marketplace:install', 'shell:', 'sql:'];
|
|
66
|
+
/**
|
|
67
|
+
* Actions that CARRY another action, and therefore hide it from a prefix test.
|
|
68
|
+
*
|
|
69
|
+
* ⚠⚠ THE GATE BELOW READ `msg.action` AND NOTHING ELSE, SO ANY WRAPPER DEFEATED IT. `agent-invoke`
|
|
70
|
+
* takes the real verb in `input.action`, and the CLI wraps every call in it the moment an agent is
|
|
71
|
+
* signed in — so `agent-invoke { action: 'shell:exec' }` presented as `agent-invoke`, matched no
|
|
72
|
+
* prefix, and would run raw shell on the user's home DO from a CONSUMER-scope client. That is
|
|
73
|
+
* exactly the bypass the comment above says must not exist ("a consumer-scope client … bypasses the
|
|
74
|
+
* scope split and runs raw SQL/shell on the user's home DO"); it simply checked the envelope
|
|
75
|
+
* instead of the letter inside it.
|
|
76
|
+
*
|
|
77
|
+
* ⚠ THE PROXIES BELONG HERE TOO. `admin:*-rpc` is caught by its own prefix, but `workspace-rpc` is
|
|
78
|
+
* deliberately NOT `admin:`-prefixed — it is the member-facing one — so without unwrapping it a
|
|
79
|
+
* `workspace-rpc { action: 'shell:exec' }` would carry a developer verb to another workspace under
|
|
80
|
+
* a consumer token.
|
|
81
|
+
*/
|
|
82
|
+
const ACTION_CARRIERS = new Set([
|
|
83
|
+
'agent-invoke', 'workspace-rpc', 'admin:team-rpc', 'admin:store-rpc', 'admin:workspace-rpc',
|
|
84
|
+
]);
|
|
85
|
+
/**
|
|
86
|
+
* Every action a message would actually run — the envelope AND whatever it carries.
|
|
87
|
+
*
|
|
88
|
+
* ⚠ BOUNDED, NEVER AN UNBOUNDED WALK OVER CALLER INPUT. A wrapper inside a wrapper is a real shape
|
|
89
|
+
* (the CLI sends `workspace-rpc { agent-invoke { verb } }`); a third level is not. Three is one
|
|
90
|
+
* more than anything produces.
|
|
91
|
+
*
|
|
92
|
+
* ⚠ IT RETURNS THE WHOLE CHAIN, NOT THE INNERMOST. Gating on the innermost alone would wave through
|
|
93
|
+
* `agent-invoke { admin:migration-run { … } }` whenever the innermost happened to be ordinary —
|
|
94
|
+
* every link has to satisfy the scope.
|
|
95
|
+
*/
|
|
96
|
+
export function actionChain(action, input, depth = 0) {
|
|
97
|
+
const name = typeof action === 'string' ? action.trim() : '';
|
|
98
|
+
if (!name)
|
|
99
|
+
return [];
|
|
100
|
+
if (depth >= 2 || !ACTION_CARRIERS.has(name))
|
|
101
|
+
return [name];
|
|
102
|
+
const inner = input;
|
|
103
|
+
return [name, ...actionChain(inner?.action, inner?.input, depth + 1)];
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Probe the IPC socket synchronously-ish to see whether another daemon is
|
|
107
|
+
* already serving it. Resolves true if a live server answered (= another
|
|
108
|
+
* daemon exists), false if the socket is stale or missing.
|
|
109
|
+
*
|
|
110
|
+
* Used as a defense-in-depth check: the daemon's own single-instance guard
|
|
111
|
+
* (findLiveDaemons in daemon.ts) handles the common case via the PID file
|
|
112
|
+
* and pgrep. This fallback catches the edge case where the PID file is
|
|
113
|
+
* missing but a live daemon somehow still holds the socket.
|
|
114
|
+
*/
|
|
115
|
+
function probeIpcSocket(timeoutMs = 400) {
|
|
116
|
+
return new Promise((resolve) => {
|
|
117
|
+
if (!existsSync(IPC_SOCKET_PATH))
|
|
118
|
+
return resolve(false);
|
|
119
|
+
const client = createConnection(IPC_SOCKET_PATH);
|
|
120
|
+
const done = (alive) => {
|
|
121
|
+
try {
|
|
122
|
+
client.destroy();
|
|
123
|
+
}
|
|
124
|
+
catch { }
|
|
125
|
+
resolve(alive);
|
|
126
|
+
};
|
|
127
|
+
const timer = setTimeout(() => done(false), timeoutMs);
|
|
128
|
+
client.once('connect', () => {
|
|
129
|
+
clearTimeout(timer);
|
|
130
|
+
done(true);
|
|
131
|
+
});
|
|
132
|
+
client.once('error', () => {
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
done(false);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
export { probeIpcSocket };
|
|
139
|
+
export class IpcServer extends EventEmitter {
|
|
140
|
+
server = null;
|
|
141
|
+
clients = new Map();
|
|
142
|
+
clientCounter = 0;
|
|
143
|
+
virtualClientCounter = 0;
|
|
144
|
+
consumerAuthToken;
|
|
145
|
+
developerAuthToken;
|
|
146
|
+
constructor(opts = {}) {
|
|
147
|
+
super();
|
|
148
|
+
this.consumerAuthToken = opts.consumerAuthToken ?? null;
|
|
149
|
+
this.developerAuthToken = opts.developerAuthToken ?? null;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Start listening for local client connections.
|
|
153
|
+
*
|
|
154
|
+
* Caller MUST have already verified no other daemon is running — the
|
|
155
|
+
* daemon's startDaemon() calls findLiveDaemons() before reaching us. We
|
|
156
|
+
* unlink any leftover socket file here because it's expected to be stale
|
|
157
|
+
* at this point; a live server would have been caught upstream.
|
|
158
|
+
*/
|
|
159
|
+
start() {
|
|
160
|
+
// Clean up stale socket
|
|
161
|
+
try {
|
|
162
|
+
if (existsSync(IPC_SOCKET_PATH))
|
|
163
|
+
unlinkSync(IPC_SOCKET_PATH);
|
|
164
|
+
}
|
|
165
|
+
catch { }
|
|
166
|
+
this.server = createServer((socket) => {
|
|
167
|
+
const clientId = `ipc_${++this.clientCounter}`;
|
|
168
|
+
// Connections start unauthenticated and scope=null. The client must
|
|
169
|
+
// present a token via the `auth` handshake; the matching token
|
|
170
|
+
// determines scope (consumer or developer). Until then, only
|
|
171
|
+
// UNAUTH_ALLOWED_TYPES (ping, auth) are processed. This is stricter
|
|
172
|
+
// than the previous "socket perms is the boundary" model — any
|
|
173
|
+
// same-user process can still *connect*, but cannot invoke verbs
|
|
174
|
+
// until it proves it knows the right token, and cannot escalate to
|
|
175
|
+
// developer scope unless dev mode is on.
|
|
176
|
+
const client = {
|
|
177
|
+
id: clientId,
|
|
178
|
+
socket,
|
|
179
|
+
subscriptions: ['*'], // Default: receive everything
|
|
180
|
+
buffer: '',
|
|
181
|
+
authenticated: false,
|
|
182
|
+
scope: null,
|
|
183
|
+
};
|
|
184
|
+
this.clients.set(clientId, client);
|
|
185
|
+
// Send initial status. `requireAuth: true` always — clients are
|
|
186
|
+
// expected to send `auth` with a token from ~/.beehaven/ipc-auth
|
|
187
|
+
// (or ipc-auth-dev for developer scope).
|
|
188
|
+
this.sendToClient(client, {
|
|
189
|
+
type: 'status',
|
|
190
|
+
connected: true,
|
|
191
|
+
clientId,
|
|
192
|
+
daemonPid: process.pid,
|
|
193
|
+
requireAuth: true,
|
|
194
|
+
});
|
|
195
|
+
socket.on('data', (data) => {
|
|
196
|
+
client.buffer += data.toString();
|
|
197
|
+
const lines = client.buffer.split('\n');
|
|
198
|
+
client.buffer = lines.pop() || '';
|
|
199
|
+
for (const line of lines) {
|
|
200
|
+
if (!line.trim())
|
|
201
|
+
continue;
|
|
202
|
+
try {
|
|
203
|
+
const msg = JSON.parse(line);
|
|
204
|
+
this.handleClientMessage(client, msg);
|
|
205
|
+
}
|
|
206
|
+
catch { }
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
socket.on('close', () => {
|
|
210
|
+
if (client.label) {
|
|
211
|
+
this.broadcast({
|
|
212
|
+
type: 'session:leave',
|
|
213
|
+
clientId,
|
|
214
|
+
label: client.label,
|
|
215
|
+
app: client.sessionApp,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
this.clients.delete(clientId);
|
|
219
|
+
this.runDisconnectHooks(clientId);
|
|
220
|
+
});
|
|
221
|
+
socket.on('error', () => {
|
|
222
|
+
if (client.label) {
|
|
223
|
+
this.broadcast({
|
|
224
|
+
type: 'session:leave',
|
|
225
|
+
clientId,
|
|
226
|
+
label: client.label,
|
|
227
|
+
app: client.sessionApp,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
this.clients.delete(clientId);
|
|
231
|
+
this.runDisconnectHooks(clientId);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
// Chmod the socket file after bind instead of changing the
|
|
235
|
+
// process-wide umask — umask changes are global and race with any
|
|
236
|
+
// other mkdirSync / writeFileSync happening in parallel (e.g.
|
|
237
|
+
// LocalHive pre-creating building data dirs during startup).
|
|
238
|
+
this.server.listen(IPC_SOCKET_PATH, () => {
|
|
239
|
+
/**
|
|
240
|
+
* ⚠⚠ CHMOD, THEN **VERIFY**. This was `chmodSync(…, 0o600)` inside an empty `catch {}`, and
|
|
241
|
+
* the mode it claims has been wrong on every start measured: the live socket comes up
|
|
242
|
+
* `srwxr-xr-x` (0755), reproducibly, in both dev and normal mode. `chmod` is not being
|
|
243
|
+
* refused — it succeeds from plain node AND from the packaged Electron binary — so the
|
|
244
|
+
* binding path simply is not this one in a shipped build. Either way the effect is the same:
|
|
245
|
+
* a security property that TWO docstrings state as fact ("chmod 0600 — the kernel proving the
|
|
246
|
+
* caller is this UNIX user") was never checked and was never true.
|
|
247
|
+
*
|
|
248
|
+
* ⚠ THE PROPERTY STILL HOLDS TODAY, BY A NARROWER MARGIN THAN DOCUMENTED. Connecting to a
|
|
249
|
+
* unix socket requires WRITE permission, and 0755 grants group/other only `r-x` — so no other
|
|
250
|
+
* user can connect. The protection is the absent write bit, not the documented mode, and the
|
|
251
|
+
* distance to an exploitable state is one umask change nobody would notice.
|
|
252
|
+
*
|
|
253
|
+
* ⚠ IT WARNS RATHER THAN REFUSING TO LISTEN. A daemon that will not start because a mode bit
|
|
254
|
+
* is 0755 turns a hardening check into an outage, on a socket that is still same-user-only.
|
|
255
|
+
* The line is what makes it findable; making it fatal is a separate decision with a migration.
|
|
256
|
+
*/
|
|
257
|
+
/**
|
|
258
|
+
* ⚠⚠ `require` IS NOT DEFINED HERE — THIS HARDENING NEVER RAN, ON ANY BUILD. The package is
|
|
259
|
+
* `"type": "module"` and ships as unbundled ESM, so both lines threw `require is not defined`
|
|
260
|
+
* on every daemon start; the catch turned that into a warning, `mode` stayed null, and the
|
|
261
|
+
* mode assertion below was skipped along with it. The socket kept its umask default for the
|
|
262
|
+
* life of the feature while the log said only that the chmod "could not" be set.
|
|
263
|
+
*
|
|
264
|
+
* ⚠ THE SAME LESSON IS ALREADY WRITTEN DOWN ONE FILE OVER — `relay.ts`'s keychain read
|
|
265
|
+
* carries the comment "require() fails in ESM" and uses execSync for exactly this reason. It
|
|
266
|
+
* was learned once and not applied here. Import at the top; never reach for `require` in this
|
|
267
|
+
* package.
|
|
268
|
+
*/
|
|
269
|
+
let mode = null;
|
|
270
|
+
try {
|
|
271
|
+
chmodSync(IPC_SOCKET_PATH, 0o600);
|
|
272
|
+
mode = statSync(IPC_SOCKET_PATH).mode & 0o777;
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
console.warn(`[ipc] SECURITY: could not set 0600 on ${IPC_SOCKET_PATH} — ${err?.code ?? err?.message ?? err}`);
|
|
276
|
+
}
|
|
277
|
+
if (mode !== null && mode !== 0o600) {
|
|
278
|
+
console.warn(`[ipc] SECURITY: socket mode is 0${mode.toString(8)}, expected 0600. Other-user connect is still `
|
|
279
|
+
+ `blocked (no write bit), but the documented guarantee does not hold — do not weaken it further.`);
|
|
280
|
+
}
|
|
281
|
+
console.log(`[ipc] Listening on ${IPC_SOCKET_PATH}${mode !== null ? ` (mode 0${mode.toString(8)})` : ''}`);
|
|
282
|
+
});
|
|
283
|
+
this.server.on('error', (err) => {
|
|
284
|
+
console.warn(`[ipc] Server error: ${err.message}`);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
/** Stop the IPC server */
|
|
288
|
+
stop() {
|
|
289
|
+
for (const client of this.clients.values()) {
|
|
290
|
+
if (client.socket)
|
|
291
|
+
client.socket.destroy();
|
|
292
|
+
}
|
|
293
|
+
this.clients.clear();
|
|
294
|
+
if (this.server) {
|
|
295
|
+
this.server.close();
|
|
296
|
+
this.server = null;
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
if (existsSync(IPC_SOCKET_PATH))
|
|
300
|
+
unlinkSync(IPC_SOCKET_PATH);
|
|
301
|
+
}
|
|
302
|
+
catch { }
|
|
303
|
+
}
|
|
304
|
+
/** Broadcast a message from the DO to all subscribed local clients */
|
|
305
|
+
/**
|
|
306
|
+
* ⚠ RETURNS THE NUMBER OF CLIENTS IT REACHED. It returned `void`, which meant a caller could
|
|
307
|
+
* not tell "delivered to the desktop app" from "nobody is listening" — and the whole point of
|
|
308
|
+
* a local fallback is deciding between exactly those two. Same defect as `broadcastToWeb` in
|
|
309
|
+
* the worker, where an always-zero `delivered` sent an agent chasing a tab that was never there.
|
|
310
|
+
* ⚠ Additive: every existing caller ignores the value and is unaffected.
|
|
311
|
+
*/
|
|
312
|
+
broadcast(msg) {
|
|
313
|
+
const type = msg?.type || '';
|
|
314
|
+
let sent = 0;
|
|
315
|
+
for (const client of this.clients.values()) {
|
|
316
|
+
if (this.matchesSubscription(client, type)) {
|
|
317
|
+
this.sendToClient(client, { type: 'broadcast', msg });
|
|
318
|
+
sent++;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return sent;
|
|
322
|
+
}
|
|
323
|
+
/** Send an RPC result back to a specific client */
|
|
324
|
+
sendRpcResult(clientId, rid, result) {
|
|
325
|
+
const client = this.clients.get(clientId);
|
|
326
|
+
if (client) {
|
|
327
|
+
this.sendToClient(client, { type: 'rpc-result', rid, result });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/** Send an RPC error back to a specific client */
|
|
331
|
+
sendRpcError(clientId, rid, error) {
|
|
332
|
+
const client = this.clients.get(clientId);
|
|
333
|
+
if (client) {
|
|
334
|
+
this.sendToClient(client, { type: 'rpc-error', rid, error });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/** Number of connected local clients */
|
|
338
|
+
get clientCount() {
|
|
339
|
+
return this.clients.size;
|
|
340
|
+
}
|
|
341
|
+
/** Get info about all active sessions (clients that announced themselves) */
|
|
342
|
+
getSessionInfo() {
|
|
343
|
+
const sessions = [];
|
|
344
|
+
for (const client of this.clients.values()) {
|
|
345
|
+
if (client.label) {
|
|
346
|
+
sessions.push({ clientId: client.id, label: client.label, app: client.sessionApp });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return sessions;
|
|
350
|
+
}
|
|
351
|
+
// ─── Internal ───
|
|
352
|
+
handleClientMessage(client, msg) {
|
|
353
|
+
// ── Auth gate ──────────────────────────────────────────────────────
|
|
354
|
+
// Real-socket clients must complete the `auth` handshake before any
|
|
355
|
+
// sensitive message type is honored. Virtual clients (in-process)
|
|
356
|
+
// are pre-authenticated by construction. `UNAUTH_ALLOWED_TYPES`
|
|
357
|
+
// lists what's safe to process before auth completes (ping, auth).
|
|
358
|
+
//
|
|
359
|
+
// If the server was constructed without an authToken (authToken=null),
|
|
360
|
+
// fail closed: reject everything except the allowlist. This makes a
|
|
361
|
+
// misconfigured daemon unreachable rather than silently permissive.
|
|
362
|
+
if (!client.authenticated && !UNAUTH_ALLOWED_TYPES.has(msg.type)) {
|
|
363
|
+
if (msg.rid) {
|
|
364
|
+
this.sendToClient(client, {
|
|
365
|
+
type: 'rpc-error',
|
|
366
|
+
rid: msg.rid,
|
|
367
|
+
error: 'IPC auth required — send { type: "auth", token: "…" } first',
|
|
368
|
+
code: 'UNAUTHENTICATED',
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
this.sendToClient(client, {
|
|
373
|
+
type: 'error',
|
|
374
|
+
error: 'IPC auth required',
|
|
375
|
+
code: 'UNAUTHENTICATED',
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
// ── Scope gate ─────────────────────────────────────────────────────
|
|
381
|
+
// Developer-only verbs are blocked for consumer-scope clients even
|
|
382
|
+
// after a successful auth handshake. The error surfaces as a normal
|
|
383
|
+
// RPC error so the CLI can render a useful "needs HIVE_DEV_MODE"
|
|
384
|
+
// message rather than a hung connection.
|
|
385
|
+
if (client.scope !== 'developer') {
|
|
386
|
+
/**
|
|
387
|
+
* ⚠⚠ EVERY ACTION IN THE CHAIN, NOT JUST THE ENVELOPE — see `actionChain`. A wrapper used to
|
|
388
|
+
* defeat this gate completely: the CLI wraps each call in `agent-invoke` whenever an agent is
|
|
389
|
+
* signed in, so the prefix test saw `agent-invoke` and the carried verb was never examined.
|
|
390
|
+
*/
|
|
391
|
+
const chain = msg.type === 'rpc' ? actionChain(msg.action, msg.input) : [];
|
|
392
|
+
const offender = chain.find((a) => DEVELOPER_ONLY_RPC_PREFIXES.some((p) => a.startsWith(p)));
|
|
393
|
+
const blocked = DEVELOPER_ONLY_VERBS.has(msg.type) || !!offender;
|
|
394
|
+
if (blocked) {
|
|
395
|
+
/**
|
|
396
|
+
* ⚠ NAME THE ACTION THAT WAS ACTUALLY REFUSED, not the envelope. "`agent-invoke` requires
|
|
397
|
+
* developer scope" is false — it does not — and it sends the reader looking in the wrong
|
|
398
|
+
* place for a verb that is perfectly ordinary.
|
|
399
|
+
*/
|
|
400
|
+
const detail = msg.type === 'rpc'
|
|
401
|
+
? `RPC '${offender ?? msg.action}' requires developer scope`
|
|
402
|
+
+ (offender && offender !== msg.action ? ` (carried by '${msg.action}')` : '')
|
|
403
|
+
: `verb '${msg.type}' requires developer scope`;
|
|
404
|
+
this.sendToClient(client, {
|
|
405
|
+
type: 'rpc-error',
|
|
406
|
+
rid: msg.rid ?? undefined,
|
|
407
|
+
error: `${detail}. Enable Developer Mode in the Hive Mac app (or set HIVE_DEV_MODE=1) and relaunch.`,
|
|
408
|
+
code: 'FORBIDDEN_SCOPE',
|
|
409
|
+
});
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
switch (msg.type) {
|
|
414
|
+
case 'auth': {
|
|
415
|
+
// Scope-aware handshake. The presented token is matched against
|
|
416
|
+
// both the consumer and developer tokens; the matching one
|
|
417
|
+
// determines client.scope. A null scope means rejection.
|
|
418
|
+
const presented = typeof msg.token === 'string' ? msg.token : '';
|
|
419
|
+
const scope = resolveAuthScope(presented, {
|
|
420
|
+
consumer: this.consumerAuthToken,
|
|
421
|
+
developer: this.developerAuthToken,
|
|
422
|
+
});
|
|
423
|
+
if (!scope) {
|
|
424
|
+
this.sendToClient(client, {
|
|
425
|
+
type: 'auth-result',
|
|
426
|
+
ok: false,
|
|
427
|
+
error: 'Invalid auth token',
|
|
428
|
+
});
|
|
429
|
+
try {
|
|
430
|
+
client.socket?.end();
|
|
431
|
+
}
|
|
432
|
+
catch { }
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
client.authenticated = true;
|
|
436
|
+
client.scope = scope;
|
|
437
|
+
this.sendToClient(client, {
|
|
438
|
+
type: 'auth-result',
|
|
439
|
+
ok: true,
|
|
440
|
+
scope,
|
|
441
|
+
});
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
case 'ping':
|
|
445
|
+
this.sendToClient(client, { type: 'pong' });
|
|
446
|
+
break;
|
|
447
|
+
case 'subscribe':
|
|
448
|
+
if (Array.isArray(msg.patterns)) {
|
|
449
|
+
client.subscriptions = msg.patterns;
|
|
450
|
+
}
|
|
451
|
+
break;
|
|
452
|
+
case 'session:announce':
|
|
453
|
+
// Client labels itself (e.g. "terminal", "browser", "claude")
|
|
454
|
+
client.label = msg.label;
|
|
455
|
+
client.sessionApp = msg.app;
|
|
456
|
+
this.broadcast({
|
|
457
|
+
type: 'session:join',
|
|
458
|
+
clientId: client.id,
|
|
459
|
+
label: msg.label,
|
|
460
|
+
app: msg.app,
|
|
461
|
+
});
|
|
462
|
+
break;
|
|
463
|
+
case 'rpc':
|
|
464
|
+
// Forward RPC to relay → DO. Emit event for daemon to handle.
|
|
465
|
+
this.emit('rpc', {
|
|
466
|
+
clientId: client.id,
|
|
467
|
+
rid: msg.rid,
|
|
468
|
+
action: msg.action,
|
|
469
|
+
input: msg.input || {},
|
|
470
|
+
});
|
|
471
|
+
break;
|
|
472
|
+
case 'send':
|
|
473
|
+
// Forward raw message to relay → DO
|
|
474
|
+
this.emit('send', msg.msg);
|
|
475
|
+
break;
|
|
476
|
+
case 'app-rpc':
|
|
477
|
+
// Forward RPC to a local app building (not the cloud relay).
|
|
478
|
+
// Used by the CLI for controlling local buildings like AbletonWorkspace.
|
|
479
|
+
this.emit('app-rpc', {
|
|
480
|
+
clientId: client.id,
|
|
481
|
+
rid: msg.rid,
|
|
482
|
+
action: msg.action,
|
|
483
|
+
input: msg.input || {},
|
|
484
|
+
building: msg.building || 'ableton',
|
|
485
|
+
instanceId: msg.instanceId || 'live',
|
|
486
|
+
// Optional signed AgentIdentity token — the daemon verifies it and
|
|
487
|
+
// attaches a CallerContext to the building call.
|
|
488
|
+
agent: typeof msg.agent === 'string' ? msg.agent : undefined,
|
|
489
|
+
});
|
|
490
|
+
break;
|
|
491
|
+
case 'pipeline':
|
|
492
|
+
// Chained app-rpc: steps run in sequence, each step's result is
|
|
493
|
+
// bound to `$<id>` for later steps. Progress is emitted as
|
|
494
|
+
// `pipeline:step` broadcasts so the UI + Claude can observe.
|
|
495
|
+
this.emit('pipeline', {
|
|
496
|
+
clientId: client.id,
|
|
497
|
+
rid: msg.rid,
|
|
498
|
+
title: msg.title,
|
|
499
|
+
steps: Array.isArray(msg.steps) ? msg.steps : [],
|
|
500
|
+
defaultBuilding: msg.building || 'ableton',
|
|
501
|
+
defaultInstance: msg.instanceId || 'live',
|
|
502
|
+
});
|
|
503
|
+
break;
|
|
504
|
+
case 'daemon-fetch':
|
|
505
|
+
// HTTP fetch proxy: the daemon makes the HTTP request with its
|
|
506
|
+
// Firebase ID token attached as `Authorization: Bearer …` and
|
|
507
|
+
// returns the response. Lets local CLI tools (beehaven admin,
|
|
508
|
+
// smoke tests, scripts) hit any worker route without ever
|
|
509
|
+
// touching the keychain token themselves.
|
|
510
|
+
this.emit('daemon-fetch', {
|
|
511
|
+
clientId: client.id,
|
|
512
|
+
rid: msg.rid,
|
|
513
|
+
method: msg.method || 'GET',
|
|
514
|
+
url: msg.url,
|
|
515
|
+
body: msg.body,
|
|
516
|
+
headers: msg.headers || {},
|
|
517
|
+
});
|
|
518
|
+
break;
|
|
519
|
+
// ── Daemon Omni (Claude Agent SDK, hosted in this process) ──────
|
|
520
|
+
//
|
|
521
|
+
// These MUST be explicit cases. The `default` arm below forwards an
|
|
522
|
+
// unrecognised message to the RELAY — so a missing case here would not
|
|
523
|
+
// fail loudly, it would quietly ship the user's prompt to the cloud,
|
|
524
|
+
// which is both wrong and the exact path disabled for Anthropic
|
|
525
|
+
// credential-use compliance. Adding an `omni:*` verb means adding it
|
|
526
|
+
// here too.
|
|
527
|
+
//
|
|
528
|
+
// Deliberately NOT in DEVELOPER_ONLY_VERBS: the avatar and mwah are
|
|
529
|
+
// consumer-scope clients and this is the runtime behind their chat. The
|
|
530
|
+
// security boundary for driving your own Claude subscription is the
|
|
531
|
+
// socket's 0600 mode — the kernel proving the caller is this UNIX user —
|
|
532
|
+
// not which CLI happened to connect.
|
|
533
|
+
case 'omni:prompt':
|
|
534
|
+
case 'omni:end':
|
|
535
|
+
case 'omni:sessions':
|
|
536
|
+
case 'omni:branch':
|
|
537
|
+
/**
|
|
538
|
+
* ⚠ THESE THREE WERE MISSING, AND THE FAILURE WAS A TEN-MINUTE HANG.
|
|
539
|
+
*
|
|
540
|
+
* The note above says it plainly — "adding an `omni:*` verb means adding
|
|
541
|
+
* it here too" — and adding the verbs to the daemon and the client without
|
|
542
|
+
* this line meant they fell through to the CLOUD branch instead of being
|
|
543
|
+
* emitted locally. `omni:answer` never reached the ask registry, so a
|
|
544
|
+
* question the turn was parked on could only ever end by timing out:
|
|
545
|
+
* measured at 10m12s, after which Omni correctly reported "no response —
|
|
546
|
+
* you may have been away". Every part looked right in isolation.
|
|
547
|
+
*/
|
|
548
|
+
case 'omni:answer':
|
|
549
|
+
case 'omni:interrupt':
|
|
550
|
+
case 'omni:surface':
|
|
551
|
+
/**
|
|
552
|
+
* ⚠ AND I MADE THE SAME OMISSION ONE VERB LATER. `omni:set-cwd` was added to the daemon and
|
|
553
|
+
* to `IpcClient`, both correct in isolation, and without this line it fell through to the
|
|
554
|
+
* CLOUD branch and every call timed out at 30s. The note above says exactly this and it was
|
|
555
|
+
* still missed — which is why there is now a test that reads BOTH files and fails when they
|
|
556
|
+
* disagree, rather than a third warning comment.
|
|
557
|
+
*/
|
|
558
|
+
case 'omni:set-cwd':
|
|
559
|
+
/**
|
|
560
|
+
* ⚠ FOUND BY THE GUARD ON ITS FIRST RUN, already committed and already broken.
|
|
561
|
+
* `omni:company-status` was added to the daemon earlier the same day and never routed here,
|
|
562
|
+
* so the company-mode diagnostics — the surface whose entire job is reporting that something
|
|
563
|
+
* is silently wrong — would itself have hung silently.
|
|
564
|
+
*/
|
|
565
|
+
case 'omni:company-status':
|
|
566
|
+
this.emit(msg.type, {
|
|
567
|
+
clientId: client.id,
|
|
568
|
+
rid: msg.rid,
|
|
569
|
+
input: msg.input || {},
|
|
570
|
+
});
|
|
571
|
+
break;
|
|
572
|
+
case 'get-token':
|
|
573
|
+
// Return the daemon's current Firebase ID token. Use sparingly
|
|
574
|
+
// — `daemon-fetch` is the preferred pattern because it keeps the
|
|
575
|
+
// token inside the daemon process. `get-token` exists for tools
|
|
576
|
+
// that need to call third-party services with a Firebase JWT
|
|
577
|
+
// (e.g. Cloud Run endpoints, Firestore REST).
|
|
578
|
+
this.emit('get-token', { clientId: client.id, rid: msg.rid });
|
|
579
|
+
break;
|
|
580
|
+
default:
|
|
581
|
+
// Forward unknown messages as raw sends
|
|
582
|
+
this.emit('send', msg);
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
sendToClient(client, msg) {
|
|
587
|
+
try {
|
|
588
|
+
// Virtual clients (dashboard /apps/ws bridge, in-process
|
|
589
|
+
// integrations) provide their own send callback instead of a socket.
|
|
590
|
+
if (client.virtualSend) {
|
|
591
|
+
client.virtualSend(msg);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (client.socket && !client.socket.destroyed) {
|
|
595
|
+
client.socket.write(JSON.stringify(msg) + '\n');
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
catch { }
|
|
599
|
+
}
|
|
600
|
+
// ── Virtual client API ────────────────────────────────────────────────
|
|
601
|
+
//
|
|
602
|
+
// A virtual client is an in-process IPC client that has no socket — its
|
|
603
|
+
// outgoing messages are delivered to a `virtualSend` callback instead.
|
|
604
|
+
// The dashboard uses this to bridge browser WebSocket connections to the
|
|
605
|
+
// IPC bus so the same pipeline/app-rpc handlers serve both Claude and
|
|
606
|
+
// the human UI.
|
|
607
|
+
/** Register a virtual client and return its id. */
|
|
608
|
+
createVirtualClient(virtualSend, initialSubscriptions = ['*']) {
|
|
609
|
+
const id = `virt_${++this.virtualClientCounter}`;
|
|
610
|
+
const client = {
|
|
611
|
+
id,
|
|
612
|
+
virtualSend,
|
|
613
|
+
subscriptions: initialSubscriptions,
|
|
614
|
+
buffer: '',
|
|
615
|
+
// Virtual clients are in-process — they're already inside the trust
|
|
616
|
+
// boundary. The dashboard /apps/ws bridge enforces its own auth
|
|
617
|
+
// (session cookie, origin check) before creating the virtual client,
|
|
618
|
+
// so we hand them developer scope to mirror what a logged-in tray
|
|
619
|
+
// user can already do.
|
|
620
|
+
authenticated: true,
|
|
621
|
+
scope: 'developer',
|
|
622
|
+
};
|
|
623
|
+
this.clients.set(id, client);
|
|
624
|
+
// Mimic the on-connect status frame real clients receive so the bridge
|
|
625
|
+
// client's logic (and therefore the browser) can key off the same event.
|
|
626
|
+
this.sendToClient(client, {
|
|
627
|
+
type: 'status',
|
|
628
|
+
connected: true,
|
|
629
|
+
clientId: id,
|
|
630
|
+
daemonPid: process.pid,
|
|
631
|
+
});
|
|
632
|
+
return id;
|
|
633
|
+
}
|
|
634
|
+
/** Deliver a parsed message as if the virtual client had sent it. */
|
|
635
|
+
injectMessage(clientId, msg) {
|
|
636
|
+
const client = this.clients.get(clientId);
|
|
637
|
+
if (!client)
|
|
638
|
+
return;
|
|
639
|
+
this.handleClientMessage(client, msg);
|
|
640
|
+
}
|
|
641
|
+
/** Remove a virtual client (on WebSocket close). */
|
|
642
|
+
releaseVirtualClient(clientId) {
|
|
643
|
+
this.clients.delete(clientId);
|
|
644
|
+
this.runDisconnectHooks(clientId);
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Run something when a client goes away.
|
|
648
|
+
*
|
|
649
|
+
* ⚠ EXISTS BECAUSE A CAPABILITY DECLARED BY A CLIENT MUST DIE WITH IT. The Mac
|
|
650
|
+
* app declares that it can render floating windows; if it quits without that
|
|
651
|
+
* being undone, the daemon goes on believing a windowing surface is attached
|
|
652
|
+
* and Omni keeps reporting that it opened windows nobody can see. Anything
|
|
653
|
+
* else a client claims about itself has the same shape.
|
|
654
|
+
*/
|
|
655
|
+
disconnectHooks = new Map();
|
|
656
|
+
onClientDisconnect(clientId, fn) {
|
|
657
|
+
const list = this.disconnectHooks.get(clientId) ?? [];
|
|
658
|
+
list.push(fn);
|
|
659
|
+
this.disconnectHooks.set(clientId, list);
|
|
660
|
+
}
|
|
661
|
+
runDisconnectHooks(clientId) {
|
|
662
|
+
const list = this.disconnectHooks.get(clientId);
|
|
663
|
+
if (!list)
|
|
664
|
+
return;
|
|
665
|
+
this.disconnectHooks.delete(clientId);
|
|
666
|
+
for (const fn of list) {
|
|
667
|
+
try {
|
|
668
|
+
fn();
|
|
669
|
+
}
|
|
670
|
+
catch { /* a hook must not take down the cleanup path */ }
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
matchesSubscription(client, type) {
|
|
674
|
+
return client.subscriptions.some((pattern) => {
|
|
675
|
+
if (pattern === '*')
|
|
676
|
+
return true;
|
|
677
|
+
if (pattern.endsWith('*')) {
|
|
678
|
+
return type.startsWith(pattern.slice(0, -1));
|
|
679
|
+
}
|
|
680
|
+
return type === pattern;
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
export { IPC_SOCKET_PATH };
|