shadow-claw 1.23.4 → 1.24.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 +42 -5
- package/bin/cli.mjs +166 -0
- package/bin/cli.test.mjs +60 -0
- package/bin/commands/backup.mjs +70 -0
- package/bin/commands/clients.mjs +41 -0
- package/bin/commands/peer-id.mjs +76 -0
- package/bin/commands/peer-id.test.mjs +104 -0
- package/bin/commands/send.mjs +51 -0
- package/bin/commands/tasks.mjs +59 -0
- package/bin/commands/webrtc-listen.mjs +254 -0
- package/bin/prerender-dsd-shell/prerender-dsd-shell.test.mjs +46 -0
- package/bin/rolldown.config.test.mjs +401 -8
- package/bin/utils/control-client.mjs +243 -0
- package/bin/utils/control-client.test.mjs +83 -0
- package/bin/utils/ulid.mjs +45 -0
- package/bin/utils/webrtc-control-client.mjs +699 -0
- package/bin/utils/webrtc-control-client.test.mjs +318 -0
- package/dist/electron/main.cjs +1 -1
- package/dist/public/AGENTS.md +5 -3
- package/dist/public/README.md +42 -5
- package/dist/public/agent.worker.js +1 -1
- package/dist/public/backup-controller-C_s9in2t.js +1 -0
- package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +1 -0
- package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +1 -0
- package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +1 -1
- package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
- package/dist/public/components/shadow-claw-settings/shadow-claw-settings.html +1 -1
- package/dist/public/config-CrHchneq.js +1 -0
- package/dist/public/{configurePeerJs-B4Ym9VMj.js → configurePeerJs-CxpZeDJJ.js} +1 -1
- package/dist/public/{connections-BEn6JYdu.js → connections-DvbgdkaB.js} +1 -1
- package/dist/public/{custom-element-security-DdYlUozq.js → custom-element-security-NCqQ5lmC.js} +1 -1
- package/dist/public/docs/README.md +3 -1
- package/dist/public/docs/architecture/overview.md +12 -12
- package/dist/public/docs/decisions/bundled-typescript-architecture.md +1 -1
- package/dist/public/docs/decisions/indexeddb-and-opfs-storage.md +2 -2
- package/dist/public/docs/decisions/native-web-components-and-signals.md +1 -1
- package/dist/public/docs/example/article/index.html +1 -1
- package/dist/public/docs/guides/configuring-messaging-channels.md +41 -6
- package/dist/public/docs/guides/server-development-configuration.md +24 -0
- package/dist/public/docs/publishing/index.html +1 -1
- package/dist/public/docs/skill-creator/index.html +1 -1
- package/dist/public/docs/subsystems/backup.md +56 -0
- package/dist/public/docs/subsystems/cli.md +124 -0
- package/dist/public/docs/subsystems/control-plane.md +158 -0
- package/dist/public/docs/subsystems/notifications.md +54 -2
- package/dist/public/docs/subsystems/reactive-ui.md +1 -0
- package/dist/public/downloadGroupFile-By0GR6eV.js +1 -0
- package/dist/public/e2e/README.md +5 -1
- package/dist/public/{e2e-bridge-BQ4KzUt5.js → e2e-bridge-IGWsRl4i.js} +1 -1
- package/dist/public/file-viewer-D0eflgt6.js +1 -0
- package/dist/public/getAllTasks-vpBlgGsc.js +1 -0
- package/dist/public/getGroupDir-P1h9wl6S.js +1 -0
- package/dist/public/{git-DEUvQTUA.js → git-7Vq_SJSp.js} +2 -2
- package/dist/public/{iframe-sanitizer-0NS4XLOd.js → iframe-sanitizer-y2Z1pPwY.js} +1 -1
- package/dist/public/index.html +1 -1
- package/dist/public/index.js +1 -1
- package/dist/public/{initChatSplitResize-llGkSJXg.js → initChatSplitResize-CzV2E7Ul.js} +1 -1
- package/dist/public/initControlPlane-C9jpnWev.js +1 -0
- package/dist/public/llms.txt +1 -1
- package/dist/public/main/index.html +1 -1
- package/dist/public/main/memory/index.html +1 -1
- package/dist/public/{markdown-CyYR4App.js → markdown-BnpKxSxl.js} +1 -1
- package/dist/public/{mcp-reconnect-CSx4aMwy.js → mcp-reconnect-9emsqGRe.js} +1 -1
- package/dist/public/{memoryStorage-Dwg8NhAZ.js → memoryStorage-C0KvLNUp.js} +1 -1
- package/dist/public/{orchestrator-DRfEudXk.js → orchestrator-3h9xxHUm.js} +676 -676
- package/dist/public/orchestrator-nJj59RqD.js +15 -0
- package/dist/public/{peerjs-CurBuKvk.js → peerjs-BP4laMXj.js} +1 -1
- package/dist/public/{push-client-Dii-vPEh.js → push-client-CrjEXK3U.js} +1 -1
- package/dist/public/service-worker/push-handler.js +1 -1
- package/dist/public/service-worker.js +1 -1
- package/dist/public/{shadow-claw-5IHaDDMJ.js → shadow-claw-CbwETBWf.js} +2 -2
- package/dist/public/{shadow-claw-accounts-CZvz4iQz.js → shadow-claw-accounts-B4sBTQEj.js} +1 -1
- package/dist/public/{shadow-claw-channels-BKXFyRS3.js → shadow-claw-channels-BDWG5Qa3.js} +1 -1
- package/dist/public/{shadow-claw-chat-CCLuhWdI.js → shadow-claw-chat-Crv0DTCY.js} +6 -6
- package/dist/public/shadow-claw-control-plane-CX1IGbh9.js +199 -0
- package/dist/public/{shadow-claw-conversations-DV-87UUh.js → shadow-claw-conversations-_9qAaRXk.js} +1 -1
- package/dist/public/shadow-claw-dialog-BrOwAOdk.js +1 -0
- package/dist/public/{shadow-claw-file-viewer-Dk2dbAUd.js → shadow-claw-file-viewer-Blf6yGVD.js} +1 -1
- package/dist/public/shadow-claw-files-CYySBDCl.js +840 -0
- package/dist/public/{shadow-claw-git-DRA7_qck.js → shadow-claw-git-v8S0JjuR.js} +2 -2
- package/dist/public/{shadow-claw-integrations-1Xu5snuV.js → shadow-claw-integrations-D7ufpwhX.js} +1 -1
- package/dist/public/{shadow-claw-llm-6Aci1BDP.js → shadow-claw-llm-GWgoVfhI.js} +2 -2
- package/dist/public/{shadow-claw-mcp-remote-BUjAeVXC.js → shadow-claw-mcp-remote-kY4LMFNN.js} +1 -1
- package/dist/public/{shadow-claw-networking-FQe4nCp3.js → shadow-claw-networking-qHbP95z9.js} +1 -1
- package/dist/public/{shadow-claw-notifications-DV2wibL3.js → shadow-claw-notifications-YyHOK2Fr.js} +1 -1
- package/dist/public/{shadow-claw-page-header-B63S4HTU.js → shadow-claw-page-header-DMKbkv3s.js} +1 -1
- package/dist/public/{shadow-claw-pages-DE3jWj5K.js → shadow-claw-pages-DFZHhzFZ.js} +1 -1
- package/dist/public/{shadow-claw-settings-S6DAKfYx.js → shadow-claw-settings-DhxrKIfA.js} +10 -3
- package/dist/public/{shadow-claw-storage-DaacA3_K.js → shadow-claw-storage-BFynbP0j.js} +2 -2
- package/dist/public/{shadow-claw-task-server-IjKX042p.js → shadow-claw-task-server-3BIeb5oZ.js} +25 -4
- package/dist/public/{shadow-claw-tasks-Cy0JYmGU.js → shadow-claw-tasks-Caxc9atL.js} +1 -1
- package/dist/public/{shadow-claw-terminal-znAt7HyP.js → shadow-claw-terminal-Beb5qF-U.js} +1 -1
- package/dist/public/{shadow-claw-tools-Cutv9Si5.js → shadow-claw-tools-BfJx3A9W.js} +1 -1
- package/dist/public/{shadow-claw-webvm-BH8mPDEu.js → shadow-claw-webvm-BCS8ykeP.js} +1 -1
- package/dist/public/{syncWebMcpRegistration-BqJxaMGR.js → syncWebMcpRegistration-B_M2fCjH.js} +1 -1
- package/dist/server.js +286 -189
- package/package.json +2 -1
- package/src/__mocks__/jszip.cjs +15 -5
- package/src/components/settings/shadow-claw-accounts/shadow-claw-accounts.test.ts +35 -0
- package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +117 -0
- package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +81 -0
- package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.test.ts +127 -0
- package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.ts +292 -0
- package/src/components/settings/shadow-claw-git/shadow-claw-git.test.ts +72 -0
- package/src/components/settings/shadow-claw-integrations/connection-test-auth.test.ts +20 -30
- package/src/components/settings/shadow-claw-integrations/shadow-claw-integrations.test.ts +116 -0
- package/src/components/settings/shadow-claw-llm/model-ranking.test.ts +143 -51
- package/src/components/settings/shadow-claw-llm/shadow-claw-llm.test.ts +307 -4
- package/src/components/settings/shadow-claw-llm/shadow-claw-llm.ts +8 -8
- package/src/components/settings/shadow-claw-mcp-remote/shadow-claw-mcp-remote.test.ts +31 -0
- package/src/components/settings/shadow-claw-peerjs/shadow-claw-peerjs.test.ts +186 -0
- package/src/components/settings/shadow-claw-storage/shadow-claw-storage.test.ts +76 -0
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +23 -2
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +19 -0
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.ts +2 -1
- package/src/components/shadow-claw/utils/initChatSplitResize.test.ts +180 -13
- package/src/components/shadow-claw/utils/setupEffects.test.ts +39 -0
- package/src/components/shadow-claw-a2ui/catalog/basic/shared.test.ts +117 -0
- package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.test.ts +1159 -1
- package/src/components/shadow-claw-chat/shadow-claw-chat.test.ts +1523 -19
- package/src/components/shadow-claw-chat/utils/escapeHtml.test.ts +13 -0
- package/src/components/shadow-claw-chat/utils/getPeerChatDisplayStatus.test.ts +20 -0
- package/src/components/shadow-claw-conversations/shadow-claw-conversations.test.ts +494 -0
- package/src/components/shadow-claw-dialog/shadow-claw-dialog.ts +13 -2
- package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +273 -0
- package/src/components/shadow-claw-files/shadow-claw-files.test.ts +408 -1
- package/src/components/shadow-claw-pdf-viewer/utils/installGetOrInsertComputedPolyfill.test.ts +65 -0
- package/src/components/shadow-claw-settings/shadow-claw-settings.html +7 -0
- package/src/components/shadow-claw-settings/shadow-claw-settings.test.ts +221 -0
- package/src/components/shadow-claw-settings/shadow-claw-settings.ts +2 -0
- package/src/components/shadow-claw-tasks/shadow-claw-tasks.test.ts +407 -0
- package/src/components/shadow-claw-terminal/utils/consumeEscapeSequence.test.ts +58 -0
- package/src/components/shadow-claw-tools/shadow-claw-tools.test.ts +80 -0
- package/src/config/config.ts +10 -0
- package/src/core/backup-controller.test.ts +101 -0
- package/src/core/backup-controller.ts +137 -0
- package/src/core/control-plane-client.test.ts +408 -0
- package/src/core/control-plane-client.ts +425 -0
- package/src/core/orchestrator/orchestrator.ts +4 -0
- package/src/core/orchestrator/utils/compactContext.test.ts +78 -0
- package/src/core/orchestrator/utils/operations/channel.test.ts +254 -9
- package/src/core/orchestrator/utils/operations/provider.test.ts +326 -34
- package/src/core/orchestrator/utils/operations/vm.test.ts +114 -2
- package/src/core/orchestrator/utils/syncProxyConfigToServiceWorker.test.ts +60 -0
- package/src/core/utils/initControlPlane.test.ts +276 -0
- package/src/core/utils/initControlPlane.ts +437 -0
- package/src/core/utils/initializeApp.ts +18 -0
- package/src/db/getSession.test.ts +13 -1
- package/src/server/app.test.ts +12 -0
- package/src/server/app.ts +18 -8
- package/src/server/client-registry.test.ts +250 -0
- package/src/server/client-registry.ts +477 -0
- package/src/server/config.ts +15 -0
- package/src/server/control-plane-types.ts +120 -0
- package/src/server/control-plane.test.ts +472 -0
- package/src/server/control-plane.ts +581 -0
- package/src/server/routes/backup.test.ts +263 -0
- package/src/server/routes/backup.ts +262 -0
- package/src/server/routes/integrations-email.test.ts +532 -0
- package/src/server/routes/llamafile.test.ts +172 -48
- package/src/server/routes/mesh-llm.test.ts +192 -0
- package/src/server/routes/ollama.test.ts +49 -0
- package/src/server/server-peer.test.ts +86 -0
- package/src/server/server-peer.ts +330 -0
- package/src/server/server.ts +35 -0
- package/src/server/services/llamafile-manager.test.ts +230 -6
- package/src/server/services/transformers-runtime.test.ts +186 -18
- package/src/server/webrtc.test.ts +21 -0
- package/src/server/webrtc.ts +72 -0
- package/src/service-worker/fetch-proxy.test.ts +64 -1
- package/src/service-worker/push-handler.ts +39 -0
- package/src/shell/vm.test.ts +744 -12
- package/src/storage/ensureMainGroupIndex.test.ts +153 -0
- package/src/storage/getGroupFile.test.ts +66 -0
- package/src/storage/memoryStorage.test.ts +82 -2
- package/src/storage/writeGroupFileBytes.test.ts +143 -0
- package/src/stores/chat-ui.test.ts +20 -0
- package/src/stores/orchestrator.test.ts +134 -0
- package/src/stores/toast.test.ts +50 -0
- package/src/stores/tools.test.ts +147 -10
- package/src/subsystems/channels/imessage.test.ts +59 -0
- package/src/subsystems/channels/peerjs.test.ts +500 -62
- package/src/subsystems/channels/peerjs.ts +71 -4
- package/src/subsystems/channels/telegram.test.ts +177 -0
- package/src/subsystems/email/catalog.test.ts +1 -0
- package/src/subsystems/email/connections.test.ts +39 -1
- package/src/subsystems/git/credentials.test.ts +160 -825
- package/src/subsystems/git/git.test.ts +463 -0
- package/src/subsystems/notifications/__mocks__/node-sqlite.cjs +12 -1
- package/src/subsystems/notifications/push-routes.test.ts +33 -0
- package/src/subsystems/notifications/push-routes.ts +23 -0
- package/src/subsystems/notifications/push-store.ts +1 -1
- package/src/subsystems/providers/builtin-ai-tasks.test.ts +74 -0
- package/src/subsystems/providers/litert-lm-provider.test.ts +212 -51
- package/src/subsystems/providers/model-registry.test.ts +115 -0
- package/src/subsystems/providers/providers.test.ts +94 -1
- package/src/subsystems/providers/transformers-js-provider.test.ts +279 -0
- package/src/subsystems/providers/utils/createModelCacheFetch.test.ts +2 -2
- package/src/subsystems/providers/utils/downloadModelToCache.test.ts +2 -2
- package/src/subsystems/providers/utils/loadModelStream.test.ts +2 -2
- package/src/subsystems/skills/activateSkill.test.ts +101 -0
- package/src/subsystems/skills/discoverSkills.test.ts +251 -0
- package/src/subsystems/tools/cron.test.ts +64 -0
- package/src/subsystems/tools/declarative.test.ts +255 -89
- package/src/testing/assets/v86.9pfs/libv86.mjs +63 -0
- package/src/testing/assets/v86.ext2/libv86.mjs +3 -0
- package/src/testing/assets_missing_v86/assets/v86.9pfs/libv86.mjs +1 -0
- package/src/testing/jest-setup.ts +328 -40
- package/src/testing/mock-v86.mjs +49 -0
- package/src/ui/a2ui/types.ts +4 -3
- package/src/ui/a2ui/utils/normaliseComponentsToMap.test.ts +102 -0
- package/src/ui/a2ui/utils/resolveChildIds.test.ts +63 -0
- package/src/ui/a2ui/utils/resolveDynamicNumber.test.ts +13 -1
- package/src/worker/tools/bash/utils/waitForVMReady.test.ts +40 -0
- package/src/worker/tools/email/email.test.ts +193 -8
- package/src/worker/tools/email/utils/asEmailAttachmentInputs.test.ts +47 -0
- package/src/worker/tools/git/git.test.ts +214 -3
- package/src/worker/tools/rooms/invite-to-room.test.ts +54 -0
- package/src/worker/tools/rooms/leave-room.test.ts +40 -0
- package/src/worker/tools/rooms/list-room-members.test.ts +75 -0
- package/src/worker/tools/tasks/tasks-utils.test.ts +22 -0
- package/src/worker/tools/ui/ask-user.test.ts +50 -0
- package/src/worker/tools/ui/clear-chat.test.ts +19 -0
- package/src/worker/tools/ui/get-current-time.test.ts +19 -0
- package/src/worker/tools/ui/javascript.test.ts +116 -0
- package/src/worker/tools/ui/list-components.test.ts +10 -0
- package/src/worker/tools/ui/list-tool-profiles.test.ts +42 -0
- package/src/worker/tools/ui/manage-tools.test.ts +34 -0
- package/src/worker/tools/ui/render-component.test.ts +346 -0
- package/src/worker/tools/ui/send-notification.test.ts +40 -0
- package/src/worker/tools/ui/utils/getAllowFullInternetAccess.test.ts +31 -0
- package/src/worker/tools/ui/utils/parseBooleanConfig.test.ts +25 -0
- package/src/worker/tools/workspace/attach-file.test.ts +84 -0
- package/src/worker/tools/workspace/copy-file.test.ts +69 -0
- package/src/worker/tools/workspace/create-directory.test.ts +40 -0
- package/src/worker/tools/workspace/delete-file.test.ts +73 -0
- package/src/worker/tools/workspace/diff-files.test.ts +94 -0
- package/src/worker/tools/workspace/list-files.test.ts +36 -0
- package/src/worker/tools/workspace/move-file.test.ts +66 -0
- package/src/worker/tools/workspace/open-file.test.ts +57 -0
- package/src/worker/tools/workspace/patch-file.test.ts +81 -0
- package/src/worker/tools/workspace/read-file.test.ts +116 -0
- package/src/worker/tools/workspace/send-file.test.ts +82 -0
- package/src/worker/tools/workspace/update-memory.test.ts +29 -0
- package/src/worker/tools/workspace/write-file.test.ts +28 -0
- package/src/worker/utils/rate-limit.test.ts +199 -0
- package/src/worker/utils/tool-state.test.ts +29 -0
- package/src/worker/utils/transformers-js-utils.test.ts +59 -0
- package/dist/public/config-BYAuxx3j.js +0 -1
- package/dist/public/downloadGroupFile-Drdvg1Ev.js +0 -1
- package/dist/public/file-viewer-CcZYw-PS.js +0 -1
- package/dist/public/orchestrator-o9Fj67BN.js +0 -15
- package/dist/public/shadow-claw-dialog-ByyHgxM9.js +0 -1
- package/dist/public/shadow-claw-files-BpfpJIuP.js +0 -840
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ShadowClaw — Client Registry Store backed by node:sqlite
|
|
3
|
+
*
|
|
4
|
+
* Persists registered clients, metadata, and control tokens for the
|
|
5
|
+
* WebSocket / SSE control plane.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { randomBytes } from "node:crypto";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { DatabaseSync } from "node:sqlite";
|
|
12
|
+
import type { ClientInfo, BackupRecord } from "./control-plane-types.js";
|
|
13
|
+
|
|
14
|
+
let db: DatabaseSync | null = null;
|
|
15
|
+
|
|
16
|
+
function rowToClientInfo(row: any): ClientInfo {
|
|
17
|
+
let capabilities: string[] = [];
|
|
18
|
+
try {
|
|
19
|
+
capabilities = JSON.parse(row.capabilities);
|
|
20
|
+
} catch {
|
|
21
|
+
capabilities = [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
clientId: `${row.client_id}`,
|
|
26
|
+
deviceLabel: `${row.device_label}`,
|
|
27
|
+
capabilities,
|
|
28
|
+
version: `${row.version}`,
|
|
29
|
+
peerId: row.peer_id ? `${row.peer_id}` : undefined,
|
|
30
|
+
connectedAt: Number(row.connected_at),
|
|
31
|
+
lastSeen: Number(row.last_seen),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Open (or create) the client registry SQLite database.
|
|
37
|
+
*/
|
|
38
|
+
export function openClientStore(
|
|
39
|
+
dbPath: string = "database/clients.db",
|
|
40
|
+
): DatabaseSync {
|
|
41
|
+
if (db) {
|
|
42
|
+
return db;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
db = new DatabaseSync(dbPath);
|
|
46
|
+
|
|
47
|
+
db.exec(`
|
|
48
|
+
CREATE TABLE IF NOT EXISTS clients (
|
|
49
|
+
client_id TEXT PRIMARY KEY,
|
|
50
|
+
device_label TEXT NOT NULL,
|
|
51
|
+
capabilities TEXT NOT NULL,
|
|
52
|
+
version TEXT NOT NULL,
|
|
53
|
+
peer_id TEXT,
|
|
54
|
+
connected_at INTEGER NOT NULL,
|
|
55
|
+
last_seen INTEGER NOT NULL
|
|
56
|
+
)
|
|
57
|
+
`);
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
db.exec(`ALTER TABLE clients ADD COLUMN peer_id TEXT`);
|
|
61
|
+
} catch (_) {}
|
|
62
|
+
|
|
63
|
+
db.exec(`
|
|
64
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
|
65
|
+
key TEXT PRIMARY KEY,
|
|
66
|
+
value TEXT NOT NULL
|
|
67
|
+
)
|
|
68
|
+
`);
|
|
69
|
+
|
|
70
|
+
db.exec(`
|
|
71
|
+
CREATE TABLE IF NOT EXISTS backups (
|
|
72
|
+
id TEXT PRIMARY KEY,
|
|
73
|
+
client_id TEXT NOT NULL,
|
|
74
|
+
timestamp INTEGER NOT NULL,
|
|
75
|
+
file_count INTEGER NOT NULL,
|
|
76
|
+
total_bytes INTEGER NOT NULL,
|
|
77
|
+
manifest TEXT
|
|
78
|
+
)
|
|
79
|
+
`);
|
|
80
|
+
|
|
81
|
+
return db;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Close the client registry database.
|
|
86
|
+
*/
|
|
87
|
+
export function closeClientStore(): void {
|
|
88
|
+
if (db) {
|
|
89
|
+
db.close();
|
|
90
|
+
db = null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Register or update a connected client.
|
|
96
|
+
*/
|
|
97
|
+
export function registerClient(client: {
|
|
98
|
+
clientId: string;
|
|
99
|
+
deviceLabel: string;
|
|
100
|
+
capabilities: string[];
|
|
101
|
+
version: string;
|
|
102
|
+
peerId?: string;
|
|
103
|
+
}): ClientInfo {
|
|
104
|
+
if (!db) {
|
|
105
|
+
throw new Error("Client store not opened. Call openClientStore() first.");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const now = Date.now();
|
|
109
|
+
const existing = getClient(client.clientId);
|
|
110
|
+
const connectedAt = existing ? existing.connectedAt : now;
|
|
111
|
+
const peerId = client.peerId || existing?.peerId || null;
|
|
112
|
+
const capabilitiesJson = JSON.stringify(client.capabilities || []);
|
|
113
|
+
|
|
114
|
+
db.prepare(
|
|
115
|
+
`
|
|
116
|
+
INSERT INTO clients (client_id, device_label, capabilities, version, peer_id, connected_at, last_seen)
|
|
117
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
118
|
+
ON CONFLICT(client_id) DO UPDATE SET
|
|
119
|
+
device_label = excluded.device_label,
|
|
120
|
+
capabilities = excluded.capabilities,
|
|
121
|
+
version = excluded.version,
|
|
122
|
+
peer_id = excluded.peer_id,
|
|
123
|
+
last_seen = excluded.last_seen
|
|
124
|
+
`,
|
|
125
|
+
).run(
|
|
126
|
+
client.clientId,
|
|
127
|
+
client.deviceLabel,
|
|
128
|
+
capabilitiesJson,
|
|
129
|
+
client.version,
|
|
130
|
+
peerId,
|
|
131
|
+
connectedAt,
|
|
132
|
+
now,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
clientId: client.clientId,
|
|
137
|
+
deviceLabel: client.deviceLabel,
|
|
138
|
+
capabilities: client.capabilities,
|
|
139
|
+
version: client.version,
|
|
140
|
+
peerId: peerId || undefined,
|
|
141
|
+
connectedAt,
|
|
142
|
+
lastSeen: now,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Update the lastSeen timestamp for a client.
|
|
148
|
+
*/
|
|
149
|
+
export function updateClientHeartbeat(
|
|
150
|
+
clientId: string,
|
|
151
|
+
timestamp: number = Date.now(),
|
|
152
|
+
): boolean {
|
|
153
|
+
if (!db) {
|
|
154
|
+
throw new Error("Client store not opened. Call openClientStore() first.");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const result = db
|
|
158
|
+
.prepare("UPDATE clients SET last_seen = ? WHERE client_id = ?")
|
|
159
|
+
.run(timestamp, clientId);
|
|
160
|
+
|
|
161
|
+
return Number(result.changes) > 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Remove a registered client.
|
|
166
|
+
*/
|
|
167
|
+
export function unregisterClient(clientId: string): boolean {
|
|
168
|
+
if (!db) {
|
|
169
|
+
throw new Error("Client store not opened. Call openClientStore() first.");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const result = db
|
|
173
|
+
.prepare("DELETE FROM clients WHERE client_id = ?")
|
|
174
|
+
.run(clientId);
|
|
175
|
+
|
|
176
|
+
return Number(result.changes) > 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Retrieve a registered client by ID.
|
|
181
|
+
*/
|
|
182
|
+
export function getClient(clientId: string): ClientInfo | null {
|
|
183
|
+
if (!db) {
|
|
184
|
+
throw new Error("Client store not opened. Call openClientStore() first.");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const row = db
|
|
188
|
+
.prepare("SELECT * FROM clients WHERE client_id = ?")
|
|
189
|
+
.get(clientId);
|
|
190
|
+
|
|
191
|
+
return row ? rowToClientInfo(row) : null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get all registered clients ordered by last seen.
|
|
196
|
+
*/
|
|
197
|
+
export function getAllClients(): ClientInfo[] {
|
|
198
|
+
if (!db) {
|
|
199
|
+
throw new Error("Client store not opened. Call openClientStore() first.");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const rows = db
|
|
203
|
+
.prepare("SELECT * FROM clients ORDER BY last_seen DESC")
|
|
204
|
+
.all();
|
|
205
|
+
|
|
206
|
+
return rows.map(rowToClientInfo);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Prune clients that have not sent a heartbeat within maxAgeMs.
|
|
211
|
+
*/
|
|
212
|
+
export function pruneStaleClients(
|
|
213
|
+
maxAgeMs: number,
|
|
214
|
+
now: number = Date.now(),
|
|
215
|
+
): number {
|
|
216
|
+
if (!db) {
|
|
217
|
+
throw new Error("Client store not opened. Call openClientStore() first.");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const cutoff = now - maxAgeMs;
|
|
221
|
+
const result = db
|
|
222
|
+
.prepare("DELETE FROM clients WHERE last_seen < ?")
|
|
223
|
+
.run(cutoff);
|
|
224
|
+
|
|
225
|
+
return Number(result.changes);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Interface for the persisted control token file.
|
|
230
|
+
*/
|
|
231
|
+
export interface ControlTokenFile {
|
|
232
|
+
token: string;
|
|
233
|
+
createdAt: number;
|
|
234
|
+
createdAtIso: string;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Resolve the path to .cache/control-token.json
|
|
239
|
+
*/
|
|
240
|
+
export function getControlTokenFilePath(cacheDir?: string): string {
|
|
241
|
+
if (cacheDir) {
|
|
242
|
+
return path.join(cacheDir, "control-token.json");
|
|
243
|
+
}
|
|
244
|
+
return path.join(process.cwd(), ".cache", "control-token.json");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Save the control token and timestamp to .cache/control-token.json
|
|
249
|
+
*/
|
|
250
|
+
export function saveControlTokenFile(
|
|
251
|
+
token: string,
|
|
252
|
+
cacheDir?: string,
|
|
253
|
+
createdAt: number = Date.now(),
|
|
254
|
+
): ControlTokenFile {
|
|
255
|
+
const filePath = getControlTokenFilePath(cacheDir);
|
|
256
|
+
const dir = path.dirname(filePath);
|
|
257
|
+
if (!fs.existsSync(dir)) {
|
|
258
|
+
try {
|
|
259
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
260
|
+
} catch (_) {}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let created = createdAt;
|
|
264
|
+
if (fs.existsSync(filePath)) {
|
|
265
|
+
try {
|
|
266
|
+
const existing = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
267
|
+
if (existing && existing.token === token && existing.createdAt) {
|
|
268
|
+
created = Number(existing.createdAt);
|
|
269
|
+
}
|
|
270
|
+
} catch (_) {}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const payload: ControlTokenFile = {
|
|
274
|
+
token,
|
|
275
|
+
createdAt: created,
|
|
276
|
+
createdAtIso: new Date(created).toISOString(),
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2) + "\n", "utf8");
|
|
281
|
+
} catch (_) {}
|
|
282
|
+
|
|
283
|
+
return payload;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Read the control token and timestamp from .cache/control-token.json
|
|
288
|
+
*/
|
|
289
|
+
export function readControlTokenFile(
|
|
290
|
+
cacheDir?: string,
|
|
291
|
+
): ControlTokenFile | null {
|
|
292
|
+
const filePath = getControlTokenFilePath(cacheDir);
|
|
293
|
+
if (!fs.existsSync(filePath)) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
298
|
+
const parsed = JSON.parse(raw);
|
|
299
|
+
if (parsed && typeof parsed.token === "string" && parsed.token.trim()) {
|
|
300
|
+
const created = Number(parsed.createdAt) || Date.now();
|
|
301
|
+
return {
|
|
302
|
+
token: parsed.token.trim(),
|
|
303
|
+
createdAt: created,
|
|
304
|
+
createdAtIso: parsed.createdAtIso || new Date(created).toISOString(),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
} catch (_) {}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Generate a cryptographically secure random control token (32 bytes = 64 hex characters = 256 bits).
|
|
313
|
+
*/
|
|
314
|
+
export function generateControlToken(bytes: number = 32): string {
|
|
315
|
+
return randomBytes(bytes).toString("hex");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Get or generate a persistent control token for authenticating control-plane commands.
|
|
320
|
+
*/
|
|
321
|
+
export function getOrCreateControlToken(
|
|
322
|
+
configuredToken?: string,
|
|
323
|
+
cacheDir?: string,
|
|
324
|
+
): string {
|
|
325
|
+
if (configuredToken) {
|
|
326
|
+
saveControlTokenFile(configuredToken, cacheDir);
|
|
327
|
+
if (db) {
|
|
328
|
+
db.prepare(
|
|
329
|
+
`
|
|
330
|
+
INSERT INTO metadata (key, value)
|
|
331
|
+
VALUES ('control_token', ?)
|
|
332
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
333
|
+
`,
|
|
334
|
+
).run(configuredToken);
|
|
335
|
+
}
|
|
336
|
+
return configuredToken;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// 1. Check if token file already exists in .cache/control-token.json
|
|
340
|
+
const existingFile = readControlTokenFile(cacheDir);
|
|
341
|
+
if (existingFile?.token) {
|
|
342
|
+
if (db) {
|
|
343
|
+
db.prepare(
|
|
344
|
+
`
|
|
345
|
+
INSERT INTO metadata (key, value)
|
|
346
|
+
VALUES ('control_token', ?)
|
|
347
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
348
|
+
`,
|
|
349
|
+
).run(existingFile.token);
|
|
350
|
+
}
|
|
351
|
+
return existingFile.token;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// 2. Check if token exists in SQLite metadata table
|
|
355
|
+
if (db) {
|
|
356
|
+
const row = db
|
|
357
|
+
.prepare("SELECT value FROM metadata WHERE key = 'control_token'")
|
|
358
|
+
.get() as { value: string } | undefined;
|
|
359
|
+
|
|
360
|
+
if (row?.value) {
|
|
361
|
+
const token = `${row.value}`;
|
|
362
|
+
saveControlTokenFile(token, cacheDir);
|
|
363
|
+
return token;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// 3. Generate new cryptographically secure random token and persist to file + SQLite
|
|
368
|
+
const token = generateControlToken();
|
|
369
|
+
saveControlTokenFile(token, cacheDir);
|
|
370
|
+
|
|
371
|
+
if (db) {
|
|
372
|
+
db.prepare(
|
|
373
|
+
`
|
|
374
|
+
INSERT INTO metadata (key, value)
|
|
375
|
+
VALUES ('control_token', ?)
|
|
376
|
+
`,
|
|
377
|
+
).run(token);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return token;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Record a completed backup in the database.
|
|
385
|
+
*/
|
|
386
|
+
export function recordBackup(backup: {
|
|
387
|
+
id: string;
|
|
388
|
+
clientId: string;
|
|
389
|
+
timestamp?: number;
|
|
390
|
+
fileCount: number;
|
|
391
|
+
totalBytes: number;
|
|
392
|
+
manifest?: any;
|
|
393
|
+
}): BackupRecord {
|
|
394
|
+
if (!db) {
|
|
395
|
+
throw new Error("Client store not opened. Call openClientStore() first.");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const timestamp = backup.timestamp || Date.now();
|
|
399
|
+
const manifestJson = backup.manifest ? JSON.stringify(backup.manifest) : null;
|
|
400
|
+
|
|
401
|
+
db.prepare(
|
|
402
|
+
`
|
|
403
|
+
INSERT INTO backups (id, client_id, timestamp, file_count, total_bytes, manifest)
|
|
404
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
405
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
406
|
+
timestamp = excluded.timestamp,
|
|
407
|
+
file_count = excluded.file_count,
|
|
408
|
+
total_bytes = excluded.total_bytes,
|
|
409
|
+
manifest = excluded.manifest
|
|
410
|
+
`,
|
|
411
|
+
).run(
|
|
412
|
+
backup.id,
|
|
413
|
+
backup.clientId,
|
|
414
|
+
timestamp,
|
|
415
|
+
backup.fileCount,
|
|
416
|
+
backup.totalBytes,
|
|
417
|
+
manifestJson,
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
id: backup.id,
|
|
422
|
+
clientId: backup.clientId,
|
|
423
|
+
timestamp,
|
|
424
|
+
fileCount: backup.fileCount,
|
|
425
|
+
totalBytes: backup.totalBytes,
|
|
426
|
+
manifest: backup.manifest,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* List backups, optionally filtered by clientId.
|
|
432
|
+
*/
|
|
433
|
+
export function listBackups(clientId?: string): BackupRecord[] {
|
|
434
|
+
if (!db) {
|
|
435
|
+
throw new Error("Client store not opened. Call openClientStore() first.");
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
let rows: any[];
|
|
439
|
+
if (clientId) {
|
|
440
|
+
rows = db
|
|
441
|
+
.prepare(
|
|
442
|
+
"SELECT * FROM backups WHERE client_id = ? ORDER BY timestamp DESC",
|
|
443
|
+
)
|
|
444
|
+
.all(clientId);
|
|
445
|
+
} else {
|
|
446
|
+
rows = db.prepare("SELECT * FROM backups ORDER BY timestamp DESC").all();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return rows.map((row) => ({
|
|
450
|
+
id: `${row.id}`,
|
|
451
|
+
clientId: `${row.client_id}`,
|
|
452
|
+
timestamp: Number(row.timestamp),
|
|
453
|
+
fileCount: Number(row.file_count),
|
|
454
|
+
totalBytes: Number(row.total_bytes),
|
|
455
|
+
manifest: row.manifest ? JSON.parse(row.manifest) : undefined,
|
|
456
|
+
}));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Delete a backup record by ID.
|
|
461
|
+
*/
|
|
462
|
+
export function deleteBackup(id: string, clientId?: string): boolean {
|
|
463
|
+
if (!db) {
|
|
464
|
+
throw new Error("Client store not opened. Call openClientStore() first.");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
let result;
|
|
468
|
+
if (clientId) {
|
|
469
|
+
result = db
|
|
470
|
+
.prepare("DELETE FROM backups WHERE id = ? AND client_id = ?")
|
|
471
|
+
.run(id, clientId);
|
|
472
|
+
} else {
|
|
473
|
+
result = db.prepare("DELETE FROM backups WHERE id = ?").run(id);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return Number(result.changes) > 0;
|
|
477
|
+
}
|
package/src/server/config.ts
CHANGED
|
@@ -22,6 +22,10 @@ export interface ServerConfig {
|
|
|
22
22
|
* allowing requests to loopback, RFC-1918, and link-local addresses.
|
|
23
23
|
* Controlled by --allow-private-proxy (or SHADOWCLAW_ALLOW_PRIVATE_PROXY=1).
|
|
24
24
|
*/
|
|
25
|
+
/**
|
|
26
|
+
* Optional control token override for authenticating control-plane commands.
|
|
27
|
+
*/
|
|
28
|
+
controlToken?: string;
|
|
25
29
|
allowPrivateProxy: boolean;
|
|
26
30
|
}
|
|
27
31
|
|
|
@@ -59,6 +63,10 @@ export function parseConfig(): ServerConfig {
|
|
|
59
63
|
.option(
|
|
60
64
|
"--database-dir <path>",
|
|
61
65
|
"Directory where SQLite databases are stored",
|
|
66
|
+
)
|
|
67
|
+
.option(
|
|
68
|
+
"--control-token <token>",
|
|
69
|
+
"Secret token for control-plane authentication",
|
|
62
70
|
);
|
|
63
71
|
|
|
64
72
|
program.parse();
|
|
@@ -164,6 +172,12 @@ export function parseConfig(): ServerConfig {
|
|
|
164
172
|
(env.SHADOWCLAW_ALLOW_PRIVATE_PROXY || "").toLowerCase().trim(),
|
|
165
173
|
);
|
|
166
174
|
|
|
175
|
+
// Control Token
|
|
176
|
+
const envControlToken = (env.SHADOWCLAW_CONTROL_TOKEN || "").trim();
|
|
177
|
+
const controlToken = options.controlToken
|
|
178
|
+
? options.controlToken.trim()
|
|
179
|
+
: envControlToken || undefined;
|
|
180
|
+
|
|
167
181
|
return {
|
|
168
182
|
port,
|
|
169
183
|
bindHost,
|
|
@@ -173,6 +187,7 @@ export function parseConfig(): ServerConfig {
|
|
|
173
187
|
peerjs,
|
|
174
188
|
rootPath,
|
|
175
189
|
databaseDir,
|
|
190
|
+
controlToken,
|
|
176
191
|
allowPrivateProxy,
|
|
177
192
|
};
|
|
178
193
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ShadowClaw — Control Plane Protocol Types
|
|
3
|
+
*
|
|
4
|
+
* Shared message types for the WebSocket control-plane channel between
|
|
5
|
+
* the Express server, CLI commands, and connected browser/Electron clients.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Envelope
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
/** Every control-plane message is wrapped in this envelope. */
|
|
13
|
+
export interface ControlMessage {
|
|
14
|
+
/** ULID or UUID for request/response correlation. */
|
|
15
|
+
id: string;
|
|
16
|
+
/** Message type discriminator. */
|
|
17
|
+
type: string;
|
|
18
|
+
/** For responses — the `id` of the originating request. */
|
|
19
|
+
replyTo?: string;
|
|
20
|
+
/** Type-specific payload. */
|
|
21
|
+
payload: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Client → Server messages
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
export type ControlPlaneTransport = "sse" | "websocket";
|
|
29
|
+
|
|
30
|
+
export interface ClientRegisterPayload {
|
|
31
|
+
clientId: string;
|
|
32
|
+
deviceLabel: string;
|
|
33
|
+
capabilities: string[];
|
|
34
|
+
version: string;
|
|
35
|
+
transport?: ControlPlaneTransport;
|
|
36
|
+
peerId?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ClientHeartbeatPayload {
|
|
40
|
+
clientId: string;
|
|
41
|
+
timestamp?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type HeartbeatPayload = ClientHeartbeatPayload;
|
|
45
|
+
|
|
46
|
+
export interface CommandResultPayload {
|
|
47
|
+
commandId: string;
|
|
48
|
+
success: boolean;
|
|
49
|
+
data?: unknown;
|
|
50
|
+
error?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface BackupReadyPayload {
|
|
54
|
+
clientId: string;
|
|
55
|
+
groupId?: string;
|
|
56
|
+
totalBytes: number;
|
|
57
|
+
fileCount: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Server → Client messages
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
export type CommandAction =
|
|
65
|
+
| "send-message"
|
|
66
|
+
| "list-tasks"
|
|
67
|
+
| "trigger-backup"
|
|
68
|
+
| "invoke-tool"
|
|
69
|
+
| "read-state";
|
|
70
|
+
|
|
71
|
+
export interface CommandExecutePayload {
|
|
72
|
+
commandId: string;
|
|
73
|
+
action: CommandAction;
|
|
74
|
+
args: Record<string, unknown>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface BackupAckPayload {
|
|
78
|
+
uploadUrl: string;
|
|
79
|
+
token: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface BackupRecord {
|
|
83
|
+
id: string;
|
|
84
|
+
clientId: string;
|
|
85
|
+
timestamp: number;
|
|
86
|
+
fileCount: number;
|
|
87
|
+
totalBytes: number;
|
|
88
|
+
manifest?: unknown;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Client info (stored in registry)
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
export interface ClientInfo {
|
|
96
|
+
clientId: string;
|
|
97
|
+
deviceLabel: string;
|
|
98
|
+
capabilities: string[];
|
|
99
|
+
version: string;
|
|
100
|
+
transport?: ControlPlaneTransport;
|
|
101
|
+
peerId?: string;
|
|
102
|
+
connectedAt: number;
|
|
103
|
+
lastSeen: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Type discriminated unions for dispatch
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
export type ClientToServerType =
|
|
111
|
+
| "client:register"
|
|
112
|
+
| "client:heartbeat"
|
|
113
|
+
| "command:result"
|
|
114
|
+
| "backup:ready";
|
|
115
|
+
|
|
116
|
+
export type ServerToClientType =
|
|
117
|
+
| "command:execute"
|
|
118
|
+
| "backup:ack"
|
|
119
|
+
| "server:registered"
|
|
120
|
+
| "server:error";
|