From fc17b80e831cde15290720798c21739b14989e81 Mon Sep 17 00:00:00 2001 From: Ammar Date: Mon, 15 Jun 2026 17:20:54 -0500 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=A4=96=20refactor:=20remove=20mux.md?= =?UTF-8?q?=20link=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the mux.md end-to-end-encrypted share/paste feature, which has regressed and is no longer maintained: - Sharing UI: ShareTranscriptDialog, ShareMessagePopover, ShareSigningBadges, the AssistantMessage "Share" button, "Share transcript" menu/keybind, and the telemetry-gated link-sharing context. - Consuming: web_fetch no longer special-cases mux.md URLs. - Signing: signingService + orpc `signing` namespace/schemas (only used by mux.md sharing). - Client lib, share-expiration prefs/storage, preload/vite/env overrides. - Docs page, nav/redirects, @coder/mux-md-client dependency; regenerated built-in skill docs index. Plan-file display: replace the "Share" (copy mux.md link) action with a "Copy file path" action. --- bun.lock | 128 +--- docs/docs.json | 3 - docs/img/message-sharing.webp | Bin 169596 -> 0 bytes docs/reference/telemetry.mdx | 6 - docs/workspaces/sharing.mdx | 100 --- package.json | 3 +- .../AgentListItem/AgentListItem.stories.tsx | 21 +- .../AgentListItem/AgentListItem.test.tsx | 8 - .../AgentListItem/AgentListItem.tsx | 21 - .../components/AppLoader/AppLoader.tsx | 9 +- .../LeftSidebar/LeftSidebar.stories.tsx | 17 +- .../ProjectSidebar/ProjectSidebar.test.tsx | 2 - .../ShareMessagePopover.tsx | 553 -------------- .../ShareSigningBadges.stories.tsx | 66 -- .../ShareSigningBadges/ShareSigningBadges.tsx | 187 ----- .../ShareTranscriptDialog.test.tsx | 149 ---- .../ShareTranscriptDialog.tsx | 510 ------------- .../SshPromptDialog/SshPromptDialog.test.tsx | 4 +- .../WorkspaceActionsMenuContent.test.tsx | 1 - .../WorkspaceActionsMenuContent.tsx | 16 - .../WorkspaceMenuBar.test.tsx | 6 - .../WorkspaceMenuBar/WorkspaceMenuBar.tsx | 30 - .../contexts/TelemetryEnabledContext.tsx | 70 -- .../features/Messages/AssistantMessage.tsx | 21 +- .../RightSidebar/RightSidebar.stories.tsx | 71 +- .../Settings/Sections/KeybindsSection.tsx | 2 - .../Tools/ProposePlanToolCall.test.tsx | 4 - .../features/Tools/ProposePlanToolCall.tsx | 30 +- src/browser/stories/helpers/chatSetup.ts | 7 - src/browser/stories/mocks/orpc.ts | 24 - src/browser/utils/sharedUrlCache.test.ts | 165 ----- src/browser/utils/sharedUrlCache.ts | 131 ---- src/browser/utils/ui/keybinds.ts | 5 - .../config/schemas/appConfigOnDisk.test.ts | 1 - .../config/schemas/userPreferences.test.ts | 1 - src/common/config/schemas/userPreferences.ts | 30 - src/common/constants/storage.ts | 14 - src/common/lib/muxMd.test.ts | 199 ------ src/common/lib/muxMd.ts | 187 ----- src/common/lib/shareExpiration.ts | 49 -- src/common/orpc/schemas.ts | 3 - src/common/orpc/schemas/api.ts | 3 - src/common/orpc/schemas/signing.ts | 72 -- .../userPreferencesStorage.test.ts | 1 - .../preferences/userPreferencesStorage.ts | 42 -- src/common/types/global.d.ts | 9 - src/common/utils/tools/tools.ts | 2 - src/desktop/preload.ts | 2 - src/node/builtinSkills/mux-docs.md | 1 - src/node/orpc/context.ts | 2 - src/node/orpc/router.ts | 21 - .../builtInSkillContent.generated.ts | 113 --- src/node/services/serviceContainer.ts | 4 - src/node/services/signingService.test.ts | 325 --------- src/node/services/signingService.ts | 675 ------------------ src/node/services/tools/web_fetch.test.ts | 131 ---- src/node/services/tools/web_fetch.ts | 52 -- tests/ui/chat/shareMessage.test.ts | 134 ---- vite.config.ts | 1 - 59 files changed, 86 insertions(+), 4358 deletions(-) delete mode 100644 docs/img/message-sharing.webp delete mode 100644 docs/workspaces/sharing.mdx delete mode 100644 src/browser/components/ShareMessagePopover/ShareMessagePopover.tsx delete mode 100644 src/browser/components/ShareSigningBadges/ShareSigningBadges.stories.tsx delete mode 100644 src/browser/components/ShareSigningBadges/ShareSigningBadges.tsx delete mode 100644 src/browser/components/ShareTranscriptDialog/ShareTranscriptDialog.test.tsx delete mode 100644 src/browser/components/ShareTranscriptDialog/ShareTranscriptDialog.tsx delete mode 100644 src/browser/contexts/TelemetryEnabledContext.tsx delete mode 100644 src/browser/utils/sharedUrlCache.test.ts delete mode 100644 src/browser/utils/sharedUrlCache.ts delete mode 100644 src/common/lib/muxMd.test.ts delete mode 100644 src/common/lib/muxMd.ts delete mode 100644 src/common/lib/shareExpiration.ts delete mode 100644 src/common/orpc/schemas/signing.ts delete mode 100644 src/node/services/signingService.test.ts delete mode 100644 src/node/services/signingService.ts delete mode 100644 tests/ui/chat/shareMessage.test.ts diff --git a/bun.lock b/bun.lock index 0a8d1f0c7b..65a1e11fab 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "mux", @@ -15,7 +16,6 @@ "@ai-sdk/openai-compatible": "^2.0.46", "@ai-sdk/xai": "^3.0.88", "@aws-sdk/credential-providers": "^3.940.0", - "@coder/mux-md-client": "0.1.0-main.32", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", @@ -561,8 +561,6 @@ "@chevrotain/utils": ["@chevrotain/utils@11.0.3", "", {}, "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="], - "@coder/mux-md-client": ["@coder/mux-md-client@0.1.0-main.32", "", { "dependencies": { "@noble/curves": "^2.0.1", "@noble/ed25519": "^3.0.0", "@noble/hashes": "^2.0.1" } }, "sha512-KNCXhnhq1VEr0TbLXm3P5QKGqkUBmjKLfRECviQY3SHJuYC2Pswzi3qz8HR1PIDeysIZ2kP7YJbXr2VbQeos8g=="], - "@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="], "@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="], @@ -879,12 +877,6 @@ "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.6", "", { "os": "win32", "cpu": "x64" }, "sha512-QGs18P4OKdK9y2F3Th42+KGnwsc2iaThOe6jxQgP62kslUU4W+g6AzI6bdIn/pslhSfxjAMU5SjakfT5Fyo/xA=="], - "@noble/curves": ["@noble/curves@2.0.1", "", { "dependencies": { "@noble/hashes": "2.0.1" } }, "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw=="], - - "@noble/ed25519": ["@noble/ed25519@3.0.0", "", {}, "sha512-QyteqMNm0GLqfa5SoYbSC3+Pvykwpn95Zgth4MFVSMKBB75ELl9tX1LAVsN4c3HXOrakHsF2gL4zWDAYCcsnzg=="], - - "@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], @@ -1459,7 +1451,7 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + "@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], "@types/plist": ["@types/plist@3.0.5", "", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="], @@ -3613,7 +3605,7 @@ "undici": ["undici@7.16.0", "", {}, "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g=="], - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "unicode-canonical-property-names-ecmascript": ["unicode-canonical-property-names-ecmascript@2.0.1", "", {}, "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg=="], @@ -3835,26 +3827,14 @@ "@istanbuljs/load-nyc-config/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], - "@jest/console/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "@jest/console/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "@jest/core/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "@jest/core/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], "@jest/core/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "@jest/core/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], - "@jest/environment/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - - "@jest/fake-timers/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - - "@jest/pattern/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - - "@jest/reporters/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "@jest/reporters/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "@jest/reporters/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], @@ -3871,8 +3851,6 @@ "@jest/transform/write-file-atomic": ["write-file-atomic@5.0.1", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw=="], - "@jest/types/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "@jest/types/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], @@ -3963,18 +3941,18 @@ "@types/body-parser/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], - "@types/connect/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], + "@types/cacheable-request/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], - "@types/cors/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], + "@types/connect/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], "@types/express-serve-static-core/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], - "@types/fs-extra/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - - "@types/jsdom/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], + "@types/keyv/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], "@types/plist/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], + "@types/responselike/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + "@types/send/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], "@types/serve-static/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], @@ -3985,10 +3963,6 @@ "@types/wait-on/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], - "@types/write-file-atomic/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - - "@types/ws/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "@types/yauzl/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -4023,8 +3997,6 @@ "builder-util/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], - "bun-types/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "caching-transform/write-file-atomic": ["write-file-atomic@3.0.3", "", { "dependencies": { "imurmurhash": "^0.1.4", "is-typedarray": "^1.0.0", "signal-exit": "^3.0.2", "typedarray-to-buffer": "^3.1.5" } }, "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q=="], "chokidar/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -4061,6 +4033,8 @@ "dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], + "electron/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + "electron-builder/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "electron-publish/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -4107,8 +4081,6 @@ "globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - "happy-dom/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "hasha/type-fest": ["type-fest@0.8.1", "", {}, "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA=="], "hast-util-to-parse5/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], @@ -4149,8 +4121,6 @@ "jest-environment-node/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], - "jest-haste-map/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "jest-haste-map/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "jest-junit/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], @@ -4161,8 +4131,6 @@ "jest-message-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "jest-mock/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "jest-process-manager/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "jest-process-manager/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], @@ -4175,8 +4143,6 @@ "jest-runner/source-map-support": ["source-map-support@0.5.13", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w=="], - "jest-runtime/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "jest-runtime/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "jest-runtime/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], @@ -4185,8 +4151,6 @@ "jest-snapshot/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "jest-util/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "jest-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "jest-util/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], @@ -4195,16 +4159,12 @@ "jest-watch-typeahead/slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="], - "jest-watcher/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "jest-watcher/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], "jest-watcher/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "jest-watcher/string-length": ["string-length@4.0.2", "", { "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" } }, "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ=="], - "jest-worker/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "jsdom/parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="], "jsdom/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], @@ -4349,28 +4309,16 @@ "@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - "@jest/console/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "@jest/console/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "@jest/console/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "@jest/core/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "@jest/core/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], "@jest/core/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "@jest/core/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "@jest/environment/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@jest/fake-timers/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@jest/pattern/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@jest/reporters/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "@jest/reporters/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "@jest/reporters/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -4393,8 +4341,6 @@ "@jest/transform/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "@jest/types/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "@jest/types/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "@jest/types/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -4441,38 +4387,14 @@ "@testing-library/jest-dom/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "@types/asn1/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@types/body-parser/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@types/connect/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@types/cors/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@types/cacheable-request/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "@types/express-serve-static-core/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@types/keyv/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "@types/fs-extra/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@types/jsdom/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@types/plist/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@types/send/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@types/serve-static/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@types/responselike/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], - "@types/sshpk/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@types/wait-on/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@types/write-file-atomic/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@types/ws/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@types/yauzl/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@2.0.5", "", { "dependencies": { "tinyrainbow": "^1.2.0" } }, "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ=="], @@ -4507,8 +4429,6 @@ "builder-util/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - "bun-types/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "caching-transform/write-file-atomic/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -4545,6 +4465,8 @@ "electron-publish/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "electron/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "eslint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -4563,14 +4485,10 @@ "global-prefix/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "happy-dom/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], "htmlparser2/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - "jest-circus/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "jest-circus/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "jest-circus/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -4599,10 +4517,6 @@ "jest-each/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "jest-environment-node/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "jest-haste-map/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "jest-junit/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "jest-matcher-utils/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -4613,8 +4527,6 @@ "jest-message-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "jest-mock/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "jest-process-manager/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "jest-process-manager/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -4623,14 +4535,10 @@ "jest-resolve/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "jest-runner/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "jest-runner/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "jest-runner/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "jest-runtime/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "jest-runtime/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "jest-runtime/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -4647,8 +4555,6 @@ "jest-snapshot/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "jest-util/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "jest-util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "jest-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -4657,8 +4563,6 @@ "jest-validate/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "jest-watcher/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "jest-watcher/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], "jest-watcher/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -4667,8 +4571,6 @@ "jest-watcher/string-length/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "jest-worker/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "jsdom/parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], diff --git a/docs/docs.json b/docs/docs.json index 3f3b1d0ecd..2fd9ea3cbc 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -46,7 +46,6 @@ "pages": [ "workspaces", "workspaces/fork", - "workspaces/sharing", "workspaces/muxignore", { "group": "Compaction", @@ -166,8 +165,6 @@ { "source": "/guides/compaction", "destination": "/workspaces/compaction" }, { "source": "/guides/compaction/manual", "destination": "/workspaces/compaction/manual" }, { "source": "/guides/compaction/automatic", "destination": "/workspaces/compaction/automatic" }, - { "source": "/sharing", "destination": "/workspaces/sharing" }, - { "source": "/guides/sharing", "destination": "/workspaces/sharing" }, { "source": "/prompting-tips", "destination": "/agents/prompting-tips" }, { "source": "/prompting-tips.html", "destination": "/agents/prompting-tips" }, { "source": "/guides/prompting-tips", "destination": "/agents/prompting-tips" }, diff --git a/docs/img/message-sharing.webp b/docs/img/message-sharing.webp deleted file mode 100644 index 51cbe4fdb18ab63602f9d9920e56ae31490e2929..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 169596 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 - Disabling telemetry also hides the **Share** button on assistant messages. Link sharing uses - [mux.md](https://mux.md), a separate Mux service, and is gated on telemetry enablement to respect - your privacy preferences. - - ## Source code - **Payload definitions**: [`src/common/telemetry/payload.ts`](https://github.com/coder/mux/blob/main/src/common/telemetry/payload.ts) diff --git a/docs/workspaces/sharing.mdx b/docs/workspaces/sharing.mdx deleted file mode 100644 index eaef4fb365..0000000000 --- a/docs/workspaces/sharing.mdx +++ /dev/null @@ -1,100 +0,0 @@ ---- -title: Message Sharing -sidebarTitle: Sharing -description: Share encrypted messages with cryptographic signatures via Mux ---- - -Mux lets you share assistant messages via [mux.md](https://mux.md), an end-to-end encrypted paste service. Shared messages can also be cryptographically signed to prove authorship. - -![Sharing](../img/message-sharing.webp) - -## How sharing works - -1. **End-to-end encryption**: Content is encrypted in your browser using AES-256-GCM before upload. The encryption key stays in the URL fragment and is never sent to the server. - -2. **Optional signing**: When a signing key is available, Mux signs the content with your private key. Recipients can verify the signature on mux.md. - -3. **Expiration**: Shares can expire after 1 hour, 24 hours, 7 days, 30 days, or never. You can change expiration after sharing. - -## Message signing - -Signing proves that you authored a shared message. When enabled, mux.md displays your GitHub username alongside a “Verified” badge. - -### Signing key discovery - -Mux looks for a signing key in these locations (first match wins): - -1. `~/.mux/message_signing_key` — Mux-specific key (can be a symlink) -2. `~/.ssh/id_ed25519` — standard SSH Ed25519 key -3. `~/.ssh/id_ecdsa` — standard SSH ECDSA key - -Mux can also sign using keys loaded in your SSH agent when `SSH_AUTH_SOCK` is set (for example, -1Password's SSH agent). Agent-backed keys are preferred over the default `~/.ssh/id_*` files. - - - To reuse an existing key without copying it: - -```bash -ln -s ~/.ssh/id_ed25519 ~/.mux/message_signing_key -``` - - - -### Supported key types - -- **Ed25519** (recommended) — fast, secure, 32-byte keys -- **ECDSA** — P-256, P-384, and P-521 curves supported - -Encrypted key files (passphrase-protected) are skipped automatically unless the key is available via ssh-agent. - -### GitHub identity detection - -To display your GitHub username on signed shares, Mux detects your identity via the GitHub CLI: - -```bash -gh auth status -``` - -If you're logged in with `gh auth login`, your GitHub username appears on mux.md alongside the signature verification. - - - GitHub CLI is optional. Without it, shares are still signed—recipients just see the public key - fingerprint instead of your username. - - -### Enabling/disabling signing - -Click the pen icon in the share popover to toggle signing on or off. This setting persists across sessions. - -When signing is disabled (or no key is found), shares are still encrypted—they just won’t include a signature. - -## Using shared content in Mux - -Mux can read mux.md URLs using the `web_fetch` tool. When you paste a mux.md link into chat, Mux decrypts the content client-side and makes it available to the assistant. - -This enables several workflows: - -- **Cross-session sharing**: Move context or instructions between workspaces in the same Mux client -- **Team collaboration**: Send encrypted snippets to teammates who can paste them into their own Mux sessions -- **Preserving context**: Save important outputs and retrieve them later in new conversations - -Paste any `https://mux.md/#` URL into your message, and Mux will fetch and decrypt the content. - - - The encryption key (after `#`) never leaves your client. Only the encrypted blob travels over the - network. - - -## Generating a signing key - -If you don’t already have an SSH key, generate one: - -```bash -# Ed25519 (recommended) -ssh-keygen -t ed25519 -f ~/.mux/message_signing_key -N "" - -# Or ECDSA -ssh-keygen -t ecdsa -b 256 -f ~/.mux/message_signing_key -N "" -``` - -The `-N ""` flag creates a key without a passphrase (required for Mux to use it automatically). diff --git a/package.json b/package.json index 8b154ee48b..4ee49dcb01 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,6 @@ "@ai-sdk/openai-compatible": "^2.0.46", "@ai-sdk/xai": "^3.0.88", "@aws-sdk/credential-providers": "^3.940.0", - "@coder/mux-md-client": "0.1.0-main.32", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", @@ -123,8 +122,8 @@ "quickjs-emscripten-core": "^0.31.0", "react": "18.3.1", "react-colorful": "^5.6.1", - "react-resizable-panels": "^3.0.6", "react-dom": "18.3.1", + "react-resizable-panels": "^3.0.6", "react-router-dom": "^7.11.0", "recharts": "^2.15.3", "rehype-harden": "^1.1.5", diff --git a/src/browser/components/AgentListItem/AgentListItem.stories.tsx b/src/browser/components/AgentListItem/AgentListItem.stories.tsx index e1d2d5bb72..4f4829eca7 100644 --- a/src/browser/components/AgentListItem/AgentListItem.stories.tsx +++ b/src/browser/components/AgentListItem/AgentListItem.stories.tsx @@ -4,7 +4,6 @@ import { useEffect } from "react"; import { AgentListItem } from "@/browser/components/AgentListItem/AgentListItem"; import { APIProvider } from "@/browser/contexts/API"; import { ProjectProvider } from "@/browser/contexts/ProjectContext"; -import { TelemetryEnabledProvider } from "@/browser/contexts/TelemetryEnabledContext"; import { TitleEditProvider } from "@/browser/contexts/WorkspaceTitleEditContext"; import { DndProvider } from "react-dnd"; import { HTML5Backend } from "react-dnd-html5-backend"; @@ -183,17 +182,15 @@ function StoryScaffold(props: { return ( - - Promise.resolve({ success: true })}> - - -

-
{props.children}
-
- - - - + Promise.resolve({ success: true })}> + + +
+
{props.children}
+
+
+
+
); diff --git a/src/browser/components/AgentListItem/AgentListItem.test.tsx b/src/browser/components/AgentListItem/AgentListItem.test.tsx index c23732ddb0..2841b32ba5 100644 --- a/src/browser/components/AgentListItem/AgentListItem.test.tsx +++ b/src/browser/components/AgentListItem/AgentListItem.test.tsx @@ -8,7 +8,6 @@ import type * as ReactDndModuleType from "react-dnd"; import type * as ReactDndHtml5BackendModuleType from "react-dnd-html5-backend"; import type * as APIModuleType from "@/browser/contexts/API"; import type * as ProjectContextModuleType from "@/browser/contexts/ProjectContext"; -import type * as TelemetryEnabledContextModuleType from "@/browser/contexts/TelemetryEnabledContext"; import type * as WorkspaceTitleEditContextModuleType from "@/browser/contexts/WorkspaceTitleEditContext"; import type * as ContextMenuPositionModuleType from "@/browser/hooks/useContextMenuPosition"; import type * as ExperimentsModuleType from "@/browser/hooks/useExperiments"; @@ -136,8 +135,6 @@ function installAgentListItemTestDoubles() { const actualApi = require("@/browser/contexts/API?real=1") as typeof APIModuleType; const actualProjectContext = require("@/browser/contexts/ProjectContext?real=1") as typeof ProjectContextModuleType; - const actualTelemetryEnabledContext = - require("@/browser/contexts/TelemetryEnabledContext?real=1") as typeof TelemetryEnabledContextModuleType; const actualWorkspaceTitleEditContext = require("@/browser/contexts/WorkspaceTitleEditContext?real=1") as typeof WorkspaceTitleEditContextModuleType; const actualContextMenuPosition = @@ -183,11 +180,6 @@ function installAgentListItemTestDoubles() { }), })); - void mock.module("@/browser/contexts/TelemetryEnabledContext", () => ({ - ...actualTelemetryEnabledContext, - useLinkSharingEnabled: () => false, - })); - void mock.module("@/browser/contexts/WorkspaceTitleEditContext", () => ({ ...actualWorkspaceTitleEditContext, useTitleEdit: () => ({ diff --git a/src/browser/components/AgentListItem/AgentListItem.tsx b/src/browser/components/AgentListItem/AgentListItem.tsx index 19d0a0baa8..1882973726 100644 --- a/src/browser/components/AgentListItem/AgentListItem.tsx +++ b/src/browser/components/AgentListItem/AgentListItem.tsx @@ -69,9 +69,7 @@ import { WORKSPACE_DRAG_TYPE, type WorkspaceDragItem, } from "../WorkspaceSectionDropZone/WorkspaceSectionDropZone"; -import { useLinkSharingEnabled } from "@/browser/contexts/TelemetryEnabledContext"; import { formatKeybind, KEYBINDS } from "@/browser/utils/ui/keybinds"; -import { ShareTranscriptDialog } from "../ShareTranscriptDialog/ShareTranscriptDialog"; import { WorkspaceHeartbeatModal } from "../WorkspaceHeartbeatModal"; import { AutomationModal } from "../AutomationModal"; import { WorkspaceActionsMenuContent } from "../WorkspaceActionsMenuContent/WorkspaceActionsMenuContent"; @@ -510,8 +508,6 @@ function RegularAgentListItemInner(props: AgentListItemProps) { const automationScheduleProjectPath = projectWorkflowScheduleMatch?.projectPath ?? automationProjectPath; - const linkSharingEnabled = useLinkSharingEnabled(); - const [shareTranscriptOpen, setShareTranscriptOpen] = useState(false); const [automationModalOpen, setAutomationModalOpen] = useState(false); const [heartbeatModalOpen, setHeartbeatModalOpen] = useState(false); const overflowMenuButtonRef = useRef(null); @@ -572,9 +568,6 @@ function RegularAgentListItemInner(props: AgentListItemProps) { node.focus(); }, []); - // SHARE_TRANSCRIPT keybind is handled in WorkspaceMenuBar (always mounted), - // so it works even when the sidebar is collapsed and list items are unmounted. - const startEditing = () => { if (requestEdit(workspaceId, workspaceTitle)) { setEditingTitle(workspaceTitle); @@ -971,12 +964,10 @@ function RegularAgentListItemInner(props: AgentListItemProps) { onForkChat={(anchorEl) => { void onForkWorkspace(workspaceId, anchorEl); }} - onShareTranscript={() => setShareTranscriptOpen(true)} onArchiveChat={(anchorEl) => { void onArchiveWorkspace(workspaceId, anchorEl); }} onCloseMenu={() => ctxMenu.close()} - linkSharingEnabled={linkSharingEnabled === true} /> {!isSelected && !isUnread && ( )} - {/* Share transcript dialog – rendered as a sibling to the overflow menu. - Triggered by the menu item above or the Ctrl+Shift+L keybind. - Uses a Dialog (modal) so it stays visible regardless of popover dismissal. */} - {linkSharingEnabled === true && ( - - )} ) )} diff --git a/src/browser/components/AppLoader/AppLoader.tsx b/src/browser/components/AppLoader/AppLoader.tsx index a5deec8631..a1be2d41e6 100644 --- a/src/browser/components/AppLoader/AppLoader.tsx +++ b/src/browser/components/AppLoader/AppLoader.tsx @@ -18,7 +18,6 @@ import { PolicyBlockedScreen } from "@/browser/components/PolicyBlockedScreen/Po import { APIProvider, useAPI, type APIClient } from "@/browser/contexts/API"; import { WorkspaceProvider, useWorkspaceContext } from "../../contexts/WorkspaceContext"; import { RouterProvider } from "../../contexts/RouterContext"; -import { TelemetryEnabledProvider } from "../../contexts/TelemetryEnabledContext"; import { hydrateUserPreferencesLocalCache, UserPreferencesProvider, @@ -269,11 +268,9 @@ function AppLoaderInner() { transition={prefersReducedMotion ? { duration: 0 } : { duration: 0.3, ease: "easeOut" }} className="bg-surface-primary h-full" > - - - - - + + + )} diff --git a/src/browser/components/LeftSidebar/LeftSidebar.stories.tsx b/src/browser/components/LeftSidebar/LeftSidebar.stories.tsx index 1bf8ad809b..ad5a736a80 100644 --- a/src/browser/components/LeftSidebar/LeftSidebar.stories.tsx +++ b/src/browser/components/LeftSidebar/LeftSidebar.stories.tsx @@ -28,7 +28,6 @@ import { SettingsProvider } from "@/browser/contexts/SettingsContext"; import { ConfirmDialogProvider } from "@/browser/contexts/ConfirmDialogContext"; import { ExperimentsProvider } from "@/browser/contexts/ExperimentsContext"; import { AboutDialogProvider } from "@/browser/contexts/AboutDialogContext"; -import { TelemetryEnabledProvider } from "@/browser/contexts/TelemetryEnabledContext"; import { TooltipProvider } from "@/browser/components/Tooltip/Tooltip"; import { useWorkspaceRecency } from "@/browser/stores/WorkspaceStore"; import { buildSortedWorkspacesByProject } from "@/browser/utils/ui/workspaceFiltering"; @@ -161,15 +160,13 @@ function LeftSidebarStoryShell(props: LeftSidebarStoryShellProps) { - - - - - - - - - + + + + + + + diff --git a/src/browser/components/ProjectSidebar/ProjectSidebar.test.tsx b/src/browser/components/ProjectSidebar/ProjectSidebar.test.tsx index bbfb4fed42..f557c84ba4 100644 --- a/src/browser/components/ProjectSidebar/ProjectSidebar.test.tsx +++ b/src/browser/components/ProjectSidebar/ProjectSidebar.test.tsx @@ -16,7 +16,6 @@ import type { AgentRowRenderMeta } from "@/browser/utils/ui/workspaceFiltering"; import type { FrontendWorkspaceMetadata } from "@/common/types/workspace"; import * as DesktopTitlebarModule from "@/browser/hooks/useDesktopTitlebar"; import * as ThemeContextModule from "@/browser/contexts/ThemeContext"; -import * as TelemetryEnabledContextModule from "@/browser/contexts/TelemetryEnabledContext"; import * as APIModule from "@/browser/contexts/API"; import * as ConfirmDialogContextModule from "@/browser/contexts/ConfirmDialogContext"; import * as ProjectContextModule from "@/browser/contexts/ProjectContext"; @@ -456,7 +455,6 @@ function installProjectSidebarTestDoubles() { )) as typeof ReactColorfulModule.HexColorPicker); spyOn(DesktopTitlebarModule, "isDesktopMode").mockImplementation(() => false); - spyOn(TelemetryEnabledContextModule, "useLinkSharingEnabled").mockImplementation(() => false); spyOn(ThemeContextModule, "useTheme").mockImplementation(() => ({ theme: "light", themePreference: "light", diff --git a/src/browser/components/ShareMessagePopover/ShareMessagePopover.tsx b/src/browser/components/ShareMessagePopover/ShareMessagePopover.tsx deleted file mode 100644 index 20efca9126..0000000000 --- a/src/browser/components/ShareMessagePopover/ShareMessagePopover.tsx +++ /dev/null @@ -1,553 +0,0 @@ -import React, { useState, useEffect, useRef, useCallback } from "react"; -import { Popover, PopoverContent, PopoverTrigger } from "@/browser/components/Popover/Popover"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/browser/components/SelectPrimitive/SelectPrimitive"; -import { Tooltip, TooltipTrigger, TooltipContent } from "@/browser/components/Tooltip/Tooltip"; -import { Button } from "@/browser/components/Button/Button"; -import { Check, ExternalLink, Link2, Loader2, Trash2 } from "lucide-react"; -import { CopyIcon } from "@/browser/components/icons/CopyIcon/CopyIcon"; -import { copyToClipboard } from "@/browser/utils/clipboard"; - -import { - uploadToMuxMd, - deleteFromMuxMd, - updateMuxMdExpiration, - type SignatureEnvelope, -} from "@/common/lib/muxMd"; -import { - getShareData, - setShareData, - removeShareData, - updateShareExpiration, - type ShareData, -} from "@/browser/utils/sharedUrlCache"; -import { cn } from "@/common/lib/utils"; -import { - type ExpirationValue, - EXPIRATION_OPTIONS, - expirationToMs, - timestampToExpiration, - formatExpiration, -} from "@/common/lib/shareExpiration"; -import { SHARE_EXPIRATION_KEY, SHARE_SIGNING_KEY } from "@/common/constants/storage"; -import { - readPersistedState, - updatePersistedState, - usePersistedState, -} from "@/browser/hooks/usePersistedState"; -import { useLinkSharingEnabled } from "@/browser/contexts/TelemetryEnabledContext"; -import { useAPI } from "@/browser/contexts/API"; -import type { SigningCapabilities } from "@/common/orpc/schemas"; -import { EncryptionBadge, SigningBadge } from "../ShareSigningBadges/ShareSigningBadges"; - -interface ShareMessagePopoverProps { - content: string; - model?: string; - thinking?: string; - disabled?: boolean; - /** Workspace name used for uploaded filename (e.g., "my-workspace" -> "my-workspace.md") */ - workspaceName?: string; -} - -export const ShareMessagePopover: React.FC = ({ - content, - model, - thinking, - disabled = false, - workspaceName, -}) => { - // Hide share button when user explicitly disabled telemetry - const linkSharingEnabled = useLinkSharingEnabled(); - const { api } = useAPI(); - - const [isOpen, setIsOpen] = useState(false); - const [isUploading, setIsUploading] = useState(false); - const [isUpdating, setIsUpdating] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); - const [showUpdated, setShowUpdated] = useState(false); - const [copied, setCopied] = useState(false); - const [error, setError] = useState(null); - const urlInputRef = useRef(null); - - // Current share data (from upload or cache) - const [shareData, setLocalShareData] = useState(null); - - // Signing capabilities and enabled state - const [signingCapabilities, setSigningCapabilities] = useState(null); - const [signingCapabilitiesLoaded, setSigningCapabilitiesLoaded] = useState(false); - const [signingEnabled, setSigningEnabled] = usePersistedState(SHARE_SIGNING_KEY, true, { - listener: true, - }); - - // Load signing capabilities on first popover open - useEffect(() => { - if (isOpen && !signingCapabilitiesLoaded && api) { - void api.signing - .capabilities({}) - .then(setSigningCapabilities) - .catch(() => { - // Signing unavailable - leave capabilities null - }) - .finally(() => { - setSigningCapabilitiesLoaded(true); - }); - } - }, [isOpen, api, signingCapabilitiesLoaded]); - - // Load cached data when content changes - useEffect(() => { - if (content) { - const cached = getShareData(content); - setLocalShareData(cached ?? null); - } - }, [content]); - - // Auto-upload when popover opens, no cached data exists, and signing capabilities are loaded - useEffect(() => { - const canAutoUpload = - isOpen && content && !shareData && !isUploading && !error && signingCapabilitiesLoaded; - - if (canAutoUpload) { - void handleShare(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isOpen, signingCapabilitiesLoaded]); - - // Auto-select URL text when popover opens with share data or share completes - useEffect(() => { - if (isOpen && shareData && urlInputRef.current) { - // Small delay to ensure input is rendered - requestAnimationFrame(() => { - urlInputRef.current?.select(); - }); - } - }, [isOpen, shareData]); - - const isAlreadyShared = Boolean(shareData); - - // Get preferred expiration from localStorage - const getPreferredExpiration = (): ExpirationValue => { - return readPersistedState(SHARE_EXPIRATION_KEY, "never"); - }; - - // Save preferred expiration to localStorage - const savePreferredExpiration = (value: ExpirationValue) => { - updatePersistedState(SHARE_EXPIRATION_KEY, value); - }; - - // Retry key detection (user may have created a key after app launch) - const handleRetryKeyDetection = async () => { - if (!api) return; - try { - // Clear backend cache (will retry key loading on next capabilities call) - await api.signing.clearIdentityCache({}); - // Re-fetch capabilities - const caps = await api.signing.capabilities({}); - setSigningCapabilities(caps); - } catch { - // Silently fail - capabilities stay as-is - } - }; - - // Derive filename: prefer workspaceName, fallback to default - const getFileName = (): string => { - if (workspaceName) { - // Sanitize workspace name for filename (remove unsafe chars) - const safeName = workspaceName.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-"); - return `${safeName}.md`; - } - return "message.md"; - }; - - // Upload with preferred expiration and optional signing - const handleShare = async () => { - if (!content || isUploading) return; - - setIsUploading(true); - setError(null); - - try { - // Get preferred expiration and include in upload request - const preferred = getPreferredExpiration(); - const ms = expirationToMs(preferred); - const expiresAt = ms ? new Date(Date.now() + ms) : undefined; - - // Request a mux.md signature envelope from the backend when signing is enabled. - let signature: SignatureEnvelope | undefined; - if (signingEnabled && signingCapabilities?.publicKey && api) { - try { - signature = await api.signing.signMessage({ content }); - } catch (signErr) { - console.warn("Failed to sign share content, uploading without signature:", signErr); - // Continue without signature - don't fail the upload - } - } - - const result = await uploadToMuxMd( - content, - { - name: getFileName(), - type: "text/markdown", - size: new TextEncoder().encode(content).length, - model, - thinking, - }, - { expiresAt, signature } - ); - - const data: ShareData = { - url: result.url, - id: result.id, - mutateKey: result.mutateKey, - expiresAt: result.expiresAt, - signed: Boolean(signature), - }; - - // Cache the share data - setShareData(content, data); - setLocalShareData(data); - } catch (err) { - console.error("Share failed:", err); - setError(err instanceof Error ? err.message : "Failed to upload"); - } finally { - setIsUploading(false); - } - }; - - // Update expiration on server and cache - const handleUpdateExpiration = async ( - data: ShareData, - value: ExpirationValue, - silent = false - ) => { - if (!data.mutateKey) return; - - if (!silent) setIsUpdating(true); - setError(null); - setShowUpdated(false); - - try { - const ms = expirationToMs(value); - const expiresAt = ms ? new Date(Date.now() + ms) : "never"; - const newExpiration = await updateMuxMdExpiration(data.id, data.mutateKey, expiresAt); - - // Update cache - updateShareExpiration(content, newExpiration); - setLocalShareData((prev) => (prev ? { ...prev, expiresAt: newExpiration } : null)); - - // Save preference for future shares - savePreferredExpiration(value); - - // Show success indicator briefly - if (!silent) { - setShowUpdated(true); - setTimeout(() => setShowUpdated(false), 2000); - } - } catch (err) { - console.error("Update expiration failed:", err); - if (!silent) { - setError(err instanceof Error ? err.message : "Failed to update expiration"); - } - } finally { - if (!silent) setIsUpdating(false); - } - }; - - // Delete from server and remove from cache - const handleDelete = async () => { - if (!shareData?.mutateKey) return; - - setIsDeleting(true); - setError(null); - - try { - await deleteFromMuxMd(shareData.id, shareData.mutateKey); - - // Remove from cache - removeShareData(content); - setLocalShareData(null); - - // Close the popover after successful delete - setIsOpen(false); - } catch (err) { - console.error("Delete failed:", err); - setError(err instanceof Error ? err.message : "Failed to delete"); - } finally { - setIsDeleting(false); - } - }; - - // Toggle signing and regenerate URL inline if already shared - const handleToggleSigning = async () => { - const newSigningEnabled = !signingEnabled; - setSigningEnabled(newSigningEnabled); - - // If we have an existing share, regenerate with new signing state - if (shareData?.mutateKey && !isUploading) { - setIsUploading(true); - setError(null); - - try { - // Delete the old share - await deleteFromMuxMd(shareData.id, shareData.mutateKey); - removeShareData(content); - - // Request a mux.md signature envelope from the backend if signing is now enabled. - let signature: SignatureEnvelope | undefined; - if (newSigningEnabled && signingCapabilities?.publicKey && api) { - try { - signature = await api.signing.signMessage({ content }); - } catch { - // Continue without signature - } - } - - // Re-upload with current expiration preference - const preferred = getPreferredExpiration(); - const ms = expirationToMs(preferred); - const expiresAt = ms ? new Date(Date.now() + ms) : undefined; - - const result = await uploadToMuxMd( - content, - { - name: getFileName(), - type: "text/markdown", - size: new TextEncoder().encode(content).length, - model, - thinking, - }, - { expiresAt, signature } - ); - - const data: ShareData = { - url: result.url, - id: result.id, - mutateKey: result.mutateKey, - expiresAt: result.expiresAt, - signed: Boolean(signature), - }; - - setShareData(content, data); - setLocalShareData(data); - } catch (err) { - console.error("Failed to regenerate share:", err); - setError(err instanceof Error ? err.message : "Failed to update signing"); - } finally { - setIsUploading(false); - } - } - }; - - const handleCopy = useCallback(() => { - if (shareData?.url) { - void copyToClipboard(shareData.url).then(() => { - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }); - } - }, [shareData?.url]); - - const handleOpenInBrowser = useCallback(() => { - if (shareData?.url) { - window.open(shareData.url, "_blank", "noopener,noreferrer"); - } - }, [shareData?.url]); - - const handleOpenChange = (open: boolean) => { - setIsOpen(open); - if (!open) { - // Reset transient state when closing - setTimeout(() => { - setError(null); - }, 150); - } - }; - - const currentExpiration = timestampToExpiration(shareData?.expiresAt); - const isBusy = isUploading || isUpdating || isDeleting; - - // Don't render the share button if link sharing is disabled or still loading - if (linkSharingEnabled !== true) { - return null; - } - - return ( - - - - - - {!shareData ? ( - // Uploading state (auto-triggered on open) -
-
- Share - - setSigningEnabled(!signingEnabled)} - onRetryKeyDetection={() => void handleRetryKeyDetection()} - /> -
- - {error ? ( - <> -
- {error} -
- - - ) : ( -
- - Encrypting... -
- )} -
- ) : ( - // Post-upload: show URL, expiration controls, and delete option -
-
-
- Shared - - void handleToggleSigning()} - onRetryKeyDetection={() => void handleRetryKeyDetection()} - /> -
- {shareData.mutateKey && ( - - - - - Delete - - )} -
- - {/* URL input with inline copy/open buttons */} -
- e.target.select()} - /> - - - - - {copied ? "Copied!" : "Copy"} - - - - - - Open - -
- - {/* Expiration control */} -
- Expires: - {shareData.mutateKey ? ( - - ) : ( - - {formatExpiration(shareData.expiresAt)} - - )} - {/* Inline status: spinner while updating, checkmark on success */} - {isUpdating && } - {showUpdated && } -
- - {error && ( -
- {error} -
- )} -
- )} -
-
- ); -}; diff --git a/src/browser/components/ShareSigningBadges/ShareSigningBadges.stories.tsx b/src/browser/components/ShareSigningBadges/ShareSigningBadges.stories.tsx deleted file mode 100644 index 4247064b08..0000000000 --- a/src/browser/components/ShareSigningBadges/ShareSigningBadges.stories.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { userEvent, waitFor, within } from "@storybook/test"; -import { lightweightMeta } from "@/browser/stories/meta.js"; -import { SigningBadge } from "./ShareSigningBadges.js"; - -const meta = { - ...lightweightMeta, - title: "App/Chat/Components/ShareSigningBadges", - component: SigningBadge, -} satisfies Meta; - -export default meta; - -type Story = StoryObj; - -/** - * Story showing the signing badge in warning state when key requires passphrase. - * The signing badge displays yellow when a compatible key exists but is passphrase-protected. - */ -export const SigningBadgePassphraseWarning: Story = { - args: { - signed: false, - signingEnabled: true, - capabilities: { - publicKey: null, - githubUser: null, - error: { - message: - "Signing key requires a passphrase. Create an unencrypted key at ~/.mux/message_signing_key or use ssh-add.", - hasEncryptedKey: true, - }, - }, - }, - render: (args) => ( -
- -
- ), - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - const badge = await canvas.findByRole("button", { name: /disable signing/i }); - - await userEvent.hover(badge); - - await waitFor( - () => { - const tooltip = document.querySelector('[role="tooltip"]'); - if (!(tooltip instanceof HTMLElement)) { - throw new Error("Signing warning tooltip not visible"); - } - if (!tooltip.textContent?.includes("Key requires passphrase")) { - throw new Error("Expected passphrase warning content in tooltip"); - } - }, - { interval: 50, timeout: 5000 } - ); - }, - parameters: { - docs: { - description: { - story: - "Shows the signing badge in warning state (yellow) when a signing key exists but is passphrase-protected.", - }, - }, - }, -}; diff --git a/src/browser/components/ShareSigningBadges/ShareSigningBadges.tsx b/src/browser/components/ShareSigningBadges/ShareSigningBadges.tsx deleted file mode 100644 index a5380288a5..0000000000 --- a/src/browser/components/ShareSigningBadges/ShareSigningBadges.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import React from "react"; -import { AlertTriangle, Check, Lock, PenTool } from "lucide-react"; -import { - Tooltip, - TooltipTrigger, - TooltipContent, - HelpIndicator, -} from "@/browser/components/Tooltip/Tooltip"; -import { cn } from "@/common/lib/utils"; -import type { SigningCapabilities } from "@/common/orpc/schemas"; - -/** Encryption info tooltip shown next to share headers */ -export const EncryptionBadge = () => ( - - - ? - - -

-

-

- Content is encrypted in your browser (AES-256-GCM). The key stays in the URL fragment and is - never sent to the server. -

-
-
-); - -/** Signing status badge - interactive button with full signing info tooltip */ -export interface SigningBadgeProps { - /** Whether signing is/was enabled for this share */ - signed: boolean; - /** Signing capabilities from backend */ - capabilities: SigningCapabilities | null; - /** Whether signing is globally enabled */ - signingEnabled: boolean; - /** Toggle signing on/off */ - onToggleSigning?: () => void; - /** Callback to retry key detection (only shown when no key) */ - onRetryKeyDetection?: () => void; -} - -/** Truncate public key for display */ -function truncatePublicKey(key: string): string { - // Format: "ssh-ed25519 AAAA...XXXX comment" - const parts = key.split(" "); - if (parts.length < 2) return key; - const keyType = parts[0]; - const keyData = parts[1]; - if (keyData.length <= 16) return key; - return `${keyType} ${keyData.slice(0, 8)}...${keyData.slice(-8)}`; -} - -export const SigningBadge = ({ - signed, - capabilities, - signingEnabled, - onToggleSigning, - onRetryKeyDetection, -}: SigningBadgeProps) => { - const hasKey = Boolean(capabilities?.publicKey); - const hasEncryptedKey = capabilities?.error?.hasEncryptedKey ?? false; - - // Color states: - // - blue = signed/enabled with key - // - yellow/warning = encrypted key found but unusable - // - muted = disabled or no key at all - const isActive = signed || (signingEnabled && hasKey); - const iconColor = isActive ? "text-blue-400" : hasEncryptedKey ? "text-yellow-500" : "text-muted"; - - // Determine status header content - const getStatusHeader = (): React.ReactNode => { - if (signed) { - return ( - - - ); - } - if (signingEnabled && hasKey) return "Signing enabled"; - if (hasEncryptedKey) { - return ( - - - ); - } - return "Signing disabled"; - }; - - // Build tooltip content with full signing info - const tooltipContent = ( -
- {/* Status header */} -

{getStatusHeader()}

- - {/* Show signing details when key is available */} - {hasKey && capabilities && ( -
- {capabilities.githubUser &&

GitHub: @{capabilities.githubUser}

} - {capabilities.publicKey && ( -

{truncatePublicKey(capabilities.publicKey)}

- )} -
- )} - - {/* Encrypted key warning message */} - {!hasKey && hasEncryptedKey && ( -

- Use an unencrypted key file, or ensure your SSH agent (e.g. 1Password) is running and - SSH_AUTH_SOCK is set - {onRetryKeyDetection && ( - <> - {" · "} - - - )} -

- )} - - {/* No key message + retry */} - {!hasKey && !hasEncryptedKey && ( -

- No signing key found - {onRetryKeyDetection && ( - <> - {" · "} - - - )} -

- )} - - {/* Docs link - always visible */} - e.stopPropagation()} - > - Learn more - -
- ); - - return ( - - - - - {tooltipContent} - - ); -}; diff --git a/src/browser/components/ShareTranscriptDialog/ShareTranscriptDialog.test.tsx b/src/browser/components/ShareTranscriptDialog/ShareTranscriptDialog.test.tsx deleted file mode 100644 index 3c20cb0882..0000000000 --- a/src/browser/components/ShareTranscriptDialog/ShareTranscriptDialog.test.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import type { ReactNode } from "react"; -import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test"; -import type { APIClient } from "@/browser/contexts/API"; -import type { WorkspaceStore } from "@/browser/stores/WorkspaceStore"; -import { useWorkspaceStoreRaw } from "@/browser/stores/WorkspaceStore"; -import { APIProvider } from "@/browser/contexts/API"; -import { TooltipProvider } from "@/browser/components/Tooltip/Tooltip"; -import { addEphemeralMessage } from "@/browser/stores/WorkspaceStore"; -import * as muxMd from "@/common/lib/muxMd"; -import { GlobalWindow } from "happy-dom"; -import { cleanup, fireEvent, render, waitFor, within } from "@testing-library/react"; - -void mock.module("@/browser/components/Dialog/Dialog", () => ({ - Dialog: (props: { open: boolean; children: ReactNode }) => - props.open ?
{props.children}
: null, - DialogContent: (props: { children: ReactNode }) =>
{props.children}
, - DialogHeader: (props: { children: ReactNode }) =>
{props.children}
, - DialogTitle: (props: { children: ReactNode; className?: string }) => ( -

{props.children}

- ), -})); - -import { ShareTranscriptDialog } from "../ShareTranscriptDialog/ShareTranscriptDialog"; - -const TEST_WORKSPACE_ID = "ws-1"; - -function getStore(): WorkspaceStore { - return (useWorkspaceStoreRaw as unknown as () => WorkspaceStore)(); -} - -function createApiClient(): APIClient { - return { - signing: { - capabilities: () => Promise.resolve({ publicKey: null, githubUser: null, error: null }), - clearIdentityCache: () => Promise.resolve({ success: true }), - signMessage: () => Promise.resolve({ sig: "sig", publicKey: "public-key" }), - }, - workspace: { - getPlanContent: () => Promise.resolve({ success: false, error: "not-needed" }), - }, - } as unknown as APIClient; -} - -function renderDialog() { - return render( - - - undefined} - /> - - - ); -} - -describe("ShareTranscriptDialog", () => { - let originalWindow: typeof globalThis.window; - let originalDocument: typeof globalThis.document; - let originalGetComputedStyle: typeof globalThis.getComputedStyle; - - beforeEach(() => { - originalWindow = globalThis.window; - originalDocument = globalThis.document; - - const dom = new GlobalWindow(); - globalThis.window = dom as unknown as Window & typeof globalThis; - globalThis.document = globalThis.window.document; - - originalGetComputedStyle = globalThis.getComputedStyle; - globalThis.getComputedStyle = globalThis.window.getComputedStyle.bind(globalThis.window); - - // Ensure test isolation from other suites that attach a mock ORPC client. - // Share dialog tests operate on local ephemeral messages and should not race - // onChat reconnect loops from unrelated WorkspaceStore tests. - getStore().setClient(null); - - spyOn(console, "error").mockImplementation(() => undefined); - - spyOn(muxMd, "uploadToMuxMd").mockResolvedValue({ - url: "https://mux.md/s/share-1", - id: "share-1", - key: "encryption-key", - mutateKey: "mutate-1", - expiresAt: Date.now() + 60_000, - }); - spyOn(muxMd, "deleteFromMuxMd").mockResolvedValue(undefined); - getStore().addWorkspace({ - id: TEST_WORKSPACE_ID, - name: "workspace-1", - title: "Workspace 1", - projectName: "project", - projectPath: "/tmp/project", - namedWorkspacePath: "/tmp/project/workspace-1", - runtimeConfig: { type: "local" }, - createdAt: new Date().toISOString(), - }); - addEphemeralMessage(TEST_WORKSPACE_ID, { - id: "user-message-1", - role: "user", - parts: [{ type: "text", text: "hello" }], - }); - }); - - afterEach(() => { - getStore().removeWorkspace(TEST_WORKSPACE_ID); - cleanup(); - mock.restore(); - globalThis.getComputedStyle = originalGetComputedStyle; - globalThis.window = originalWindow; - globalThis.document = originalDocument; - }); - - test("deletes an existing shared transcript link and clears the URL", async () => { - renderDialog(); - const body = within(document.body); - - fireEvent.click(body.getByRole("button", { name: "Generate link" })); - - await waitFor(() => expect(body.getByTestId("share-transcript-url")).toBeTruthy()); - - fireEvent.click(body.getByTestId("delete-share-transcript-url")); - - await waitFor(() => expect(muxMd.deleteFromMuxMd).toHaveBeenCalledWith("share-1", "mutate-1")); - await waitFor(() => expect(body.queryByTestId("share-transcript-url")).toBeNull()); - }); - - test("keeps shared transcript URL and surfaces an error when delete fails", async () => { - (muxMd.deleteFromMuxMd as unknown as ReturnType).mockImplementationOnce(() => - Promise.reject(new Error("Delete failed")) - ); - - renderDialog(); - const body = within(document.body); - - fireEvent.click(body.getByRole("button", { name: "Generate link" })); - - await waitFor(() => expect(body.getByTestId("share-transcript-url")).toBeTruthy()); - - fireEvent.click(body.getByTestId("delete-share-transcript-url")); - - await waitFor(() => expect(muxMd.deleteFromMuxMd).toHaveBeenCalledWith("share-1", "mutate-1")); - await waitFor(() => expect(body.getByRole("alert").textContent).toContain("Delete failed")); - expect(body.getByTestId("share-transcript-url")).toBeTruthy(); - }); -}); diff --git a/src/browser/components/ShareTranscriptDialog/ShareTranscriptDialog.tsx b/src/browser/components/ShareTranscriptDialog/ShareTranscriptDialog.tsx deleted file mode 100644 index 3aba3ace8b..0000000000 --- a/src/browser/components/ShareTranscriptDialog/ShareTranscriptDialog.tsx +++ /dev/null @@ -1,510 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from "react"; -import { Check, ExternalLink, Loader2, Trash2 } from "lucide-react"; - -import { CopyIcon } from "@/browser/components/icons/CopyIcon/CopyIcon"; -import { Button } from "@/browser/components/Button/Button"; -import { Checkbox } from "@/browser/components/Checkbox/Checkbox"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "@/browser/components/Dialog/Dialog"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/browser/components/SelectPrimitive/SelectPrimitive"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@/browser/components/Tooltip/Tooltip"; -import { useWorkspaceStoreRaw } from "@/browser/stores/WorkspaceStore"; -import { useAPI } from "@/browser/contexts/API"; -import { copyToClipboard } from "@/browser/utils/clipboard"; -import { getSendOptionsFromStorage } from "@/browser/utils/messages/sendOptions"; -import { - readPersistedState, - updatePersistedState, - usePersistedState, -} from "@/browser/hooks/usePersistedState"; -import { SHARE_EXPIRATION_KEY, SHARE_SIGNING_KEY } from "@/common/constants/storage"; -import { - deleteFromMuxMd, - uploadToMuxMd, - updateMuxMdExpiration, - type FileInfo, - type SignatureEnvelope, -} from "@/common/lib/muxMd"; -import { - EXPIRATION_OPTIONS, - type ExpirationValue, - expirationToMs, - timestampToExpiration, -} from "@/common/lib/shareExpiration"; -import type { MuxMessage } from "@/common/types/message"; -import { buildChatJsonlForSharing } from "@/common/utils/messages/transcriptShare"; -import type { SigningCapabilities } from "@/common/orpc/schemas"; -import assert from "@/common/utils/assert"; -import { EncryptionBadge, SigningBadge } from "../ShareSigningBadges/ShareSigningBadges"; - -interface ShareTranscriptDialogProps { - workspaceId: string; - workspaceName: string; - /** Human-readable workspace title shown in the dialog header */ - workspaceTitle?: string; - open: boolean; - onOpenChange: (open: boolean) => void; -} - -function getTranscriptFileName(workspaceName: string): string { - const trimmed = workspaceName.trim(); - if (!trimmed) { - return "chat.jsonl"; - } - - // Keep this consistent with existing share filename sanitization. - // (mux.md expects `FileInfo.name` to be safe for display and download.) - const safeName = trimmed - .replace(/[^a-zA-Z0-9_-]/g, "-") - .replace(/-+/g, "-") - .replace(/^-+|-+$/g, ""); - - if (!safeName) { - return "chat.jsonl"; - } - - return `${safeName}-chat.jsonl`; -} - -function transcriptContainsProposePlanToolCall(messages: MuxMessage[]): boolean { - return messages.some( - (msg) => - msg.role === "assistant" && - msg.parts.some((part) => part.type === "dynamic-tool" && part.toolName === "propose_plan") - ); -} - -export function ShareTranscriptDialog(props: ShareTranscriptDialogProps) { - const store = useWorkspaceStoreRaw(); - const { api } = useAPI(); - - const [includeToolOutput, setIncludeToolOutput] = useState(true); - const [isUploading, setIsUploading] = useState(false); - const [error, setError] = useState(null); - const [shareUrl, setShareUrl] = useState(null); - const [shareId, setShareId] = useState(null); - const [shareMutateKey, setShareMutateKey] = useState(null); - const [shareExpiresAt, setShareExpiresAt] = useState(); - const [isUpdatingExpiration, setIsUpdatingExpiration] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); - const [copied, setCopied] = useState(false); - - // Signing capabilities and enabled state (matching per-message sharing) - const [signingCapabilities, setSigningCapabilities] = useState(null); - const [signingCapabilitiesLoaded, setSigningCapabilitiesLoaded] = useState(false); - const [signingEnabled, setSigningEnabled] = usePersistedState(SHARE_SIGNING_KEY, true, { - listener: true, - }); - const [signed, setSigned] = useState(false); - - const urlInputRef = useRef(null); - - // Guards against cross-workspace leakage when users switch workspaces mid-upload. - const uploadSeqRef = useRef(0); - - const clearSharedTranscriptState = useCallback(() => { - setShareUrl(null); - setShareId(null); - setShareMutateKey(null); - setShareExpiresAt(undefined); - setSigned(false); - setCopied(false); - }, []); - - const isBusy = isUploading || isUpdatingExpiration || isDeleting; - - useEffect(() => { - uploadSeqRef.current += 1; - clearSharedTranscriptState(); - setError(null); - setIsUploading(false); - setIsUpdatingExpiration(false); - setIsDeleting(false); - }, [clearSharedTranscriptState, props.workspaceId]); - - // Load signing capabilities when the dialog first opens. - // Defensive: tests and legacy mocks may provide a partial API client without signing endpoints. - useEffect(() => { - const signingApi = api?.signing; - if (!props.open || signingCapabilitiesLoaded || !signingApi?.capabilities) { - return; - } - - void signingApi - .capabilities({}) - .then(setSigningCapabilities) - .catch(() => { - // Signing unavailable – leave capabilities null - }) - .finally(() => { - setSigningCapabilitiesLoaded(true); - }); - }, [api, props.open, signingCapabilitiesLoaded]); - - useEffect(() => { - if (!props.open || !shareUrl) { - return; - } - - urlInputRef.current?.focus(); - urlInputRef.current?.select(); - }, [props.open, shareUrl]); - - // Retry key detection (user may have created a key after app launch). - // Defensive: no-op if signing endpoints are unavailable in the injected API client. - const handleRetryKeyDetection = async () => { - if (!api?.signing?.clearIdentityCache || !api.signing.capabilities) return; - try { - await api.signing.clearIdentityCache({}); - const caps = await api.signing.capabilities({}); - setSigningCapabilities(caps); - } catch { - // Silently fail – capabilities stay as-is - } - }; - - const handleGenerateLink = useCallback(async () => { - if (isBusy) { - return; - } - - const uploadSeq = uploadSeqRef.current + 1; - uploadSeqRef.current = uploadSeq; - - setIsUploading(true); - setError(null); - - try { - const workspaceId = props.workspaceId; - const workspaceState = store.getWorkspaceState(workspaceId); - - let planSnapshot: { path: string; content: string } | undefined; - if (api && transcriptContainsProposePlanToolCall(workspaceState.muxMessages)) { - try { - const res = await api.workspace.getPlanContent({ workspaceId }); - if (res.success) { - planSnapshot = { path: res.data.path, content: res.data.content }; - } else { - console.warn("Failed to read plan content for transcript sharing:", res.error); - } - } catch (err) { - console.warn("Failed to read plan content for transcript sharing:", err); - // Ignore failures - plan content is optional for sharing. - } - } - - const chatJsonl = buildChatJsonlForSharing(workspaceState.muxMessages, { - includeToolOutput, - workspaceId, - planSnapshot, - }); - - if (!chatJsonl) { - if (uploadSeqRef.current === uploadSeq) { - setError("No messages to share yet"); - } - return; - } - - // Request a signing envelope when signing is enabled - let signature: SignatureEnvelope | undefined; - if (signingEnabled && signingCapabilities?.publicKey && api?.signing?.signMessage) { - try { - signature = await api.signing.signMessage({ content: chatJsonl }); - } catch (signErr) { - console.warn("Failed to sign transcript, uploading without signature:", signErr); - // Continue without signature – don't fail the upload - } - } - - const sendOptions = getSendOptionsFromStorage(workspaceId); - - // Prefer the user-facing workspace title for uploaded filename when available. - const fileInfo: FileInfo = { - name: getTranscriptFileName(props.workspaceTitle ?? props.workspaceName), - type: "application/x-ndjson", - size: new TextEncoder().encode(chatJsonl).length, - model: workspaceState.currentModel ?? sendOptions.model, - thinking: workspaceState.currentThinkingLevel ?? sendOptions.thinkingLevel, - }; - - const result = await uploadToMuxMd(chatJsonl, fileInfo, { - expiresAt: (() => { - const preferred = readPersistedState(SHARE_EXPIRATION_KEY, "never"); - const expMs = expirationToMs(preferred); - return expMs ? new Date(Date.now() + expMs) : undefined; - })(), - signature, - }); - if (uploadSeqRef.current === uploadSeq) { - setShareUrl(result.url); - setShareId(result.id); - setShareMutateKey(result.mutateKey); - setShareExpiresAt(result.expiresAt); - setSigned(Boolean(signature)); - } - } catch (err) { - console.error("Failed to share transcript:", err); - if (uploadSeqRef.current === uploadSeq) { - setError(err instanceof Error ? err.message : "Failed to upload"); - } - } finally { - if (uploadSeqRef.current === uploadSeq) { - setIsUploading(false); - } - } - }, [ - api, - includeToolOutput, - isBusy, - props.workspaceId, - props.workspaceName, - props.workspaceTitle, - signingCapabilities, - signingEnabled, - store, - ]); - - const handleUpdateExpiration = async (value: ExpirationValue) => { - if (!shareId || !shareMutateKey || isBusy) return; - - // Capture the current upload sequence so we can discard the result if a new - // link is generated (workspace switch or re-upload) before this resolves. - const seq = uploadSeqRef.current; - - setIsUpdatingExpiration(true); - try { - const ms = expirationToMs(value); - const expiresAtArg = ms ? new Date(Date.now() + ms) : "never"; - const newExpiration = await updateMuxMdExpiration(shareId, shareMutateKey, expiresAtArg); - if (uploadSeqRef.current === seq) { - setShareExpiresAt(newExpiration); - updatePersistedState(SHARE_EXPIRATION_KEY, value); - } - } catch (err) { - console.error("Update expiration failed:", err); - } finally { - if (uploadSeqRef.current === seq) { - setIsUpdatingExpiration(false); - } - } - }; - - const handleDelete = useCallback(async () => { - if (!shareId || !shareMutateKey || isDeleting) { - return; - } - - const seq = uploadSeqRef.current; - assert(shareUrl, "Deleting a shared transcript requires a visible shareUrl"); - - setIsDeleting(true); - setError(null); - - try { - await deleteFromMuxMd(shareId, shareMutateKey); - if (uploadSeqRef.current === seq) { - clearSharedTranscriptState(); - } - } catch (err) { - console.error("Delete transcript share failed:", err); - if (uploadSeqRef.current === seq) { - setError(err instanceof Error ? err.message : "Failed to delete shared transcript"); - } - } finally { - if (uploadSeqRef.current === seq) { - setIsDeleting(false); - } - } - }, [clearSharedTranscriptState, isDeleting, shareId, shareMutateKey, shareUrl]); - - const handleCopy = useCallback(() => { - if (!shareUrl) { - return; - } - - void copyToClipboard(shareUrl).then(() => { - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }); - }, [shareUrl]); - - const handleOpenInBrowser = useCallback(() => { - if (!shareUrl) { - return; - } - - window.open(shareUrl, "_blank", "noopener,noreferrer"); - }, [shareUrl]); - - const handleOpenChange = (open: boolean) => { - props.onOpenChange(open); - - if (!open) { - setError(null); - setCopied(false); - } - }; - - return ( - - - - - Share transcript - - setSigningEnabled(!signingEnabled)} - onRetryKeyDetection={() => void handleRetryKeyDetection()} - /> - - {props.workspaceTitle && ( -

{props.workspaceTitle}

- )} -
- -
- - - - - {shareUrl && ( - <> -
- e.target.select()} - /> - - - - - {copied ? "Copied!" : "Copy"} - - - - - - Open - - {shareMutateKey && ( - - - - - Delete - - )} -
- - {/* Expiration control */} -
- Expires: - - {isUpdatingExpiration && ( - - )} -
- - )} - - {error && ( -
- {error} -
- )} -
-
-
- ); -} diff --git a/src/browser/components/SshPromptDialog/SshPromptDialog.test.tsx b/src/browser/components/SshPromptDialog/SshPromptDialog.test.tsx index 6c15389a02..eb94320112 100644 --- a/src/browser/components/SshPromptDialog/SshPromptDialog.test.tsx +++ b/src/browser/components/SshPromptDialog/SshPromptDialog.test.tsx @@ -10,8 +10,8 @@ import { import type { ReactNode } from "react"; import { installDom } from "../../../../tests/ui/dom"; -// Self-contained dialog stub — bun's mock.module is process-global and -// ShareTranscriptDialog.test.tsx registers an incomplete stub that omits +// Self-contained dialog stub — bun's mock.module is process-global, so other +// test files may register incomplete Dialog stubs that omit // DialogDescription/DialogFooter/Warning*. Our own complete mock prevents // Radix context errors when tests run in the same bun process. void mock.module("@/browser/components/Dialog/Dialog", () => ({ diff --git a/src/browser/components/WorkspaceActionsMenuContent/WorkspaceActionsMenuContent.test.tsx b/src/browser/components/WorkspaceActionsMenuContent/WorkspaceActionsMenuContent.test.tsx index af75ce502e..15bcfbf2a6 100644 --- a/src/browser/components/WorkspaceActionsMenuContent/WorkspaceActionsMenuContent.test.tsx +++ b/src/browser/components/WorkspaceActionsMenuContent/WorkspaceActionsMenuContent.test.tsx @@ -27,7 +27,6 @@ describe("WorkspaceActionsMenuContent", () => { ); diff --git a/src/browser/components/WorkspaceActionsMenuContent/WorkspaceActionsMenuContent.tsx b/src/browser/components/WorkspaceActionsMenuContent/WorkspaceActionsMenuContent.tsx index 9cc73da0b4..307e926387 100644 --- a/src/browser/components/WorkspaceActionsMenuContent/WorkspaceActionsMenuContent.tsx +++ b/src/browser/components/WorkspaceActionsMenuContent/WorkspaceActionsMenuContent.tsx @@ -4,7 +4,6 @@ import { CalendarClock, GitBranch, HeartPulse, - Link2, Maximize2, Pencil, Server, @@ -56,10 +55,8 @@ interface WorkspaceActionsMenuContentProps { onEnterImmersiveReview?: (() => void) | null; onStopRuntime?: (() => void) | null; onForkChat?: ((anchorEl: HTMLElement) => void) | null; - onShareTranscript?: (() => void) | null; onArchiveChat?: ((anchorEl: HTMLElement) => void) | null; onCloseMenu: () => void; - linkSharingEnabled: boolean; shortcutClassName?: string; configureMcpTestId?: string; } @@ -170,19 +167,6 @@ export const WorkspaceActionsMenuContent: React.FC )} - {props.onShareTranscript && props.linkSharingEnabled === true && ( - } - onClick={(e) => { - e.stopPropagation(); - props.onCloseMenu(); - props.onShareTranscript?.(); - }} - /> - )} {props.onArchiveChat && ( ); spyOn(DesktopTitlebarModule, "isDesktopMode").mockImplementation(() => false); - spyOn(TelemetryEnabledContextModule, "useLinkSharingEnabled").mockImplementation(() => false); spyOn(TutorialContextModule, "useTutorial").mockImplementation( () => ({ startSequence: () => undefined }) as unknown as ReturnType< @@ -236,9 +233,6 @@ function installWorkspaceMenuBarTestDoubles() { spyOn(WorkspaceLinksModule, "WorkspaceLinks").mockImplementation( (() => null) as unknown as typeof WorkspaceLinksModule.WorkspaceLinks ); - spyOn(ShareTranscriptDialogModule, "ShareTranscriptDialog").mockImplementation( - (() => null) as unknown as typeof ShareTranscriptDialogModule.ShareTranscriptDialog - ); spyOn(ConfirmationModalModule, "ConfirmationModal").mockImplementation(((props: { isOpen: boolean; title: string; diff --git a/src/browser/components/WorkspaceMenuBar/WorkspaceMenuBar.tsx b/src/browser/components/WorkspaceMenuBar/WorkspaceMenuBar.tsx index aad785f31d..dc62f84068 100644 --- a/src/browser/components/WorkspaceMenuBar/WorkspaceMenuBar.tsx +++ b/src/browser/components/WorkspaceMenuBar/WorkspaceMenuBar.tsx @@ -33,7 +33,6 @@ import { useRuntimeStatus, useRuntimeStatusStoreRaw } from "@/browser/stores/Run import { useWorkspaceSidebarState } from "@/browser/stores/WorkspaceStore"; import { Button } from "@/browser/components/Button/Button"; import { isDevcontainerRuntime, type RuntimeConfig } from "@/common/types/runtime"; -import { useLinkSharingEnabled } from "@/browser/contexts/TelemetryEnabledContext"; import { useTutorial } from "@/browser/contexts/TutorialContext"; import type { TerminalSessionCreateOptions } from "@/browser/utils/terminal"; @@ -45,7 +44,6 @@ import { isDesktopMode, DESKTOP_TITLEBAR_HEIGHT_CLASS } from "@/browser/hooks/us import { useExperimentValue } from "@/browser/hooks/useExperiments"; import { DebugLlmRequestModal } from "../DebugLlmRequestModal/DebugLlmRequestModal"; import { WorkspaceLinks } from "../WorkspaceLinks/WorkspaceLinks"; -import { ShareTranscriptDialog } from "../ShareTranscriptDialog/ShareTranscriptDialog"; import { ConfirmationModal } from "../ConfirmationModal/ConfirmationModal"; import { PopoverError } from "../PopoverError/PopoverError"; import { WorkspaceActionsMenuContent } from "../WorkspaceActionsMenuContent/WorkspaceActionsMenuContent"; @@ -105,7 +103,6 @@ export const WorkspaceMenuBar: React.FC = ({ const { workspaceMetadata } = useWorkspaceContext(); const dynamicWorkflowsEnabled = useExperimentValue(EXPERIMENT_IDS.DYNAMIC_WORKFLOWS); const workspaceHeartbeatsEnabled = useExperimentValue(EXPERIMENT_IDS.WORKSPACE_HEARTBEATS); - const linkSharingEnabled = useLinkSharingEnabled(); const openTerminalPopout = useOpenTerminal(); const openInEditor = useOpenInEditor(); const gitStatus = useGitStatus(workspaceId); @@ -151,7 +148,6 @@ export const WorkspaceMenuBar: React.FC = ({ const skillsRequestIdRef = useRef(0); const [moreMenuOpen, setMoreMenuOpen] = useState(false); - const [shareTranscriptOpen, setShareTranscriptOpen] = useState(false); const [archiveConfirmOpen, setArchiveConfirmOpen] = useState(false); // Untracked paths from archive preflight that the user needs to acknowledge. // When set, the confirmation dialog warns about permanent file deletion. @@ -482,21 +478,6 @@ export const WorkspaceMenuBar: React.FC = ({ return () => window.removeEventListener("keydown", handler); }, [dynamicWorkflowsEnabled]); - // Keybind for sharing transcript — lives here (not AgentListItem) so it - // works even when the left sidebar is collapsed and list items are unmounted. - useEffect(() => { - if (linkSharingEnabled !== true) return; - - const handler = (e: KeyboardEvent) => { - if (matchesKeybind(e, KEYBINDS.SHARE_TRANSCRIPT)) { - e.preventDefault(); - setShareTranscriptOpen(true); - } - }; - window.addEventListener("keydown", handler); - return () => window.removeEventListener("keydown", handler); - }, [linkSharingEnabled]); - useEffect(() => { isSkillsMountedRef.current = true; @@ -813,14 +794,12 @@ export const WorkspaceMenuBar: React.FC = ({ onForkChat={(anchorEl) => { void handleForkChat(anchorEl); }} - onShareTranscript={() => setShareTranscriptOpen(true)} onArchiveChat={(anchorEl) => { // handleArchiveChat runs preflight and opens a confirmation dialog // when streaming or untracked files are detected. void handleArchiveChat(anchorEl); }} onCloseMenu={() => setMoreMenuOpen(false)} - linkSharingEnabled={linkSharingEnabled === true} shortcutClassName="mobile-hide-shortcut-hints" configureMcpTestId="workspace-mcp-button" /> @@ -856,15 +835,6 @@ export const WorkspaceMenuBar: React.FC = ({ open={debugLlmRequestOpen} onOpenChange={setDebugLlmRequestOpen} /> - {linkSharingEnabled === true && ( - - )} {/* Combined confirmation for archive warnings (streaming + untracked files). */} (null); - -interface TelemetryEnabledProviderProps { - children: React.ReactNode; -} - -/** - * Provider that queries the backend once to determine if telemetry is enabled. - * This is used to conditionally hide features that require network access to mux services. - */ -export function TelemetryEnabledProvider({ children }: TelemetryEnabledProviderProps) { - const { api } = useAPI(); - const [linkSharingEnabled, setLinkSharingEnabled] = useState(null); - - useEffect(() => { - if (!api) return; - - let cancelled = false; - void api.telemetry - .status() - .then((result) => { - if (!cancelled) { - // Link sharing is enabled unless user explicitly disabled telemetry - setLinkSharingEnabled(!result.explicit); - } - }) - .catch((err) => { - console.error("[TelemetryEnabledContext] Failed to check telemetry status:", err); - // Default to enabled on error so share button still shows - if (!cancelled) { - setLinkSharingEnabled(true); - } - }); - - return () => { - cancelled = true; - }; - }, [api]); - - return ( - - {children} - - ); -} - -/** - * Hook to check if link sharing is enabled. - * Returns null while loading, then true/false once known. - * Link sharing is disabled only when user explicitly sets MUX_DISABLE_TELEMETRY=1. - */ -export function useLinkSharingEnabled(): boolean | null { - const context = useContext(TelemetryEnabledContext); - if (!context) { - throw new Error("useLinkSharingEnabled must be used within a TelemetryEnabledProvider"); - } - return context.linkSharingEnabled; -} diff --git a/src/browser/features/Messages/AssistantMessage.tsx b/src/browser/features/Messages/AssistantMessage.tsx index 3348d4cfcd..28d691a1b1 100644 --- a/src/browser/features/Messages/AssistantMessage.tsx +++ b/src/browser/features/Messages/AssistantMessage.tsx @@ -17,10 +17,8 @@ import { SIDE_QUESTION_ANSWER_BLOCK_CLASS, SIDE_QUESTION_MESSAGE_WINDOW_CLASS, } from "./sideQuestionStyles"; -import { ShareMessagePopover } from "@/browser/components/ShareMessagePopover/ShareMessagePopover"; import { PopoverError } from "@/browser/components/PopoverError/PopoverError"; import { useAPI } from "@/browser/contexts/API"; -import { useOptionalWorkspaceContext } from "@/browser/contexts/WorkspaceContext"; import { Button } from "@/browser/components/Button/Button"; import { forkWorkspace } from "@/browser/utils/chatCommands"; import React, { useState } from "react"; @@ -49,14 +47,8 @@ export const AssistantMessage: React.FC = ({ }) => { const [showRaw, setShowRaw] = useState(false); const { api } = useAPI(); - const workspaceContext = useOptionalWorkspaceContext(); const forkError = usePopoverError(); - // Get workspace name from context for share filename - const workspaceName = workspaceId - ? workspaceContext?.workspaceMetadata.get(workspaceId)?.name - : undefined; - const content = message.content; const isStreaming = message.isStreaming; const isCompacted = message.isCompacted; @@ -119,7 +111,7 @@ export const AssistantMessage: React.FC = ({ if (!isStreaming && !isSideAnswer) { // Side answers intentionally show only Copy. The /btw side branch is - // meant to feel lightweight: Start Here / Fork / Share / Show Text + // meant to feel lightweight: Start Here / Fork / Show Text // would imply the message is a fork point in the main agent thread, // which it isn't. Keeping the action set minimal also keeps the pair // visually quiet against the main transcript. @@ -137,17 +129,6 @@ export const AssistantMessage: React.FC = ({ tooltip: "Fork a new workspace from this response", icon: , }); - buttons.push({ - label: "Share", - component: ( - - ), - }); buttons.push({ label: showRaw ? "Show Markdown" : "Show Text", onClick: () => setShowRaw(!showRaw), diff --git a/src/browser/features/RightSidebar/RightSidebar.stories.tsx b/src/browser/features/RightSidebar/RightSidebar.stories.tsx index e26cad6ac1..27812e4f2c 100644 --- a/src/browser/features/RightSidebar/RightSidebar.stories.tsx +++ b/src/browser/features/RightSidebar/RightSidebar.stories.tsx @@ -19,7 +19,6 @@ import { ProjectProvider } from "@/browser/contexts/ProjectContext"; import { ProviderOptionsProvider } from "@/browser/contexts/ProviderOptionsContext"; import { RouterProvider } from "@/browser/contexts/RouterContext"; import { SettingsProvider } from "@/browser/contexts/SettingsContext"; -import { TelemetryEnabledProvider } from "@/browser/contexts/TelemetryEnabledContext"; import { ThemeProvider } from "@/browser/contexts/ThemeContext"; import { ThinkingProvider } from "@/browser/contexts/ThinkingContext"; import { TutorialProvider } from "@/browser/contexts/TutorialContext"; @@ -151,42 +150,40 @@ function RightSidebarStoryShell(props: { setup: () => APIClient; children: React - - - - - - - - - - - - - - - - - {props.children} - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + {props.children} + + + + + + + + + + + + + + + diff --git a/src/browser/features/Settings/Sections/KeybindsSection.tsx b/src/browser/features/Settings/Sections/KeybindsSection.tsx index f39bb22ec6..972a3107c8 100644 --- a/src/browser/features/Settings/Sections/KeybindsSection.tsx +++ b/src/browser/features/Settings/Sections/KeybindsSection.tsx @@ -31,7 +31,6 @@ const KEYBIND_LABELS: Record = { CYCLE_MODEL: "Cycle model", OPEN_TERMINAL: "New terminal", OPEN_IN_EDITOR: "Open in editor", - SHARE_TRANSCRIPT: "Share transcript", CONFIGURE_MCP: "Configure MCP servers", CONFIGURE_HEARTBEAT: "Configure heartbeat", CONFIGURE_SCHEDULED_WORKFLOW: "Configure workspace automations", @@ -103,7 +102,6 @@ const KEYBIND_GROUPS: Array<{ label: string; keys: Array "DECREASE_THINKING", "INCREASE_THINKING", "TOGGLE_NOTIFICATIONS", - "SHARE_TRANSCRIPT", "CONFIGURE_MCP", "CONFIGURE_SCHEDULED_WORKFLOW", "CONFIGURE_PROJECT_AUTOMATIONS", diff --git a/src/browser/features/Tools/ProposePlanToolCall.test.tsx b/src/browser/features/Tools/ProposePlanToolCall.test.tsx index b6ada6b507..a9c341ba01 100644 --- a/src/browser/features/Tools/ProposePlanToolCall.test.tsx +++ b/src/browser/features/Tools/ProposePlanToolCall.test.tsx @@ -105,10 +105,6 @@ void mock.module("@/browser/contexts/WorkspaceContext", () => ({ }), })); -void mock.module("@/browser/contexts/TelemetryEnabledContext", () => ({ - useLinkSharingEnabled: () => true, -})); - void mock.module("@/browser/hooks/useReviews", () => ({ useReviews: () => ({ reviews: [], diff --git a/src/browser/features/Tools/ProposePlanToolCall.tsx b/src/browser/features/Tools/ProposePlanToolCall.tsx index 80c8b7dfd7..1cf55d47c0 100644 --- a/src/browser/features/Tools/ProposePlanToolCall.tsx +++ b/src/browser/features/Tools/ProposePlanToolCall.tsx @@ -73,7 +73,6 @@ import { Sparkles, X, } from "lucide-react"; -import { ShareMessagePopover } from "@/browser/components/ShareMessagePopover/ShareMessagePopover"; import { getErrorMessage } from "@/common/utils/errors"; /** @@ -192,12 +191,11 @@ export const ProposePlanToolCall: React.FC = (props) = const editorError = usePopoverError(); const editButtonRef = useRef(null); - // Get runtimeConfig and name for the workspace (needed for SSH-aware editor opening and share filename) + // Get runtimeConfig for the workspace (needed for SSH-aware editor opening) const workspaceMetadata = workspaceId ? workspaceContext?.workspaceMetadata.get(workspaceId) : undefined; const runtimeConfig = workspaceMetadata?.runtimeConfig; - const workspaceName = workspaceMetadata?.name; // Fresh content from disk for the latest plan (external edit detection) // Only use cache for completed tools (page reload case) - not for in-flight tools @@ -620,6 +618,8 @@ export const ProposePlanToolCall: React.FC = (props) = // Copy to clipboard with feedback const { copied, copyToClipboard } = useCopyToClipboard(); + // Separate instance so the "Copy file path" button has independent feedback. + const { copied: copiedPath, copyToClipboard: copyPathToClipboard } = useCopyToClipboard(); const handleOpenInEditor = async () => { if (!planPath || !workspaceId) return; @@ -652,19 +652,17 @@ export const ProposePlanToolCall: React.FC = (props) = icon: copied ? : , }; - const actionButtons: ButtonConfig[] = [ - copyButton, - { - label: "Share", - component: ( - - ), - }, - ]; + const actionButtons: ButtonConfig[] = [copyButton]; + + // Copy the plan's on-disk file path (replaces the former mux.md "Share" link action). + if (planPath) { + actionButtons.push({ + label: copiedPath ? "Copied" : "Copy file path", + onClick: () => void copyPathToClipboard(planPath), + icon: copiedPath ? : , + tooltip: planPath, + }); + } // Edit button config (rendered separately with ref for error positioning) const showEditButton = (isEphemeralPreview ?? isLatest) && planPath && workspaceId; diff --git a/src/browser/stories/helpers/chatSetup.ts b/src/browser/stories/helpers/chatSetup.ts index 48b297f8d5..a8214d9f33 100644 --- a/src/browser/stories/helpers/chatSetup.ts +++ b/src/browser/stories/helpers/chatSetup.ts @@ -77,12 +77,6 @@ export interface SimpleChatSetupOptions { routePriority?: string[]; /** Per-model route overrides for routing-aware stories */ routeOverrides?: Record; - /** Override signing capabilities (for testing warning states) */ - signingCapabilities?: { - publicKey: string | null; - githubUser: string | null; - error: { message: string; hasEncryptedKey: boolean } | null; - }; /** Custom executeBash mock (for file viewer stories) */ executeBash?: ( workspaceId: string, @@ -186,7 +180,6 @@ export function setupSimpleChatStory(opts: SimpleChatSetupOptions): APIClient { sessionUsage: sessionUsageMap, subagentTranscripts: opts.subagentTranscripts, idleCompactionHours, - signingCapabilities: opts.signingCapabilities, agentSkills: opts.agentSkills, invalidAgentSkills: opts.invalidAgentSkills, logEntries: opts.logEntries, diff --git a/src/browser/stories/mocks/orpc.ts b/src/browser/stories/mocks/orpc.ts index eac8b891ba..af6c56102a 100644 --- a/src/browser/stories/mocks/orpc.ts +++ b/src/browser/stories/mocks/orpc.ts @@ -267,12 +267,6 @@ export interface MockORPCClientOptions { }) => Promise<{ success: true } | { success: false; error: string }>; /** Idle compaction hours per project (null = disabled) */ idleCompactionHours?: Map; - /** Override signing capabilities response */ - signingCapabilities?: { - publicKey: string | null; - githubUser: string | null; - error: { message: string; hasEncryptedKey: boolean } | null; - }; /** Coder CLI availability info */ coderInfo?: CoderInfo; /** Coder templates available for workspace creation */ @@ -408,7 +402,6 @@ export function createMockORPCClient(options: MockORPCClientOptions = {}): APICl listBranches: customListBranches, gitInit: customGitInit, runtimeAvailability: customRuntimeAvailability, - signingCapabilities: customSigningCapabilities, coderInfo = { state: "unavailable" as const, reason: "missing" as const }, coderTemplates = [], coderPresets = new Map(), @@ -670,23 +663,6 @@ export function createMockORPCClient(options: MockORPCClientOptions = {}): APICl getViewedSplashScreens: () => Promise.resolve(["onboarding-wizard-v1"]), markSplashScreenViewed: () => Promise.resolve(undefined), }, - signing: { - capabilities: () => - Promise.resolve( - customSigningCapabilities ?? { - publicKey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMockKey", - githubUser: "mockuser", - error: null, - } - ), - sign: () => - Promise.resolve({ - signature: "mockSignature==", - publicKey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMockKey", - githubUser: "mockuser", - }), - clearIdentityCache: () => Promise.resolve({ success: true }), - }, server: { getLaunchProject: () => Promise.resolve(null), getSshHost: () => Promise.resolve(null), diff --git a/src/browser/utils/sharedUrlCache.test.ts b/src/browser/utils/sharedUrlCache.test.ts deleted file mode 100644 index ed31b77560..0000000000 --- a/src/browser/utils/sharedUrlCache.test.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from "bun:test"; -import { - getSharedUrl, - getShareData, - setShareData, - removeShareData, - updateShareExpiration, -} from "./sharedUrlCache"; - -// Mock localStorage for testing -const mockStorage = new Map(); - -const mockLocalStorage: Storage = { - getItem: (key: string) => mockStorage.get(key) ?? null, - setItem: (key: string, value: string) => { - mockStorage.set(key, value); - }, - removeItem: (key: string) => { - mockStorage.delete(key); - }, - clear: () => { - mockStorage.clear(); - }, - get length() { - return mockStorage.size; - }, - key: (index: number) => Array.from(mockStorage.keys())[index] ?? null, -}; - -// Save original window to restore after tests -const originalWindow = globalThis.window; - -beforeEach(() => { - mockStorage.clear(); - // The persisted state helpers check window.localStorage and dispatch events - globalThis.window = { - localStorage: mockLocalStorage, - dispatchEvent: () => true, - // eslint-disable-next-line @typescript-eslint/no-empty-function - addEventListener: () => {}, - } as unknown as Window & typeof globalThis; -}); - -afterEach(() => { - // Restore original window to avoid polluting other tests - globalThis.window = originalWindow; -}); - -describe("sharedUrlCache", () => { - const makeShareData = (url: string, id = "abc123", mutateKey = "key123") => ({ - url, - id, - mutateKey, - expiresAt: undefined, - }); - - it("should store and retrieve share data for content", () => { - const content = "Hello, world!"; - const data = makeShareData("https://mux.md/abc123#key"); - - setShareData(content, data); - const result = getShareData(content); - expect(result?.url).toBe(data.url); - expect(result?.id).toBe(data.id); - expect(result?.mutateKey).toBe(data.mutateKey); - }); - - it("should return URL via getSharedUrl convenience function", () => { - const content = "Hello, world!"; - const url = "https://mux.md/abc123#key"; - - setShareData(content, makeShareData(url)); - expect(getSharedUrl(content)).toBe(url); - }); - - it("should return undefined for unknown content", () => { - expect(getSharedUrl("unknown content")).toBeUndefined(); - expect(getShareData("unknown content")).toBeUndefined(); - }); - - it("should overwrite existing data for same content", () => { - const content = "Hello, world!"; - const url1 = "https://mux.md/abc123#key1"; - const url2 = "https://mux.md/def456#key2"; - - setShareData(content, makeShareData(url1, "abc123")); - setShareData(content, makeShareData(url2, "def456")); - expect(getSharedUrl(content)).toBe(url2); - expect(getShareData(content)?.id).toBe("def456"); - }); - - it("should use different keys for different content", () => { - const content1 = "Content A"; - const content2 = "Content B"; - const url1 = "https://mux.md/abc123#key1"; - const url2 = "https://mux.md/def456#key2"; - - setShareData(content1, makeShareData(url1, "abc123")); - setShareData(content2, makeShareData(url2, "def456")); - - expect(getSharedUrl(content1)).toBe(url1); - expect(getSharedUrl(content2)).toBe(url2); - }); - - it("should handle content with special characters", () => { - const content = "Hello! @#$%^&*() 你好 🎉"; - const url = "https://mux.md/abc123#key"; - - setShareData(content, makeShareData(url)); - expect(getSharedUrl(content)).toBe(url); - }); - - it("should remove share data", () => { - const content = "Hello, world!"; - setShareData(content, makeShareData("https://mux.md/abc123#key")); - expect(getSharedUrl(content)).toBeDefined(); - - removeShareData(content); - expect(getSharedUrl(content)).toBeUndefined(); - }); - - it("should update expiration", () => { - const content = "Hello, world!"; - const futureTime = Date.now() + 1000 * 60 * 60; // 1 hour from now - - setShareData(content, makeShareData("https://mux.md/abc123#key")); - expect(getShareData(content)?.expiresAt).toBeUndefined(); - - updateShareExpiration(content, futureTime); - expect(getShareData(content)?.expiresAt).toBe(futureTime); - - updateShareExpiration(content, undefined); - expect(getShareData(content)?.expiresAt).toBeUndefined(); - }); - - it("should return undefined for expired content", () => { - const content = "Hello, world!"; - const pastTime = Date.now() - 1000; // 1 second ago - - setShareData(content, { - url: "https://mux.md/abc123#key", - id: "abc123", - mutateKey: "key123", - expiresAt: pastTime, - }); - - // Should return undefined because it's expired - expect(getShareData(content)).toBeUndefined(); - expect(getSharedUrl(content)).toBeUndefined(); - }); - - it("should return data for non-expired content", () => { - const content = "Hello, world!"; - const futureTime = Date.now() + 1000 * 60 * 60; // 1 hour from now - - setShareData(content, { - url: "https://mux.md/abc123#key", - id: "abc123", - mutateKey: "key123", - expiresAt: futureTime, - }); - - expect(getShareData(content)?.url).toBe("https://mux.md/abc123#key"); - }); -}); diff --git a/src/browser/utils/sharedUrlCache.ts b/src/browser/utils/sharedUrlCache.ts deleted file mode 100644 index f879185964..0000000000 --- a/src/browser/utils/sharedUrlCache.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * LRU cache for persisting shared message URLs in localStorage. - * Uses per-entry storage keys for efficient single-entry updates. - * Maintains a separate index for LRU eviction tracking. - */ - -import { createLRUCache } from "./lruCache"; - -const MAX_ENTRIES = 1024; - -export interface ShareData { - /** Full URL with encryption key in fragment */ - url: string; - /** File ID */ - id: string; - /** Mutate key for delete/update operations */ - mutateKey: string; - /** Expiration timestamp (ms), if set */ - expiresAt?: number; - /** Whether the share was signed with user's key */ - signed?: boolean; -} - -/** - * SHA-256 hash of content, computed synchronously using SubtleCrypto workaround. - * Falls back to a simple string hash if crypto is unavailable. - */ -async function hashContentAsync(content: string): Promise { - const encoder = new TextEncoder(); - const data = encoder.encode(content); - const hashBuffer = await crypto.subtle.digest("SHA-256", data); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - // Use first 16 bytes (32 hex chars) for reasonable key length - return hashArray - .slice(0, 16) - .map((b) => b.toString(16).padStart(2, "0")) - .join(""); -} - -/** - * Synchronous hash using cached async result or fallback. - * We maintain a small in-memory cache of recent hashes to avoid async in hot paths. - */ -const hashCache = new Map(); -const MAX_HASH_CACHE = 100; - -function hashContent(content: string): string { - // Check memory cache first - const cached = hashCache.get(content); - if (cached) return cached; - - // Fallback: use simple hash for sync access, async will populate cache later - // This is a simple FNV-1a hash - well-known and simple - let hash = 2166136261; - for (let i = 0; i < content.length; i++) { - hash ^= content.charCodeAt(i); - hash = Math.imul(hash, 16777619); - } - const fallbackHash = (hash >>> 0).toString(16).padStart(8, "0"); - - // Kick off async hash computation to populate cache for next time - void hashContentAsync(content).then((sha256Hash) => { - if (hashCache.size >= MAX_HASH_CACHE) { - // Evict oldest entry - const firstKey = hashCache.keys().next().value; - if (firstKey) hashCache.delete(firstKey); - } - hashCache.set(content, sha256Hash); - }); - - return fallbackHash; -} - -// LRU cache instance for share data -const shareCache = createLRUCache({ - entryPrefix: "share:", - indexKey: "shareIndex", - maxEntries: MAX_ENTRIES, - // No TTL - expiration is handled per-entry via expiresAt field -}); - -/** - * Get the cached share data for content, if it exists and hasn't expired. - */ -export function getShareData(content: string): (ShareData & { cachedAt: number }) | undefined { - const hash = hashContent(content); - const entry = shareCache.getEntry(hash); - - if (!entry) return undefined; - - // Check if expired (custom per-entry expiration) - if (entry.data.expiresAt && entry.data.expiresAt < Date.now()) { - removeShareData(content); - return undefined; - } - - // Include cachedAt for API compatibility - return { ...entry.data, cachedAt: entry.cachedAt }; -} - -/** - * Get the cached URL for content (convenience wrapper). - */ -export function getSharedUrl(content: string): string | undefined { - return getShareData(content)?.url; -} - -/** - * Store share data for message content. - * Uses LRU eviction when cache exceeds MAX_ENTRIES. - */ -export function setShareData(content: string, data: ShareData): void { - const hash = hashContent(content); - shareCache.set(hash, data); -} - -/** - * Update expiration for cached content. - */ -export function updateShareExpiration(content: string, expiresAt: number | undefined): void { - const hash = hashContent(content); - shareCache.update(hash, (data) => ({ ...data, expiresAt })); -} - -/** - * Remove share data for content (e.g., after deletion or expiration). - */ -export function removeShareData(content: string): void { - const hash = hashContent(content); - shareCache.remove(hash); -} diff --git a/src/browser/utils/ui/keybinds.ts b/src/browser/utils/ui/keybinds.ts index 3b52fa8d4c..b044d1828e 100644 --- a/src/browser/utils/ui/keybinds.ts +++ b/src/browser/utils/ui/keybinds.ts @@ -370,11 +370,6 @@ export const KEYBINDS = { // macOS: Cmd+Shift+E, Win/Linux: Ctrl+Shift+E OPEN_IN_EDITOR: { key: "E", ctrl: true, shift: true }, - /** Share transcript for current workspace */ - // macOS: Cmd+Shift+S, Win/Linux: Ctrl+Shift+S - // (was Cmd+Shift+L, but Chrome intercepts that in server/browser mode) - SHARE_TRANSCRIPT: { key: "S", ctrl: true, shift: true }, - /** Configure MCP servers for current workspace */ // macOS: Cmd+Shift+M, Win/Linux: Ctrl+Shift+M CONFIGURE_MCP: { key: "M", ctrl: true, shift: true }, diff --git a/src/common/config/schemas/appConfigOnDisk.test.ts b/src/common/config/schemas/appConfigOnDisk.test.ts index b4533f9a6c..7d2ec31bde 100644 --- a/src/common/config/schemas/appConfigOnDisk.test.ts +++ b/src/common/config/schemas/appConfigOnDisk.test.ts @@ -26,7 +26,6 @@ describe("AppConfigOnDiskSchema", () => { const valid = { userPreferences: { appearance: { theme: "dark" }, - sharing: { expiration: "24h", signing: false }, }, }; diff --git a/src/common/config/schemas/userPreferences.test.ts b/src/common/config/schemas/userPreferences.test.ts index fda8aedf94..6c75fa5db7 100644 --- a/src/common/config/schemas/userPreferences.test.ts +++ b/src/common/config/schemas/userPreferences.test.ts @@ -44,7 +44,6 @@ describe("UserPreferencesSchema", () => { notifications: { notifyOnResponseByWorkspace: { "ws-1": true }, }, - sharing: { expiration: "7d", signing: false }, review: { includeUncommitted: true, defaultBaseByProject: { "/repo/a": "origin/main" } }, }); diff --git a/src/common/config/schemas/userPreferences.ts b/src/common/config/schemas/userPreferences.ts index 9ef663be2b..f545a13961 100644 --- a/src/common/config/schemas/userPreferences.ts +++ b/src/common/config/schemas/userPreferences.ts @@ -18,7 +18,6 @@ import { } from "@/common/constants/storage"; import { MuxProviderOptionsSchema } from "@/common/schemas/providerOptions"; import { ThinkingLevelSchema } from "@/common/types/thinking"; -import { EXPIRATION_OPTIONS, type ExpirationValue } from "@/common/lib/shareExpiration"; import { isRecord, parseAgentId, @@ -30,11 +29,6 @@ import { parseThinkingLevel, } from "@/common/preferences/userPreferenceParsing"; -const SHARE_EXPIRATION_VALUES = EXPIRATION_OPTIONS.map((option) => option.value) as [ - ExpirationValue, - ...ExpirationValue[], -]; - export const ThemePreferenceSchema = z.enum([ "auto", "light", @@ -73,12 +67,6 @@ export const UserPreferencesSchema = z.object({ projectOrder: z.array(z.string()).optional(), }) .optional(), - sharing: z - .object({ - expiration: z.enum(SHARE_EXPIRATION_VALUES).optional(), - signing: z.boolean().optional(), - }) - .optional(), ai: z .object({ globalDefaults: z @@ -424,24 +412,6 @@ export function normalizeUserPreferences(value: unknown): UserPreferences | unde preferences.navigation = navigation; } - if (isRecord(value.sharing)) { - const sharing: NonNullable = {}; - const expiration = parseEnum( - EXPIRATION_OPTIONS.map((option) => option.value), - value.sharing.expiration - ); - if (expiration) { - sharing.expiration = expiration; - } - - const signing = parseBoolean(value.sharing.signing); - if (signing !== undefined) { - sharing.signing = signing; - } - - preferences.sharing = sharing; - } - if (isRecord(value.ai)) { const ai: NonNullable = {}; if (isRecord(value.ai.globalDefaults)) { diff --git a/src/common/constants/storage.ts b/src/common/constants/storage.ts index f0cdea8435..93a233d757 100644 --- a/src/common/constants/storage.ts +++ b/src/common/constants/storage.ts @@ -370,20 +370,6 @@ export const PROVIDER_OPTIONS_GOOGLE_KEY = "provider_options_google"; */ export const VIM_ENABLED_KEY = "vimEnabled"; -/** - * Preferred expiration for mux.md shares (global) - * Stores: "1h" | "24h" | "7d" | "30d" | "never" - * Default: "7d" - */ -export const SHARE_EXPIRATION_KEY = "shareExpiration"; - -/** - * Whether to sign shared messages by default. - * Stores: boolean - * Default: true - */ -export const SHARE_SIGNING_KEY = "shareSigning"; - /** * Git status indicator display mode (global) * Stores: "line-delta" | "divergence" diff --git a/src/common/lib/muxMd.test.ts b/src/common/lib/muxMd.test.ts deleted file mode 100644 index ecc85323c9..0000000000 --- a/src/common/lib/muxMd.test.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { describe, it, expect } from "bun:test"; -import { - MUX_MD_BASE_URL, - deleteFromMuxMd, - downloadFromMuxMd, - getMuxMdBaseUrl, - isMuxMdUrl, - parseMuxMdUrl, - uploadToMuxMd, -} from "./muxMd"; - -const itIntegration = process.env.TEST_INTEGRATION === "1" ? it : it.skip; - -describe("muxMd", () => { - describe("getMuxMdBaseUrl", () => { - it("should default to the production mux.md origin", () => { - const originalOverride = process.env.MUX_MD_URL_OVERRIDE; - - try { - delete process.env.MUX_MD_URL_OVERRIDE; - expect(getMuxMdBaseUrl()).toBe(MUX_MD_BASE_URL); - } finally { - if (originalOverride === undefined) { - delete process.env.MUX_MD_URL_OVERRIDE; - } else { - process.env.MUX_MD_URL_OVERRIDE = originalOverride; - } - } - }); - - it("should normalize and accept a MUX_MD_URL_OVERRIDE host", () => { - const originalOverride = process.env.MUX_MD_URL_OVERRIDE; - process.env.MUX_MD_URL_OVERRIDE = "https://mux-md-staging.test/some/path"; - - try { - expect(getMuxMdBaseUrl()).toBe("https://mux-md-staging.test"); - - // Override host should be allowed. - expect(isMuxMdUrl("https://mux-md-staging.test/abc123#key456")).toBe(true); - - // Production links should still be recognized while an override is set. - expect(isMuxMdUrl("https://mux.md/abc123#key456")).toBe(true); - - expect(isMuxMdUrl("https://not-mux-md.test/abc123#key456")).toBe(false); - } finally { - if (originalOverride === undefined) { - delete process.env.MUX_MD_URL_OVERRIDE; - } else { - process.env.MUX_MD_URL_OVERRIDE = originalOverride; - } - } - }); - - it("should prefer window.api.muxMdUrlOverride over process.env", () => { - const originalOverride = process.env.MUX_MD_URL_OVERRIDE; - const globalWithWindow = globalThis as unknown as { - window?: { - api?: { - muxMdUrlOverride?: string; - }; - }; - }; - const originalWindow = globalWithWindow.window; - - process.env.MUX_MD_URL_OVERRIDE = "https://mux-md-staging.test"; - globalWithWindow.window = { - api: { - muxMdUrlOverride: "http://localhost:8787/foo", - }, - }; - - try { - expect(getMuxMdBaseUrl()).toBe("http://localhost:8787"); - } finally { - if (originalOverride === undefined) { - delete process.env.MUX_MD_URL_OVERRIDE; - } else { - process.env.MUX_MD_URL_OVERRIDE = originalOverride; - } - - if (originalWindow === undefined) { - delete globalWithWindow.window; - } else { - globalWithWindow.window = originalWindow; - } - } - }); - - it("should use globalThis.__MUX_MD_URL_OVERRIDE__ in browser mode without preload", () => { - const originalOverride = process.env.MUX_MD_URL_OVERRIDE; - const originalDefineOverride = globalThis.__MUX_MD_URL_OVERRIDE__; - const globalWithWindow = globalThis as unknown as { - window?: Record; - }; - const originalWindow = globalWithWindow.window; - - // When running `make dev-server`, the renderer runs in a normal browser where `window.api` - // is not available, so we rely on the Vite-injected define. - process.env.MUX_MD_URL_OVERRIDE = "https://should-not-be-used.test"; - globalThis.__MUX_MD_URL_OVERRIDE__ = "https://mux-md-staging.test/some/path"; - globalWithWindow.window = {}; - - try { - expect(getMuxMdBaseUrl()).toBe("https://mux-md-staging.test"); - } finally { - if (originalOverride === undefined) { - delete process.env.MUX_MD_URL_OVERRIDE; - } else { - process.env.MUX_MD_URL_OVERRIDE = originalOverride; - } - - globalThis.__MUX_MD_URL_OVERRIDE__ = originalDefineOverride; - - if (originalWindow === undefined) { - delete globalWithWindow.window; - } else { - globalWithWindow.window = originalWindow; - } - } - }); - }); - - describe("isMuxMdUrl", () => { - it("should detect valid mux.md URLs", () => { - expect(isMuxMdUrl("https://mux.md/abc123#key456")).toBe(true); - expect(isMuxMdUrl("https://mux.md/RQJe3#Fbbhosspt9q9Ig")).toBe(true); - }); - - it("should reject URLs without fragment", () => { - expect(isMuxMdUrl("https://mux.md/abc123")).toBe(false); - expect(isMuxMdUrl("https://mux.md/abc123#")).toBe(false); - }); - - it("should reject non-mux.md URLs", () => { - expect(isMuxMdUrl("https://example.com/page#hash")).toBe(false); - }); - }); - - describe("parseMuxMdUrl", () => { - it("should extract id and key from URL", () => { - expect(parseMuxMdUrl("https://mux.md/abc123#key456")).toEqual({ - id: "abc123", - key: "key456", - }); - }); - - it("should return null for invalid URLs", () => { - expect(parseMuxMdUrl("https://mux.md/abc123")).toBeNull(); - expect(parseMuxMdUrl("https://mux.md/#key")).toBeNull(); - expect(parseMuxMdUrl("not-a-url")).toBeNull(); - }); - }); - - // Round-trip test: upload then download - itIntegration("should upload and download content correctly", async () => { - const testContent = "# Test Message\n\nThis is a test of mux.md encryption."; - const testFileInfo = { - name: "test-message.md", - type: "text/markdown", - size: testContent.length, - model: "test-model", - }; - - // Upload - const uploadResult = await uploadToMuxMd(testContent, testFileInfo, { - expiresAt: new Date(Date.now() + 60000), // Expire in 1 minute - }); - - expect(uploadResult.url).toContain(`${getMuxMdBaseUrl()}/`); - expect(uploadResult.url).toContain("#"); - expect(uploadResult.id).toBeTruthy(); - expect(uploadResult.key).toBeTruthy(); - expect(uploadResult.mutateKey).toBeTruthy(); - - try { - // Download and decrypt - const downloadResult = await downloadFromMuxMd(uploadResult.id, uploadResult.key); - - expect(downloadResult.content).toBe(testContent); - expect(downloadResult.fileInfo).toBeDefined(); - expect(downloadResult.fileInfo?.name).toBe("test-message.md"); - expect(downloadResult.fileInfo?.model).toBe("test-model"); - } finally { - // Clean up - delete the uploaded file - await deleteFromMuxMd(uploadResult.id, uploadResult.mutateKey); - } - }); - - itIntegration("should fail gracefully for non-existent shares", async () => { - let error: Error | undefined; - try { - await downloadFromMuxMd("nonexistent123", "fakekey456"); - } catch (e) { - error = e as Error; - } - expect(error).toBeDefined(); - expect(error?.message).toMatch(/not found|expired/i); - }); -}); diff --git a/src/common/lib/muxMd.ts b/src/common/lib/muxMd.ts deleted file mode 100644 index 2a5e740e28..0000000000 --- a/src/common/lib/muxMd.ts +++ /dev/null @@ -1,187 +0,0 @@ -/** - * mux.md Client Library - * - * Thin wrapper around @coder/mux-md-client for Mux app integration. - * Re-exports types and provides convenience functions with default base URL. - */ - -import { - upload, - download, - deleteFile, - setExpiration, - parseUrl, - type FileInfo, - type SignOptions, - type SignatureEnvelope, - type UploadResult, -} from "@coder/mux-md-client"; - -// Re-export types from package -export type { FileInfo, SignOptions, SignatureEnvelope, UploadResult }; - -export const MUX_MD_BASE_URL = "https://mux.md"; -export const MUX_MD_HOST = "mux.md"; - -function getMuxMdUrlOverrideRaw(): string | undefined { - // In Electron, we expose the env var via preload so the renderer doesn't need `process.env`. - if (typeof window !== "undefined") { - const fromPreload = window.api?.muxMdUrlOverride; - if (fromPreload && fromPreload.trim().length > 0) return fromPreload; - - // In dev-server browser mode (no Electron preload), Vite injects the env var into the bundle. - const fromViteDefine = globalThis.__MUX_MD_URL_OVERRIDE__; - if (fromViteDefine && fromViteDefine.trim().length > 0) return fromViteDefine; - - // Important: avoid falling back to `process.env` in the renderer bundle. - return undefined; - } - - // In Node (main process / tests), read directly from the environment. - const fromEnv = globalThis.process?.env?.MUX_MD_URL_OVERRIDE; - if (fromEnv && fromEnv.trim().length > 0) return fromEnv; - - return undefined; -} - -function normalizeMuxMdBaseUrlOverride(raw: string): string | undefined { - try { - const parsed = new URL(raw); - if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return undefined; - return parsed.origin; - } catch { - return undefined; - } -} - -/** - * Returns the effective mux.md base URL. - * - * Supports a runtime override (via `MUX_MD_URL_OVERRIDE`) so we can test against staging/local mux.md - * deployments without rebuilding the renderer bundle. - */ -export function getMuxMdBaseUrl(): string { - const overrideRaw = getMuxMdUrlOverrideRaw(); - const override = overrideRaw ? normalizeMuxMdBaseUrlOverride(overrideRaw) : undefined; - return override ?? MUX_MD_BASE_URL; -} - -/** - * Hosts that should be treated as mux.md share links. - * - * Even when an override is set, we still allow the production host so existing share links keep - * working. - */ -export function getMuxMdAllowedHosts(): string[] { - const hosts = new Set(); - hosts.add(MUX_MD_HOST); - - try { - hosts.add(new URL(getMuxMdBaseUrl()).host); - } catch { - // Best-effort: getMuxMdBaseUrl() should always be a valid URL. - } - - return [...hosts]; -} - -// --- URL utilities --- - -/** - * Check if URL is a mux.md share link with encryption key in fragment - */ -export function isMuxMdUrl(url: string): boolean { - try { - const parsed = new URL(url); - return getMuxMdAllowedHosts().includes(parsed.host) && parseUrl(url) !== null; - } catch { - return false; - } -} - -/** - * Parse a mux.md share URL to extract ID and key. - * - * Note: `parseUrl` does not validate the host; call `isMuxMdUrl()` when validating user input. - */ -export function parseMuxMdUrl(url: string): { id: string; key: string } | null { - return parseUrl(url); -} - -export interface UploadOptions { - /** Expiration time (ISO date string or Date object) */ - expiresAt?: string | Date; - /** - * Precomputed signature envelope to embed in the encrypted payload. - * Takes precedence over `sign`. - */ - signature?: SignatureEnvelope; - /** Sign options for native signing via mux-md-client */ - sign?: SignOptions; -} - -// --- Public API --- - -/** - * Upload content to mux.md with end-to-end encryption. - */ -export async function uploadToMuxMd( - content: string, - fileInfo: FileInfo, - options: UploadOptions = {} -): Promise { - return upload(new TextEncoder().encode(content), fileInfo, { - baseUrl: getMuxMdBaseUrl(), - expiresAt: options.expiresAt, - signature: options.signature, - sign: options.sign, - }); -} - -/** - * Delete a shared file from mux.md. - */ -export async function deleteFromMuxMd(id: string, mutateKey: string): Promise { - await deleteFile(id, mutateKey, { baseUrl: getMuxMdBaseUrl() }); -} - -/** - * Update expiration of a shared file on mux.md. - */ -export async function updateMuxMdExpiration( - id: string, - mutateKey: string, - expiresAt: Date | string -): Promise { - const result = await setExpiration(id, mutateKey, expiresAt, { baseUrl: getMuxMdBaseUrl() }); - return result.expiresAt; -} - -// --- Download API --- - -export interface DownloadResult { - /** Decrypted content */ - content: string; - /** File metadata (if available) */ - fileInfo?: FileInfo; -} - -/** - * Download and decrypt content from mux.md. - */ -export async function downloadFromMuxMd( - id: string, - keyMaterial: string, - _signal?: AbortSignal, - options?: { - baseUrl?: string; - } -): Promise { - const result = await download(id, keyMaterial, { - baseUrl: options?.baseUrl ?? getMuxMdBaseUrl(), - }); - return { - content: new TextDecoder().decode(result.data), - fileInfo: result.info, - }; -} diff --git a/src/common/lib/shareExpiration.ts b/src/common/lib/shareExpiration.ts deleted file mode 100644 index 09a68387ef..0000000000 --- a/src/common/lib/shareExpiration.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Shared expiration utilities for mux.md share links. - * - * Used by both ShareMessagePopover and ShareTranscriptDialog to provide - * consistent expiration controls. - */ - -/** Expiration options with human-readable labels */ -export const EXPIRATION_OPTIONS = [ - { value: "1h", label: "1 hour", ms: 60 * 60 * 1000 }, - { value: "24h", label: "24 hours", ms: 24 * 60 * 60 * 1000 }, - { value: "7d", label: "7 days", ms: 7 * 24 * 60 * 60 * 1000 }, - { value: "30d", label: "30 days", ms: 30 * 24 * 60 * 60 * 1000 }, - { value: "never", label: "Never", ms: null }, -] as const; - -export type ExpirationValue = (typeof EXPIRATION_OPTIONS)[number]["value"]; - -/** Convert expiration value to milliseconds from now, or null for "never" */ -export function expirationToMs(value: ExpirationValue): number | null { - const opt = EXPIRATION_OPTIONS.find((o) => o.value === value); - return opt?.ms ?? null; -} - -/** Convert timestamp to expiration value (best fit) */ -export function timestampToExpiration(expiresAt: number | undefined): ExpirationValue { - if (!expiresAt) return "never"; - const remaining = expiresAt - Date.now(); - if (remaining <= 0) return "1h"; // Already expired, default to shortest - // Find the closest option - for (const opt of EXPIRATION_OPTIONS) { - if (opt.ms && remaining <= opt.ms * 1.5) return opt.value; - } - return "never"; -} - -/** Format expiration for display */ -export function formatExpiration(expiresAt: number | undefined): string { - if (!expiresAt) return "Never"; - const date = new Date(expiresAt); - const now = Date.now(); - const diff = expiresAt - now; - - if (diff <= 0) return "Expired"; - if (diff < 60 * 60 * 1000) return `${Math.ceil(diff / (60 * 1000))}m`; - if (diff < 24 * 60 * 60 * 1000) return `${Math.ceil(diff / (60 * 60 * 1000))}h`; - if (diff < 7 * 24 * 60 * 60 * 1000) return `${Math.ceil(diff / (24 * 60 * 60 * 1000))}d`; - return date.toLocaleDateString(); -} diff --git a/src/common/orpc/schemas.ts b/src/common/orpc/schemas.ts index 35ca7c0feb..efe8015334 100644 --- a/src/common/orpc/schemas.ts +++ b/src/common/orpc/schemas.ts @@ -322,9 +322,6 @@ export { ExperimentValueSchema, telemetry, TelemetryEventSchema, - signing, - type SigningCapabilities, - type SignatureEnvelope, ssh, terminal, tokenizer, diff --git a/src/common/orpc/schemas/api.ts b/src/common/orpc/schemas/api.ts index 269df7c156..36711d8611 100644 --- a/src/common/orpc/schemas/api.ts +++ b/src/common/orpc/schemas/api.ts @@ -150,9 +150,6 @@ export const experiments = { // Re-export telemetry schemas export { telemetry, TelemetryEventSchema } from "./telemetry"; -// Re-export signing schemas -export { signing, type SigningCapabilities, type SignatureEnvelope } from "./signing"; - // Re-export analytics schemas export { analytics } from "./analytics"; export { ProviderModelEntrySchema } from "../../config/schemas/providerModelEntry"; diff --git a/src/common/orpc/schemas/signing.ts b/src/common/orpc/schemas/signing.ts deleted file mode 100644 index 4dcebdfdd6..0000000000 --- a/src/common/orpc/schemas/signing.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Signing ORPC schemas - * - * Defines input/output schemas for mux.md message signing endpoints. - * Used for signing shared content with optional GitHub identity attribution. - */ - -import { z } from "zod"; - -// --- Capabilities endpoint --- - -export const signingCapabilitiesInput = z.object({}); - -export const signingErrorOutput = z.object({ - /** Error message */ - message: z.string(), - /** True if a compatible key was found but requires a passphrase */ - hasEncryptedKey: z.boolean(), -}); - -export const signingCapabilitiesOutput = z.object({ - /** Public key in OpenSSH format (ssh-ed25519 AAAA...), null if no key is available */ - publicKey: z.string().nullable(), - /** Detected GitHub username, if any */ - githubUser: z.string().nullable(), - /** Error info if key loading or identity detection failed */ - error: signingErrorOutput.nullable(), -}); - -export type SigningCapabilities = z.infer; - -// --- signMessage endpoint --- -// Returns a mux.md-compatible signature envelope for the provided content. - -export const signatureEnvelopeOutput = z.object({ - sig: z.string(), - publicKey: z.string(), - githubUser: z.string().optional(), -}); - -export type SignatureEnvelope = z.infer; - -export const signMessageInput = z - .object({ - content: z.string(), - }) - .strict(); - -export const signMessageOutput = signatureEnvelopeOutput; - -// --- Clear identity cache endpoint --- - -export const clearIdentityCacheInput = z.object({}); -export const clearIdentityCacheOutput = z.object({ - success: z.boolean(), -}); - -// Grouped schemas for router -export const signing = { - capabilities: { - input: signingCapabilitiesInput, - output: signingCapabilitiesOutput, - }, - signMessage: { - input: signMessageInput, - output: signMessageOutput, - }, - clearIdentityCache: { - input: clearIdentityCacheInput, - output: clearIdentityCacheOutput, - }, -}; diff --git a/src/common/preferences/userPreferencesStorage.test.ts b/src/common/preferences/userPreferencesStorage.test.ts index 445297b7c7..ffb3a2331b 100644 --- a/src/common/preferences/userPreferencesStorage.test.ts +++ b/src/common/preferences/userPreferencesStorage.test.ts @@ -126,7 +126,6 @@ describe("user preference localStorage registry", () => { vimEnabled: true, }, navigation: { launchBehavior: "new-chat", projectOrder: ["/repo"] }, - sharing: { expiration: "24h", signing: false }, ai: { globalDefaults: { agentId: "exec", thinkingLevel: "medium" }, projectDefaults: { diff --git a/src/common/preferences/userPreferencesStorage.ts b/src/common/preferences/userPreferencesStorage.ts index fbac391308..eefd9f51e8 100644 --- a/src/common/preferences/userPreferencesStorage.ts +++ b/src/common/preferences/userPreferencesStorage.ts @@ -18,8 +18,6 @@ import { PROVIDER_OPTIONS_ANTHROPIC_KEY, PROVIDER_OPTIONS_GOOGLE_KEY, REVIEW_INCLUDE_UNCOMMITTED_KEY, - SHARE_EXPIRATION_KEY, - SHARE_SIGNING_KEY, TERMINAL_FONT_CONFIG_KEY, TRANSCRIPT_DENSITIES, TRANSCRIPT_DENSITY_KEY, @@ -41,7 +39,6 @@ import { type LaunchBehavior, type TranscriptDensity, } from "@/common/constants/storage"; -import { EXPIRATION_OPTIONS, type ExpirationValue } from "@/common/lib/shareExpiration"; import { MuxProviderOptionsSchema } from "@/common/schemas/providerOptions"; import { isRecord, @@ -78,8 +75,6 @@ const STATIC_USER_PREFERENCE_KEYS = new Set([ PROJECT_ORDER_KEY, PROVIDER_OPTIONS_ANTHROPIC_KEY, PROVIDER_OPTIONS_GOOGLE_KEY, - SHARE_EXPIRATION_KEY, - SHARE_SIGNING_KEY, REVIEW_INCLUDE_UNCOMMITTED_KEY, getAgentIdKey(GLOBAL_SCOPE_ID), getThinkingLevelKey(GLOBAL_SCOPE_ID), @@ -117,12 +112,6 @@ function parseLaunchBehavior(value: unknown): LaunchBehavior | undefined { return parseEnum(LaunchBehaviorSchema.options, value); } -function parseExpiration(value: unknown): ExpirationValue | undefined { - return typeof value === "string" && EXPIRATION_OPTIONS.some((option) => option.value === value) - ? (value as ExpirationValue) - : undefined; -} - function parseThreshold(value: unknown): number | undefined { return typeof value === "number" && Number.isFinite(value) && @@ -178,11 +167,6 @@ function ensureNavigation( return preferences.navigation; } -function ensureSharing(preferences: UserPreferences): NonNullable { - preferences.sharing ??= {}; - return preferences.sharing; -} - function ensureAi(preferences: UserPreferences): NonNullable { preferences.ai ??= {}; return preferences.ai; @@ -313,24 +297,6 @@ export function applyStoredUserPreference( return pruneUserPreferences(next); } - if (key === SHARE_EXPIRATION_KEY) { - const parsed = parseExpiration(value); - if (!parsed) { - return removeStoredUserPreference(next, key); - } - ensureSharing(next).expiration = parsed; - return pruneUserPreferences(next); - } - - if (key === SHARE_SIGNING_KEY) { - const parsed = parseBoolean(value); - if (parsed === undefined) { - return removeStoredUserPreference(next, key); - } - ensureSharing(next).signing = parsed; - return pruneUserPreferences(next); - } - if (key === getAgentIdKey(GLOBAL_SCOPE_ID)) { const parsed = parseAgentId(value); if (!parsed) { @@ -490,8 +456,6 @@ export function removeStoredUserPreference( else if (key === VIM_ENABLED_KEY) delete next.appearance?.vimEnabled; else if (key === LAUNCH_BEHAVIOR_KEY) delete next.navigation?.launchBehavior; else if (key === PROJECT_ORDER_KEY) delete next.navigation?.projectOrder; - else if (key === SHARE_EXPIRATION_KEY) delete next.sharing?.expiration; - else if (key === SHARE_SIGNING_KEY) delete next.sharing?.signing; else if (key === getAgentIdKey(GLOBAL_SCOPE_ID)) delete next.ai?.globalDefaults?.agentId; else if (key === getThinkingLevelKey(GLOBAL_SCOPE_ID)) delete next.ai?.globalDefaults?.thinkingLevel; @@ -559,12 +523,6 @@ export function entriesFromUserPreferences( if (navigation?.projectOrder !== undefined) entries.push({ key: PROJECT_ORDER_KEY, value: navigation.projectOrder }); - const sharing = preferences.sharing; - if (sharing?.expiration !== undefined) - entries.push({ key: SHARE_EXPIRATION_KEY, value: sharing.expiration }); - if (sharing?.signing !== undefined) - entries.push({ key: SHARE_SIGNING_KEY, value: sharing.signing }); - const ai = preferences.ai; if (ai?.globalDefaults?.agentId !== undefined) entries.push({ key: getAgentIdKey(GLOBAL_SCOPE_ID), value: ai.globalDefaults.agentId }); diff --git a/src/common/types/global.d.ts b/src/common/types/global.d.ts index 29e333cdfe..03ccf96da8 100644 --- a/src/common/types/global.d.ts +++ b/src/common/types/global.d.ts @@ -10,8 +10,6 @@ declare global { chrome?: string; electron?: string; }; - // Optional mux.md base URL override (passed through Electron preload). - muxMdUrlOverride?: string; // Debug flags (dev-only, passed through preload) debugLlmRequest?: boolean; // Allow maintainers to opt into telemetry while running the dev server. @@ -62,13 +60,6 @@ declare global { __MUX_PROXY_URI_TEMPLATE__?: string | null; } - /** - * Optional mux.md base URL override injected by Vite (`define`) in dev-server browser mode. - * - * This intentionally lives on `globalThis` so shared code (compiled for Node as CJS) doesn't need - * to rely on `import.meta.env`. - */ - var __MUX_MD_URL_OVERRIDE__: string | undefined; /** * Optional tutorial sandbox override injected either by Vite (`define`) in browser-mode sandboxes * or by Electron preload. `null`/`undefined` means normal tutorial behavior. diff --git a/src/common/utils/tools/tools.ts b/src/common/utils/tools/tools.ts index 5440031043..2a0c0c5666 100644 --- a/src/common/utils/tools/tools.ts +++ b/src/common/utils/tools/tools.ts @@ -589,8 +589,6 @@ export async function getToolsForModel( // // Known limitations when the native override is active: // - Cannot reach private/localhost URLs (Anthropic's servers can't see workspace network). - // - mux.md share links rely on client-side decryption via URL fragment (#key); - // Anthropic drops the fragment when making HTTP requests, so decryption silently fails. // - Not bridgeable in the PTC sandbox (no execute()); see BridgeableToolName comment. // - Tool hooks (.mux/tool_pre/.mux/tool_post) are skipped because withHooks() returns // early when execute() is absent — same limitation as web_search (provider-native). diff --git a/src/desktop/preload.ts b/src/desktop/preload.ts index ab56c407fa..0b894632bc 100644 --- a/src/desktop/preload.ts +++ b/src/desktop/preload.ts @@ -68,8 +68,6 @@ contextBridge.exposeInMainWorld("api", { // Note: When debugging LLM requests, we also want to see synthetic/request-only // messages in the chat history so the UI matches what was sent to the provider. debugLlmRequest: process.env.MUX_DEBUG_LLM_REQUEST === "1", - // Allow testing against a mux.md staging/local deployment without rebuilding the renderer. - muxMdUrlOverride: process.env.MUX_MD_URL_OVERRIDE, // NOTE: This is intentionally async so the preload script does not rely on Node builtins // like `child_process` (which can break in hardened/sandboxed environments). getIsRosetta: () => ipcRenderer.invoke("mux:get-is-rosetta"), diff --git a/src/node/builtinSkills/mux-docs.md b/src/node/builtinSkills/mux-docs.md index 41583396ae..a4afbb907e 100644 --- a/src/node/builtinSkills/mux-docs.md +++ b/src/node/builtinSkills/mux-docs.md @@ -57,7 +57,6 @@ Use this index to find a page's: - **Workspaces** - Workspaces (`/workspaces`) → `references/docs/workspaces/index.mdx` — Isolated development environments for parallel agent work - Forking Workspaces (`/workspaces/fork`) → `references/docs/workspaces/fork.mdx` — Clone workspaces with conversation history to explore alternatives - - Message Sharing (`/workspaces/sharing`) → `references/docs/workspaces/sharing.mdx` — Share encrypted messages with cryptographic signatures via Mux - .muxignore (`/workspaces/muxignore`) → `references/docs/workspaces/muxignore.mdx` — Sync gitignored files to worktree workspaces - **Compaction** - Compaction (`/workspaces/compaction`) → `references/docs/workspaces/compaction/index.mdx` — Managing conversation context size with compaction diff --git a/src/node/orpc/context.ts b/src/node/orpc/context.ts index c1af680fb3..84f75e4bf6 100644 --- a/src/node/orpc/context.ts +++ b/src/node/orpc/context.ts @@ -28,7 +28,6 @@ import type { MemoryMetaService } from "@/node/services/memoryMeta"; import type { WorkspaceMcpOverridesService } from "@/node/services/workspaceMcpOverridesService"; import type { MCPServerManager } from "@/node/services/mcpServerManager"; import type { TelemetryService } from "@/node/services/telemetryService"; -import type { SigningService } from "@/node/services/signingService"; import type { SessionTimingService } from "@/node/services/sessionTimingService"; import type { SessionUsageService } from "@/node/services/sessionUsageService"; import type { InstructionsService } from "@/node/services/instructionsService"; @@ -89,7 +88,6 @@ export interface ORPCContext { browserControlService: BrowserControlService; browserSessionStateHub: BrowserSessionStateHub; policyService: PolicyService; - signingService: SigningService; coderService: CoderService; serverAuthService: ServerAuthService; sshPromptService: SshPromptService; diff --git a/src/node/orpc/router.ts b/src/node/orpc/router.ts index fcb6fec613..6cc241697f 100644 --- a/src/node/orpc/router.ts +++ b/src/node/orpc/router.ts @@ -6086,27 +6086,6 @@ export const router = (authToken?: string) => { }; }), }, - signing: { - capabilities: t - .input(schemas.signing.capabilities.input) - .output(schemas.signing.capabilities.output) - .handler(async ({ context }) => { - return context.signingService.getCapabilities(); - }), - signMessage: t - .input(schemas.signing.signMessage.input) - .output(schemas.signing.signMessage.output) - .handler(({ context, input }) => { - return context.signingService.signMessage(input.content); - }), - clearIdentityCache: t - .input(schemas.signing.clearIdentityCache.input) - .output(schemas.signing.clearIdentityCache.output) - .handler(({ context }) => { - context.signingService.clearIdentityCache(); - return { success: true }; - }), - }, analytics: { getSummary: t .input(schemas.analytics.getSummary.input) diff --git a/src/node/services/agentSkills/builtInSkillContent.generated.ts b/src/node/services/agentSkills/builtInSkillContent.generated.ts index 80ff8d9ccb..ff614f4535 100644 --- a/src/node/services/agentSkills/builtInSkillContent.generated.ts +++ b/src/node/services/agentSkills/builtInSkillContent.generated.ts @@ -3398,7 +3398,6 @@ export const BUILTIN_SKILL_FILES: Record> = { ' "pages": [', ' "workspaces",', ' "workspaces/fork",', - ' "workspaces/sharing",', ' "workspaces/muxignore",', " {", ' "group": "Compaction",', @@ -3518,8 +3517,6 @@ export const BUILTIN_SKILL_FILES: Record> = { ' { "source": "/guides/compaction", "destination": "/workspaces/compaction" },', ' { "source": "/guides/compaction/manual", "destination": "/workspaces/compaction/manual" },', ' { "source": "/guides/compaction/automatic", "destination": "/workspaces/compaction/automatic" },', - ' { "source": "/sharing", "destination": "/workspaces/sharing" },', - ' { "source": "/guides/sharing", "destination": "/workspaces/sharing" },', ' { "source": "/prompting-tips", "destination": "/agents/prompting-tips" },', ' { "source": "/prompting-tips.html", "destination": "/agents/prompting-tips" },', ' { "source": "/guides/prompting-tips", "destination": "/agents/prompting-tips" },', @@ -5743,12 +5740,6 @@ export const BUILTIN_SKILL_FILES: Record> = { "", "This disables telemetry collection at the backend level.", "", - "", - " Disabling telemetry also hides the **Share** button on assistant messages. Link sharing uses", - " [mux.md](https://mux.md), a separate Mux service, and is gated on telemetry enablement to respect", - " your privacy preferences.", - "", - "", "## Source code", "", "- **Payload definitions**: [`src/common/telemetry/payload.ts`](https://github.com/coder/mux/blob/main/src/common/telemetry/payload.ts)", @@ -6526,109 +6517,6 @@ export const BUILTIN_SKILL_FILES: Record> = { "Place `.muxignore` in the project root, next to your `.gitignore`. You may want to commit it to your repo so all collaborators share the same sync rules.", "", ].join("\n"), - "references/docs/workspaces/sharing.mdx": [ - "---", - "title: Message Sharing", - "sidebarTitle: Sharing", - "description: Share encrypted messages with cryptographic signatures via Mux", - "---", - "", - "Mux lets you share assistant messages via [mux.md](https://mux.md), an end-to-end encrypted paste service. Shared messages can also be cryptographically signed to prove authorship.", - "", - "![Sharing](../img/message-sharing.webp)", - "", - "## How sharing works", - "", - "1. **End-to-end encryption**: Content is encrypted in your browser using AES-256-GCM before upload. The encryption key stays in the URL fragment and is never sent to the server.", - "", - "2. **Optional signing**: When a signing key is available, Mux signs the content with your private key. Recipients can verify the signature on mux.md.", - "", - "3. **Expiration**: Shares can expire after 1 hour, 24 hours, 7 days, 30 days, or never. You can change expiration after sharing.", - "", - "## Message signing", - "", - "Signing proves that you authored a shared message. When enabled, mux.md displays your GitHub username alongside a “Verified” badge.", - "", - "### Signing key discovery", - "", - "Mux looks for a signing key in these locations (first match wins):", - "", - "1. `~/.mux/message_signing_key` — Mux-specific key (can be a symlink)", - "2. `~/.ssh/id_ed25519` — standard SSH Ed25519 key", - "3. `~/.ssh/id_ecdsa` — standard SSH ECDSA key", - "", - "Mux can also sign using keys loaded in your SSH agent when `SSH_AUTH_SOCK` is set (for example,", - "1Password's SSH agent). Agent-backed keys are preferred over the default `~/.ssh/id_*` files.", - "", - "", - " To reuse an existing key without copying it:", - "", - "```bash", - "ln -s ~/.ssh/id_ed25519 ~/.mux/message_signing_key", - "```", - "", - "", - "", - "### Supported key types", - "", - "- **Ed25519** (recommended) — fast, secure, 32-byte keys", - "- **ECDSA** — P-256, P-384, and P-521 curves supported", - "", - "Encrypted key files (passphrase-protected) are skipped automatically unless the key is available via ssh-agent.", - "", - "### GitHub identity detection", - "", - "To display your GitHub username on signed shares, Mux detects your identity via the GitHub CLI:", - "", - "```bash", - "gh auth status", - "```", - "", - "If you're logged in with `gh auth login`, your GitHub username appears on mux.md alongside the signature verification.", - "", - "", - " GitHub CLI is optional. Without it, shares are still signed—recipients just see the public key", - " fingerprint instead of your username.", - "", - "", - "### Enabling/disabling signing", - "", - "Click the pen icon in the share popover to toggle signing on or off. This setting persists across sessions.", - "", - "When signing is disabled (or no key is found), shares are still encrypted—they just won’t include a signature.", - "", - "## Using shared content in Mux", - "", - "Mux can read mux.md URLs using the `web_fetch` tool. When you paste a mux.md link into chat, Mux decrypts the content client-side and makes it available to the assistant.", - "", - "This enables several workflows:", - "", - "- **Cross-session sharing**: Move context or instructions between workspaces in the same Mux client", - "- **Team collaboration**: Send encrypted snippets to teammates who can paste them into their own Mux sessions", - "- **Preserving context**: Save important outputs and retrieve them later in new conversations", - "", - "Paste any `https://mux.md/#` URL into your message, and Mux will fetch and decrypt the content.", - "", - "", - " The encryption key (after `#`) never leaves your client. Only the encrypted blob travels over the", - " network.", - "", - "", - "## Generating a signing key", - "", - "If you don’t already have an SSH key, generate one:", - "", - "```bash", - "# Ed25519 (recommended)", - 'ssh-keygen -t ed25519 -f ~/.mux/message_signing_key -N ""', - "", - "# Or ECDSA", - 'ssh-keygen -t ecdsa -b 256 -f ~/.mux/message_signing_key -N ""', - "```", - "", - 'The `-N ""` flag creates a key without a passphrase (required for Mux to use it automatically).', - "", - ].join("\n"), "SKILL.md": [ "---", "name: mux-docs", @@ -6689,7 +6577,6 @@ export const BUILTIN_SKILL_FILES: Record> = { " - **Workspaces**", " - Workspaces (`/workspaces`) → `references/docs/workspaces/index.mdx` — Isolated development environments for parallel agent work", " - Forking Workspaces (`/workspaces/fork`) → `references/docs/workspaces/fork.mdx` — Clone workspaces with conversation history to explore alternatives", - " - Message Sharing (`/workspaces/sharing`) → `references/docs/workspaces/sharing.mdx` — Share encrypted messages with cryptographic signatures via Mux", " - .muxignore (`/workspaces/muxignore`) → `references/docs/workspaces/muxignore.mdx` — Sync gitignored files to worktree workspaces", " - **Compaction**", " - Compaction (`/workspaces/compaction`) → `references/docs/workspaces/compaction/index.mdx` — Managing conversation context size with compaction", diff --git a/src/node/services/serviceContainer.ts b/src/node/services/serviceContainer.ts index 0ecaee5616..4d29f9be28 100644 --- a/src/node/services/serviceContainer.ts +++ b/src/node/services/serviceContainer.ts @@ -50,7 +50,6 @@ import { EXPERIMENT_IDS } from "@/common/constants/experiments"; import { AgentStatusService } from "@/node/services/agentStatusService"; import { IdleCompactionService } from "@/node/services/idleCompactionService"; import type { IdleDispatcher } from "@/node/services/idleDispatcher"; -import { getSigningService, type SigningService } from "@/node/services/signingService"; import { coderService, type CoderService } from "@/node/services/coderService"; import { SshPromptService } from "@/node/services/sshPromptService"; import { WorkspaceLifecycleHooks } from "@/node/services/workspaceLifecycleHooks"; @@ -139,7 +138,6 @@ export class ServiceContainer { public readonly browserSessionStateHub: BrowserSessionStateHub; public readonly analyticsService: AnalyticsService; public readonly experimentsService: ExperimentsService; - public readonly signingService: SigningService; public readonly policyService: PolicyService; public readonly coderService: CoderService; public readonly serverAuthService: ServerAuthService; @@ -552,7 +550,6 @@ export class ServiceContainer { this.policyService, opResolver ); - this.signingService = getSigningService(); this.coderService = coderService; this.serverAuthService = new ServerAuthService(config); @@ -786,7 +783,6 @@ export class ServiceContainer { browserControlService: this.browserControlService, browserSessionStateHub: this.browserSessionStateHub, policyService: this.policyService, - signingService: this.signingService, coderService: this.coderService, serverAuthService: this.serverAuthService, sshPromptService: this.sshPromptService, diff --git a/src/node/services/signingService.test.ts b/src/node/services/signingService.test.ts deleted file mode 100644 index 14903e4a01..0000000000 --- a/src/node/services/signingService.test.ts +++ /dev/null @@ -1,325 +0,0 @@ -import { describe, it, expect, beforeAll, afterAll } from "bun:test"; -import { parsePublicKey, verifySignature, type SignatureEnvelope } from "@coder/mux-md-client"; -import { execSync } from "child_process"; -import { mkdirSync, rmSync } from "fs"; -import { tmpdir } from "os"; -import { join } from "path"; -import { SigningService } from "./signingService"; - -async function expectValidSignature(content: string, envelope: SignatureEnvelope): Promise { - const parsed = parsePublicKey(envelope.publicKey); - const signatureBytes = Buffer.from(envelope.sig, "base64"); - const messageBytes = new TextEncoder().encode(content); - - const isValid = await verifySignature(parsed, messageBytes, new Uint8Array(signatureBytes)); - expect(isValid).toBe(true); -} - -function startSshAgent(): { sshAuthSock: string; sshAgentPid: string } { - const output = execSync("ssh-agent -s").toString("utf-8"); - - const sockMatch = /SSH_AUTH_SOCK=([^;]+);/m.exec(output); - const pidMatch = /SSH_AGENT_PID=([0-9]+);/m.exec(output); - if (!sockMatch || !pidMatch) { - throw new Error(`Failed to parse ssh-agent output: ${output}`); - } - - return { sshAuthSock: sockMatch[1], sshAgentPid: pidMatch[1] }; -} - -/** Run test body with SSH_AUTH_SOCK cleared to ensure disk keys are used */ -async function withoutSshAgent(fn: () => Promise): Promise { - const savedSock = process.env.SSH_AUTH_SOCK; - const savedPid = process.env.SSH_AGENT_PID; - - delete process.env.SSH_AUTH_SOCK; - delete process.env.SSH_AGENT_PID; - - try { - return await fn(); - } finally { - if (savedSock === undefined) { - delete process.env.SSH_AUTH_SOCK; - } else { - process.env.SSH_AUTH_SOCK = savedSock; - } - - if (savedPid === undefined) { - delete process.env.SSH_AGENT_PID; - } else { - process.env.SSH_AGENT_PID = savedPid; - } - } -} - -describe("SigningService", () => { - // Create isolated temp directory for each test run - const testDir = join( - tmpdir(), - `signing-test-${Date.now()}-${Math.random().toString(36).slice(2)}` - ); - - const ed25519KeyPath = join(testDir, "id_ed25519"); - const ecdsaKeyPath = join(testDir, "id_ecdsa"); - const encryptedKeyPath = join(testDir, "id_encrypted"); - - const prevEnv = { - SSH_AUTH_SOCK: process.env.SSH_AUTH_SOCK, - SSH_AGENT_PID: process.env.SSH_AGENT_PID, - }; - - beforeAll(() => { - // Ensure these tests are not influenced by a user's existing ssh-agent. - delete process.env.SSH_AUTH_SOCK; - delete process.env.SSH_AGENT_PID; - - mkdirSync(testDir, { recursive: true }); - // Generate keys using ssh-keygen (same format users would have) - execSync(`ssh-keygen -t ed25519 -f "${ed25519KeyPath}" -N "" -q`); - execSync(`ssh-keygen -t ecdsa -b 256 -f "${ecdsaKeyPath}" -N "" -q`); - execSync(`ssh-keygen -t ed25519 -f "${encryptedKeyPath}" -N "testpassword" -q`); - }); - - afterAll(() => { - rmSync(testDir, { recursive: true, force: true }); - - if (prevEnv.SSH_AUTH_SOCK === undefined) { - delete process.env.SSH_AUTH_SOCK; - } else { - process.env.SSH_AUTH_SOCK = prevEnv.SSH_AUTH_SOCK; - } - - if (prevEnv.SSH_AGENT_PID === undefined) { - delete process.env.SSH_AGENT_PID; - } else { - process.env.SSH_AGENT_PID = prevEnv.SSH_AGENT_PID; - } - }); - - describe("with Ed25519 key", () => { - it( - "should load key and return capabilities", - () => - withoutSshAgent(async () => { - const service = new SigningService([ed25519KeyPath]); - const capabilities = await service.getCapabilities(); - - expect(capabilities.publicKey).toBeDefined(); - expect(capabilities.publicKey).toStartWith("ssh-ed25519 "); - }), - // SigningService.getCapabilities() shells out to `gh auth status` to detect - // GitHub identity. `gh` can take several seconds (network/credential lookup), - // so under CI load this comfortably exceeds Bun's default 5s per-test - // timeout even though the actual key-loading work is fast. See PR for the - // discovery context; production-side timeout is a follow-up. - { timeout: 15_000 } - ); - - it("should sign messages", () => - withoutSshAgent(async () => { - const service = new SigningService([ed25519KeyPath]); - const content = "hello world"; - const envelope = await service.signMessage(content); - - expect(envelope.publicKey).toStartWith("ssh-ed25519 "); - await expectValidSignature(content, envelope); - })); - - it("should return consistent public key across multiple calls", () => - withoutSshAgent(async () => { - const service = new SigningService([ed25519KeyPath]); - const caps1 = await service.getCapabilities(); - const caps2 = await service.getCapabilities(); - const envelope = await service.signMessage("consistency"); - - expect(caps1.publicKey).toBe(caps2.publicKey); - expect(caps1.publicKey).toBe(envelope.publicKey); - })); - }); - - describe("with ECDSA key", () => { - it("should load key and return capabilities", () => - withoutSshAgent(async () => { - const service = new SigningService([ecdsaKeyPath]); - const capabilities = await service.getCapabilities(); - - expect(capabilities.publicKey).toBeDefined(); - expect(capabilities.publicKey).toStartWith("ecdsa-sha2-nistp256 "); - })); - - it("should sign messages", () => - withoutSshAgent(async () => { - const content = "hello ecdsa"; - let envelope: SignatureEnvelope | null = null; - - // Some randomly-generated ECDSA keys trigger a downstream mux-md-client - // scalar-length parsing bug (31-byte scalar). Regenerate and retry so this - // test stays deterministic while still validating ECDSA signing behavior. - for (let attempt = 0; attempt < 3; attempt++) { - const service = new SigningService([ecdsaKeyPath]); - try { - envelope = await service.signMessage(content); - break; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - if (!message.includes("Field.fromBytes: expected 32 bytes, got 31")) { - throw error; - } - - rmSync(ecdsaKeyPath, { force: true }); - rmSync(`${ecdsaKeyPath}.pub`, { force: true }); - execSync(`ssh-keygen -t ecdsa -b 256 -f "${ecdsaKeyPath}" -N "" -q`); - } - } - - if (!envelope) { - throw new Error("Failed to generate a valid ECDSA key for signing test"); - } - - expect(envelope.publicKey).toStartWith("ecdsa-sha2-nistp256 "); - await expectValidSignature(content, envelope); - })); - }); - - describe("with no key", () => { - it("should return null publicKey when no key exists", () => - withoutSshAgent(async () => { - const service = new SigningService(["/nonexistent/path/key"]); - const caps = await service.getCapabilities(); - - expect(caps.publicKey).toBeNull(); - expect(caps.error).toBeDefined(); - expect(caps.error?.hasEncryptedKey).toBe(false); - })); - - it("should throw when signing without a key", () => - withoutSshAgent(async () => { - const service = new SigningService(["/nonexistent/path/key"]); - - let threw = false; - try { - await service.signMessage("no key"); - } catch { - threw = true; - } - expect(threw).toBe(true); - })); - }); - - describe("key path priority", () => { - it("should use first available key in path order", () => - withoutSshAgent(async () => { - // ECDSA first, Ed25519 second - should pick ECDSA - const service = new SigningService([ecdsaKeyPath, ed25519KeyPath]); - const caps = await service.getCapabilities(); - - expect(caps.publicKey).toStartWith("ecdsa-sha2-nistp256 "); - })); - - it("should skip missing paths and use next available", () => - withoutSshAgent(async () => { - // Nonexistent first, Ed25519 second - should pick Ed25519 - const service = new SigningService(["/nonexistent/key", ed25519KeyPath]); - const caps = await service.getCapabilities(); - - expect(caps.publicKey).toStartWith("ssh-ed25519 "); - })); - }); - - describe("with encrypted key", () => { - it("should detect encrypted key and return hasEncryptedKey=true", () => - withoutSshAgent(async () => { - const service = new SigningService([encryptedKeyPath]); - const caps = await service.getCapabilities(); - - expect(caps.publicKey).toBeNull(); - expect(caps.error?.hasEncryptedKey).toBe(true); - expect(caps.error?.message).toContain("passphrase"); - })); - - it("should skip encrypted key and use unencrypted fallback", () => - withoutSshAgent(async () => { - // Encrypted first, unencrypted second - should skip encrypted and use unencrypted - const service = new SigningService([encryptedKeyPath, ed25519KeyPath]); - const caps = await service.getCapabilities(); - - expect(caps.publicKey).toStartWith("ssh-ed25519 "); - // Key loaded successfully - error may exist for identity detection (gh not installed) - // but should NOT have hasEncryptedKey flag since we found a usable key - if (caps.error) { - expect(caps.error.hasEncryptedKey).toBe(false); - } - })); - - it("should reset hasEncryptedKey on cache clear", () => - withoutSshAgent(async () => { - const service = new SigningService([encryptedKeyPath]); - const caps1 = await service.getCapabilities(); - expect(caps1.error?.hasEncryptedKey).toBe(true); - - service.clearIdentityCache(); - // After clearing, a fresh load should still detect the encrypted key - const caps2 = await service.getCapabilities(); - expect(caps2.error?.hasEncryptedKey).toBe(true); - })); - }); - - describe("with ssh-agent", () => { - let sshAuthSock: string | null = null; - let sshAgentPid: string | null = null; - - beforeAll(() => { - const agent = startSshAgent(); - sshAuthSock = agent.sshAuthSock; - sshAgentPid = agent.sshAgentPid; - - process.env.SSH_AUTH_SOCK = sshAuthSock; - process.env.SSH_AGENT_PID = sshAgentPid; - - execSync(`ssh-add -q "${ed25519KeyPath}"`, { env: process.env }); - }); - - afterAll(() => { - if (sshAuthSock && sshAgentPid) { - try { - execSync("ssh-agent -k", { - env: { - ...process.env, - SSH_AUTH_SOCK: sshAuthSock, - SSH_AGENT_PID: sshAgentPid, - }, - }); - } catch { - // Best-effort cleanup. - } - } - - delete process.env.SSH_AUTH_SOCK; - delete process.env.SSH_AGENT_PID; - }); - - it("should prefer agent key over disk fallback", async () => { - // Nonexistent explicit path forces the service to choose between agent and fallback. - // The agent provides Ed25519; fallback provides ECDSA. - const service = new SigningService(["/nonexistent/key", ecdsaKeyPath]); - const caps = await service.getCapabilities(); - - expect(caps.publicKey).toStartWith("ssh-ed25519 "); - - const content = "agent signing"; - const envelope = await service.signMessage(content); - expect(envelope.publicKey).toStartWith("ssh-ed25519 "); - await expectValidSignature(content, envelope); - }); - - it("should use agent key when only encrypted disk key is present", async () => { - const service = new SigningService([encryptedKeyPath]); - const caps = await service.getCapabilities(); - - expect(caps.publicKey).toStartWith("ssh-ed25519 "); - if (caps.error) { - expect(caps.error.hasEncryptedKey).toBe(false); - } - }); - }); -}); diff --git a/src/node/services/signingService.ts b/src/node/services/signingService.ts deleted file mode 100644 index 59192a2812..0000000000 --- a/src/node/services/signingService.ts +++ /dev/null @@ -1,675 +0,0 @@ -/** - * Signing Service - * - * Provides message signing for mux.md shares. - * - * - Loads unencrypted key files from disk via sshpk - * - Can sign via SSH agent (SSH_AUTH_SOCK), including 1Password's SSH agent - * - Produces mux.md-compatible SignatureEnvelopes (no private key bytes cross IPC) - * - Detects GitHub username via `gh auth status` - */ - -import assert from "node:assert/strict"; -import { existsSync, readFileSync } from "fs"; -import { homedir } from "os"; -import { join } from "path"; -import { - computeFingerprint, - createSignatureEnvelope, - parsePublicKey, - type KeyType as MuxMdKeyType, - type SignatureEnvelope, -} from "@coder/mux-md-client"; -import sshpk from "sshpk"; -import { OpenSSHAgent, type KnownPublicKeys, type ParsedKey, type PublicKeyEntry } from "ssh2"; -import { getMuxHome } from "@/common/constants/paths"; -import { execAsync } from "@/node/utils/disposableExec"; -import { log } from "@/node/services/log"; -import { getErrorMessage } from "@/common/utils/errors"; - -interface KeyPair { - privateKey: sshpk.PrivateKey; - privateKeyBytes: Uint8Array; - publicKeyOpenSSH: string; -} - -interface IdentityStatus { - githubUser: string | null; - error: string | null; -} - -interface SigningError { - /** Error message */ - message: string; - /** True if a compatible key was found but requires a passphrase */ - hasEncryptedKey: boolean; -} - -interface SigningCapabilities { - /** Public key in OpenSSH format (ssh-ed25519 AAAA...), null if unavailable */ - publicKey: string | null; - /** Detected GitHub username, if any */ - githubUser: string | null; - /** Error info if key loading or identity detection failed */ - error: SigningError | null; -} - -type SigningKeySelection = - | { - kind: "disk"; - keyPair: KeyPair; - } - | { - kind: "ssh-agent"; - sshAuthSock: string; - publicKeyOpenSSH: string; - fingerprint: string; - }; - -interface AgentKeyCandidate { - publicKeyOpenSSH: string; - fingerprint: string; - muxKeyType: MuxMdKeyType; -} - -const SUPPORTED_AGENT_KEY_TYPES = new Set([ - "ssh-ed25519", - "ecdsa-sha2-nistp256", - "ecdsa-sha2-nistp384", - "ecdsa-sha2-nistp521", -]); - -/** - * Probe whether the @coder/mux-md-client/ssh-agent subpath is importable. - * Caches the result so we only pay the cost once. If the module is missing - * (e.g. bun resolved to a version without the export), ssh-agent signing is - * silently unavailable and the service falls back to disk keys. - */ -let sshAgentModuleAvailable: boolean | null = null; -async function isSshAgentModuleAvailable(): Promise { - if (sshAgentModuleAvailable != null) return sshAgentModuleAvailable; - try { - // eslint-disable-next-line no-restricted-syntax -- startup resilience probe - await import("@coder/mux-md-client/ssh-agent"); - sshAgentModuleAvailable = true; - } catch { - log.warn( - "[SigningService] @coder/mux-md-client/ssh-agent is not available — ssh-agent signing disabled. " + - "This usually means the package resolved to a version missing the export." - ); - sshAgentModuleAvailable = false; - } - return sshAgentModuleAvailable; -} - -const AGENT_KEY_TYPE_PRIORITY: Record = { - ed25519: 0, - "ecdsa-p256": 1, - "ecdsa-p384": 2, - "ecdsa-p521": 3, -}; - -/** Default paths to check for signing keys, in order of preference */ -export function getDefaultKeyPaths(): string[] { - return [ - join(getMuxHome(), "message_signing_key"), // Explicit mux key (any supported type, symlink-friendly) - join(homedir(), ".ssh", "id_ed25519"), // SSH Ed25519 - join(homedir(), ".ssh", "id_ecdsa"), // SSH ECDSA - ]; -} - -function normalizeOpenSshPublicKey(publicKey: string): string | null { - const parts = publicKey.trim().split(/\s+/); - if (parts.length < 2) return null; - return `${parts[0]} ${parts[1]}`; -} - -/** - * Service for message signing (Ed25519 or ECDSA). - * Loads keys from disk or uses an SSH agent if available. - */ -export class SigningService { - private signingKey: SigningKeySelection | null = null; - private keyLoadAttempted = false; - private keyLoadPromise: Promise | null = null; - private keyLoadError: string | null = null; - private hasEncryptedKey = false; - - private identityCache: IdentityStatus | null = null; - private identityPromise: Promise | null = null; - private readonly keyPaths: string[]; - - constructor(keyPaths?: string[]) { - this.keyPaths = keyPaths ?? getDefaultKeyPaths(); - assert(this.keyPaths.length > 0, "SigningService: keyPaths must be non-empty"); - } - - private static isParsedKey(value: unknown): value is ParsedKey { - return ( - typeof value === "object" && - value !== null && - "type" in value && - typeof (value as ParsedKey).getPublicSSH === "function" - ); - } - - private static unwrapSshAgentIdentity(identity: ParsedKey | PublicKeyEntry): ParsedKey | null { - if (SigningService.isParsedKey(identity)) return identity; - - const pubKey = identity.pubKey; - if (SigningService.isParsedKey(pubKey)) return pubKey; - - if (typeof pubKey === "object" && pubKey !== null && "pubKey" in pubKey) { - const inner = (pubKey as { pubKey: ParsedKey | Buffer | string }).pubKey; - if (SigningService.isParsedKey(inner)) return inner; - } - - return null; - } - private static isSupportedDiskKeyType(type: string): type is "ed25519" | "ecdsa" { - return type === "ed25519" || type === "ecdsa"; - } - - /** - * Load a signing keypair from disk using sshpk. - * Supports Ed25519 and ECDSA keys in PEM or OpenSSH format. - */ - private tryLoadDiskKeyPair(keyPaths: string[]): KeyPair | null { - for (const keyPath of keyPaths) { - if (!existsSync(keyPath)) continue; - - try { - log.info("[SigningService] Attempting to load key from:", keyPath); - const keyData = readFileSync(keyPath, "utf-8"); - - // Parse with sshpk (auto-detects format) - const privateKey = sshpk.parsePrivateKey(keyData, "auto", { filename: keyPath }); - - // Verify it's a supported key type - if (!SigningService.isSupportedDiskKeyType(privateKey.type)) { - log.info( - "[SigningService] Key at", - keyPath, - "is", - privateKey.type, - "- not supported (need ed25519 or ecdsa), skipping" - ); - continue; - } - - // Get public key in OpenSSH format - const publicKeyOpenSSH = privateKey.toPublic().toString("ssh"); - - // Extract raw private key bytes for use with mux-md-client signing - const privateKeyBytes = this.extractPrivateKeyBytes(privateKey); - - const keyPair = { privateKey, privateKeyBytes, publicKeyOpenSSH }; - - log.info("[SigningService] Loaded", privateKey.type, "key from:", keyPath); - log.info("[SigningService] Public key:", publicKeyOpenSSH.slice(0, 50) + "..."); - return keyPair; - } catch (err) { - const message = getErrorMessage(err); - // Check for encrypted key - if (message.includes("encrypted") || message.includes("passphrase")) { - log.info( - "[SigningService] Encrypted key at", - keyPath, - "- skipping (passphrase required)" - ); - this.hasEncryptedKey = true; - continue; - } - log.warn("[SigningService] Failed to load key from", keyPath + ":", message); - } - } - - return null; - } - - private async listSshAgentKeyCandidates(sshAuthSock: string): Promise { - assert( - sshAuthSock.trim().length > 0, - "listSshAgentKeyCandidates: sshAuthSock must be non-empty" - ); - - const agent = new OpenSSHAgent(sshAuthSock); - const identities = await new Promise>((resolve, reject) => { - agent.getIdentities((err, keys) => { - if (err) { - reject(err); - return; - } - resolve(keys ?? []); - }); - }); - - const candidates: AgentKeyCandidate[] = []; - - for (const identity of identities) { - const parsedIdentity = SigningService.unwrapSshAgentIdentity(identity); - if (!parsedIdentity) continue; - if (!SUPPORTED_AGENT_KEY_TYPES.has(parsedIdentity.type)) continue; - - // OpenSSH public key format: " [comment]" - const publicKeyOpenSSH = `${parsedIdentity.type} ${parsedIdentity - .getPublicSSH() - .toString("base64")}`; - - try { - const parsed = parsePublicKey(publicKeyOpenSSH); - const fingerprint = await computeFingerprint(parsed.keyBytes); - - candidates.push({ - publicKeyOpenSSH, - fingerprint, - muxKeyType: parsed.type, - }); - } catch (err) { - const message = getErrorMessage(err); - log.debug("[SigningService] Skipping unsupported SSH agent key:", message); - } - } - - return candidates; - } - - private pickPreferredAgentKey(candidates: AgentKeyCandidate[]): AgentKeyCandidate | null { - let best: AgentKeyCandidate | null = null; - - for (const candidate of candidates) { - if (!best) { - best = candidate; - continue; - } - if ( - AGENT_KEY_TYPE_PRIORITY[candidate.muxKeyType] < AGENT_KEY_TYPE_PRIORITY[best.muxKeyType] - ) { - best = candidate; - } - } - - return best; - } - - private async trySelectSshAgentKey(): Promise<{ - selection: SigningKeySelection | null; - error: string | null; - }> { - const desiredPublicKeyRaw = process.env.MUX_SIGNING_PUBLIC_KEY?.trim(); - const desiredFingerprint = process.env.MUX_SIGNING_KEY_FINGERPRINT?.trim(); - const hasOverride = Boolean(desiredPublicKeyRaw?.length) || Boolean(desiredFingerprint?.length); - - // If the ssh-agent module isn't available, skip agent key selection entirely - // so the service falls back to disk keys. But if the caller explicitly requested - // a specific key via env overrides, report an error — silently signing with a - // different disk key would produce signatures that fail verification. - if (!(await isSshAgentModuleAvailable())) { - if (hasOverride) { - return { - selection: null, - error: - "SSH agent signing module is unavailable (possible dependency resolution issue). " + - "Cannot honor MUX_SIGNING_PUBLIC_KEY/MUX_SIGNING_KEY_FINGERPRINT — try updating mux.", - }; - } - return { selection: null, error: null }; - } - - const sshAuthSock = process.env.SSH_AUTH_SOCK?.trim(); - if (!sshAuthSock) { - if (hasOverride) { - return { - selection: null, - error: - "MUX_SIGNING_PUBLIC_KEY/MUX_SIGNING_KEY_FINGERPRINT is set but SSH_AUTH_SOCK is not set", - }; - } - return { selection: null, error: null }; - } - - let candidates: AgentKeyCandidate[]; - try { - candidates = await this.listSshAgentKeyCandidates(sshAuthSock); - } catch (err) { - const message = getErrorMessage(err); - log.info("[SigningService] Failed to query SSH agent:", message); - if (hasOverride) { - return { - selection: null, - error: `Failed to query SSH agent (SSH_AUTH_SOCK=${sshAuthSock})`, - }; - } - return { selection: null, error: null }; - } - - if (candidates.length === 0) { - if (hasOverride) { - return { selection: null, error: `No supported signing keys found in SSH agent` }; - } - return { selection: null, error: null }; - } - - // Apply optional selection constraints for agent-backed keys. - if (hasOverride) { - let filtered = candidates; - - if (desiredPublicKeyRaw) { - const normalized = normalizeOpenSshPublicKey(desiredPublicKeyRaw); - if (!normalized) { - return { - selection: null, - error: - "MUX_SIGNING_PUBLIC_KEY must be an OpenSSH public key string (e.g. 'ssh-ed25519 AAAA...')", - }; - } - - try { - parsePublicKey(normalized); - } catch { - return { - selection: null, - error: "MUX_SIGNING_PUBLIC_KEY is not a supported SSH public key", - }; - } - - filtered = filtered.filter((c) => c.publicKeyOpenSSH === normalized); - } - - if (desiredFingerprint) { - filtered = filtered.filter((c) => c.fingerprint === desiredFingerprint); - } - - if (filtered.length === 0) { - return { - selection: null, - error: - "No SSH agent key matched MUX_SIGNING_PUBLIC_KEY/MUX_SIGNING_KEY_FINGERPRINT (check your SSH agent)", - }; - } - - const chosen = filtered[0]; - return { - selection: { - kind: "ssh-agent", - sshAuthSock, - publicKeyOpenSSH: chosen.publicKeyOpenSSH, - fingerprint: chosen.fingerprint, - }, - error: null, - }; - } - - // Otherwise, pick a reasonable default. - const chosen = this.pickPreferredAgentKey(candidates); - if (!chosen) { - return { selection: null, error: null }; - } - - return { - selection: { - kind: "ssh-agent", - sshAuthSock, - publicKeyOpenSSH: chosen.publicKeyOpenSSH, - fingerprint: chosen.fingerprint, - }, - error: null, - }; - } - - private async loadSigningKey(): Promise { - if (this.keyLoadAttempted) return this.signingKey; - if (this.keyLoadPromise) return this.keyLoadPromise; - - this.keyLoadPromise = (async () => { - try { - const loaded = await this.doLoadSigningKey(); - this.signingKey = loaded; - return loaded; - } catch (err) { - const message = getErrorMessage(err); - log.warn("[SigningService] Unexpected key load error:", message); - this.signingKey = null; - this.keyLoadError = "Failed to load signing key"; - return null; - } finally { - this.keyLoadAttempted = true; - this.keyLoadPromise = null; - } - })(); - - return this.keyLoadPromise; - } - - private async doLoadSigningKey(): Promise { - this.keyLoadError = null; - this.hasEncryptedKey = false; - - // 1) Explicit disk key always wins. - const explicitPath = this.keyPaths[0]; - if (explicitPath) { - const explicitDisk = this.tryLoadDiskKeyPair([explicitPath]); - if (explicitDisk) { - return { kind: "disk", keyPair: explicitDisk }; - } - } - - // 2) Prefer SSH-agent keys over default ~/.ssh/id_* keys. - const agentResult = await this.trySelectSshAgentKey(); - if (agentResult.error) { - this.keyLoadError = agentResult.error; - return null; - } - if (agentResult.selection) { - return agentResult.selection; - } - - // 3) Fall back to disk defaults. - const fallbackDisk = this.tryLoadDiskKeyPair(this.keyPaths.slice(1)); - if (fallbackDisk) { - return { kind: "disk", keyPair: fallbackDisk }; - } - - // Set appropriate error based on what we found. - if (this.hasEncryptedKey) { - this.keyLoadError = - "Signing key requires a passphrase. Encrypted key files are skipped unless loaded in your SSH agent (SSH_AUTH_SOCK)."; - } else { - this.keyLoadError = - "No signing key found. Create ~/.mux/message_signing_key or ensure your SSH agent is running (SSH_AUTH_SOCK)."; - } - log.info("[SigningService]", this.keyLoadError); - - return null; - } - - /** - * Extract raw private key bytes from sshpk key for use with mux-md-client. - */ - private extractPrivateKeyBytes(privateKey: sshpk.PrivateKey): Uint8Array { - // sshpk stores keys with a 'part' object lookup by key component name - // For Ed25519: part.k contains the 32-byte seed (private key) - // For ECDSA: part.d contains the private scalar - // The types are incomplete, so we use type assertions - const parts = privateKey.part as unknown as Record; - if (privateKey.type === "ed25519") { - const kPart = parts.k; - if (!kPart) throw new Error("Ed25519 key missing 'k' component"); - return new Uint8Array(kPart.data); - } else if (privateKey.type === "ecdsa") { - const dPart = parts.d; - if (!dPart) throw new Error("ECDSA key missing 'd' component"); - // sshpk may pad with leading zero byte for ASN.1 encoding; strip it - let data = dPart.data; - if (data[0] === 0 && data.length > 32) { - data = data.subarray(1); - } - return new Uint8Array(data); - } - throw new Error(`Unsupported key type: ${privateKey.type}`); - } - - /** - * Detect identity: GitHub username via `gh auth status`. - * Result is cached after first call. - */ - private async detectIdentity(): Promise { - if (this.identityCache) return this.identityCache; - if (this.identityPromise) return this.identityPromise; - - this.identityPromise = this.doDetectIdentity(); - this.identityCache = await this.identityPromise; - this.identityPromise = null; - - return this.identityCache; - } - - private async doDetectIdentity(): Promise { - let githubUser: string | null = null; - let error: string | null = null; - - // Detect GitHub username via CLI - try { - using proc = execAsync("gh auth status 2>&1"); - const { stdout } = await proc.result; - - const accountMatch = /account\s+(\S+)/i.exec(stdout); - if (accountMatch) { - githubUser = accountMatch[1]; - log.info("[SigningService] Detected GitHub user:", githubUser); - } else if (stdout.includes("Logged in")) { - log.warn("[SigningService] gh auth status indicates logged in but couldn't parse username"); - error = "Could not parse GitHub username from gh auth status"; - } else { - error = "Not logged in to GitHub CLI (run: gh auth login)"; - } - } catch (err) { - const message = getErrorMessage(err); - if (message.includes("command not found") || message.includes("ENOENT")) { - log.info("[SigningService] gh CLI not installed"); - error = "GitHub CLI not installed (brew install gh)"; - } else { - log.info("[SigningService] gh auth status failed:", message); - error = "GitHub CLI error"; - } - } - - return { githubUser, error }; - } - - /** - * Get signing capabilities - public key and identity info. - */ - async getCapabilities(): Promise { - const signingKey = await this.loadSigningKey(); - - if (!signingKey) { - return { - publicKey: null, - githubUser: null, - error: this.keyLoadError - ? { message: this.keyLoadError, hasEncryptedKey: this.hasEncryptedKey } - : null, - }; - } - - const identity = await this.detectIdentity(); - - const publicKey = - signingKey.kind === "disk" - ? signingKey.keyPair.publicKeyOpenSSH - : signingKey.publicKeyOpenSSH; - - return { - publicKey, - githubUser: identity.githubUser, - error: identity.error ? { message: identity.error, hasEncryptedKey: false } : null, - }; - } - - /** - * Sign a mux.md share payload. - * - * Returns a mux.md SignatureEnvelope that can be embedded during upload. - * - * @throws Error if no signing key is available. - */ - async signMessage(content: string): Promise { - assert(typeof content === "string", "signMessage: content must be a string"); - - const signingKey = await this.loadSigningKey(); - if (!signingKey) { - throw new Error(this.keyLoadError ?? "No signing key available"); - } - - const identity = await this.detectIdentity(); - const githubUser = identity.githubUser ?? undefined; - - const bytes = new TextEncoder().encode(content); - - if (signingKey.kind === "disk") { - const envelope = await createSignatureEnvelope( - bytes, - signingKey.keyPair.privateKeyBytes, - signingKey.keyPair.publicKeyOpenSSH, - { githubUser } - ); - assert( - normalizeOpenSshPublicKey(envelope.publicKey) === - normalizeOpenSshPublicKey(signingKey.keyPair.publicKeyOpenSSH), - "signMessage: disk signature returned unexpected public key" - ); - return envelope; - } - - // eslint-disable-next-line no-restricted-syntax -- not circular-dep hiding; startup resilience - const sshAgentModule = await import("@coder/mux-md-client/ssh-agent").catch((err: unknown) => { - const message = getErrorMessage(err); - log.error("[SigningService] Failed to load ssh-agent signing module:", message); - throw new Error( - "SSH agent signing is unavailable — the @coder/mux-md-client/ssh-agent module failed to load. " + - "Try updating mux or falling back to disk key signing." - ); - }); - - const envelope = await sshAgentModule.createSshAgentSignatureEnvelope(bytes, { - sshAuthSock: signingKey.sshAuthSock, - publicKey: signingKey.publicKeyOpenSSH, - fingerprint: signingKey.fingerprint, - githubUser, - }); - - assert( - normalizeOpenSshPublicKey(envelope.publicKey) === - normalizeOpenSshPublicKey(signingKey.publicKeyOpenSSH), - "signMessage: ssh-agent signature returned unexpected public key" - ); - - return envelope; - } - - /** - * Clear all cached state including key and identity. - * Allows re-detection after user creates a key or logs in. - */ - clearIdentityCache(): void { - this.signingKey = null; - this.keyLoadAttempted = false; - this.keyLoadPromise = null; - this.keyLoadError = null; - this.hasEncryptedKey = false; - - this.identityCache = null; - this.identityPromise = null; - - log.info("[SigningService] Cleared key and identity cache"); - } -} - -// Singleton instance -let signingService: SigningService | null = null; - -export function getSigningService(): SigningService { - signingService ??= new SigningService(); - return signingService; -} diff --git a/src/node/services/tools/web_fetch.test.ts b/src/node/services/tools/web_fetch.test.ts index c6cc800301..04888a16b9 100644 --- a/src/node/services/tools/web_fetch.test.ts +++ b/src/node/services/tools/web_fetch.test.ts @@ -4,7 +4,6 @@ import * as runtimeHelpers from "@/node/utils/runtime/helpers"; import { createWebFetchTool } from "./web_fetch"; import type { WebFetchToolArgs, WebFetchToolResult } from "@/common/types/tools"; import { TestTempDir, createTestToolConfig } from "./testHelpers"; -import { isMuxMdUrl, parseMuxMdUrl, uploadToMuxMd, deleteFromMuxMd } from "@/common/lib/muxMd"; import type { ToolExecutionOptions } from "ai"; import { WEB_FETCH_TIMEOUT_SECS } from "@/common/constants/toolLimits"; @@ -65,57 +64,6 @@ afterEach(() => { mock.restore(); }); -describe("mux.md URL helpers", () => { - describe("isMuxMdUrl", () => { - it("should detect valid mux.md URLs", () => { - expect(isMuxMdUrl("https://mux.md/abc123#key456")).toBe(true); - expect(isMuxMdUrl("https://mux.md/RQJe3#Fbbhosspt9q9Ig")).toBe(true); - }); - - it("should reject mux.md URLs without hash", () => { - expect(isMuxMdUrl("https://mux.md/abc123")).toBe(false); - }); - - it("should reject mux.md URLs with empty hash", () => { - expect(isMuxMdUrl("https://mux.md/abc123#")).toBe(false); - }); - - it("should reject non-mux.md URLs", () => { - expect(isMuxMdUrl("https://example.com/page#hash")).toBe(false); - expect(isMuxMdUrl("https://other.md/abc#key")).toBe(false); - }); - - it("should handle invalid URLs gracefully", () => { - expect(isMuxMdUrl("not-a-url")).toBe(false); - expect(isMuxMdUrl("")).toBe(false); - }); - }); - - describe("parseMuxMdUrl", () => { - it("should extract id and key from valid mux.md URL", () => { - const result = parseMuxMdUrl("https://mux.md/abc123#key456"); - expect(result).toEqual({ id: "abc123", key: "key456" }); - }); - - it("should handle base64url characters in key", () => { - const result = parseMuxMdUrl("https://mux.md/RQJe3#Fbbhosspt9q9Ig"); - expect(result).toEqual({ id: "RQJe3", key: "Fbbhosspt9q9Ig" }); - }); - - it("should return null for URLs without hash", () => { - expect(parseMuxMdUrl("https://mux.md/abc123")).toBeNull(); - }); - - it("should return null for URLs with empty id", () => { - expect(parseMuxMdUrl("https://mux.md/#key")).toBeNull(); - }); - - it("should return null for invalid URLs", () => { - expect(parseMuxMdUrl("not-a-url")).toBeNull(); - }); - }); -}); - describe("web_fetch tool", () => { itIntegration("should fetch and convert a real web page to markdown", async () => { using testEnv = createTestWebFetchTool(); @@ -480,31 +428,6 @@ describe("web_fetch tool", () => { } }); - it("does not treat non-mux.md URLs with fragments as mux.md shares", async () => { - using testEnv = createTestWebFetchTool(); - - spyOn(runtimeHelpers, "execBuffered").mockResolvedValue({ - stdout: - "HTTP/1.1 200 OK\r\n" + - "Content-Type: text/html; charset=utf-8\r\n\r\n" + - "Fragment Page

Hello

This is a fragment test.

", - stderr: "", - exitCode: 0, - duration: 1, - }); - - const result = (await testEnv.tool.execute!( - { url: "https://93.184.216.34/page#section1" }, - toolCallOptions - )) as WebFetchToolResult; - - expect(result.success).toBe(true); - if (result.success) { - expect(result.title).toBe("Fragment Page"); - expect(result.content).toContain("This is a fragment test."); - } - }); - itIntegration("should include HTTP status code in error for non-2xx responses", async () => { using testEnv = createTestWebFetchTool(); const args: WebFetchToolArgs = { @@ -533,58 +456,4 @@ describe("web_fetch tool", () => { expect(result.error).toContain("JavaScript"); } }); - - itIntegration("should handle expired/missing mux.md share links", async () => { - using testEnv = createTestWebFetchTool(); - const args: WebFetchToolArgs = { - url: "https://mux.md/nonexistent123#somekey456", - }; - - const result = (await testEnv.tool.execute!(args, toolCallOptions)) as WebFetchToolResult; - - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error).toContain("expired or not found"); - } - }); - - it("should return error for mux.md URLs without valid key format", async () => { - using testEnv = createTestWebFetchTool(); - const args: WebFetchToolArgs = { - url: "https://mux.md/someid", - }; - - const result = (await testEnv.tool.execute!(args, toolCallOptions)) as WebFetchToolResult; - - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error).toContain("Invalid mux.md URL format"); - } - }); - - itIntegration("should decrypt and return mux.md content correctly", async () => { - using testEnv = createTestWebFetchTool(); - - const testContent = "# Test Heading\n\nThis is **test content** for web_fetch decryption."; - const uploadResult = await uploadToMuxMd( - testContent, - { name: "test.md", type: "text/markdown", size: testContent.length }, - { expiresAt: new Date(Date.now() + 60000) } - ); - - try { - const args: WebFetchToolArgs = { url: uploadResult.url }; - const result = (await testEnv.tool.execute!(args, toolCallOptions)) as WebFetchToolResult; - - expect(result.success).toBe(true); - if (result.success) { - expect(result.content).toBe(testContent); - expect(result.title).toBe("test.md"); - expect(result.url).toBe(uploadResult.url); - expect(result.length).toBe(testContent.length); - } - } finally { - await deleteFromMuxMd(uploadResult.id, uploadResult.mutateKey); - } - }); }); diff --git a/src/node/services/tools/web_fetch.ts b/src/node/services/tools/web_fetch.ts index 3e76a05491..ef49019314 100644 --- a/src/node/services/tools/web_fetch.ts +++ b/src/node/services/tools/web_fetch.ts @@ -14,12 +14,6 @@ import { } from "@/common/constants/toolLimits"; import { EXIT_CODE_TIMEOUT } from "@/common/constants/exitCodes"; import * as runtimeHelpers from "@/node/utils/runtime/helpers"; -import { - downloadFromMuxMd, - getMuxMdAllowedHosts, - isMuxMdUrl, - parseMuxMdUrl, -} from "@/common/lib/muxMd"; import { getErrorMessage } from "@/common/utils/errors"; const USER_AGENT = "Mux/1.0 (https://github.com/coder/mux; web-fetch tool)"; @@ -605,14 +599,6 @@ function tryExtractContent( } } -function isAllowedMuxMdHost(url: string): boolean { - try { - return getMuxMdAllowedHosts().includes(new URL(url).host); - } catch { - return false; - } -} - /** * Web fetch tool factory for AI assistant * Creates a tool that fetches web pages and extracts readable content as markdown @@ -625,44 +611,6 @@ export const createWebFetchTool: ToolFactory = (config: ToolConfiguration) => { inputSchema: TOOL_DEFINITIONS.web_fetch.schema, execute: async ({ url }, { abortSignal }): Promise => { try { - // Handle mux.md share links with client-side decryption. - // Important: `parseMuxMdUrl` does not validate the host, so we must guard with `isMuxMdUrl` - // to avoid treating arbitrary URLs (including those with `#fragment`) as share links. - if (isMuxMdUrl(url)) { - const muxMdParsed = parseMuxMdUrl(url); - if (!muxMdParsed) { - return { success: false, error: "Invalid mux.md URL format" }; - } - - const baseUrl = new URL(url).origin; - - try { - const result = await downloadFromMuxMd(muxMdParsed.id, muxMdParsed.key, abortSignal, { - baseUrl, - }); - let content = result.content; - if (content.length > WEB_FETCH_MAX_OUTPUT_BYTES) { - content = content.slice(0, WEB_FETCH_MAX_OUTPUT_BYTES) + "\n\n[Content truncated]"; - } - return { - success: true, - title: result.fileInfo?.name ?? "Shared Message", - content, - url, - length: content.length, - }; - } catch (err) { - return { - success: false, - error: err instanceof Error ? err.message : "Failed to download from mux.md", - }; - } - } - - if (isAllowedMuxMdHost(url)) { - return { success: false, error: "Invalid mux.md URL format" }; - } - const { result, finalUrl } = await executeWebFetchRequest(config, url, abortSignal); if (result.exitCode !== 0) { diff --git a/tests/ui/chat/shareMessage.test.ts b/tests/ui/chat/shareMessage.test.ts deleted file mode 100644 index daaecfa5a5..0000000000 --- a/tests/ui/chat/shareMessage.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -/** - * Integration tests for the mux.md message sharing feature. - * - * Tests cover: - * - End-to-end encrypted upload to mux.md - * - URL format validation - * - Content can be retrieved and decrypted - * - Delete functionality - */ - -import "../dom"; -import { shouldRunIntegrationTests } from "../../testUtils"; -import { uploadToMuxMd, deleteFromMuxMd, getMuxMdBaseUrl } from "../../../src/common/lib/muxMd"; - -if (process.env.CI && typeof jest !== "undefined" && jest.retryTimes) { - jest.retryTimes(2, { logErrorsBeforeRetry: true }); -} - -const describeIntegration = shouldRunIntegrationTests() ? describe : describe.skip; - -// ═══════════════════════════════════════════════════════════════════════════════ -// MUX.MD UPLOAD TESTS -// ═══════════════════════════════════════════════════════════════════════════════ - -describeIntegration("mux.md sharing (upload integration)", () => { - test("should upload encrypted content and return valid URL", async () => { - const testContent = - "# Test Message\n\nThis is a test message for sharing.\n\n```typescript\nconst x = 42;\n```"; - - const result = await uploadToMuxMd(testContent, { - name: "message.md", - type: "text/markdown", - size: new TextEncoder().encode(testContent).length, - model: "test-model", - thinking: "medium", - }); - - // Verify the URL format: https://mux.md/{id}#{key} - expect(result.url).toMatch(/^https:\/\/mux\.md\/[A-Za-z0-9]+#[A-Za-z0-9_-]+$/); - expect(result.id).toMatch(/^[A-Za-z0-9]+$/); - expect(result.key).toMatch(/^[A-Za-z0-9_-]+$/); - expect(result.mutateKey).toBeTruthy(); - - // Verify URL contains the id and key - expect(result.url).toContain(result.id); - expect(result.url).toContain(result.key); - }, 30_000); - - test("should generate unique URLs for each upload", async () => { - const content = "Test content for uniqueness check"; - - // Run both uploads concurrently to reduce wall-clock time and avoid flakiness - // from sequential network latency to the external mux.md service. - const [result1, result2] = await Promise.all([ - uploadToMuxMd(content, { - name: "message.md", - type: "text/markdown", - size: content.length, - }), - uploadToMuxMd(content, { - name: "message.md", - type: "text/markdown", - size: content.length, - }), - ]); - - // Each upload should generate unique id, key, and mutateKey - expect(result1.id).not.toBe(result2.id); - expect(result1.key).not.toBe(result2.key); - expect(result1.mutateKey).not.toBe(result2.mutateKey); - }, 30_000); - - test("should handle special characters in content", async () => { - const contentWithSpecialChars = `# Special Characters Test - -Unicode: 你好世界 🎉 émojis -Code: \`const x = { a: 1, b: "test" };\` -Markdown: **bold** _italic_ [link](https://example.com) -HTML entities: & < > -`; - - const result = await uploadToMuxMd(contentWithSpecialChars, { - name: "special.md", - type: "text/markdown", - size: new TextEncoder().encode(contentWithSpecialChars).length, - }); - - expect(result.url).toMatch(/^https:\/\/mux\.md\/[A-Za-z0-9]+#[A-Za-z0-9_-]+$/); - }, 30_000); - - test("should include model metadata in upload", async () => { - const content = "Test with model metadata"; - - const result = await uploadToMuxMd(content, { - name: "message.md", - type: "text/markdown", - size: content.length, - model: "claude-sonnet-4-20250514", - thinking: "high", - }); - - // Upload should succeed - metadata is encrypted client-side - expect(result.url).toBeTruthy(); - expect(result.id).toBeTruthy(); - }, 30_000); - - test("should successfully delete uploaded content using mutateKey", async () => { - const content = "Content to be deleted"; - - // Upload first - const result = await uploadToMuxMd(content, { - name: "delete-test.md", - type: "text/markdown", - size: content.length, - }); - - expect(result.id).toBeTruthy(); - expect(result.mutateKey).toBeTruthy(); - - // Delete should complete without throwing - await expect(deleteFromMuxMd(result.id, result.mutateKey)).resolves.not.toThrow(); - - // Verify the content is no longer accessible (fetch returns 404). - // Use getMuxMdBaseUrl() to respect the MUX_MD_URL_OVERRIDE env var, - // and consume the response body to avoid leaking the TCP connection - // (which causes "worker process has failed to exit gracefully" warnings). - const response = await fetch(`${getMuxMdBaseUrl()}/${result.id}`); - await response.body?.cancel(); - expect(response.status).toBe(404); - // Higher timeout: this test makes 3 sequential HTTP calls to an external - // service (upload → delete → verification fetch). 30s is tight when the - // service is slow. - }, 60_000); -}); diff --git a/vite.config.ts b/vite.config.ts index 3728cbde10..cf98e904e8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -127,7 +127,6 @@ export default defineConfig(({ mode }) => { alias: aliasMap, }, define: { - "globalThis.__MUX_MD_URL_OVERRIDE__": JSON.stringify(process.env.MUX_MD_URL_OVERRIDE ?? ""), "globalThis.__MUX_ENABLE_TUTORIALS_IN_SANDBOX__": enableTutorialsInSandboxDefine, ...(isProfiling ? { __PROFILE__: "true" } : {}), }, From 87e9e39bc8ff56d4f7240381490b1f8df61e39c0 Mon Sep 17 00:00:00 2001 From: Ammar Date: Mon, 15 Jun 2026 17:24:15 -0500 Subject: [PATCH 2/2] ci: update flake offline-cache hash after dropping @coder/mux-md-client --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index bf576d1d70..7245e7a718 100644 --- a/flake.nix +++ b/flake.nix @@ -83,7 +83,7 @@ outputHashMode = "recursive"; # Marker used by scripts/update_flake_hash.sh to update this hash in place. - outputHash = "sha256-BlvooN6emK0yOaX6flxb7FtQm684NpF1P4e+TAaRQBs="; # mux-offline-cache-hash + outputHash = "sha256-aegNSZ3QGLP3GPdaRB4Baf22Rhxs4EqUQJyMBuryjTo="; # mux-offline-cache-hash }; configurePhase = ''