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
package/src/server/routes/mcp.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { McpServer } from "../mcp/mcp-server.js";
|
|
|
16
16
|
import { registerBuiltInTools } from "../mcp/tools/built-in-tools.js";
|
|
17
17
|
import { createClientToolRelay } from "../mcp/tools/client-tool-relay.js";
|
|
18
18
|
import { getOrCreateControlToken } from "../client-registry.js";
|
|
19
|
+
import { getPackageVersion } from "../utils/packageVersion.js";
|
|
19
20
|
|
|
20
21
|
export interface McpRouteOptions {
|
|
21
22
|
mcpServer?: McpServer;
|
|
@@ -36,7 +37,6 @@ export function registerMcpRoutes(
|
|
|
36
37
|
options.mcpServer ||
|
|
37
38
|
new McpServer({
|
|
38
39
|
name: "shadow-claw",
|
|
39
|
-
version: "1.25.0",
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
// Register ShadowClaw built-in and client-relayed tools
|
|
@@ -143,7 +143,7 @@ export function registerMcpRoutes(
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
if (app && typeof app.post === "function") {
|
|
146
|
-
|
|
146
|
+
const mcpPostHandler = async (req: Request, res: Response) => {
|
|
147
147
|
// 1. Origin header security check for DNS rebinding protection
|
|
148
148
|
const origin = req.headers.origin;
|
|
149
149
|
if (origin && !isTrustedOrigin(origin, req.headers.host)) {
|
|
@@ -165,6 +165,15 @@ export function registerMcpRoutes(
|
|
|
165
165
|
: isLocalRequest(req);
|
|
166
166
|
|
|
167
167
|
if (!isAuthenticated) {
|
|
168
|
+
const scheme =
|
|
169
|
+
req.secure || req.headers["x-forwarded-proto"] === "https"
|
|
170
|
+
? "https"
|
|
171
|
+
: "http";
|
|
172
|
+
const host = req.headers.host || "127.0.0.1";
|
|
173
|
+
res.setHeader(
|
|
174
|
+
"WWW-Authenticate",
|
|
175
|
+
`Bearer resource_metadata="${scheme}://${host}/.well-known/oauth-protected-resource"`,
|
|
176
|
+
);
|
|
168
177
|
res.status(401).json({
|
|
169
178
|
jsonrpc: "2.0",
|
|
170
179
|
id: req.body?.id ?? null,
|
|
@@ -222,7 +231,134 @@ export function registerMcpRoutes(
|
|
|
222
231
|
}
|
|
223
232
|
|
|
224
233
|
res.status(statusCode).json(response);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
app.post("/mcp", mcpPostHandler);
|
|
237
|
+
app.post("/.well-known/mcp", mcpPostHandler);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (app && typeof app.get === "function") {
|
|
241
|
+
const getBaseUrl = (req: Request) => {
|
|
242
|
+
const scheme =
|
|
243
|
+
req.secure || req.headers["x-forwarded-proto"] === "https"
|
|
244
|
+
? "https"
|
|
245
|
+
: "http";
|
|
246
|
+
const host = req.headers.host || "127.0.0.1";
|
|
247
|
+
return `${scheme}://${host}`;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// MCP Server Card Discovery (SEP-2127 / well-known discovery)
|
|
251
|
+
const handleServerCard = (req: Request, res: Response) => {
|
|
252
|
+
const origin = getBaseUrl(req);
|
|
253
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
254
|
+
res.json({
|
|
255
|
+
$schema: "https://modelcontextprotocol.io/schemas/server-card/v1.json",
|
|
256
|
+
name: "shadow-claw",
|
|
257
|
+
version: getPackageVersion(),
|
|
258
|
+
description:
|
|
259
|
+
"Browser-native personal AI assistant with local and remote LLM orchestration, Web Workers, OPFS storage, and agentic tool execution.",
|
|
260
|
+
title: "ShadowClaw MCP Server",
|
|
261
|
+
websiteUrl: `${origin}/shadow-claw/`,
|
|
262
|
+
repository: {
|
|
263
|
+
url: "https://github.com/xt-ml/shadow-claw",
|
|
264
|
+
source: "github",
|
|
265
|
+
},
|
|
266
|
+
icons: [
|
|
267
|
+
{
|
|
268
|
+
src: `${origin}/assets/icons/favicon.svg`,
|
|
269
|
+
mimeType: "image/svg+xml",
|
|
270
|
+
sizes: ["any"],
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
src: `${origin}/assets/icons/512.png`,
|
|
274
|
+
mimeType: "image/png",
|
|
275
|
+
sizes: ["512x512"],
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
remotes: [
|
|
279
|
+
{
|
|
280
|
+
type: "streamable-http",
|
|
281
|
+
url: `${origin}/mcp`,
|
|
282
|
+
supportedProtocolVersions: [
|
|
283
|
+
"2026-07-28",
|
|
284
|
+
"2025-11-25",
|
|
285
|
+
"2024-11-05",
|
|
286
|
+
],
|
|
287
|
+
headers: [
|
|
288
|
+
{
|
|
289
|
+
name: "MCP-Protocol-Version",
|
|
290
|
+
default: "2026-07-28",
|
|
291
|
+
isRequired: false,
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: "x-control-token",
|
|
295
|
+
description: "Control plane token for authorized client access",
|
|
296
|
+
isSecret: true,
|
|
297
|
+
isRequired: false,
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
});
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
app.get("/.well-known/mcp", handleServerCard);
|
|
306
|
+
app.get("/.well-known/mcp.json", handleServerCard);
|
|
307
|
+
app.get("/mcp/server-card", handleServerCard);
|
|
308
|
+
|
|
309
|
+
// AI Catalog Discovery (SEP-2127)
|
|
310
|
+
app.get("/.well-known/ai-catalog.json", (req: Request, res: Response) => {
|
|
311
|
+
const origin = getBaseUrl(req);
|
|
312
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
313
|
+
res.json({
|
|
314
|
+
$schema: "https://agent-card.github.io/schemas/ai-catalog/v1.json",
|
|
315
|
+
version: "1.0",
|
|
316
|
+
name: "ShadowClaw AI Catalog",
|
|
317
|
+
servers: [
|
|
318
|
+
{
|
|
319
|
+
name: "shadow-claw",
|
|
320
|
+
card: `${origin}/.well-known/mcp.json`,
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
});
|
|
225
324
|
});
|
|
325
|
+
|
|
326
|
+
// MCP Servers Manifest (Discussion #1147)
|
|
327
|
+
app.get("/.well-known/mcp/servers.json", (req: Request, res: Response) => {
|
|
328
|
+
const origin = getBaseUrl(req);
|
|
329
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
330
|
+
res.json({
|
|
331
|
+
version: "1.0",
|
|
332
|
+
servers: [
|
|
333
|
+
{
|
|
334
|
+
name: "shadow-claw",
|
|
335
|
+
description: "ShadowClaw Browser-Native AI Assistant MCP Server",
|
|
336
|
+
endpoint: `${origin}/mcp`,
|
|
337
|
+
capabilities: ["tools", "extensions"],
|
|
338
|
+
card: `${origin}/.well-known/mcp.json`,
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Protected Resource Metadata (RFC 9728)
|
|
345
|
+
const handleProtectedResource = (req: Request, res: Response) => {
|
|
346
|
+
const origin = getBaseUrl(req);
|
|
347
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
348
|
+
res.json({
|
|
349
|
+
resource: `${origin}/mcp`,
|
|
350
|
+
authorization_servers: [origin],
|
|
351
|
+
bearer_methods_supported: ["header"],
|
|
352
|
+
resource_documentation:
|
|
353
|
+
"https://xt-ml.github.io/shadow-claw/docs/subsystems/mcp-server.md",
|
|
354
|
+
});
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
app.get("/.well-known/oauth-protected-resource", handleProtectedResource);
|
|
358
|
+
app.get(
|
|
359
|
+
"/.well-known/oauth-protected-resource.json",
|
|
360
|
+
handleProtectedResource,
|
|
361
|
+
);
|
|
226
362
|
}
|
|
227
363
|
|
|
228
364
|
return mcpServer;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, it, expect } from "@jest/globals";
|
|
2
|
+
import { getPackageVersion } from "./packageVersion.js";
|
|
3
|
+
|
|
4
|
+
describe("packageVersion", () => {
|
|
5
|
+
it("resolves the current package.json version as a valid semver string", () => {
|
|
6
|
+
const version = getPackageVersion();
|
|
7
|
+
expect(typeof version).toBe("string");
|
|
8
|
+
expect(version).toMatch(/^\d+\.\d+\.\d+/);
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
let cachedVersion: string | null = null;
|
|
6
|
+
|
|
7
|
+
export function getPackageVersion(): string {
|
|
8
|
+
if (cachedVersion) {
|
|
9
|
+
return cachedVersion;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
let dir = currentDir;
|
|
15
|
+
while (dir && dir !== path.dirname(dir)) {
|
|
16
|
+
const candidate = path.join(dir, "package.json");
|
|
17
|
+
if (fs.existsSync(candidate)) {
|
|
18
|
+
const pkg = JSON.parse(fs.readFileSync(candidate, "utf8"));
|
|
19
|
+
if (pkg.version) {
|
|
20
|
+
const versionStr = String(pkg.version);
|
|
21
|
+
cachedVersion = versionStr;
|
|
22
|
+
return versionStr;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
dir = path.dirname(dir);
|
|
26
|
+
}
|
|
27
|
+
} catch {}
|
|
28
|
+
|
|
29
|
+
const fallback = "1.27.1";
|
|
30
|
+
cachedVersion = fallback;
|
|
31
|
+
return fallback;
|
|
32
|
+
}
|
|
@@ -105,4 +105,77 @@ describe("shouldBypassFetchProxy", () => {
|
|
|
105
105
|
shouldBypassFetchProxy(classAUrl, "https://xt-ml.github.io/shadow-claw/"),
|
|
106
106
|
).toBe(true);
|
|
107
107
|
});
|
|
108
|
+
|
|
109
|
+
it("bypasses single-label local hostnames like hostname", () => {
|
|
110
|
+
const pushUrl = new URL("https://hostname:8888/push/vapid-public-key");
|
|
111
|
+
expect(
|
|
112
|
+
shouldBypassFetchProxy(pushUrl, "https://xt-ml.github.io/shadow-claw/"),
|
|
113
|
+
).toBe(true);
|
|
114
|
+
|
|
115
|
+
const taskUrl = new URL("https://hostname:8888/schedule/tasks");
|
|
116
|
+
expect(
|
|
117
|
+
shouldBypassFetchProxy(taskUrl, "https://xt-ml.github.io/shadow-claw/"),
|
|
118
|
+
).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("bypasses mDNS .local and local network domain endpoints", () => {
|
|
122
|
+
const localDomainUrl = new URL(
|
|
123
|
+
"https://hostname.local:8888/api/control/events",
|
|
124
|
+
);
|
|
125
|
+
expect(
|
|
126
|
+
shouldBypassFetchProxy(
|
|
127
|
+
localDomainUrl,
|
|
128
|
+
"https://xt-ml.github.io/shadow-claw/",
|
|
129
|
+
),
|
|
130
|
+
).toBe(true);
|
|
131
|
+
|
|
132
|
+
const lanDomainUrl = new URL("https://server.lan:8888/schedule/tasks");
|
|
133
|
+
expect(
|
|
134
|
+
shouldBypassFetchProxy(
|
|
135
|
+
lanDomainUrl,
|
|
136
|
+
"https://xt-ml.github.io/shadow-claw/",
|
|
137
|
+
),
|
|
138
|
+
).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("bypasses built-in backend routes even on custom domains", () => {
|
|
142
|
+
const pushSubscribeUrl = new URL(
|
|
143
|
+
"https://custom.example.com/push/subscribe",
|
|
144
|
+
);
|
|
145
|
+
expect(
|
|
146
|
+
shouldBypassFetchProxy(
|
|
147
|
+
pushSubscribeUrl,
|
|
148
|
+
"https://xt-ml.github.io/shadow-claw/",
|
|
149
|
+
),
|
|
150
|
+
).toBe(true);
|
|
151
|
+
|
|
152
|
+
const scheduleUrl = new URL("https://custom.example.com/schedule/tasks");
|
|
153
|
+
expect(
|
|
154
|
+
shouldBypassFetchProxy(
|
|
155
|
+
scheduleUrl,
|
|
156
|
+
"https://xt-ml.github.io/shadow-claw/",
|
|
157
|
+
),
|
|
158
|
+
).toBe(true);
|
|
159
|
+
|
|
160
|
+
const controlUrl = new URL(
|
|
161
|
+
"https://custom.example.com/api/control/messages",
|
|
162
|
+
);
|
|
163
|
+
expect(
|
|
164
|
+
shouldBypassFetchProxy(
|
|
165
|
+
controlUrl,
|
|
166
|
+
"https://xt-ml.github.io/shadow-claw/",
|
|
167
|
+
),
|
|
168
|
+
).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("bypasses requests destined for the proxy server itself", () => {
|
|
172
|
+
const targetUrl = new URL("https://proxy.example.com:9000/api/custom");
|
|
173
|
+
expect(
|
|
174
|
+
shouldBypassFetchProxy(
|
|
175
|
+
targetUrl,
|
|
176
|
+
"https://xt-ml.github.io/shadow-claw/",
|
|
177
|
+
"https://proxy.example.com:9000/proxy",
|
|
178
|
+
),
|
|
179
|
+
).toBe(true);
|
|
180
|
+
});
|
|
108
181
|
});
|
|
@@ -16,14 +16,49 @@ const BYPASS_PROVIDERS = [
|
|
|
16
16
|
"api.telegram.org",
|
|
17
17
|
];
|
|
18
18
|
|
|
19
|
+
const BACKEND_ROUTE_PREFIXES = [
|
|
20
|
+
"/api/control/",
|
|
21
|
+
"/api/backup/",
|
|
22
|
+
"/api/activity-log/",
|
|
23
|
+
"/api/health",
|
|
24
|
+
"/push/",
|
|
25
|
+
"/schedule/",
|
|
26
|
+
"/mcp",
|
|
27
|
+
"/ws/",
|
|
28
|
+
"/peerjs/",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const LOCAL_DOMAINS = [".local", ".lan", ".home", ".internal"];
|
|
32
|
+
|
|
19
33
|
export function shouldBypassFetchProxy(
|
|
20
34
|
requestUrl: URL,
|
|
21
35
|
locationOrigin: string,
|
|
36
|
+
proxyUrl?: string,
|
|
22
37
|
): boolean {
|
|
23
38
|
if (requestUrl.origin === locationOrigin) {
|
|
24
39
|
return true;
|
|
25
40
|
}
|
|
26
41
|
|
|
42
|
+
// Never proxy requests directed to the proxy host itself (avoids self-proxy loops)
|
|
43
|
+
if (proxyUrl) {
|
|
44
|
+
try {
|
|
45
|
+
const proxyOrigin = new URL(proxyUrl, locationOrigin).origin;
|
|
46
|
+
if (requestUrl.origin === proxyOrigin) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
} catch (_) {}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Never proxy requests to ShadowClaw built-in backend infrastructure routes
|
|
53
|
+
const pathname = requestUrl.pathname;
|
|
54
|
+
if (
|
|
55
|
+
BACKEND_ROUTE_PREFIXES.some(
|
|
56
|
+
(prefix) => pathname === prefix || pathname.startsWith(prefix),
|
|
57
|
+
)
|
|
58
|
+
) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
27
62
|
const hostname = requestUrl.hostname.toLowerCase();
|
|
28
63
|
|
|
29
64
|
if (
|
|
@@ -62,5 +97,14 @@ export function shouldBypassFetchProxy(
|
|
|
62
97
|
return true;
|
|
63
98
|
}
|
|
64
99
|
|
|
100
|
+
// Local/private hostnames: single-label (no dots, e.g. "hostname") or local network domain suffixes
|
|
101
|
+
const isLocalHostname =
|
|
102
|
+
!hostname.includes(".") ||
|
|
103
|
+
LOCAL_DOMAINS.some((domain) => hostname.endsWith(domain));
|
|
104
|
+
|
|
105
|
+
if (isLocalHostname) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
65
109
|
return false;
|
|
66
110
|
}
|
|
@@ -291,4 +291,40 @@ describe("IMessageChannel", () => {
|
|
|
291
291
|
const mode = await channel.ensureApiMode();
|
|
292
292
|
expect(mode).toBe("beeper-desktop");
|
|
293
293
|
});
|
|
294
|
+
|
|
295
|
+
it("ensureConnected aborts hanging poll, resets backoff, and launches fresh poll", async () => {
|
|
296
|
+
const channel = new IMessageChannel();
|
|
297
|
+
channel.configure("https://bridge.example", "secret", []);
|
|
298
|
+
|
|
299
|
+
let pollCount = 0;
|
|
300
|
+
jest.spyOn(channel, "requestJson").mockImplementation((async (
|
|
301
|
+
_path: string,
|
|
302
|
+
_opts: any,
|
|
303
|
+
_timeout: number,
|
|
304
|
+
signal?: AbortSignal,
|
|
305
|
+
) => {
|
|
306
|
+
pollCount++;
|
|
307
|
+
return new Promise((_resolve, reject) => {
|
|
308
|
+
signal?.addEventListener("abort", () => {
|
|
309
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
}) as any);
|
|
313
|
+
|
|
314
|
+
channel.start();
|
|
315
|
+
expect(channel.running).toBe(true);
|
|
316
|
+
expect(pollCount).toBe(1);
|
|
317
|
+
|
|
318
|
+
// Simulate high backoff
|
|
319
|
+
channel.reconnectDelayMs = 30000;
|
|
320
|
+
const oldAbort = channel.abortController;
|
|
321
|
+
|
|
322
|
+
channel.ensureConnected();
|
|
323
|
+
|
|
324
|
+
expect(channel.reconnectDelayMs).toBe(1000);
|
|
325
|
+
expect(oldAbort?.signal.aborted).toBe(true);
|
|
326
|
+
expect(pollCount).toBe(2);
|
|
327
|
+
|
|
328
|
+
channel.stop();
|
|
329
|
+
});
|
|
294
330
|
});
|
|
@@ -134,6 +134,17 @@ export class IMessageChannel implements Channel {
|
|
|
134
134
|
void this.poll();
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
ensureConnected(): void {
|
|
138
|
+
if (!this.isConfigured() || !this.running) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
this.reconnectDelayMs = 1000;
|
|
143
|
+
this.abortController?.abort();
|
|
144
|
+
this.abortController = new AbortController();
|
|
145
|
+
void this.poll();
|
|
146
|
+
}
|
|
147
|
+
|
|
137
148
|
stop(): void {
|
|
138
149
|
this.running = false;
|
|
139
150
|
this.abortController?.abort();
|
|
@@ -38,11 +38,19 @@ class MockDataConnection {
|
|
|
38
38
|
|
|
39
39
|
class MockPeer {
|
|
40
40
|
destroyed = false;
|
|
41
|
+
disconnected = false;
|
|
42
|
+
open = true;
|
|
41
43
|
id: string;
|
|
44
|
+
reconnect = jest.fn(() => {
|
|
45
|
+
this.disconnected = false;
|
|
46
|
+
this.open = true;
|
|
47
|
+
this.emit("open", this.id);
|
|
48
|
+
});
|
|
42
49
|
private handlers: Map<string, EventHandler[]> = new Map();
|
|
43
50
|
|
|
44
51
|
constructor(id: string, _opts?: unknown) {
|
|
45
52
|
this.id = id;
|
|
53
|
+
this.open = true;
|
|
46
54
|
setTimeout(() => this.emit("open", id), 0);
|
|
47
55
|
}
|
|
48
56
|
|
|
@@ -55,6 +63,7 @@ class MockPeer {
|
|
|
55
63
|
|
|
56
64
|
destroy() {
|
|
57
65
|
this.destroyed = true;
|
|
66
|
+
this.open = false;
|
|
58
67
|
this.emit("disconnected");
|
|
59
68
|
}
|
|
60
69
|
|
|
@@ -829,5 +838,52 @@ describe("PeerJsChannel", () => {
|
|
|
829
838
|
expect((ch as any)._isTrustedPeer("cli-12345")).toBe(true);
|
|
830
839
|
expect((ch as any)._isTrustedPeer("other-12345")).toBe(false);
|
|
831
840
|
});
|
|
841
|
+
|
|
842
|
+
it("ensureConnected reconnects disconnected peer and resets backoff", async () => {
|
|
843
|
+
const ch = new PeerJsChannel();
|
|
844
|
+
ch.configure("my-test-peer", []);
|
|
845
|
+
ch.start();
|
|
846
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
847
|
+
|
|
848
|
+
const peer = (ch as any).peer as MockPeer;
|
|
849
|
+
expect(peer).toBeDefined();
|
|
850
|
+
|
|
851
|
+
// Simulate disconnect
|
|
852
|
+
peer.disconnected = true;
|
|
853
|
+
peer.open = false;
|
|
854
|
+
(ch as any)._reconnectAttempts = 3;
|
|
855
|
+
(ch as any)._reconnectTimer = setTimeout(() => {}, 8000);
|
|
856
|
+
|
|
857
|
+
ch.ensureConnected(false);
|
|
858
|
+
|
|
859
|
+
expect((ch as any)._reconnectTimer).toBeNull();
|
|
860
|
+
expect((ch as any)._reconnectAttempts).toBe(0);
|
|
861
|
+
expect(peer.reconnect).toHaveBeenCalled();
|
|
862
|
+
|
|
863
|
+
ch.stop();
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
it("ensureConnected re-initializes a destroyed peer instance", async () => {
|
|
867
|
+
const ch = new PeerJsChannel();
|
|
868
|
+
ch.configure("my-test-peer-2", []);
|
|
869
|
+
ch.start();
|
|
870
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
871
|
+
|
|
872
|
+
const initialPeer = (ch as any).peer as MockPeer;
|
|
873
|
+
expect(initialPeer).toBeDefined();
|
|
874
|
+
|
|
875
|
+
// Destroy peer instance
|
|
876
|
+
initialPeer.destroy();
|
|
877
|
+
expect(initialPeer.destroyed).toBe(true);
|
|
878
|
+
|
|
879
|
+
ch.ensureConnected(true);
|
|
880
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
881
|
+
|
|
882
|
+
const newPeer = (ch as any).peer as MockPeer;
|
|
883
|
+
expect(newPeer).not.toBe(initialPeer);
|
|
884
|
+
expect(newPeer.destroyed).toBe(false);
|
|
885
|
+
|
|
886
|
+
ch.stop();
|
|
887
|
+
});
|
|
832
888
|
});
|
|
833
889
|
});
|
|
@@ -346,6 +346,35 @@ export class PeerJsChannel implements Channel {
|
|
|
346
346
|
this._initPeer();
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
ensureConnected(force: boolean = false): void {
|
|
350
|
+
if (!this.running || !this.isConfigured()) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (this._reconnectTimer !== null) {
|
|
355
|
+
clearTimeout(this._reconnectTimer);
|
|
356
|
+
this._reconnectTimer = null;
|
|
357
|
+
}
|
|
358
|
+
this._reconnectAttempts = 0;
|
|
359
|
+
|
|
360
|
+
const peer = this.peer as any;
|
|
361
|
+
if (!peer || peer.destroyed || (force && !peer.open)) {
|
|
362
|
+
if (peer && !peer.destroyed) {
|
|
363
|
+
try {
|
|
364
|
+
peer.destroy();
|
|
365
|
+
} catch (_) {}
|
|
366
|
+
}
|
|
367
|
+
this.peer = null;
|
|
368
|
+
this._initPeer();
|
|
369
|
+
} else if (peer.disconnected) {
|
|
370
|
+
try {
|
|
371
|
+
peer.reconnect?.();
|
|
372
|
+
} catch {
|
|
373
|
+
this._initPeer();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
349
378
|
stop(): void {
|
|
350
379
|
this.running = false;
|
|
351
380
|
if (this._reconnectTimer !== null) {
|
|
@@ -508,4 +508,42 @@ describe("TelegramChannel", () => {
|
|
|
508
508
|
),
|
|
509
509
|
).toBe(true);
|
|
510
510
|
});
|
|
511
|
+
|
|
512
|
+
it("ensureConnected aborts hanging poll, resets backoff, and launches fresh poll", async () => {
|
|
513
|
+
const channel = new TelegramChannel();
|
|
514
|
+
channel.configure("test-token", ["123"]);
|
|
515
|
+
|
|
516
|
+
let fetchCount = 0;
|
|
517
|
+
jest.spyOn(globalThis, "fetch").mockImplementation((async (
|
|
518
|
+
_url: any,
|
|
519
|
+
opts: any,
|
|
520
|
+
) => {
|
|
521
|
+
fetchCount++;
|
|
522
|
+
// Return a promise that hangs until aborted
|
|
523
|
+
return new Promise((_resolve, reject) => {
|
|
524
|
+
opts?.signal?.addEventListener("abort", () => {
|
|
525
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
}) as any);
|
|
529
|
+
|
|
530
|
+
channel.start();
|
|
531
|
+
expect(channel.running).toBe(true);
|
|
532
|
+
expect(fetchCount).toBe(1);
|
|
533
|
+
|
|
534
|
+
// Simulate high backoff delay
|
|
535
|
+
channel.pollRetryDelayMs = 30_000;
|
|
536
|
+
|
|
537
|
+
const oldSessionId = channel.pollSessionId;
|
|
538
|
+
const oldAbort = channel.abortController;
|
|
539
|
+
|
|
540
|
+
channel.ensureConnected();
|
|
541
|
+
|
|
542
|
+
expect(channel.pollRetryDelayMs).toBe(1_000);
|
|
543
|
+
expect(oldAbort?.signal.aborted).toBe(true);
|
|
544
|
+
expect(channel.pollSessionId).toBe(oldSessionId + 1);
|
|
545
|
+
expect(fetchCount).toBe(2);
|
|
546
|
+
|
|
547
|
+
channel.stop();
|
|
548
|
+
});
|
|
511
549
|
});
|
|
@@ -117,6 +117,19 @@ export class TelegramChannel implements Channel {
|
|
|
117
117
|
void this.poll(sessionId, controller);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
ensureConnected(): void {
|
|
121
|
+
if (!this.token || !this.running) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.resetPollBackoff();
|
|
126
|
+
this.abortController?.abort();
|
|
127
|
+
const sessionId = ++this.pollSessionId;
|
|
128
|
+
const controller = new AbortController();
|
|
129
|
+
this.abortController = controller;
|
|
130
|
+
void this.poll(sessionId, controller);
|
|
131
|
+
}
|
|
132
|
+
|
|
120
133
|
stop(): void {
|
|
121
134
|
this.running = false;
|
|
122
135
|
this.resetPollBackoff();
|
|
@@ -26,8 +26,10 @@ export type KnownChannelType =
|
|
|
26
26
|
| "telegram";
|
|
27
27
|
|
|
28
28
|
export interface Channel {
|
|
29
|
+
ensureConnected?(force?: boolean): void;
|
|
29
30
|
onMessage(callback: ChannelMessageCallback): void;
|
|
30
31
|
onTyping?(callback: ChannelTypingCallback): void;
|
|
32
|
+
running?: boolean;
|
|
31
33
|
send(
|
|
32
34
|
groupId: string,
|
|
33
35
|
text: string,
|