shadow-claw 1.27.0 → 1.28.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/.well-known/ai-catalog.json +11 -0
- package/.well-known/mcp/index.html +19 -0
- package/.well-known/mcp/server-card.json +52 -0
- package/.well-known/mcp/servers.json +15 -0
- package/.well-known/mcp.json +52 -0
- package/.well-known/oauth-protected-resource +10 -0
- package/.well-known/oauth-protected-resource.json +10 -0
- package/bin/assert-version-bump.mjs +61 -3
- package/bin/assert-version-bump.test.mjs +77 -1
- package/bin/build/build.mjs +116 -49
- package/bin/build/build.test.mjs +103 -0
- package/bin/cli.mjs +299 -237
- package/bin/cli.test.mjs +27 -0
- package/bin/commands/mcp.mjs +132 -2
- package/bin/commands/mcp.test.mjs +166 -0
- package/bin/commands/skills-index.mjs +139 -20
- package/bin/commands/skills-index.test.mjs +82 -0
- package/bin/utils/control-client.mjs +17 -0
- package/bin/utils/control-client.test.mjs +46 -0
- package/bin/version.mjs +208 -0
- package/bin/version.test.mjs +138 -0
- package/dist/electron/main.cjs +195 -185
- package/dist/lib/components/index.js +21 -21
- package/dist/lib/components/settings/shadow-claw-channel-config.js +1 -1
- package/dist/lib/components/settings/shadow-claw-control-plane.js +1 -1
- package/dist/lib/components/settings/shadow-claw-git.js +1 -1
- package/dist/lib/components/settings/shadow-claw-llm.js +1 -1
- package/dist/lib/components/settings/shadow-claw-networking.js +1 -1
- package/dist/lib/components/settings/shadow-claw-notifications.js +1 -1
- package/dist/lib/components/settings/shadow-claw-peerjs.js +1 -1
- package/dist/lib/components/settings/shadow-claw-storage.js +1 -1
- package/dist/lib/components/settings/shadow-claw-task-server.js +1 -1
- package/dist/lib/components/settings/shadow-claw-webvm.js +1 -1
- package/dist/lib/components/shadow-claw/shadow-claw.d.ts +1 -0
- package/dist/lib/components/shadow-claw/utils/setupAppLifecycle.d.ts +23 -0
- package/dist/lib/components/shadow-claw-channel-config.js +1 -1
- package/dist/lib/components/shadow-claw-channels.js +1 -1
- package/dist/lib/components/shadow-claw-chat.js +1 -1
- package/dist/lib/components/shadow-claw-control-plane.js +1 -1
- package/dist/lib/components/shadow-claw-conversations.js +1 -1
- package/dist/lib/components/shadow-claw-file-viewer.js +1 -1
- package/dist/lib/components/shadow-claw-files.js +1 -1
- package/dist/lib/components/shadow-claw-git.js +1 -1
- package/dist/lib/components/shadow-claw-llm.js +1 -1
- package/dist/lib/components/shadow-claw-networking.js +1 -1
- package/dist/lib/components/shadow-claw-notifications.js +1 -1
- package/dist/lib/components/shadow-claw-pages.js +1 -1
- package/dist/lib/components/shadow-claw-peerjs.js +1 -1
- package/dist/lib/components/shadow-claw-settings.js +1 -1
- package/dist/lib/components/shadow-claw-storage.js +1 -1
- package/dist/lib/components/shadow-claw-task-server.js +1 -1
- package/dist/lib/components/shadow-claw-tasks.js +1 -1
- package/dist/lib/components/shadow-claw-terminal.js +1 -1
- package/dist/lib/components/shadow-claw-tools.js +1 -1
- package/dist/lib/components/shadow-claw-webvm.js +1 -1
- package/dist/lib/components/shadow-claw.js +1 -1
- package/dist/lib/{configurePeerJs-oODzltaC.js → configurePeerJs-D8Ph0-A9.js} +2 -2
- package/dist/lib/{configurePeerJs-oODzltaC.js.map → configurePeerJs-D8Ph0-A9.js.map} +1 -1
- package/dist/lib/core/control-plane-client.d.ts +2 -0
- package/dist/lib/core/orchestrator/orchestrator.d.ts +10 -0
- package/dist/lib/core/orchestrator/utils/operations/channel.d.ts +2 -2
- package/dist/lib/core/utils/initControlPlane.d.ts +6 -0
- package/dist/lib/index.js +21 -21
- package/dist/lib/{initControlPlane-C2zURVIS.js → initControlPlane-BIrWmpzc.js} +76 -9
- package/dist/lib/initControlPlane-BIrWmpzc.js.map +1 -0
- package/dist/lib/{markdown-C6EHl9zs.js → markdown-DEUoM1c6.js} +2 -2
- package/dist/lib/{markdown-C6EHl9zs.js.map → markdown-DEUoM1c6.js.map} +1 -1
- package/dist/lib/{orchestrator-KRjx0eba.js → orchestrator-BtVj9bBB.js} +11 -10
- package/dist/lib/{orchestrator-KRjx0eba.js.map → orchestrator-BtVj9bBB.js.map} +1 -1
- package/dist/lib/{orchestrator-CACLa_Ro.js → orchestrator-CFCW-l1O.js} +52 -4
- package/dist/lib/orchestrator-CFCW-l1O.js.map +1 -0
- package/dist/lib/{peerjs-DFWOBcXf.js → peerjs-BoBkx9DJ.js} +23 -3
- package/dist/lib/peerjs-BoBkx9DJ.js.map +1 -0
- package/dist/lib/{push-client-_tiWQsCT.js → push-client-D6hbzDNV.js} +10 -4
- package/dist/lib/push-client-D6hbzDNV.js.map +1 -0
- package/dist/lib/{shadow-claw-6UHdiy4a.js → shadow-claw-Cuf2O5bZ.js} +71 -4
- package/dist/lib/shadow-claw-Cuf2O5bZ.js.map +1 -0
- package/dist/lib/{shadow-claw-channel-config-DmfwJc6G.js → shadow-claw-channel-config-BDPKt6Bd.js} +3 -3
- package/dist/lib/{shadow-claw-channel-config-DmfwJc6G.js.map → shadow-claw-channel-config-BDPKt6Bd.js.map} +1 -1
- package/dist/lib/{shadow-claw-channels-1N6Jl-By.js → shadow-claw-channels-BsP5JABM.js} +2 -2
- package/dist/lib/{shadow-claw-channels-1N6Jl-By.js.map → shadow-claw-channels-BsP5JABM.js.map} +1 -1
- package/dist/lib/{shadow-claw-chat-BMoqQJRK.js → shadow-claw-chat-Cam2lX4k.js} +4 -4
- package/dist/lib/{shadow-claw-chat-BMoqQJRK.js.map → shadow-claw-chat-Cam2lX4k.js.map} +1 -1
- package/dist/lib/{shadow-claw-control-plane-UG0o7pCz.js → shadow-claw-control-plane-DB2qcCFs.js} +20 -10
- package/dist/lib/shadow-claw-control-plane-DB2qcCFs.js.map +1 -0
- package/dist/lib/{shadow-claw-conversations-BwXep3iu.js → shadow-claw-conversations-C9Vf4XGx.js} +2 -2
- package/dist/lib/{shadow-claw-conversations-BwXep3iu.js.map → shadow-claw-conversations-C9Vf4XGx.js.map} +1 -1
- package/dist/lib/{shadow-claw-file-viewer-DUxzVxDT.js → shadow-claw-file-viewer-B4HYiI0v.js} +3 -3
- package/dist/lib/{shadow-claw-file-viewer-DUxzVxDT.js.map → shadow-claw-file-viewer-B4HYiI0v.js.map} +1 -1
- package/dist/lib/{shadow-claw-files-DIREEnT2.js → shadow-claw-files-DVROUESO.js} +2 -2
- package/dist/lib/{shadow-claw-files-DIREEnT2.js.map → shadow-claw-files-DVROUESO.js.map} +1 -1
- package/dist/lib/{shadow-claw-git-Bg1SWgsF.js → shadow-claw-git-BqrO7cHJ.js} +3 -3
- package/dist/lib/{shadow-claw-git-Bg1SWgsF.js.map → shadow-claw-git-BqrO7cHJ.js.map} +1 -1
- package/dist/lib/{shadow-claw-llm-CazBrVpM.js → shadow-claw-llm-BexUQdYo.js} +2 -2
- package/dist/lib/{shadow-claw-llm-CazBrVpM.js.map → shadow-claw-llm-BexUQdYo.js.map} +1 -1
- package/dist/lib/{shadow-claw-networking-Df85tCbO.js → shadow-claw-networking-CfXRGpUD.js} +2 -2
- package/dist/lib/{shadow-claw-networking-Df85tCbO.js.map → shadow-claw-networking-CfXRGpUD.js.map} +1 -1
- package/dist/lib/{shadow-claw-notifications-v62FryTc.js → shadow-claw-notifications-C9KPMgx_.js} +2 -2
- package/dist/lib/{shadow-claw-notifications-v62FryTc.js.map → shadow-claw-notifications-C9KPMgx_.js.map} +1 -1
- package/dist/lib/{shadow-claw-pages-BgPyIEZO.js → shadow-claw-pages-CGzGXmrI.js} +3 -3
- package/dist/lib/{shadow-claw-pages-BgPyIEZO.js.map → shadow-claw-pages-CGzGXmrI.js.map} +1 -1
- package/dist/lib/{shadow-claw-peerjs-xkBXILed.js → shadow-claw-peerjs-B0ZB7yhB.js} +3 -3
- package/dist/lib/{shadow-claw-peerjs-xkBXILed.js.map → shadow-claw-peerjs-B0ZB7yhB.js.map} +1 -1
- package/dist/lib/{shadow-claw-settings-CN89xRVU.js → shadow-claw-settings-DBfMZdDS.js} +2 -2
- package/dist/lib/{shadow-claw-settings-CN89xRVU.js.map → shadow-claw-settings-DBfMZdDS.js.map} +1 -1
- package/dist/lib/{shadow-claw-storage-VnhcObCe.js → shadow-claw-storage-CM-w6mgG.js} +2 -2
- package/dist/lib/{shadow-claw-storage-VnhcObCe.js.map → shadow-claw-storage-CM-w6mgG.js.map} +1 -1
- package/dist/lib/{shadow-claw-task-server-jgEC2hHB.js → shadow-claw-task-server-DV4b1g2-.js} +4 -2
- package/dist/lib/{shadow-claw-task-server-jgEC2hHB.js.map → shadow-claw-task-server-DV4b1g2-.js.map} +1 -1
- package/dist/lib/{shadow-claw-tasks-i_evIufy.js → shadow-claw-tasks-K-foOHSE.js} +3 -3
- package/dist/lib/{shadow-claw-tasks-i_evIufy.js.map → shadow-claw-tasks-K-foOHSE.js.map} +1 -1
- package/dist/lib/{shadow-claw-terminal-CwScROlu.js → shadow-claw-terminal-CpAknKyA.js} +2 -2
- package/dist/lib/{shadow-claw-terminal-CwScROlu.js.map → shadow-claw-terminal-CpAknKyA.js.map} +1 -1
- package/dist/lib/{shadow-claw-tools-DD3fyXP4.js → shadow-claw-tools-D98XZ0Ep.js} +3 -3
- package/dist/lib/{shadow-claw-tools-DD3fyXP4.js.map → shadow-claw-tools-D98XZ0Ep.js.map} +1 -1
- package/dist/lib/{shadow-claw-webvm-DbJq4I3-.js → shadow-claw-webvm-DLOb8dMI.js} +2 -2
- package/dist/lib/{shadow-claw-webvm-DbJq4I3-.js.map → shadow-claw-webvm-DLOb8dMI.js.map} +1 -1
- package/dist/lib/subsystems/channels/imessage.d.ts +1 -0
- package/dist/lib/subsystems/channels/peerjs.d.ts +1 -0
- package/dist/lib/subsystems/channels/telegram.d.ts +1 -0
- package/dist/lib/subsystems/channels/types.d.ts +2 -0
- package/dist/lib/subsystems/notifications/push-client.d.ts +1 -1
- package/dist/lib/{syncWebMcpRegistration-DNLnQ-jz.js → syncWebMcpRegistration-NspKefOI.js} +2 -2
- package/dist/lib/{syncWebMcpRegistration-DNLnQ-jz.js.map → syncWebMcpRegistration-NspKefOI.js.map} +1 -1
- package/dist/public/.well-known/ai-catalog.json +11 -0
- package/dist/public/.well-known/mcp/index.html +19 -0
- package/dist/public/.well-known/mcp/server-card.json +52 -0
- package/dist/public/.well-known/mcp/servers.json +15 -0
- package/dist/public/.well-known/mcp.json +52 -0
- package/dist/public/.well-known/oauth-protected-resource +10 -0
- package/dist/public/.well-known/oauth-protected-resource.json +10 -0
- package/dist/public/404.html +28 -1
- package/dist/public/AGENTS.md +1 -1
- package/dist/public/agent.worker.js +1 -1
- package/dist/public/{configurePeerJs-BqgeBjP7.js → configurePeerJs-BhBokQlC.js} +1 -1
- package/dist/public/docs/example/article/index.html +3 -3
- package/dist/public/docs/publishing/index.html +3 -3
- package/dist/public/docs/skill-creator/index.html +3 -3
- package/dist/public/docs/subsystems/cli.md +11 -8
- package/dist/public/docs/subsystems/mcp-server.md +1 -0
- package/dist/public/docs/subsystems/notifications.md +10 -0
- package/dist/public/{e2e-bridge-Ceqr5kGc.js → e2e-bridge-DcBoeFvd.js} +1 -1
- package/dist/public/{file-viewer-C-6Jc_tb.js → file-viewer-CKtVC9Ix.js} +1 -1
- package/dist/public/index.css +1 -1
- package/dist/public/index.html +3 -3
- package/dist/public/index.js +1 -1
- package/dist/public/initControlPlane-Bi8ouFEb.js +1 -0
- package/dist/public/llms.txt +36 -1
- package/dist/public/main/index.html +3 -3
- package/dist/public/main/memory/index.html +3 -3
- package/dist/public/{markdown-C3M4OW_A.js → markdown-C-hCVJtc.js} +1 -1
- package/dist/public/orchestrator-8qNGfw8r.js +15 -0
- package/dist/public/{orchestrator-DHjdLtqa.js → orchestrator-BT3Ks5ip.js} +2 -2
- package/dist/public/peerjs-DfhwEiiq.js +1 -0
- package/dist/public/push-client-BVLrE9pK.js +1 -0
- package/dist/public/service-worker/fetch-proxy.js +1 -1
- package/dist/public/service-worker.js +1 -1
- package/dist/public/{shadow-claw-EAt2ml5Y.js → shadow-claw-DCjxNDCX.js} +3 -3
- package/dist/public/{shadow-claw-channels-XsfuWFF8.js → shadow-claw-channels-CITDrqMl.js} +1 -1
- package/dist/public/{shadow-claw-chat-CbSaOZ4q.js → shadow-claw-chat-BQrSgSKF.js} +1 -1
- package/dist/public/{shadow-claw-control-plane-DHvN3Ukg.js → shadow-claw-control-plane-Cp001OY0.js} +2 -2
- package/dist/public/{shadow-claw-conversations-BWs5VVfs.js → shadow-claw-conversations-EhX4RvIj.js} +1 -1
- package/dist/public/{shadow-claw-file-viewer-BWsoW5QS.js → shadow-claw-file-viewer-DiQXIjZD.js} +1 -1
- package/dist/public/{shadow-claw-files-CG7MqWiV.js → shadow-claw-files-lkTnSJsn.js} +1 -1
- package/dist/public/{shadow-claw-git-DBRPZNav.js → shadow-claw-git-BlcA_z8x.js} +2 -2
- package/dist/public/{shadow-claw-llm-DMvLgKXo.js → shadow-claw-llm-CW16X9Rg.js} +1 -1
- package/dist/public/{shadow-claw-networking-Dd8Rw31D.js → shadow-claw-networking-ySsMBY_i.js} +1 -1
- package/dist/public/{shadow-claw-notifications-Dbevj-lq.js → shadow-claw-notifications-CyV2zCQ8.js} +1 -1
- package/dist/public/{shadow-claw-pages-3_lQAv0t.js → shadow-claw-pages-Cx7cF9ah.js} +1 -1
- package/dist/public/{shadow-claw-settings-t8TIlgrM.js → shadow-claw-settings-zm4PHHGi.js} +2 -2
- package/dist/public/{shadow-claw-storage-CfEXoSIE.js → shadow-claw-storage-dy9JAEGd.js} +1 -1
- package/dist/public/{shadow-claw-task-server-DxQSsgRZ.js → shadow-claw-task-server-Bleww48s.js} +2 -2
- package/dist/public/{shadow-claw-tasks-DNpicQwz.js → shadow-claw-tasks-CTI5oaLN.js} +1 -1
- package/dist/public/{shadow-claw-terminal-DpY6zU_r.js → shadow-claw-terminal-mZ3i4HaQ.js} +1 -1
- package/dist/public/{shadow-claw-tools-WSwWohnk.js → shadow-claw-tools-B_nuk8Zl.js} +1 -1
- package/dist/public/{shadow-claw-webvm-CP6oFjg5.js → shadow-claw-webvm-Vl_jH5YL.js} +1 -1
- package/dist/public/{syncWebMcpRegistration-DOn9gLnD.js → syncWebMcpRegistration-n2vAMJ4w.js} +1 -1
- package/dist/server.js +197 -187
- package/package.json +2 -1
- package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.test.ts +6 -0
- package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.ts +19 -9
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +14 -0
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.ts +12 -0
- package/src/components/shadow-claw/shadow-claw.ts +6 -0
- package/src/components/shadow-claw/utils/bindEventListeners.test.ts +57 -0
- package/src/components/shadow-claw/utils/bindEventListeners.ts +14 -0
- package/src/components/shadow-claw/utils/setupAppLifecycle.test.ts +201 -0
- package/src/components/shadow-claw/utils/setupAppLifecycle.ts +101 -0
- package/src/core/control-plane-client.test.ts +82 -1
- package/src/core/control-plane-client.ts +35 -1
- package/src/core/orchestrator/orchestrator.test.ts +49 -0
- package/src/core/orchestrator/orchestrator.ts +43 -0
- package/src/core/orchestrator/utils/operations/channel.test.ts +17 -0
- package/src/core/orchestrator/utils/operations/channel.ts +11 -5
- package/src/core/orchestrator/utils/operations/task.test.ts +26 -0
- package/src/core/orchestrator/utils/operations/task.ts +8 -1
- package/src/core/utils/initControlPlane.test.ts +138 -0
- package/src/core/utils/initControlPlane.ts +82 -0
- package/src/core/utils/initializeApp.test.ts +27 -1
- package/src/core/utils/initializeApp.ts +6 -13
- package/src/server/mcp/mcp-server.test.ts +6 -0
- package/src/server/mcp/mcp-server.ts +2 -1
- package/src/server/mcp/tools/built-in-tools.ts +88 -1
- package/src/server/mcp/tools/client-tool-relay.ts +6 -0
- package/src/server/mcp/tools/mcp-tools.test.ts +128 -0
- package/src/server/openapi/openapi-coverage.test.ts +2 -1
- package/src/server/openapi/openapi-spec.ts +2 -1
- package/src/server/openapi/paths/mcp.ts +104 -0
- package/src/server/openapi/paths/push.ts +3 -0
- package/src/server/routes/mcp.test.ts +92 -0
- package/src/server/routes/mcp.ts +138 -2
- package/src/server/utils/packageVersion.test.ts +10 -0
- package/src/server/utils/packageVersion.ts +32 -0
- package/src/service-worker/fetch-proxy-rules.test.ts +73 -0
- package/src/service-worker/fetch-proxy-rules.ts +44 -0
- package/src/service-worker/fetch-proxy.ts +1 -1
- package/src/subsystems/channels/imessage.test.ts +36 -0
- package/src/subsystems/channels/imessage.ts +11 -0
- package/src/subsystems/channels/peerjs.test.ts +56 -0
- package/src/subsystems/channels/peerjs.ts +29 -0
- package/src/subsystems/channels/telegram.test.ts +38 -0
- package/src/subsystems/channels/telegram.ts +13 -0
- package/src/subsystems/channels/types.ts +2 -0
- package/src/subsystems/notifications/push-client.test.ts +70 -0
- package/src/subsystems/notifications/push-client.ts +28 -3
- package/src/subsystems/notifications/push-routes.test.ts +82 -0
- package/src/subsystems/notifications/push-routes.ts +70 -16
- package/src/subsystems/notifications/push-store.test.ts +59 -0
- package/src/subsystems/notifications/push-store.ts +113 -19
- package/src/testing/jest-setup.ts +15 -0
- package/dist/lib/initControlPlane-C2zURVIS.js.map +0 -1
- package/dist/lib/orchestrator-CACLa_Ro.js.map +0 -1
- package/dist/lib/peerjs-DFWOBcXf.js.map +0 -1
- package/dist/lib/push-client-_tiWQsCT.js.map +0 -1
- package/dist/lib/shadow-claw-6UHdiy4a.js.map +0 -1
- package/dist/lib/shadow-claw-control-plane-UG0o7pCz.js.map +0 -1
- package/dist/public/initControlPlane-BN9cWyO0.js +0 -1
- package/dist/public/orchestrator-CMbCTjBf.js +0 -15
- package/dist/public/peerjs-DUCKH-m9.js +0 -1
- package/dist/public/push-client-D0lkwrK0.js +0 -1
|
@@ -53,6 +53,7 @@ const {
|
|
|
53
53
|
unsubscribeFromPush,
|
|
54
54
|
getCurrentSubscription,
|
|
55
55
|
urlBase64ToUint8Array,
|
|
56
|
+
getPushFetchOptions,
|
|
56
57
|
} = await import("./push-client.js");
|
|
57
58
|
|
|
58
59
|
describe("push-client", () => {
|
|
@@ -62,6 +63,60 @@ describe("push-client", () => {
|
|
|
62
63
|
mockPushManager.getSubscription.mockResolvedValue(null);
|
|
63
64
|
});
|
|
64
65
|
|
|
66
|
+
describe("getPushFetchOptions", () => {
|
|
67
|
+
it("sets targetAddressSpace loopback for localhost", () => {
|
|
68
|
+
expect(
|
|
69
|
+
getPushFetchOptions("http://localhost:8888/push/vapid-public-key"),
|
|
70
|
+
).toEqual({
|
|
71
|
+
targetAddressSpace: "loopback",
|
|
72
|
+
});
|
|
73
|
+
expect(
|
|
74
|
+
getPushFetchOptions("http://127.0.0.1:8888/push/vapid-public-key"),
|
|
75
|
+
).toEqual({
|
|
76
|
+
targetAddressSpace: "loopback",
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("sets targetAddressSpace private for private IP addresses", () => {
|
|
81
|
+
expect(
|
|
82
|
+
getPushFetchOptions("https://10.9.8.226:8888/push/vapid-public-key"),
|
|
83
|
+
).toEqual({
|
|
84
|
+
targetAddressSpace: "private",
|
|
85
|
+
});
|
|
86
|
+
expect(
|
|
87
|
+
getPushFetchOptions("https://192.168.1.50:8888/push/vapid-public-key"),
|
|
88
|
+
).toEqual({
|
|
89
|
+
targetAddressSpace: "private",
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("sets targetAddressSpace private for local single-label hostnames and .local domains", () => {
|
|
94
|
+
expect(
|
|
95
|
+
getPushFetchOptions("https://hostname:8888/push/vapid-public-key"),
|
|
96
|
+
).toEqual({
|
|
97
|
+
targetAddressSpace: "private",
|
|
98
|
+
});
|
|
99
|
+
expect(
|
|
100
|
+
getPushFetchOptions(
|
|
101
|
+
"https://hostname.local:8888/push/vapid-public-key",
|
|
102
|
+
),
|
|
103
|
+
).toEqual({
|
|
104
|
+
targetAddressSpace: "private",
|
|
105
|
+
});
|
|
106
|
+
expect(
|
|
107
|
+
getPushFetchOptions("https://my-nas.lan:8888/push/vapid-public-key"),
|
|
108
|
+
).toEqual({
|
|
109
|
+
targetAddressSpace: "private",
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("does not set targetAddressSpace for public domains", () => {
|
|
114
|
+
expect(
|
|
115
|
+
getPushFetchOptions("https://example.com/push/vapid-public-key"),
|
|
116
|
+
).toEqual({});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
65
120
|
describe("urlBase64ToUint8Array", () => {
|
|
66
121
|
it("converts a URL-safe base64 string to Uint8Array", () => {
|
|
67
122
|
// "AQAB" in standard base64 = [1, 0, 1]
|
|
@@ -117,6 +172,21 @@ describe("push-client", () => {
|
|
|
117
172
|
expect(postCall[1].method).toBe("POST");
|
|
118
173
|
expect(postCall[1].targetAddressSpace).toBe("loopback");
|
|
119
174
|
});
|
|
175
|
+
|
|
176
|
+
it("includes clientId and deviceLabel in the request body", async () => {
|
|
177
|
+
(fetch as any)
|
|
178
|
+
.mockResolvedValueOnce({
|
|
179
|
+
ok: true,
|
|
180
|
+
json: () => Promise.resolve({ publicKey: "test-vapid-key" }),
|
|
181
|
+
})
|
|
182
|
+
.mockResolvedValueOnce({ ok: true });
|
|
183
|
+
|
|
184
|
+
await subscribeToPush("client-explicit-123", "Pixel 9 Pro");
|
|
185
|
+
const postCall = (fetch as any).mock.calls[1];
|
|
186
|
+
const body = JSON.parse(postCall[1].body);
|
|
187
|
+
expect(body.clientId).toBe("client-explicit-123");
|
|
188
|
+
expect(body.deviceLabel).toBe("Pixel 9 Pro");
|
|
189
|
+
});
|
|
120
190
|
});
|
|
121
191
|
|
|
122
192
|
describe("unsubscribeFromPush", () => {
|
|
@@ -53,7 +53,14 @@ export function getPushFetchOptions(
|
|
|
53
53
|
host === "[::1]"
|
|
54
54
|
) {
|
|
55
55
|
opts.targetAddressSpace = "loopback";
|
|
56
|
-
} else if (
|
|
56
|
+
} else if (
|
|
57
|
+
/^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/.test(host) ||
|
|
58
|
+
!host.includes(".") ||
|
|
59
|
+
host.endsWith(".local") ||
|
|
60
|
+
host.endsWith(".lan") ||
|
|
61
|
+
host.endsWith(".home") ||
|
|
62
|
+
host.endsWith(".internal")
|
|
63
|
+
) {
|
|
57
64
|
opts.targetAddressSpace = "private";
|
|
58
65
|
}
|
|
59
66
|
} catch (_) {}
|
|
@@ -87,7 +94,10 @@ export async function getVapidPublicKey(): Promise<string> {
|
|
|
87
94
|
* Subscribe to push notifications.
|
|
88
95
|
* Requests permission, creates a push subscription, and sends it to the server.
|
|
89
96
|
*/
|
|
90
|
-
export async function subscribeToPush(
|
|
97
|
+
export async function subscribeToPush(
|
|
98
|
+
clientId?: string,
|
|
99
|
+
deviceLabel?: string,
|
|
100
|
+
): Promise<PushSubscription> {
|
|
91
101
|
const registration = await navigator.serviceWorker.ready;
|
|
92
102
|
const publicKey = await getVapidPublicKey();
|
|
93
103
|
|
|
@@ -96,13 +106,28 @@ export async function subscribeToPush(): Promise<PushSubscription> {
|
|
|
96
106
|
applicationServerKey: urlBase64ToUint8Array(publicKey) as any,
|
|
97
107
|
});
|
|
98
108
|
|
|
109
|
+
const resolvedClientId =
|
|
110
|
+
clientId ||
|
|
111
|
+
(typeof localStorage !== "undefined"
|
|
112
|
+
? localStorage.getItem(CONFIG_KEYS.CONTROL_PLANE_CLIENT_ID) || undefined
|
|
113
|
+
: undefined);
|
|
114
|
+
|
|
115
|
+
const subJson =
|
|
116
|
+
typeof subscription.toJSON === "function"
|
|
117
|
+
? subscription.toJSON()
|
|
118
|
+
: JSON.parse(JSON.stringify(subscription));
|
|
119
|
+
|
|
99
120
|
const url = await getPushUrl("/push/subscribe");
|
|
100
121
|
await fetch(
|
|
101
122
|
url,
|
|
102
123
|
getPushFetchOptions(url, {
|
|
103
124
|
method: "POST",
|
|
104
125
|
headers: { "Content-Type": "application/json" },
|
|
105
|
-
body: JSON.stringify(
|
|
126
|
+
body: JSON.stringify({
|
|
127
|
+
...subJson,
|
|
128
|
+
...(resolvedClientId ? { clientId: resolvedClientId } : {}),
|
|
129
|
+
...(deviceLabel ? { deviceLabel } : {}),
|
|
130
|
+
}),
|
|
106
131
|
}),
|
|
107
132
|
);
|
|
108
133
|
|
|
@@ -23,6 +23,8 @@ jest.unstable_mockModule("./push-store.js", () => ({
|
|
|
23
23
|
removeSubscriptionById: jest.fn(),
|
|
24
24
|
getSubscription: jest.fn(),
|
|
25
25
|
getAllSubscriptions: jest.fn(() => []),
|
|
26
|
+
getSubscriptionsByClientId: jest.fn(() => []),
|
|
27
|
+
findSubscriptionsForClient: jest.fn(() => []),
|
|
26
28
|
}));
|
|
27
29
|
|
|
28
30
|
const { registerPushRoutes, broadcastPush } = await import("./push-routes.js");
|
|
@@ -112,6 +114,24 @@ describe("push-routes", () => {
|
|
|
112
114
|
const req = createMockReq(subscription);
|
|
113
115
|
const res = createMockRes();
|
|
114
116
|
await app.routes.post["/push/subscribe"](req, res);
|
|
117
|
+
expect(store.saveSubscription).toHaveBeenCalledWith({
|
|
118
|
+
...subscription,
|
|
119
|
+
clientId: undefined,
|
|
120
|
+
deviceLabel: undefined,
|
|
121
|
+
});
|
|
122
|
+
expect(res.statusCode).toBe(201);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("saves the subscription with clientId and deviceLabel", async () => {
|
|
126
|
+
const subscription: any = {
|
|
127
|
+
endpoint: "https://fcm.example.com/abc-with-client",
|
|
128
|
+
keys: { p256dh: "key1", auth: "key2" },
|
|
129
|
+
clientId: "client-test-777",
|
|
130
|
+
deviceLabel: "Test Device 777",
|
|
131
|
+
};
|
|
132
|
+
const req = createMockReq(subscription);
|
|
133
|
+
const res = createMockRes();
|
|
134
|
+
await app.routes.post["/push/subscribe"](req, res);
|
|
115
135
|
expect(store.saveSubscription).toHaveBeenCalledWith(subscription);
|
|
116
136
|
expect(res.statusCode).toBe(201);
|
|
117
137
|
});
|
|
@@ -308,6 +328,33 @@ describe("push-routes", () => {
|
|
|
308
328
|
await app.routes.post["/push/broadcast"](req, res);
|
|
309
329
|
expect(res.statusCode).toBe(400);
|
|
310
330
|
});
|
|
331
|
+
|
|
332
|
+
it("targets a specific client when clientId is provided in POST /push/broadcast", async () => {
|
|
333
|
+
(store.findSubscriptionsForClient as any).mockReturnValue([
|
|
334
|
+
{
|
|
335
|
+
id: 1,
|
|
336
|
+
endpoint: "https://fcm.example.com/client-specific",
|
|
337
|
+
keys_p256dh: "k1",
|
|
338
|
+
keys_auth: "k2",
|
|
339
|
+
client_id: "client-target-99",
|
|
340
|
+
},
|
|
341
|
+
]);
|
|
342
|
+
webpush.sendNotification.mockResolvedValueOnce({
|
|
343
|
+
statusCode: 201,
|
|
344
|
+
} as any);
|
|
345
|
+
|
|
346
|
+
const req = createMockReq({
|
|
347
|
+
body: "Specific client alert",
|
|
348
|
+
clientId: "client-target-99",
|
|
349
|
+
});
|
|
350
|
+
const res = createMockRes();
|
|
351
|
+
await app.routes.post["/push/broadcast"](req, res);
|
|
352
|
+
expect(res.statusCode).toBe(200);
|
|
353
|
+
expect(store.findSubscriptionsForClient).toHaveBeenCalledWith(
|
|
354
|
+
"client-target-99",
|
|
355
|
+
);
|
|
356
|
+
expect(res._json.sent).toBe(1);
|
|
357
|
+
});
|
|
311
358
|
});
|
|
312
359
|
|
|
313
360
|
describe("POST /push/command", () => {
|
|
@@ -367,5 +414,40 @@ describe("push-routes", () => {
|
|
|
367
414
|
expect(result.noSubscribers).toBeUndefined();
|
|
368
415
|
expect(result.sent).toBe(1);
|
|
369
416
|
});
|
|
417
|
+
|
|
418
|
+
it("targets a specific client and returns notFound when no subscription matches", async () => {
|
|
419
|
+
(store.findSubscriptionsForClient as any).mockReturnValue([]);
|
|
420
|
+
const result = await broadcastPush(
|
|
421
|
+
{ title: "Test", body: "Hello" },
|
|
422
|
+
{ clientId: "unknown-client" },
|
|
423
|
+
);
|
|
424
|
+
expect(result).toEqual({ sent: 0, failed: 0, notFound: true });
|
|
425
|
+
expect(store.findSubscriptionsForClient).toHaveBeenCalledWith(
|
|
426
|
+
"unknown-client",
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("targets a specific client and sends push when matching subscription exists", async () => {
|
|
431
|
+
(store.findSubscriptionsForClient as any).mockReturnValue([
|
|
432
|
+
{
|
|
433
|
+
id: 1,
|
|
434
|
+
endpoint: "https://fcm.example.com/target-sub",
|
|
435
|
+
keys_p256dh: "k1",
|
|
436
|
+
keys_auth: "k2",
|
|
437
|
+
client_id: "client-target-123",
|
|
438
|
+
},
|
|
439
|
+
]);
|
|
440
|
+
webpush.sendNotification.mockResolvedValueOnce({
|
|
441
|
+
statusCode: 201,
|
|
442
|
+
} as any);
|
|
443
|
+
const result = await broadcastPush(
|
|
444
|
+
{ title: "Test", body: "Hello" },
|
|
445
|
+
{ clientId: "client-target-123" },
|
|
446
|
+
);
|
|
447
|
+
expect(result).toEqual({ sent: 1, failed: 0 });
|
|
448
|
+
expect(store.findSubscriptionsForClient).toHaveBeenCalledWith(
|
|
449
|
+
"client-target-123",
|
|
450
|
+
);
|
|
451
|
+
});
|
|
370
452
|
});
|
|
371
453
|
});
|
|
@@ -15,25 +15,54 @@ import {
|
|
|
15
15
|
removeSubscriptionById,
|
|
16
16
|
getSubscription,
|
|
17
17
|
getAllSubscriptions,
|
|
18
|
+
findSubscriptionsForClient,
|
|
19
|
+
type PushSubscriptionRow,
|
|
18
20
|
} from "./push-store.js";
|
|
19
21
|
import type { Express } from "express";
|
|
20
22
|
|
|
23
|
+
export interface BroadcastPushOptions {
|
|
24
|
+
clientId?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface BroadcastPushResult {
|
|
28
|
+
sent: number;
|
|
29
|
+
failed: number;
|
|
30
|
+
noSubscribers?: true;
|
|
31
|
+
notFound?: true;
|
|
32
|
+
}
|
|
33
|
+
|
|
21
34
|
/**
|
|
22
|
-
* Broadcast a push notification payload to all subscriptions.
|
|
23
|
-
* Used by
|
|
35
|
+
* Broadcast a push notification payload to all subscriptions, or send to a specific client.
|
|
36
|
+
* Used by /push/broadcast, MCP send_notification tool, and the server-side task scheduler.
|
|
24
37
|
*/
|
|
25
38
|
export async function broadcastPush(
|
|
26
39
|
payload: any,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
options?: BroadcastPushOptions,
|
|
41
|
+
): Promise<BroadcastPushResult> {
|
|
42
|
+
const targetClientId =
|
|
43
|
+
options?.clientId ||
|
|
44
|
+
(typeof payload?.clientId === "string" &&
|
|
45
|
+
payload.type !== "remote-command" &&
|
|
46
|
+
payload.clientId.trim()
|
|
47
|
+
? payload.clientId.trim()
|
|
48
|
+
: undefined);
|
|
49
|
+
|
|
50
|
+
let subs: PushSubscriptionRow[];
|
|
51
|
+
if (targetClientId) {
|
|
52
|
+
subs = findSubscriptionsForClient(targetClientId);
|
|
53
|
+
if (subs.length === 0) {
|
|
54
|
+
return { sent: 0, failed: 0, notFound: true };
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
subs = getAllSubscriptions();
|
|
58
|
+
if (subs.length === 0) {
|
|
59
|
+
return { sent: 0, failed: 0, noSubscribers: true };
|
|
60
|
+
}
|
|
32
61
|
}
|
|
33
62
|
|
|
34
63
|
const keys = getOrCreateVapidKeys();
|
|
35
64
|
const notification = JSON.stringify(payload);
|
|
36
|
-
const
|
|
65
|
+
const sendOptions = {
|
|
37
66
|
TTL: 60 * 60,
|
|
38
67
|
vapidDetails: {
|
|
39
68
|
subject: keys.subject,
|
|
@@ -53,7 +82,11 @@ export async function broadcastPush(
|
|
|
53
82
|
};
|
|
54
83
|
|
|
55
84
|
try {
|
|
56
|
-
await webpush.sendNotification(
|
|
85
|
+
await webpush.sendNotification(
|
|
86
|
+
pushSubscription,
|
|
87
|
+
notification,
|
|
88
|
+
sendOptions,
|
|
89
|
+
);
|
|
57
90
|
sent++;
|
|
58
91
|
} catch (err: any) {
|
|
59
92
|
failed++;
|
|
@@ -86,7 +119,25 @@ export function registerPushRoutes(app: Express): void {
|
|
|
86
119
|
return res.status(400).json({ error: "Invalid subscription object" });
|
|
87
120
|
}
|
|
88
121
|
|
|
89
|
-
|
|
122
|
+
const clientId =
|
|
123
|
+
subscription.clientId ||
|
|
124
|
+
subscription.client_id ||
|
|
125
|
+
(typeof req.query?.clientId === "string"
|
|
126
|
+
? req.query.clientId
|
|
127
|
+
: undefined);
|
|
128
|
+
|
|
129
|
+
const deviceLabel =
|
|
130
|
+
subscription.deviceLabel ||
|
|
131
|
+
subscription.device_label ||
|
|
132
|
+
(typeof req.query?.deviceLabel === "string"
|
|
133
|
+
? req.query.deviceLabel
|
|
134
|
+
: undefined);
|
|
135
|
+
|
|
136
|
+
saveSubscription({
|
|
137
|
+
...subscription,
|
|
138
|
+
clientId,
|
|
139
|
+
deviceLabel,
|
|
140
|
+
});
|
|
90
141
|
res.sendStatus(201);
|
|
91
142
|
});
|
|
92
143
|
|
|
@@ -177,19 +228,22 @@ export function registerPushRoutes(app: Express): void {
|
|
|
177
228
|
res.sendStatus(200);
|
|
178
229
|
});
|
|
179
230
|
|
|
180
|
-
// Broadcast a notification to all subscriptions
|
|
231
|
+
// Broadcast a notification to all subscriptions (or to a specific client if clientId is provided)
|
|
181
232
|
app.post("/push/broadcast", async (req, res) => {
|
|
182
|
-
const { title, body } = req.body || {};
|
|
233
|
+
const { title, body, clientId } = req.body || {};
|
|
183
234
|
|
|
184
235
|
if (!body) {
|
|
185
236
|
return res.status(400).json({ error: "Missing body" });
|
|
186
237
|
}
|
|
187
238
|
|
|
188
239
|
try {
|
|
189
|
-
const result = await broadcastPush(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
240
|
+
const result = await broadcastPush(
|
|
241
|
+
{
|
|
242
|
+
title: title || "ShadowClaw",
|
|
243
|
+
body,
|
|
244
|
+
},
|
|
245
|
+
clientId ? { clientId } : undefined,
|
|
246
|
+
);
|
|
193
247
|
res.json(result);
|
|
194
248
|
} catch (err: any) {
|
|
195
249
|
res.status(500).json({ error: err.message });
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
getSubscription,
|
|
8
8
|
getAllSubscriptions,
|
|
9
9
|
removeSubscriptionById,
|
|
10
|
+
getSubscriptionsByClientId,
|
|
11
|
+
findSubscriptionsForClient,
|
|
10
12
|
} from "./push-store.js";
|
|
11
13
|
|
|
12
14
|
// Use in-memory DB for tests
|
|
@@ -148,4 +150,61 @@ describe("push-store", () => {
|
|
|
148
150
|
expect(all[0].endpoint).toBe(MOCK_SUBSCRIPTION_2.endpoint);
|
|
149
151
|
});
|
|
150
152
|
});
|
|
153
|
+
|
|
154
|
+
describe("client-targeted subscriptions", () => {
|
|
155
|
+
it("stores and retrieves subscriptions with clientId and deviceLabel", () => {
|
|
156
|
+
saveSubscription({
|
|
157
|
+
...MOCK_SUBSCRIPTION,
|
|
158
|
+
clientId: "client-01jtest123",
|
|
159
|
+
deviceLabel: "Pixel 9 Pro",
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const sub = getSubscription(MOCK_SUBSCRIPTION.endpoint);
|
|
163
|
+
expect(sub).toBeDefined();
|
|
164
|
+
expect(sub?.client_id).toBe("client-01jtest123");
|
|
165
|
+
expect(sub?.device_label).toBe("Pixel 9 Pro");
|
|
166
|
+
|
|
167
|
+
const byClient = getSubscriptionsByClientId("client-01jtest123");
|
|
168
|
+
expect(byClient).toHaveLength(1);
|
|
169
|
+
expect(byClient[0].endpoint).toBe(MOCK_SUBSCRIPTION.endpoint);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("finds subscriptions by exact clientId, prefix, device label, and row ID", () => {
|
|
173
|
+
saveSubscription({
|
|
174
|
+
...MOCK_SUBSCRIPTION,
|
|
175
|
+
clientId: "client-01jtest123",
|
|
176
|
+
deviceLabel: "Pixel 9 Pro",
|
|
177
|
+
});
|
|
178
|
+
saveSubscription({
|
|
179
|
+
...MOCK_SUBSCRIPTION_2,
|
|
180
|
+
clientId: "client-02ktest456",
|
|
181
|
+
deviceLabel: "MacBook Air",
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Exact clientId
|
|
185
|
+
const exact = findSubscriptionsForClient("client-01jtest123");
|
|
186
|
+
expect(exact).toHaveLength(1);
|
|
187
|
+
expect(exact[0].endpoint).toBe(MOCK_SUBSCRIPTION.endpoint);
|
|
188
|
+
|
|
189
|
+
// Prefix clientId without 'client-'
|
|
190
|
+
const prefix = findSubscriptionsForClient("01jtest");
|
|
191
|
+
expect(prefix).toHaveLength(1);
|
|
192
|
+
expect(prefix[0].endpoint).toBe(MOCK_SUBSCRIPTION.endpoint);
|
|
193
|
+
|
|
194
|
+
// Device label substring (case-insensitive)
|
|
195
|
+
const byLabel = findSubscriptionsForClient("macbook");
|
|
196
|
+
expect(byLabel).toHaveLength(1);
|
|
197
|
+
expect(byLabel[0].endpoint).toBe(MOCK_SUBSCRIPTION_2.endpoint);
|
|
198
|
+
|
|
199
|
+
// Row ID
|
|
200
|
+
const all = getAllSubscriptions();
|
|
201
|
+
const byId = findSubscriptionsForClient(String(all[0].id));
|
|
202
|
+
expect(byId).toHaveLength(1);
|
|
203
|
+
expect(byId[0].id).toBe(all[0].id);
|
|
204
|
+
|
|
205
|
+
// Non-existent target
|
|
206
|
+
const notFound = findSubscriptionsForClient("nonexistent-device");
|
|
207
|
+
expect(notFound).toHaveLength(0);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
151
210
|
});
|
|
@@ -24,6 +24,8 @@ export interface PushSubscriptionRow {
|
|
|
24
24
|
endpoint: string;
|
|
25
25
|
keys_p256dh: string;
|
|
26
26
|
keys_auth: string;
|
|
27
|
+
client_id?: string;
|
|
28
|
+
device_label?: string;
|
|
27
29
|
created_at: string;
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -33,6 +35,10 @@ export interface PushSubscriptionInput {
|
|
|
33
35
|
p256dh: string;
|
|
34
36
|
auth: string;
|
|
35
37
|
};
|
|
38
|
+
clientId?: string;
|
|
39
|
+
client_id?: string;
|
|
40
|
+
deviceLabel?: string;
|
|
41
|
+
device_label?: string;
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
/**
|
|
@@ -62,10 +68,20 @@ export function openPushStore(
|
|
|
62
68
|
endpoint TEXT NOT NULL UNIQUE,
|
|
63
69
|
keys_p256dh TEXT NOT NULL,
|
|
64
70
|
keys_auth TEXT NOT NULL,
|
|
71
|
+
client_id TEXT,
|
|
72
|
+
device_label TEXT,
|
|
65
73
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
66
74
|
)
|
|
67
75
|
`);
|
|
68
76
|
|
|
77
|
+
try {
|
|
78
|
+
db.exec(`ALTER TABLE subscriptions ADD COLUMN client_id TEXT`);
|
|
79
|
+
} catch (_) {}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
db.exec(`ALTER TABLE subscriptions ADD COLUMN device_label TEXT`);
|
|
83
|
+
} catch (_) {}
|
|
84
|
+
|
|
69
85
|
return db;
|
|
70
86
|
}
|
|
71
87
|
|
|
@@ -112,6 +128,18 @@ export function getOrCreateVapidKeys(
|
|
|
112
128
|
};
|
|
113
129
|
}
|
|
114
130
|
|
|
131
|
+
function rowToSubscription(row: any): PushSubscriptionRow {
|
|
132
|
+
return {
|
|
133
|
+
id: Number(row.id),
|
|
134
|
+
endpoint: `${row.endpoint}`,
|
|
135
|
+
keys_p256dh: `${row.keys_p256dh}`,
|
|
136
|
+
keys_auth: `${row.keys_auth}`,
|
|
137
|
+
client_id: row.client_id ? `${row.client_id}` : undefined,
|
|
138
|
+
device_label: row.device_label ? `${row.device_label}` : undefined,
|
|
139
|
+
created_at: `${row.created_at}`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
115
143
|
/**
|
|
116
144
|
* Save a push subscription (upsert by endpoint).
|
|
117
145
|
*/
|
|
@@ -120,12 +148,26 @@ export function saveSubscription(subscription: PushSubscriptionInput): void {
|
|
|
120
148
|
throw new Error("Push store not opened.");
|
|
121
149
|
}
|
|
122
150
|
|
|
151
|
+
const clientId = subscription.clientId || subscription.client_id || null;
|
|
152
|
+
const deviceLabel =
|
|
153
|
+
subscription.deviceLabel || subscription.device_label || null;
|
|
154
|
+
|
|
123
155
|
db.prepare(
|
|
124
|
-
|
|
156
|
+
`
|
|
157
|
+
INSERT INTO subscriptions (endpoint, keys_p256dh, keys_auth, client_id, device_label)
|
|
158
|
+
VALUES (?, ?, ?, ?, ?)
|
|
159
|
+
ON CONFLICT(endpoint) DO UPDATE SET
|
|
160
|
+
keys_p256dh = excluded.keys_p256dh,
|
|
161
|
+
keys_auth = excluded.keys_auth,
|
|
162
|
+
client_id = COALESCE(excluded.client_id, subscriptions.client_id),
|
|
163
|
+
device_label = COALESCE(excluded.device_label, subscriptions.device_label)
|
|
164
|
+
`,
|
|
125
165
|
).run(
|
|
126
166
|
subscription.endpoint,
|
|
127
167
|
subscription.keys.p256dh,
|
|
128
168
|
subscription.keys.auth,
|
|
169
|
+
clientId,
|
|
170
|
+
deviceLabel,
|
|
129
171
|
);
|
|
130
172
|
}
|
|
131
173
|
|
|
@@ -165,15 +207,7 @@ export function getSubscription(
|
|
|
165
207
|
.prepare("SELECT * FROM subscriptions WHERE endpoint = ?")
|
|
166
208
|
.get(endpoint);
|
|
167
209
|
|
|
168
|
-
return result
|
|
169
|
-
? {
|
|
170
|
-
id: Number(result.id),
|
|
171
|
-
endpoint: `${result.endpoint}`,
|
|
172
|
-
keys_p256dh: `${result.keys_p256dh}`,
|
|
173
|
-
keys_auth: `${result.keys_auth}`,
|
|
174
|
-
created_at: `${result.created_at}`,
|
|
175
|
-
}
|
|
176
|
-
: undefined;
|
|
210
|
+
return result ? rowToSubscription(result) : undefined;
|
|
177
211
|
}
|
|
178
212
|
|
|
179
213
|
/**
|
|
@@ -188,13 +222,73 @@ export function getAllSubscriptions(): PushSubscriptionRow[] {
|
|
|
188
222
|
.prepare("SELECT * FROM subscriptions ORDER BY created_at DESC, id DESC")
|
|
189
223
|
.all();
|
|
190
224
|
|
|
191
|
-
return result
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
225
|
+
return result ? result.map(rowToSubscription) : [];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get subscriptions matching a specific client ID.
|
|
230
|
+
*/
|
|
231
|
+
export function getSubscriptionsByClientId(
|
|
232
|
+
clientId: string,
|
|
233
|
+
): PushSubscriptionRow[] {
|
|
234
|
+
if (!db) {
|
|
235
|
+
throw new Error("Push store not opened.");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const result = db
|
|
239
|
+
.prepare(
|
|
240
|
+
"SELECT * FROM subscriptions WHERE client_id = ? ORDER BY created_at DESC, id DESC",
|
|
241
|
+
)
|
|
242
|
+
.all(clientId);
|
|
243
|
+
|
|
244
|
+
return result ? result.map(rowToSubscription) : [];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Find subscriptions for a client by exact ID, row ID, ID prefix, or device label.
|
|
249
|
+
*/
|
|
250
|
+
export function findSubscriptionsForClient(
|
|
251
|
+
target: string,
|
|
252
|
+
): PushSubscriptionRow[] {
|
|
253
|
+
if (!db) {
|
|
254
|
+
throw new Error("Push store not opened.");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const all = getAllSubscriptions();
|
|
258
|
+
if (!target || !target.trim()) {
|
|
259
|
+
return all;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const trimmed = target.trim();
|
|
263
|
+
|
|
264
|
+
// 1. Exact match on client_id
|
|
265
|
+
const exact = all.filter((s) => s.client_id === trimmed);
|
|
266
|
+
if (exact.length > 0) return exact;
|
|
267
|
+
|
|
268
|
+
// 2. Exact match on row ID if target is numeric
|
|
269
|
+
const numericId = parseInt(trimmed, 10);
|
|
270
|
+
if (!isNaN(numericId) && numericId.toString() === trimmed) {
|
|
271
|
+
const byId = all.filter((s) => s.id === numericId);
|
|
272
|
+
if (byId.length > 0) return byId;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 3. Prefix match on client_id (e.g. ULID prefix or client- prefix)
|
|
276
|
+
const prefix = all.filter(
|
|
277
|
+
(s) =>
|
|
278
|
+
s.client_id &&
|
|
279
|
+
(s.client_id.startsWith(trimmed) ||
|
|
280
|
+
s.client_id.replace(/^client-/, "").startsWith(trimmed) ||
|
|
281
|
+
trimmed.startsWith(s.client_id)),
|
|
282
|
+
);
|
|
283
|
+
if (prefix.length > 0) return prefix;
|
|
284
|
+
|
|
285
|
+
// 4. Case-insensitive substring match on device_label
|
|
286
|
+
const byLabel = all.filter(
|
|
287
|
+
(s) =>
|
|
288
|
+
s.device_label &&
|
|
289
|
+
s.device_label.toLowerCase().includes(trimmed.toLowerCase()),
|
|
290
|
+
);
|
|
291
|
+
if (byLabel.length > 0) return byLabel;
|
|
292
|
+
|
|
293
|
+
return [];
|
|
200
294
|
}
|
|
@@ -514,9 +514,11 @@ const expectedLogs = [
|
|
|
514
514
|
"PeerTaskManager: task",
|
|
515
515
|
"[ShadowClaw] Failed to load models from",
|
|
516
516
|
"PeerJsChannel: connected to signaling server",
|
|
517
|
+
"PeerJsChannel: disconnected from signaling server",
|
|
517
518
|
"PeerJsChannel: connection opened with",
|
|
518
519
|
"PeerJsChannel: rejecting connection from untrusted peer:",
|
|
519
520
|
"PeerJsChannel: connection closed with",
|
|
521
|
+
"[ControlPlaneClient] SSE connection error:",
|
|
520
522
|
"[WebVM] Ignoring invalid VM boot host:",
|
|
521
523
|
"[WebVM] Ignoring invalid VM relay URL:",
|
|
522
524
|
"[WebVM]",
|
|
@@ -566,6 +568,8 @@ const expectedLogs = [
|
|
|
566
568
|
"Failed to check Prompt API onboarding:",
|
|
567
569
|
"[webrtc-listen]",
|
|
568
570
|
"[ShadowClaw MCP]",
|
|
571
|
+
"fatal: not a git repository",
|
|
572
|
+
"Stopping at filesystem boundary",
|
|
569
573
|
];
|
|
570
574
|
|
|
571
575
|
function isExpectedLog(...args: any[]) {
|
|
@@ -577,6 +581,17 @@ function isExpectedLog(...args: any[]) {
|
|
|
577
581
|
return expectedLogs.some((expected) => str.includes(expected));
|
|
578
582
|
}
|
|
579
583
|
|
|
584
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
585
|
+
(process.stderr as any).write = (chunk: any, ...args: any[]): boolean => {
|
|
586
|
+
const str = typeof chunk === "string" ? chunk : (chunk?.toString?.() ?? "");
|
|
587
|
+
if (isExpectedLog(str)) {
|
|
588
|
+
const cb = args.find((a) => typeof a === "function");
|
|
589
|
+
if (cb) cb();
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
return (originalStderrWrite as any)(chunk, ...args);
|
|
593
|
+
};
|
|
594
|
+
|
|
580
595
|
console.error = (...args: any[]) => {
|
|
581
596
|
if (isExpectedLog(...args)) return;
|
|
582
597
|
originalConsoleError(...args);
|