@thevinci/web 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +197 -0
- package/bin/cli-entry.js +55 -0
- package/bin/cli-output.js +145 -0
- package/bin/cli.js +4887 -0
- package/bin/cli.test.js +64 -0
- package/dist/apple-touch-icon-120x120.png +0 -0
- package/dist/apple-touch-icon-152x152.png +0 -0
- package/dist/apple-touch-icon-167x167.png +0 -0
- package/dist/apple-touch-icon-180x180.png +0 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/apple-touch-icon.svg +528 -0
- package/dist/assets/JsonTreeView-CSm9OzXG.js +1 -0
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/MarkdownRendererImpl-DensKOLc.js +6 -0
- package/dist/assets/MultiRunWindow-Bo7THayo.js +1 -0
- package/dist/assets/OnboardingScreen-BDqmzTVR.js +2 -0
- package/dist/assets/SettingsWindow-coz__Ykw.js +1 -0
- package/dist/assets/TerminalView-DrZ-i3Dr.js +1 -0
- package/dist/assets/ToolOutputDialog-Eglzslt3.js +16 -0
- package/dist/assets/es-4o9ciP61.js +15 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/dist/assets/index-DLTDToSP.css +1 -0
- package/dist/assets/index-DgiFEKGN.js +1 -0
- package/dist/assets/ko-B20imCHE.js +15 -0
- package/dist/assets/main-BV3KOtdA.css +1 -0
- package/dist/assets/main-CDKJj0sH.js +226 -0
- package/dist/assets/main-LC-PSNVM.js +2 -0
- package/dist/assets/miniChat-CQUiG_cr.js +2 -0
- package/dist/assets/modelPrefsAutoSave-Dm799vzR.js +6986 -0
- package/dist/assets/pl-DQJ7LSzj.js +15 -0
- package/dist/assets/pt-BR-OmjHUz9y.js +15 -0
- package/dist/assets/renderElectronMiniChatApp-CARbeW0G.js +2 -0
- package/dist/assets/uk-BNFxOlO4.js +15 -0
- package/dist/assets/vendor--DBfsbEis.css +1 -0
- package/dist/assets/vendor-.bun-B9l0ZNi2.js +4094 -0
- package/dist/assets/wasm-CG6Dc4jp.js +1 -0
- package/dist/assets/wasmSttWorker-Dtlxac_K.js +1 -0
- package/dist/assets/wasmSttWorker-oo7Dm_jy.js +1806 -0
- package/dist/assets/worker-CbT6TVo7.js +155 -0
- package/dist/assets/zh-CN-C6T-Ac7F.js +15 -0
- package/dist/favicon-16.png +0 -0
- package/dist/favicon-32.png +0 -0
- package/dist/favicon.png +0 -0
- package/dist/favicon.svg +528 -0
- package/dist/index.html +607 -0
- package/dist/logo-dark-192x192.png +0 -0
- package/dist/logo-dark-512x512.png +0 -0
- package/dist/logo-dark-512x512.svg +528 -0
- package/dist/logo-light-192x192.png +0 -0
- package/dist/logo-light-512x512.png +0 -0
- package/dist/logo-light-512x512.svg +528 -0
- package/dist/mini-chat.html +16 -0
- package/dist/pwa-192.png +0 -0
- package/dist/pwa-512.png +0 -0
- package/dist/pwa-maskable-192.png +0 -0
- package/dist/pwa-maskable-512.png +0 -0
- package/dist/site.webmanifest +21 -0
- package/dist/sw.js +1 -0
- package/package.json +118 -0
- package/public/apple-touch-icon-120x120.png +0 -0
- package/public/apple-touch-icon-152x152.png +0 -0
- package/public/apple-touch-icon-167x167.png +0 -0
- package/public/apple-touch-icon-180x180.png +0 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/apple-touch-icon.svg +528 -0
- package/public/favicon-16.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/favicon.png +0 -0
- package/public/favicon.svg +528 -0
- package/public/logo-dark-192x192.png +0 -0
- package/public/logo-dark-512x512.png +0 -0
- package/public/logo-dark-512x512.svg +528 -0
- package/public/logo-light-192x192.png +0 -0
- package/public/logo-light-512x512.png +0 -0
- package/public/logo-light-512x512.svg +528 -0
- package/public/pwa-192.png +0 -0
- package/public/pwa-512.png +0 -0
- package/public/pwa-maskable-192.png +0 -0
- package/public/pwa-maskable-512.png +0 -0
- package/public/site.webmanifest +21 -0
- package/server/TERMINAL_WS_PROTOCOL.md +48 -0
- package/server/index.d.ts +39 -0
- package/server/index.js +1311 -0
- package/server/lib/cloudflare-tunnel.js +650 -0
- package/server/lib/event-stream/DOCUMENTATION.md +61 -0
- package/server/lib/event-stream/directory-ws-bridge.js +185 -0
- package/server/lib/event-stream/global-hub.js +158 -0
- package/server/lib/event-stream/global-hub.test.js +140 -0
- package/server/lib/event-stream/global-ws-bridge.js +206 -0
- package/server/lib/event-stream/index.js +25 -0
- package/server/lib/event-stream/protocol.js +131 -0
- package/server/lib/event-stream/protocol.test.js +182 -0
- package/server/lib/event-stream/runtime.js +180 -0
- package/server/lib/event-stream/runtime.test.js +512 -0
- package/server/lib/event-stream/upstream-reader.js +226 -0
- package/server/lib/event-stream/upstream-reader.test.js +276 -0
- package/server/lib/fs/DOCUMENTATION.md +36 -0
- package/server/lib/fs/routes.js +1040 -0
- package/server/lib/fs/search.js +238 -0
- package/server/lib/git/DOCUMENTATION.md +152 -0
- package/server/lib/git/credentials.js +74 -0
- package/server/lib/git/identity-storage.js +112 -0
- package/server/lib/git/index.js +6 -0
- package/server/lib/git/routes.js +972 -0
- package/server/lib/git/service.js +3432 -0
- package/server/lib/git/service.test.js +39 -0
- package/server/lib/github/DOCUMENTATION.md +171 -0
- package/server/lib/github/auth.js +307 -0
- package/server/lib/github/device-flow.js +50 -0
- package/server/lib/github/index.js +24 -0
- package/server/lib/github/octokit.js +10 -0
- package/server/lib/github/pr-status.js +519 -0
- package/server/lib/github/repo/fork-detection.js +102 -0
- package/server/lib/github/repo/index.js +55 -0
- package/server/lib/github/routes.js +1560 -0
- package/server/lib/magic-prompts/routes.js +63 -0
- package/server/lib/magic-prompts/runtime.js +119 -0
- package/server/lib/notifications/DOCUMENTATION.md +122 -0
- package/server/lib/notifications/emitter-runtime.js +102 -0
- package/server/lib/notifications/index.js +4 -0
- package/server/lib/notifications/message.js +52 -0
- package/server/lib/notifications/message.test.js +34 -0
- package/server/lib/notifications/push-runtime.js +304 -0
- package/server/lib/notifications/routes.js +315 -0
- package/server/lib/notifications/runtime.js +566 -0
- package/server/lib/notifications/template-runtime.js +349 -0
- package/server/lib/notifications/template-runtime.test.js +26 -0
- package/server/lib/opencode/DOCUMENTATION.md +362 -0
- package/server/lib/opencode/agents.js +634 -0
- package/server/lib/opencode/auth-state-runtime.js +88 -0
- package/server/lib/opencode/auth.js +83 -0
- package/server/lib/opencode/bootstrap-runtime.js +131 -0
- package/server/lib/opencode/cli-entry-runtime.js +43 -0
- package/server/lib/opencode/cli-options.js +128 -0
- package/server/lib/opencode/commands.js +339 -0
- package/server/lib/opencode/config-entity-routes.js +370 -0
- package/server/lib/opencode/core-routes.js +500 -0
- package/server/lib/opencode/core-routes.test.js +26 -0
- package/server/lib/opencode/env-config.js +74 -0
- package/server/lib/opencode/env-keys.js +68 -0
- package/server/lib/opencode/env-runtime.js +1162 -0
- package/server/lib/opencode/env-runtime.test.js +116 -0
- package/server/lib/opencode/feature-routes-runtime.js +244 -0
- package/server/lib/opencode/hmr-state-runtime.js +85 -0
- package/server/lib/opencode/index.js +66 -0
- package/server/lib/opencode/lifecycle.js +1019 -0
- package/server/lib/opencode/lifecycle.test.js +240 -0
- package/server/lib/opencode/mcp.js +278 -0
- package/server/lib/opencode/network-runtime.js +104 -0
- package/server/lib/opencode/network-runtime.test.js +37 -0
- package/server/lib/opencode/opencode-resolution-runtime.js +71 -0
- package/server/lib/opencode/path-utils.js +100 -0
- package/server/lib/opencode/path-utils.test.js +71 -0
- package/server/lib/opencode/project-directory-runtime.js +124 -0
- package/server/lib/opencode/project-icon-routes.js +399 -0
- package/server/lib/opencode/project-icon-routes.test.js +107 -0
- package/server/lib/opencode/providers.js +96 -0
- package/server/lib/opencode/proxy.js +445 -0
- package/server/lib/opencode/pwa-manifest-routes.js +257 -0
- package/server/lib/opencode/pwa-manifest-routes.test.js +133 -0
- package/server/lib/opencode/routes.js +541 -0
- package/server/lib/opencode/server-startup-runtime.js +156 -0
- package/server/lib/opencode/server-utils-runtime.js +168 -0
- package/server/lib/opencode/server-utils-runtime.test.js +135 -0
- package/server/lib/opencode/session-runtime.js +356 -0
- package/server/lib/opencode/session-runtime.test.js +151 -0
- package/server/lib/opencode/settings-helpers.js +770 -0
- package/server/lib/opencode/settings-helpers.test.js +109 -0
- package/server/lib/opencode/settings-normalization-runtime.js +428 -0
- package/server/lib/opencode/settings-runtime.js +826 -0
- package/server/lib/opencode/settings-runtime.test.js +85 -0
- package/server/lib/opencode/shared.js +615 -0
- package/server/lib/opencode/shutdown-runtime.js +139 -0
- package/server/lib/opencode/shutdown-runtime.test.js +58 -0
- package/server/lib/opencode/skill-routes.js +701 -0
- package/server/lib/opencode/skills.js +548 -0
- package/server/lib/opencode/startup-pipeline-runtime.js +130 -0
- package/server/lib/opencode/static-routes-runtime.js +65 -0
- package/server/lib/opencode/theme-runtime.js +167 -0
- package/server/lib/opencode/tunnel-auth.js +591 -0
- package/server/lib/opencode/tunnel-wiring-runtime.js +94 -0
- package/server/lib/opencode/vinci-routes.js +76 -0
- package/server/lib/opencode/watcher.js +115 -0
- package/server/lib/opencode/watcher.test.js +239 -0
- package/server/lib/preview/proxy-runtime.js +1333 -0
- package/server/lib/preview/proxy-runtime.test.js +144 -0
- package/server/lib/projects/project-config.js +567 -0
- package/server/lib/projects/project-config.test.js +175 -0
- package/server/lib/projects/project-id.js +13 -0
- package/server/lib/quota/DOCUMENTATION.md +58 -0
- package/server/lib/quota/index.js +25 -0
- package/server/lib/quota/providers/claude.js +107 -0
- package/server/lib/quota/providers/codex.js +113 -0
- package/server/lib/quota/providers/copilot.js +165 -0
- package/server/lib/quota/providers/google/api.js +92 -0
- package/server/lib/quota/providers/google/auth.js +108 -0
- package/server/lib/quota/providers/google/index.js +124 -0
- package/server/lib/quota/providers/google/transforms.js +109 -0
- package/server/lib/quota/providers/index.js +168 -0
- package/server/lib/quota/providers/interface.js +55 -0
- package/server/lib/quota/providers/kimi.js +108 -0
- package/server/lib/quota/providers/minimax-cn-coding-plan.js +140 -0
- package/server/lib/quota/providers/minimax-coding-plan.js +139 -0
- package/server/lib/quota/providers/nanogpt.js +124 -0
- package/server/lib/quota/providers/ollama-cloud.js +112 -0
- package/server/lib/quota/providers/openai.js +91 -0
- package/server/lib/quota/providers/openrouter.js +92 -0
- package/server/lib/quota/providers/zai.js +91 -0
- package/server/lib/quota/providers/zhipuai-coding-plan.js +133 -0
- package/server/lib/quota/providers/zhipuai.js +114 -0
- package/server/lib/quota/routes.js +27 -0
- package/server/lib/quota/utils/auth.js +50 -0
- package/server/lib/quota/utils/formatters.js +85 -0
- package/server/lib/quota/utils/formatters.test.js +54 -0
- package/server/lib/quota/utils/index.js +10 -0
- package/server/lib/quota/utils/transformers.js +55 -0
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +44 -0
- package/server/lib/scheduled-tasks/routes.js +235 -0
- package/server/lib/scheduled-tasks/runtime.js +773 -0
- package/server/lib/scheduled-tasks/runtime.test.js +100 -0
- package/server/lib/security/request-security.js +115 -0
- package/server/lib/session-folders/routes.js +63 -0
- package/server/lib/session-folders/routes.test.js +102 -0
- package/server/lib/skills-catalog/DOCUMENTATION.md +178 -0
- package/server/lib/skills-catalog/cache.js +29 -0
- package/server/lib/skills-catalog/clawdhub/api.js +158 -0
- package/server/lib/skills-catalog/clawdhub/index.js +30 -0
- package/server/lib/skills-catalog/clawdhub/install.js +238 -0
- package/server/lib/skills-catalog/clawdhub/scan.js +113 -0
- package/server/lib/skills-catalog/curated-sources.js +21 -0
- package/server/lib/skills-catalog/git.js +77 -0
- package/server/lib/skills-catalog/index.js +42 -0
- package/server/lib/skills-catalog/install.js +294 -0
- package/server/lib/skills-catalog/scan.js +221 -0
- package/server/lib/skills-catalog/source.js +87 -0
- package/server/lib/terminal/DOCUMENTATION.md +76 -0
- package/server/lib/terminal/index.js +31 -0
- package/server/lib/terminal/output-replay-buffer.js +78 -0
- package/server/lib/terminal/output-replay-buffer.test.js +75 -0
- package/server/lib/terminal/runtime.js +850 -0
- package/server/lib/terminal/runtime.test.js +96 -0
- package/server/lib/terminal/terminal-ws-protocol.js +68 -0
- package/server/lib/terminal/terminal-ws-protocol.test.js +145 -0
- package/server/lib/text/DOCUMENTATION.md +35 -0
- package/server/lib/text/summarization.js +138 -0
- package/server/lib/text/summarization.test.js +34 -0
- package/server/lib/tts/DOCUMENTATION.md +146 -0
- package/server/lib/tts/base-url.js +62 -0
- package/server/lib/tts/capability-runtime.js +31 -0
- package/server/lib/tts/index.js +19 -0
- package/server/lib/tts/routes.js +261 -0
- package/server/lib/tts/routes.test.js +53 -0
- package/server/lib/tts/service.js +178 -0
- package/server/lib/tts/stt.js +75 -0
- package/server/lib/tunnels/DOCUMENTATION.md +18 -0
- package/server/lib/tunnels/index.js +166 -0
- package/server/lib/tunnels/managed-config.js +201 -0
- package/server/lib/tunnels/providers/cloudflare.js +260 -0
- package/server/lib/tunnels/registry.js +51 -0
- package/server/lib/tunnels/routes.js +605 -0
- package/server/lib/tunnels/types.js +219 -0
- package/server/lib/ui-auth/DOCUMENTATION.md +38 -0
- package/server/lib/ui-auth/ui-auth.js +673 -0
- package/server/lib/ui-auth/ui-passkeys.js +545 -0
- package/server/opencode-proxy.test.js +151 -0
- package/server/proxy-headers.js +61 -0
- package/server/proxy-headers.test.js +58 -0
- package/server/sse-routes.test.js +152 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
const PUSH_SUBSCRIPTIONS_VERSION = 1;
|
|
2
|
+
|
|
3
|
+
const isLoopbackHttpOrigin = (value) => {
|
|
4
|
+
if (typeof value !== 'string') {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
return value.startsWith('http://localhost')
|
|
9
|
+
|| value.startsWith('http://127.0.0.1')
|
|
10
|
+
|| value.startsWith('http://[::1]');
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const createPushRuntime = (deps) => {
|
|
14
|
+
const {
|
|
15
|
+
fsPromises,
|
|
16
|
+
path,
|
|
17
|
+
webPush,
|
|
18
|
+
PUSH_SUBSCRIPTIONS_FILE_PATH,
|
|
19
|
+
readSettingsFromDiskMigrated,
|
|
20
|
+
writeSettingsToDisk,
|
|
21
|
+
} = deps;
|
|
22
|
+
|
|
23
|
+
let persistPushSubscriptionsLock = Promise.resolve();
|
|
24
|
+
let pushInitialized = false;
|
|
25
|
+
|
|
26
|
+
const uiVisibilityByToken = new Map();
|
|
27
|
+
let globalVisibilityState = false;
|
|
28
|
+
|
|
29
|
+
const readPushSubscriptionsFromDisk = async () => {
|
|
30
|
+
try {
|
|
31
|
+
const raw = await fsPromises.readFile(PUSH_SUBSCRIPTIONS_FILE_PATH, 'utf8');
|
|
32
|
+
const parsed = JSON.parse(raw);
|
|
33
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
34
|
+
return { version: PUSH_SUBSCRIPTIONS_VERSION, subscriptionsBySession: {} };
|
|
35
|
+
}
|
|
36
|
+
if (typeof parsed.version !== 'number' || parsed.version !== PUSH_SUBSCRIPTIONS_VERSION) {
|
|
37
|
+
return { version: PUSH_SUBSCRIPTIONS_VERSION, subscriptionsBySession: {} };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const subscriptionsBySession =
|
|
41
|
+
parsed.subscriptionsBySession && typeof parsed.subscriptionsBySession === 'object'
|
|
42
|
+
? parsed.subscriptionsBySession
|
|
43
|
+
: {};
|
|
44
|
+
|
|
45
|
+
return { version: PUSH_SUBSCRIPTIONS_VERSION, subscriptionsBySession };
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error && typeof error === 'object' && error.code === 'ENOENT') {
|
|
48
|
+
return { version: PUSH_SUBSCRIPTIONS_VERSION, subscriptionsBySession: {} };
|
|
49
|
+
}
|
|
50
|
+
console.warn('Failed to read push subscriptions file:', error);
|
|
51
|
+
return { version: PUSH_SUBSCRIPTIONS_VERSION, subscriptionsBySession: {} };
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const writePushSubscriptionsToDisk = async (data) => {
|
|
56
|
+
await fsPromises.mkdir(path.dirname(PUSH_SUBSCRIPTIONS_FILE_PATH), { recursive: true });
|
|
57
|
+
await fsPromises.writeFile(PUSH_SUBSCRIPTIONS_FILE_PATH, JSON.stringify(data, null, 2), 'utf8');
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const persistPushSubscriptionUpdate = async (mutate) => {
|
|
61
|
+
persistPushSubscriptionsLock = persistPushSubscriptionsLock.then(async () => {
|
|
62
|
+
await fsPromises.mkdir(path.dirname(PUSH_SUBSCRIPTIONS_FILE_PATH), { recursive: true });
|
|
63
|
+
const current = await readPushSubscriptionsFromDisk();
|
|
64
|
+
const next = mutate({
|
|
65
|
+
version: PUSH_SUBSCRIPTIONS_VERSION,
|
|
66
|
+
subscriptionsBySession: current.subscriptionsBySession || {},
|
|
67
|
+
});
|
|
68
|
+
await writePushSubscriptionsToDisk(next);
|
|
69
|
+
return next;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return persistPushSubscriptionsLock;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const getOrCreateVapidKeys = async () => {
|
|
76
|
+
const settings = await readSettingsFromDiskMigrated();
|
|
77
|
+
const existing = settings?.vapidKeys;
|
|
78
|
+
if (existing && typeof existing.publicKey === 'string' && typeof existing.privateKey === 'string') {
|
|
79
|
+
return { publicKey: existing.publicKey, privateKey: existing.privateKey };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const generated = webPush.generateVAPIDKeys();
|
|
83
|
+
const next = {
|
|
84
|
+
...settings,
|
|
85
|
+
vapidKeys: {
|
|
86
|
+
publicKey: generated.publicKey,
|
|
87
|
+
privateKey: generated.privateKey,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
await writeSettingsToDisk(next);
|
|
92
|
+
return { publicKey: generated.publicKey, privateKey: generated.privateKey };
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const normalizePushSubscriptions = (record) => {
|
|
96
|
+
if (!Array.isArray(record)) return [];
|
|
97
|
+
return record
|
|
98
|
+
.map((entry) => {
|
|
99
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
100
|
+
const endpoint = entry.endpoint;
|
|
101
|
+
const p256dh = entry.p256dh;
|
|
102
|
+
const auth = entry.auth;
|
|
103
|
+
if (typeof endpoint !== 'string' || typeof p256dh !== 'string' || typeof auth !== 'string') {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
endpoint,
|
|
108
|
+
p256dh,
|
|
109
|
+
auth,
|
|
110
|
+
createdAt: typeof entry.createdAt === 'number' ? entry.createdAt : null,
|
|
111
|
+
};
|
|
112
|
+
})
|
|
113
|
+
.filter(Boolean);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const addOrUpdatePushSubscription = async (uiSessionToken, subscription, userAgent) => {
|
|
117
|
+
if (!uiSessionToken) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
await ensurePushInitialized();
|
|
122
|
+
|
|
123
|
+
const now = Date.now();
|
|
124
|
+
|
|
125
|
+
await persistPushSubscriptionUpdate((current) => {
|
|
126
|
+
const subsBySession = { ...(current.subscriptionsBySession || {}) };
|
|
127
|
+
const existing = Array.isArray(subsBySession[uiSessionToken]) ? subsBySession[uiSessionToken] : [];
|
|
128
|
+
|
|
129
|
+
const filtered = existing.filter((entry) => entry && typeof entry.endpoint === 'string' && entry.endpoint !== subscription.endpoint);
|
|
130
|
+
|
|
131
|
+
filtered.unshift({
|
|
132
|
+
endpoint: subscription.endpoint,
|
|
133
|
+
p256dh: subscription.p256dh,
|
|
134
|
+
auth: subscription.auth,
|
|
135
|
+
createdAt: now,
|
|
136
|
+
lastSeenAt: now,
|
|
137
|
+
userAgent: typeof userAgent === 'string' && userAgent.length > 0 ? userAgent : undefined,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
subsBySession[uiSessionToken] = filtered.slice(0, 10);
|
|
141
|
+
|
|
142
|
+
return { version: PUSH_SUBSCRIPTIONS_VERSION, subscriptionsBySession: subsBySession };
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const removePushSubscription = async (uiSessionToken, endpoint) => {
|
|
147
|
+
if (!uiSessionToken || !endpoint) return;
|
|
148
|
+
|
|
149
|
+
await ensurePushInitialized();
|
|
150
|
+
|
|
151
|
+
await persistPushSubscriptionUpdate((current) => {
|
|
152
|
+
const subsBySession = { ...(current.subscriptionsBySession || {}) };
|
|
153
|
+
const existing = Array.isArray(subsBySession[uiSessionToken]) ? subsBySession[uiSessionToken] : [];
|
|
154
|
+
const filtered = existing.filter((entry) => entry && typeof entry.endpoint === 'string' && entry.endpoint !== endpoint);
|
|
155
|
+
if (filtered.length === 0) {
|
|
156
|
+
delete subsBySession[uiSessionToken];
|
|
157
|
+
} else {
|
|
158
|
+
subsBySession[uiSessionToken] = filtered;
|
|
159
|
+
}
|
|
160
|
+
return { version: PUSH_SUBSCRIPTIONS_VERSION, subscriptionsBySession: subsBySession };
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const removePushSubscriptionFromAllSessions = async (endpoint) => {
|
|
165
|
+
if (!endpoint) return;
|
|
166
|
+
|
|
167
|
+
await ensurePushInitialized();
|
|
168
|
+
|
|
169
|
+
await persistPushSubscriptionUpdate((current) => {
|
|
170
|
+
const subsBySession = { ...(current.subscriptionsBySession || {}) };
|
|
171
|
+
for (const [token, entries] of Object.entries(subsBySession)) {
|
|
172
|
+
if (!Array.isArray(entries)) continue;
|
|
173
|
+
const filtered = entries.filter((entry) => entry && typeof entry.endpoint === 'string' && entry.endpoint !== endpoint);
|
|
174
|
+
if (filtered.length === 0) {
|
|
175
|
+
delete subsBySession[token];
|
|
176
|
+
} else {
|
|
177
|
+
subsBySession[token] = filtered;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { version: PUSH_SUBSCRIPTIONS_VERSION, subscriptionsBySession: subsBySession };
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const sendPushToSubscription = async (sub, payload) => {
|
|
185
|
+
await ensurePushInitialized();
|
|
186
|
+
const body = JSON.stringify(payload);
|
|
187
|
+
|
|
188
|
+
const pushSubscription = {
|
|
189
|
+
endpoint: sub.endpoint,
|
|
190
|
+
keys: {
|
|
191
|
+
p256dh: sub.p256dh,
|
|
192
|
+
auth: sub.auth,
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
await webPush.sendNotification(pushSubscription, body);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
const statusCode = typeof error?.statusCode === 'number' ? error.statusCode : null;
|
|
200
|
+
if (statusCode === 410 || statusCode === 404) {
|
|
201
|
+
await removePushSubscriptionFromAllSessions(sub.endpoint);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
console.warn('[Push] Failed to send notification:', error);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const sendPushToAllUiSessions = async (payload, options = {}) => {
|
|
209
|
+
const requireNoSse = options.requireNoSse === true;
|
|
210
|
+
const store = await readPushSubscriptionsFromDisk();
|
|
211
|
+
const sessions = store.subscriptionsBySession || {};
|
|
212
|
+
const subscriptionsByEndpoint = new Map();
|
|
213
|
+
|
|
214
|
+
for (const record of Object.values(sessions)) {
|
|
215
|
+
const subscriptions = normalizePushSubscriptions(record);
|
|
216
|
+
if (subscriptions.length === 0) continue;
|
|
217
|
+
|
|
218
|
+
for (const sub of subscriptions) {
|
|
219
|
+
if (!subscriptionsByEndpoint.has(sub.endpoint)) {
|
|
220
|
+
subscriptionsByEndpoint.set(sub.endpoint, sub);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
await Promise.all(Array.from(subscriptionsByEndpoint.values()).map(async (sub) => {
|
|
226
|
+
if (requireNoSse && isAnyUiVisible()) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
await sendPushToSubscription(sub, payload);
|
|
230
|
+
}));
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const updateUiVisibility = (token, visible) => {
|
|
234
|
+
if (!token) return;
|
|
235
|
+
const now = Date.now();
|
|
236
|
+
const nextVisible = Boolean(visible);
|
|
237
|
+
uiVisibilityByToken.set(token, { visible: nextVisible, updatedAt: now });
|
|
238
|
+
globalVisibilityState = nextVisible;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const isAnyUiVisible = () => globalVisibilityState === true;
|
|
242
|
+
|
|
243
|
+
const isUiVisible = (token) => uiVisibilityByToken.get(token)?.visible === true;
|
|
244
|
+
|
|
245
|
+
const resolveVapidSubject = async () => {
|
|
246
|
+
const configured = process.env.VINCI_VAPID_SUBJECT;
|
|
247
|
+
if (typeof configured === 'string' && configured.trim().length > 0) {
|
|
248
|
+
return configured.trim();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const originEnv = process.env.VINCI_PUBLIC_ORIGIN;
|
|
252
|
+
if (typeof originEnv === 'string' && originEnv.trim().length > 0) {
|
|
253
|
+
const trimmed = originEnv.trim();
|
|
254
|
+
if (isLoopbackHttpOrigin(trimmed)) {
|
|
255
|
+
return 'mailto:vinci@localhost';
|
|
256
|
+
}
|
|
257
|
+
return trimmed;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const settings = await readSettingsFromDiskMigrated();
|
|
262
|
+
const stored = settings?.publicOrigin;
|
|
263
|
+
if (typeof stored === 'string' && stored.trim().length > 0) {
|
|
264
|
+
const trimmed = stored.trim();
|
|
265
|
+
if (isLoopbackHttpOrigin(trimmed)) {
|
|
266
|
+
return 'mailto:vinci@localhost';
|
|
267
|
+
}
|
|
268
|
+
return trimmed;
|
|
269
|
+
}
|
|
270
|
+
} catch {
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return 'mailto:vinci@localhost';
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const ensurePushInitialized = async () => {
|
|
277
|
+
if (pushInitialized) return;
|
|
278
|
+
const keys = await getOrCreateVapidKeys();
|
|
279
|
+
const subject = await resolveVapidSubject();
|
|
280
|
+
|
|
281
|
+
if (subject === 'mailto:vinci@localhost') {
|
|
282
|
+
console.warn('[Push] No public origin configured for VAPID; set VINCI_VAPID_SUBJECT or enable push once from a real origin.');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
webPush.setVapidDetails(subject, keys.publicKey, keys.privateKey);
|
|
286
|
+
pushInitialized = true;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const setPushInitialized = (value) => {
|
|
290
|
+
pushInitialized = value === true;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
getOrCreateVapidKeys,
|
|
295
|
+
addOrUpdatePushSubscription,
|
|
296
|
+
removePushSubscription,
|
|
297
|
+
sendPushToAllUiSessions,
|
|
298
|
+
updateUiVisibility,
|
|
299
|
+
isAnyUiVisible,
|
|
300
|
+
isUiVisible,
|
|
301
|
+
ensurePushInitialized,
|
|
302
|
+
setPushInitialized,
|
|
303
|
+
};
|
|
304
|
+
};
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
const parsePushSubscribeBody = (body) => {
|
|
2
|
+
if (!body || typeof body !== 'object') return null;
|
|
3
|
+
const endpoint = body.endpoint;
|
|
4
|
+
const keys = body.keys;
|
|
5
|
+
const p256dh = keys?.p256dh;
|
|
6
|
+
const auth = keys?.auth;
|
|
7
|
+
|
|
8
|
+
if (typeof endpoint !== 'string' || endpoint.trim().length === 0) return null;
|
|
9
|
+
if (typeof p256dh !== 'string' || p256dh.trim().length === 0) return null;
|
|
10
|
+
if (typeof auth !== 'string' || auth.trim().length === 0) return null;
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
endpoint: endpoint.trim(),
|
|
14
|
+
keys: { p256dh: p256dh.trim(), auth: auth.trim() },
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const parsePushUnsubscribeBody = (body) => {
|
|
19
|
+
if (!body || typeof body !== 'object') return null;
|
|
20
|
+
const endpoint = body.endpoint;
|
|
21
|
+
if (typeof endpoint !== 'string' || endpoint.trim().length === 0) return null;
|
|
22
|
+
return { endpoint: endpoint.trim() };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const registerNotificationRoutes = (app, dependencies) => {
|
|
26
|
+
const {
|
|
27
|
+
uiAuthController,
|
|
28
|
+
ensurePushInitialized,
|
|
29
|
+
ensureGlobalWatcherStarted,
|
|
30
|
+
getOrCreateVapidKeys,
|
|
31
|
+
getUiSessionTokenFromRequest,
|
|
32
|
+
readSettingsFromDiskMigrated,
|
|
33
|
+
writeSettingsToDisk,
|
|
34
|
+
addOrUpdatePushSubscription,
|
|
35
|
+
removePushSubscription,
|
|
36
|
+
updateUiVisibility,
|
|
37
|
+
isUiVisible,
|
|
38
|
+
getUiNotificationClients,
|
|
39
|
+
writeSseEvent,
|
|
40
|
+
getSessionActivitySnapshot,
|
|
41
|
+
getSessionStateSnapshot,
|
|
42
|
+
getSessionAttentionSnapshot,
|
|
43
|
+
getSessionState,
|
|
44
|
+
getSessionAttentionState,
|
|
45
|
+
markSessionViewed,
|
|
46
|
+
markSessionUnviewed,
|
|
47
|
+
markUserMessageSent,
|
|
48
|
+
setPushInitialized,
|
|
49
|
+
setAutoAcceptSession,
|
|
50
|
+
} = dependencies;
|
|
51
|
+
|
|
52
|
+
const ensureSessionWatcher = async () => {
|
|
53
|
+
if (typeof ensureGlobalWatcherStarted !== 'function') {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
await ensureGlobalWatcherStarted();
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.warn('[OpenCodeWatcher] lazy start failed:', error?.message ?? error);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
app.get('/api/push/vapid-public-key', async (_req, res) => {
|
|
64
|
+
try {
|
|
65
|
+
await ensurePushInitialized();
|
|
66
|
+
const keys = await getOrCreateVapidKeys();
|
|
67
|
+
res.json({ publicKey: keys.publicKey });
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.warn('[Push] Failed to load VAPID key:', error);
|
|
70
|
+
res.status(500).json({ error: 'Failed to load push key' });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
app.post('/api/push/subscribe', async (req, res) => {
|
|
75
|
+
await ensurePushInitialized();
|
|
76
|
+
await ensureSessionWatcher();
|
|
77
|
+
|
|
78
|
+
const uiToken = uiAuthController?.ensureSessionToken
|
|
79
|
+
? await uiAuthController.ensureSessionToken(req, res)
|
|
80
|
+
: getUiSessionTokenFromRequest(req);
|
|
81
|
+
if (!uiToken) {
|
|
82
|
+
return res.status(401).json({ error: 'UI session missing' });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const parsed = parsePushSubscribeBody(req.body);
|
|
86
|
+
if (!parsed) {
|
|
87
|
+
return res.status(400).json({ error: 'Invalid body' });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const { endpoint, keys } = parsed;
|
|
91
|
+
|
|
92
|
+
const origin = typeof req.body?.origin === 'string' ? req.body.origin.trim() : '';
|
|
93
|
+
if (origin.startsWith('http://') || origin.startsWith('https://')) {
|
|
94
|
+
try {
|
|
95
|
+
const settings = await readSettingsFromDiskMigrated();
|
|
96
|
+
if (typeof settings?.publicOrigin !== 'string' || settings.publicOrigin.trim().length === 0) {
|
|
97
|
+
await writeSettingsToDisk({
|
|
98
|
+
...settings,
|
|
99
|
+
publicOrigin: origin,
|
|
100
|
+
});
|
|
101
|
+
setPushInitialized(false);
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
await addOrUpdatePushSubscription(
|
|
108
|
+
uiToken,
|
|
109
|
+
{
|
|
110
|
+
endpoint,
|
|
111
|
+
p256dh: keys.p256dh,
|
|
112
|
+
auth: keys.auth,
|
|
113
|
+
},
|
|
114
|
+
req.headers['user-agent']
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
return res.json({ ok: true });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
app.delete('/api/push/subscribe', async (req, res) => {
|
|
121
|
+
await ensurePushInitialized();
|
|
122
|
+
|
|
123
|
+
const uiToken = uiAuthController?.ensureSessionToken
|
|
124
|
+
? await uiAuthController.ensureSessionToken(req, res)
|
|
125
|
+
: getUiSessionTokenFromRequest(req);
|
|
126
|
+
if (!uiToken) {
|
|
127
|
+
return res.status(401).json({ error: 'UI session missing' });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const parsed = parsePushUnsubscribeBody(req.body);
|
|
131
|
+
if (!parsed) {
|
|
132
|
+
return res.status(400).json({ error: 'Invalid body' });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await removePushSubscription(uiToken, parsed.endpoint);
|
|
136
|
+
return res.json({ ok: true });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
app.post('/api/push/visibility', async (req, res) => {
|
|
140
|
+
const uiToken = uiAuthController?.ensureSessionToken
|
|
141
|
+
? await uiAuthController.ensureSessionToken(req, res)
|
|
142
|
+
: getUiSessionTokenFromRequest(req);
|
|
143
|
+
if (!uiToken) {
|
|
144
|
+
return res.status(401).json({ error: 'UI session missing' });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const visible = req.body && typeof req.body === 'object' ? req.body.visible : null;
|
|
148
|
+
updateUiVisibility(uiToken, visible === true);
|
|
149
|
+
return res.json({ ok: true });
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
app.get('/api/push/visibility', (req, res) => {
|
|
153
|
+
const uiToken = getUiSessionTokenFromRequest(req);
|
|
154
|
+
if (!uiToken) {
|
|
155
|
+
return res.status(401).json({ error: 'UI session missing' });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return res.json({
|
|
159
|
+
ok: true,
|
|
160
|
+
visible: isUiVisible(uiToken),
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
app.get('/api/notifications/stream', async (req, res) => {
|
|
165
|
+
const uiToken = uiAuthController?.ensureSessionToken
|
|
166
|
+
? await uiAuthController.ensureSessionToken(req, res)
|
|
167
|
+
: getUiSessionTokenFromRequest(req);
|
|
168
|
+
if (!uiToken) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
|
173
|
+
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
|
174
|
+
res.setHeader('Connection', 'keep-alive');
|
|
175
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
176
|
+
res.flushHeaders?.();
|
|
177
|
+
|
|
178
|
+
const clients = getUiNotificationClients();
|
|
179
|
+
clients.add(res);
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
writeSseEvent(res, {
|
|
183
|
+
type: 'vinci:notification-stream-ready',
|
|
184
|
+
properties: { uiToken },
|
|
185
|
+
});
|
|
186
|
+
} catch {
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
req.on('close', () => {
|
|
190
|
+
clients.delete(res);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
app.get('/api/session-activity', (_req, res) => {
|
|
195
|
+
void ensureSessionWatcher();
|
|
196
|
+
res.json(getSessionActivitySnapshot());
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
app.get('/api/sessions/snapshot', async (_req, res) => {
|
|
200
|
+
await ensureSessionWatcher();
|
|
201
|
+
res.json({
|
|
202
|
+
statusSessions: getSessionStateSnapshot(),
|
|
203
|
+
attentionSessions: getSessionAttentionSnapshot(),
|
|
204
|
+
serverTime: Date.now(),
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
app.get('/api/sessions/status', async (_req, res) => {
|
|
209
|
+
await ensureSessionWatcher();
|
|
210
|
+
const snapshot = getSessionStateSnapshot();
|
|
211
|
+
res.json({
|
|
212
|
+
sessions: snapshot,
|
|
213
|
+
serverTime: Date.now(),
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
app.get('/api/sessions/:id/status', async (req, res) => {
|
|
218
|
+
await ensureSessionWatcher();
|
|
219
|
+
const sessionId = req.params.id;
|
|
220
|
+
const state = getSessionState(sessionId);
|
|
221
|
+
|
|
222
|
+
if (!state) {
|
|
223
|
+
return res.status(404).json({
|
|
224
|
+
error: 'Session not found or no state available',
|
|
225
|
+
sessionId,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return res.json({
|
|
230
|
+
sessionId,
|
|
231
|
+
...state,
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
app.get('/api/sessions/attention', async (_req, res) => {
|
|
236
|
+
await ensureSessionWatcher();
|
|
237
|
+
const snapshot = getSessionAttentionSnapshot();
|
|
238
|
+
res.json({
|
|
239
|
+
sessions: snapshot,
|
|
240
|
+
serverTime: Date.now(),
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
app.get('/api/sessions/:id/attention', async (req, res) => {
|
|
245
|
+
await ensureSessionWatcher();
|
|
246
|
+
const sessionId = req.params.id;
|
|
247
|
+
const state = getSessionAttentionState(sessionId);
|
|
248
|
+
|
|
249
|
+
if (!state) {
|
|
250
|
+
return res.status(404).json({
|
|
251
|
+
error: 'Session not found or no attention state available',
|
|
252
|
+
sessionId,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return res.json({
|
|
257
|
+
sessionId,
|
|
258
|
+
...state,
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
app.post('/api/sessions/:id/view', (req, res) => {
|
|
263
|
+
const sessionId = req.params.id;
|
|
264
|
+
const clientId = req.headers['x-client-id'] || req.ip || 'anonymous';
|
|
265
|
+
|
|
266
|
+
markSessionViewed(sessionId, clientId);
|
|
267
|
+
|
|
268
|
+
return res.json({
|
|
269
|
+
success: true,
|
|
270
|
+
sessionId,
|
|
271
|
+
viewed: true,
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
app.post('/api/sessions/:id/unview', (req, res) => {
|
|
276
|
+
const sessionId = req.params.id;
|
|
277
|
+
const clientId = req.headers['x-client-id'] || req.ip || 'anonymous';
|
|
278
|
+
|
|
279
|
+
markSessionUnviewed(sessionId, clientId);
|
|
280
|
+
|
|
281
|
+
return res.json({
|
|
282
|
+
success: true,
|
|
283
|
+
sessionId,
|
|
284
|
+
viewed: false,
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
app.post('/api/sessions/:id/message-sent', (req, res) => {
|
|
289
|
+
const sessionId = req.params.id;
|
|
290
|
+
|
|
291
|
+
markUserMessageSent(sessionId);
|
|
292
|
+
|
|
293
|
+
return res.json({
|
|
294
|
+
success: true,
|
|
295
|
+
sessionId,
|
|
296
|
+
messageSent: true,
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Mirror client-side Permission Auto-Accept state to the server so it can
|
|
301
|
+
// suppress permission notifications at the source (the 500ms debounce race
|
|
302
|
+
// otherwise leaks notifications for auto-accepted permissions).
|
|
303
|
+
app.post('/api/notifications/auto-accept', (req, res) => {
|
|
304
|
+
const body = req.body && typeof req.body === 'object' ? req.body : {};
|
|
305
|
+
const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
|
|
306
|
+
const enabled = body.enabled === true;
|
|
307
|
+
if (!sessionId) {
|
|
308
|
+
return res.status(400).json({ error: 'sessionId required' });
|
|
309
|
+
}
|
|
310
|
+
if (typeof setAutoAcceptSession === 'function') {
|
|
311
|
+
setAutoAcceptSession(sessionId, enabled);
|
|
312
|
+
}
|
|
313
|
+
return res.json({ success: true, sessionId, enabled });
|
|
314
|
+
});
|
|
315
|
+
};
|