synabun 2.0.0 → 2.0.7
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/CHANGELOG.md +97 -0
- package/NOTICE +5 -0
- package/README.md +2 -1
- package/THIRD-PARTY-LICENSES.md +59 -0
- package/benchmarks/fixtures/browser/README.md +1 -1
- package/benchmarks/fixtures/desktop/README.md +1 -1
- package/docs/whatsapp.md +153 -18
- package/hooks/claude-code/pre-compact.mjs +11 -0
- package/hooks/claude-code/prompt-submit.mjs +60 -184
- package/hooks/claude-code/session-start.mjs +73 -16
- package/hooks/claude-code/shared.mjs +26 -3
- package/hooks/claude-code/stop.mjs +5 -5
- package/lib/child-path.js +303 -0
- package/lib/claude-hooks.js +2 -1
- package/lib/data-home-migration.js +2 -0
- package/lib/desktop-app.js +265 -0
- package/lib/external-tools.js +7 -3
- package/lib/log-redact.js +102 -0
- package/lib/packaged-runtime.js +138 -11
- package/lib/start-launcher.js +88 -1
- package/mcp-server/dist/http.d.ts +3 -1
- package/mcp-server/dist/http.d.ts.map +1 -1
- package/mcp-server/dist/http.js +15 -4
- package/mcp-server/dist/http.js.map +1 -1
- package/mcp-server/dist/index.d.ts.map +1 -1
- package/mcp-server/dist/index.js +9 -6
- package/mcp-server/dist/index.js.map +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts +108 -0
- package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/budget-judgments.js +153 -0
- package/mcp-server/dist/services/budget-judgments.js.map +1 -0
- package/mcp-server/dist/services/codex-config-heal.d.ts +5 -1
- package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
- package/mcp-server/dist/services/codex-config-heal.js +41 -4
- package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
- package/mcp-server/dist/services/identity.d.ts +11 -0
- package/mcp-server/dist/services/identity.d.ts.map +1 -1
- package/mcp-server/dist/services/identity.js +24 -0
- package/mcp-server/dist/services/identity.js.map +1 -1
- package/mcp-server/dist/services/neural-interface.d.ts +13 -0
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
- package/mcp-server/dist/services/neural-interface.js +34 -2
- package/mcp-server/dist/services/neural-interface.js.map +1 -1
- package/mcp-server/dist/services/typesafe-config.d.ts +1 -1
- package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -1
- package/mcp-server/dist/services/typesafe-config.js +4 -1
- package/mcp-server/dist/services/typesafe-config.js.map +1 -1
- package/mcp-server/dist/tools/agents.d.ts +10 -7
- package/mcp-server/dist/tools/agents.d.ts.map +1 -1
- package/mcp-server/dist/tools/agents.js +34 -13
- package/mcp-server/dist/tools/agents.js.map +1 -1
- package/mcp-server/dist/tools/whatsapp-send-file.d.ts +17 -0
- package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -0
- package/mcp-server/dist/tools/whatsapp-send-file.js +44 -0
- package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -0
- package/mcp-server/dist/tui.js +2 -1
- package/mcp-server/dist/tui.js.map +1 -1
- package/mcp-server/dist/version.d.ts +5 -0
- package/mcp-server/dist/version.d.ts.map +1 -0
- package/mcp-server/dist/version.js +21 -0
- package/mcp-server/dist/version.js.map +1 -0
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -3
- package/mcp-server/run.mjs +15 -9
- package/mcp-server/src/http.ts +18 -3
- package/mcp-server/src/index.ts +9 -6
- package/mcp-server/src/services/budget-judgments.ts +183 -0
- package/mcp-server/src/services/codex-config-heal.ts +35 -4
- package/mcp-server/src/services/identity.ts +27 -0
- package/mcp-server/src/services/neural-interface.ts +34 -2
- package/mcp-server/src/services/typesafe-config.ts +4 -1
- package/mcp-server/src/tools/agents.ts +33 -14
- package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
- package/mcp-server/src/tui.ts +2 -1
- package/mcp-server/src/version.ts +22 -0
- package/neural-interface/i18n/en.json +2126 -209
- package/neural-interface/i18n/pt-BR.json +2123 -207
- package/neural-interface/lib/account-usage.js +163 -0
- package/neural-interface/lib/assistant-api.js +316 -57
- package/neural-interface/lib/assistant-brains/claude.js +19 -2
- package/neural-interface/lib/assistant-brains/codex.js +3 -0
- package/neural-interface/lib/assistant-brains/opencode.js +17 -4
- package/neural-interface/lib/assistant-budget-planner.js +390 -0
- package/neural-interface/lib/assistant-budget-split.js +408 -0
- package/neural-interface/lib/assistant-budget.js +94 -95
- package/neural-interface/lib/assistant-catalog.js +179 -27
- package/neural-interface/lib/assistant-config.js +91 -14
- package/neural-interface/lib/assistant-dispatch.js +494 -138
- package/neural-interface/lib/assistant-hidden-models.js +393 -66
- package/neural-interface/lib/assistant-jev.js +26 -3
- package/neural-interface/lib/assistant-memory.js +24 -10
- package/neural-interface/lib/assistant-persona.js +73 -12
- package/neural-interface/lib/assistant-plan-permissions.js +8 -1
- package/neural-interface/lib/assistant-pricing.js +21 -6
- package/neural-interface/lib/assistant-route-accounts.js +14 -0
- package/neural-interface/lib/assistant-route-gate.js +19 -3
- package/neural-interface/lib/assistant-router.js +373 -42
- package/neural-interface/lib/assistant-runtime.js +488 -59
- package/neural-interface/lib/assistant-task-prompt.js +13 -3
- package/neural-interface/lib/assistant-test-density.js +142 -0
- package/neural-interface/lib/assistant-user-context-budgets.js +123 -0
- package/neural-interface/lib/assistant-user-context.js +908 -0
- package/neural-interface/lib/assistant-user-knowledge.js +328 -0
- package/neural-interface/lib/assistant-user-learn.js +404 -0
- package/neural-interface/lib/assistant-user-threads.js +226 -0
- package/neural-interface/lib/assistant-user-writer.js +183 -0
- package/neural-interface/lib/augmented-path.js +317 -53
- package/neural-interface/lib/browser-background-tab.js +134 -0
- package/neural-interface/lib/browser-screenshot.js +678 -61
- package/neural-interface/lib/claude-accounts.js +342 -31
- package/neural-interface/lib/claude-agent-bridge.js +123 -9
- package/neural-interface/lib/claude-executable.js +6 -1
- package/neural-interface/lib/claude-model-catalog.js +33 -5
- package/neural-interface/lib/claude-panel-auth.js +221 -0
- package/neural-interface/lib/claude-profile-api-key.js +62 -0
- package/neural-interface/lib/claude-temporary.js +46 -7
- package/neural-interface/lib/cli-install-api.js +137 -0
- package/neural-interface/lib/cli-install.js +797 -0
- package/neural-interface/lib/cli-path.js +130 -0
- package/neural-interface/lib/cli-resolver.js +744 -0
- package/neural-interface/lib/cli-update-planner.js +143 -7
- package/neural-interface/lib/cloudflared/install.js +533 -0
- package/neural-interface/lib/cloudflared/manager.js +545 -0
- package/neural-interface/lib/cloudflared/pins.js +115 -0
- package/neural-interface/lib/cloudflared/pins.json +63 -0
- package/neural-interface/lib/cloudflared/tunnel.js +265 -0
- package/neural-interface/lib/codex-account-auth.js +178 -0
- package/neural-interface/lib/codex-accounts.js +489 -0
- package/neural-interface/lib/codex-capabilities.js +2 -1
- package/neural-interface/lib/codex-compat-rpc.js +8 -2
- package/neural-interface/lib/codex-mcp-approvals.js +5 -1
- package/neural-interface/lib/codex-panel-auth.js +62 -0
- package/neural-interface/lib/codex-session-settings.js +1 -1
- package/neural-interface/lib/codex-settings-schema.js +120 -0
- package/neural-interface/lib/desktop/config.js +3 -0
- package/neural-interface/lib/editor-completion-endpoint.js +264 -0
- package/neural-interface/lib/editor-completion-opencode.js +436 -0
- package/neural-interface/lib/editor-completion.js +714 -0
- package/neural-interface/lib/editor-config.js +149 -0
- package/neural-interface/lib/editor-files-api.js +268 -0
- package/neural-interface/lib/editor-git-api.js +134 -0
- package/neural-interface/lib/file-explorer-api.js +1039 -0
- package/neural-interface/lib/file-explorer-system.js +355 -0
- package/neural-interface/lib/file-explorer-trash.ps1 +85 -0
- package/neural-interface/lib/file-explorer-watch.js +239 -0
- package/neural-interface/lib/git-bash/bzip2.js +339 -0
- package/neural-interface/lib/git-bash/claude-settings.js +73 -0
- package/neural-interface/lib/git-bash/extract-worker.js +34 -0
- package/neural-interface/lib/git-bash/install.js +529 -0
- package/neural-interface/lib/git-bash/licenses/COPYING +360 -0
- package/neural-interface/lib/git-bash/licenses/LGPL-2.1 +511 -0
- package/neural-interface/lib/git-bash/manager.js +268 -0
- package/neural-interface/lib/git-bash/pins.js +206 -0
- package/neural-interface/lib/git-bash/pins.json +34 -0
- package/neural-interface/lib/git-bash/resolver.js +194 -0
- package/neural-interface/lib/git-diff-api.js +103 -0
- package/neural-interface/lib/git-read.js +234 -0
- package/neural-interface/lib/greeting/api.js +89 -0
- package/neural-interface/lib/greeting/budgets.js +90 -0
- package/neural-interface/lib/greeting/engine.js +461 -0
- package/neural-interface/lib/greeting/facts.js +320 -0
- package/neural-interface/lib/greeting/sources.js +252 -0
- package/neural-interface/lib/greeting/writer.js +139 -0
- package/neural-interface/lib/http-guards.js +189 -13
- package/neural-interface/lib/install-details.js +145 -0
- package/neural-interface/lib/machine-names.js +88 -0
- package/neural-interface/lib/mcp-installer.js +14 -2
- package/neural-interface/lib/memory-api.js +31 -7
- package/neural-interface/lib/native-loop-providers.js +26 -8
- package/neural-interface/lib/native-loop-runtime.js +9 -4
- package/neural-interface/lib/os-path.js +319 -0
- package/neural-interface/lib/plugin-installer.js +4 -2
- package/neural-interface/lib/project-storage.js +5 -7
- package/neural-interface/lib/provider-panel-api-key-auth.js +92 -0
- package/neural-interface/lib/provider-panel-terminal-auth.js +95 -0
- package/neural-interface/lib/pty-host/core.js +2 -1
- package/neural-interface/lib/pty-host/manager.js +27 -2
- package/neural-interface/lib/remote-policy.js +20 -11
- package/neural-interface/lib/request-gate.js +589 -0
- package/neural-interface/lib/session-title-generator.js +4 -3
- package/neural-interface/lib/system-about.js +340 -0
- package/neural-interface/lib/system-activity.js +66 -0
- package/neural-interface/lib/terminal-env.js +11 -3
- package/neural-interface/lib/terminal-path.js +55 -0
- package/neural-interface/lib/ui-blob-api.js +108 -0
- package/neural-interface/lib/ui-blob-store.js +68 -0
- package/neural-interface/lib/virtual-socket.js +12 -1
- package/neural-interface/lib/whatsapp/baileys-adapter.js +165 -45
- package/neural-interface/lib/whatsapp/bridge.js +78 -34
- package/neural-interface/lib/whatsapp/commands.js +13 -14
- package/neural-interface/lib/whatsapp/config.js +134 -20
- package/neural-interface/lib/whatsapp/fake-baileys.js +99 -14
- package/neural-interface/lib/whatsapp/host-core.js +168 -35
- package/neural-interface/lib/whatsapp/inbound.js +5 -3
- package/neural-interface/lib/whatsapp/local-file.js +524 -0
- package/neural-interface/lib/whatsapp/manager.js +92 -32
- package/neural-interface/lib/whatsapp/paths.js +11 -1
- package/neural-interface/lib/whatsapp/protocol.js +65 -5
- package/neural-interface/lib/whatsapp/redact.js +63 -0
- package/neural-interface/lib/whatsapp/service.js +343 -91
- package/neural-interface/lib/whatsapp/transcription.js +463 -0
- package/neural-interface/package-lock.json +701 -2
- package/neural-interface/package.json +13 -2
- package/neural-interface/public/index.html +2 -0
- package/neural-interface/public/index2d.html +2 -0
- package/neural-interface/public/install-app.html +9 -6
- package/neural-interface/public/offline.html +19 -3
- package/neural-interface/public/onboarding.html +107 -2
- package/neural-interface/public/shared/account-i18n.js +7 -0
- package/neural-interface/public/shared/account-menu.js +539 -0
- package/neural-interface/public/shared/account-usage.js +19 -0
- package/neural-interface/public/shared/account-view.js +335 -0
- package/neural-interface/public/shared/agent-runtime-options.js +50 -14
- package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
- package/neural-interface/public/shared/assistant/asst-api.js +32 -2
- package/neural-interface/public/shared/assistant/asst-automation.js +286 -0
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +430 -81
- package/neural-interface/public/shared/assistant/asst-budget.js +259 -303
- package/neural-interface/public/shared/assistant/asst-composer.js +11 -1
- package/neural-interface/public/shared/assistant/asst-mascot.js +140 -61
- package/neural-interface/public/shared/assistant/asst-menu.js +27 -11
- package/neural-interface/public/shared/assistant/asst-panel.js +198 -42
- package/neural-interface/public/shared/assistant/asst-render.js +197 -23
- package/neural-interface/public/shared/assistant/asst-route-models.js +108 -0
- package/neural-interface/public/shared/assistant/asst-route.js +95 -343
- package/neural-interface/public/shared/assistant/asst-routes-editor.js +534 -0
- package/neural-interface/public/shared/assistant/asst-sanitize.js +8 -1
- package/neural-interface/public/shared/assistant/asst-sidepanel.js +31 -9
- package/neural-interface/public/shared/assistant/asst-slash.js +1 -1
- package/neural-interface/public/shared/assistant/asst-state.js +186 -2
- package/neural-interface/public/shared/assistant/asst-styles.js +247 -11
- package/neural-interface/public/shared/blob-store.js +111 -22
- package/neural-interface/public/shared/cdx/cdx-commands.js +4 -1
- package/neural-interface/public/shared/cdx/cdx-features.js +22 -14
- package/neural-interface/public/shared/cdx/cdx-icons.js +2 -0
- package/neural-interface/public/shared/cdx/cdx-panel.js +80 -118
- package/neural-interface/public/shared/cdx/cdx-render.js +25 -89
- package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +164 -107
- package/neural-interface/public/shared/cdx/cdx-tabs.js +348 -668
- package/neural-interface/public/shared/cdx/cdx-ws.js +2 -0
- package/neural-interface/public/shared/claude-account-view.js +3 -0
- package/neural-interface/public/shared/cli-install-view.js +392 -0
- package/neural-interface/public/shared/cli-status.js +282 -22
- package/neural-interface/public/shared/constants.js +6 -1
- package/neural-interface/public/shared/cp/cp-events.js +2 -1
- package/neural-interface/public/shared/cp/cp-rehydrate.js +2 -1
- package/neural-interface/public/shared/cp/cp-styles.js +61 -0
- package/neural-interface/public/shared/desktop-bridge.js +39 -0
- package/neural-interface/public/shared/desktop-titlebar-page.js +59 -0
- package/neural-interface/public/shared/desktop-titlebar-view.js +122 -0
- package/neural-interface/public/shared/desktop-titlebar.js +153 -0
- package/neural-interface/public/shared/desktop-update-view.js +462 -0
- package/neural-interface/public/shared/editor/editor-ai-logic.js +409 -0
- package/neural-interface/public/shared/editor/editor-ai.js +619 -0
- package/neural-interface/public/shared/editor/editor-commands.js +212 -0
- package/neural-interface/public/shared/editor/editor-compare.js +103 -0
- package/neural-interface/public/shared/editor/editor-completion-list.js +172 -0
- package/neural-interface/public/shared/editor/editor-doc.js +257 -0
- package/neural-interface/public/shared/editor/editor-git-logic.js +131 -0
- package/neural-interface/public/shared/editor/editor-git.js +95 -0
- package/neural-interface/public/shared/editor/editor-languages.js +156 -0
- package/neural-interface/public/shared/editor/editor-lint.js +229 -0
- package/neural-interface/public/shared/editor/editor-loader.js +51 -0
- package/neural-interface/public/shared/editor/editor-markdown.js +90 -0
- package/neural-interface/public/shared/editor/editor-menu.js +233 -0
- package/neural-interface/public/shared/editor/editor-minimap.js +371 -0
- package/neural-interface/public/shared/editor/editor-palette-logic.js +192 -0
- package/neural-interface/public/shared/editor/editor-palette.js +125 -0
- package/neural-interface/public/shared/editor/editor-panel-logic.js +288 -0
- package/neural-interface/public/shared/editor/editor-panel.js +2099 -0
- package/neural-interface/public/shared/editor/editor-prefs.js +224 -0
- package/neural-interface/public/shared/editor/editor-safety.js +500 -0
- package/neural-interface/public/shared/editor/editor-search.js +352 -0
- package/neural-interface/public/shared/editor/editor-statusbar.js +144 -0
- package/neural-interface/public/shared/editor/editor-tabs-logic.js +331 -0
- package/neural-interface/public/shared/editor/editor-tabs.js +233 -0
- package/neural-interface/public/shared/editor/editor-theme.js +244 -0
- package/neural-interface/public/shared/editor/editor-view.js +332 -0
- package/neural-interface/public/shared/explorer/fx-api.js +216 -0
- package/neural-interface/public/shared/explorer/fx-links.js +204 -0
- package/neural-interface/public/shared/explorer/fx-logic.js +537 -0
- package/neural-interface/public/shared/explorer/fx-main.js +865 -0
- package/neural-interface/public/shared/explorer/fx-names.js +87 -0
- package/neural-interface/public/shared/explorer/fx-ops.js +370 -0
- package/neural-interface/public/shared/explorer/fx-popups.js +490 -0
- package/neural-interface/public/shared/explorer/fx-tree.js +790 -0
- package/neural-interface/public/shared/file-links.js +339 -0
- package/neural-interface/public/shared/git-bash-view.js +145 -0
- package/neural-interface/public/shared/greeting/greeting-card.js +178 -0
- package/neural-interface/public/shared/greeting/greeting-view.js +148 -0
- package/neural-interface/public/shared/html-shell.js +161 -165
- package/neural-interface/public/shared/i18n.js +19 -1
- package/neural-interface/public/shared/install-details-view.js +86 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +4 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-greeting.js +47 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +131 -17
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +22 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +158 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +3 -2
- package/neural-interface/public/shared/pwa-install-page.js +32 -2
- package/neural-interface/public/shared/pwa-install.css +3 -3
- package/neural-interface/public/shared/pwa-install.js +100 -1
- package/neural-interface/public/shared/settings/settings-about-pane.js +271 -0
- package/neural-interface/public/shared/settings/settings-about.js +254 -0
- package/neural-interface/public/shared/settings/settings-code-editor-pane.js +412 -0
- package/neural-interface/public/shared/settings/settings-code-editor.js +328 -0
- package/neural-interface/public/shared/settings/settings-greeting.js +133 -0
- package/neural-interface/public/shared/settings/settings-ia.js +33 -4
- package/neural-interface/public/shared/settings/settings-user-context.js +154 -0
- package/neural-interface/public/shared/showcase-privacy.js +489 -0
- package/neural-interface/public/shared/start-bridge.js +238 -19
- package/neural-interface/public/shared/storage.js +30 -3
- package/neural-interface/public/shared/styles.css +1710 -770
- package/neural-interface/public/shared/synabun-mascot.js +68 -58
- package/neural-interface/public/shared/tunnel-view.js +167 -0
- package/neural-interface/public/shared/ui-claude-panel.js +369 -229
- package/neural-interface/public/shared/ui-cli-banner.js +355 -0
- package/neural-interface/public/shared/ui-detail.js +15 -0
- package/neural-interface/public/shared/ui-explorer.js +22 -0
- package/neural-interface/public/shared/ui-file-explorer.js +481 -2496
- package/neural-interface/public/shared/ui-file-links.js +103 -0
- package/neural-interface/public/shared/ui-git-bash.js +124 -0
- package/neural-interface/public/shared/ui-help.js +26 -0
- package/neural-interface/public/shared/ui-invite.js +126 -116
- package/neural-interface/public/shared/ui-judgments.js +10 -1
- package/neural-interface/public/shared/ui-keybinds.js +4 -0
- package/neural-interface/public/shared/ui-loading.js +187 -19
- package/neural-interface/public/shared/ui-menubar.js +25 -33
- package/neural-interface/public/shared/ui-native-loop-router-state.js +11 -0
- package/neural-interface/public/shared/ui-native-loop-router.js +15 -2
- package/neural-interface/public/shared/ui-navbar.js +48 -22
- package/neural-interface/public/shared/ui-provider-auth.js +104 -0
- package/neural-interface/public/shared/ui-provider-terminal-auth.js +223 -0
- package/neural-interface/public/shared/ui-sessions.js +9 -3
- package/neural-interface/public/shared/ui-settings.js +509 -490
- package/neural-interface/public/shared/ui-sidepanel-runtime.js +2 -1
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -1
- package/neural-interface/public/shared/ui-sync.js +22 -0
- package/neural-interface/public/shared/ui-terminal.js +40 -5
- package/neural-interface/public/shared/ui-tool-updates.js +5 -0
- package/neural-interface/public/shared/ui-tunnel.js +130 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +6 -2
- package/neural-interface/public/shared/ui-update.js +594 -5
- package/neural-interface/public/shared/ui-whatsapp.js +21 -6
- package/neural-interface/public/shared/ui-whiteboard.js +12 -0
- package/neural-interface/public/shared/ui-worker-end.js +245 -0
- package/neural-interface/public/shared/ui-workspaces.js +41 -27
- package/neural-interface/public/shared/whatsapp/wa-view.js +51 -8
- package/neural-interface/public/shared/worker-end.js +253 -0
- package/neural-interface/public/sw.js +2 -2
- package/neural-interface/public/variant/2d/main.js +10 -1
- package/neural-interface/public/variant/3d/main.js +10 -1
- package/neural-interface/public/vendor/codemirror/chunks/apl-CSDKT72Z.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/asciiarmor-JYYPU5BM.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/asn1-NRWCZDA6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/asterisk-5OU6D4ZQ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/brainfuck-ID4N4SYC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-4RZWGCS2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-ACTJM3JR.js +11 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-S6BMAXLK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-TO2RDOHQ.js +3 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-US2GMZZQ.js +13 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-WYYO5ZAJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-XALZTDJC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-YRNUTRDV.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-Z3QOKH62.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/clike-OWQBSOGL.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/clojure-KV75UQWC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/cmake-GTEP5Y3C.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/cobol-WCZ7QEGJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/coffeescript-TGLBOT6B.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/commonlisp-WVVI432V.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/crystal-S6R6HLKQ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/css-PDA2RINP.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/cypher-HL5QPL4C.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/d-HHJH2HAJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/diff-MN3A35BY.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-25SQLBVS.js +3 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-27ZGGJJS.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-4E52OJIF.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-6FT73XDC.js +2 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-BE6YGRMW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-CJSGKW2O.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-CRPH2WLI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-CTG7JEQN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-I3FEVELX.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-IM5BBSWZ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-KERUKTIF.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-LXTMWZAM.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-MABASSET.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-MJVBUZLL.js +3 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-MNB3WZVU.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-QYWXOE3U.js +7 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-TEBOFWLZ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-UNXFROM3.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-XL2SLQO6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-XO42Y3AP.js +6 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-Y2WWDWJK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-YFEZKWMU.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-ZY7SWQO6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dockerfile-3Y4R4NCH.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dtd-5343GJ4P.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dylan-FI2CXSW2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ebnf-GNQ77YWA.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ecl-MRPUAE2L.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/eiffel-DZROES27.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/elm-4KICRFKQ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/erlang-3IHYC263.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/factor-C6D3E5UK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/fcl-VYTWUJMI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/forth-ZBO2EH5M.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/fortran-5NDCRWIK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/gas-SKRMOXFP.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/gherkin-L45YXZLW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/groovy-GLPTLN44.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/haskell-E7NF7P4Y.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/haxe-K2NVDOB6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/http-Q6DRQIJB.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/idl-MZYNJVLN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/javascript-6YRYK2HR.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/julia-HXEL2T5G.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/livescript-MV7CI2JZ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/lua-E36NHOOD.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mathematica-3XOZZCMC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mbox-ODYGFKS7.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mirc-WB43AWF5.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mllike-ZT6ET7ZH.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/modelica-LLMZPFGA.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mscgen-2OPNNYFH.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mumps-6P3HWJH6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/nginx-TCGKQTRW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/nsis-ATIGIXTJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ntriples-5PLBI5OV.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/octave-QF2S3NL6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/oz-JFIVFFH2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/pascal-JG6SW76V.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/perl-4B3IWVVO.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/pig-ABEBH4MI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/powershell-H32XE7TL.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/properties-NN644MMA.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/protobuf-YKWQK2J3.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/pug-RIYXEKFW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/puppet-JZQ5WPEV.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/python-EBCGGHAC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/q-5UMH2YQY.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/r-7XMGG453.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/rpm-IEV7TSLR.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ruby-T4SPFPNM.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sas-GEDX67ET.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/scheme-FTLOWKPC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/shell-HUFRKFSJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sieve-P5QUDVYO.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/smalltalk-2SORJNQI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/solr-M22TGAR5.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sparql-4M3KL752.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/spreadsheet-3K23LA4N.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sql-VHRFMUBI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/stex-QZDAQUY2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/stylus-7ZDKAKBD.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/swift-WCLCLGMR.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/tcl-JLMU6WOK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/textile-4CHDKZ4T.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/tiddlywiki-DKMCZZQ4.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/tiki-F4JIG36Q.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/toml-BHMHTIM2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/troff-YQ2KV5NT.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ttcn-ZHRCVFJY.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ttcn-cfg-KJOVTY4X.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/turtle-535P5N6I.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/vb-76UMQHOT.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/vbscript-CGNDTK6Z.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/velocity-VSJDO5T5.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/verilog-4T6ETXIP.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/vhdl-T7FNAXUN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/webidl-P6MJSJ6J.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/xquery-UOVILRGN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/yacas-CV2APROL.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/z80-3KFNVM4M.js +1 -0
- package/neural-interface/public/vendor/codemirror/codemirror-6.43.14-75f05e67.min.js +2 -0
- package/neural-interface/public/vendor/codemirror/manifest.json +60 -0
- package/neural-interface/scripts/build-vendor.mjs +78 -2
- package/neural-interface/scripts/vendor-codemirror-entry.js +121 -0
- package/neural-interface/server.js +2085 -1629
- package/neural-interface/templates/rulesets/README.md +1 -1
- package/package.json +5 -8
- package/setup.js +53 -15
- package/benchmarks/browser-live-results.json +0 -79
- package/benchmarks/browser-v2-results.json +0 -258
- package/neural-interface/lib/codex-capabilities.d.ts +0 -41
- package/neural-interface/public/category-logos/claude.svg +0 -1
- package/neural-interface/public/category-logos/gemini-color.svg +0 -1
- package/neural-interface/public/category-logos/openai.svg +0 -1
- package/neural-interface/public/category-logos/synabun.png +0 -0
- package/neural-interface/public/claude.svg +0 -1
- package/neural-interface/public/gallery.svg +0 -6
- package/neural-interface/public/leonardoai.svg +0 -1
- package/neural-interface/public/mcp.svg +0 -4
- package/neural-interface/public/opencode-logo-dark-square.svg +0 -1
- package/neural-interface/public/pin-svgrepo-com.svg +0 -25
- package/neural-interface/public/sessionicon.svg +0 -8
- package/neural-interface/public/toolbar-album.svg +0 -8
- package/neural-interface/public/toolbar-question.svg +0 -6
- package/neural-interface/public/toolbar-sessions.svg +0 -6
- package/neural-interface/scripts/codex-protocol-fixture.mjs +0 -83
- package/skills/synabun/synabunicon.png +0 -0
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun — cloudflared: the copy the tunnel runs
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// The public address (invite links, remote MCP) is a Cloudflare quick tunnel,
|
|
6
|
+
// and cloudflared is the program that opens it. SynaBun brings its own, so the
|
|
7
|
+
// tunnel works on a machine where nobody installed anything. Which copy runs,
|
|
8
|
+
// in this order:
|
|
9
|
+
//
|
|
10
|
+
// bundled the one inside an installer: <resources>/cloudflared/, only in
|
|
11
|
+
// a packaged application (lib/packaged-runtime.js)
|
|
12
|
+
// downloaded one this module fetched earlier, in the data home:
|
|
13
|
+
// DATA_HOME/runtime/cloudflared/<version>/ (outside data/, so no
|
|
14
|
+
// backup ever holds it)
|
|
15
|
+
// a download of the pinned release file (pins.js), into that place
|
|
16
|
+
// system a cloudflared on PATH, only when no download is possible: no
|
|
17
|
+
// file is published for this system, or the download failed
|
|
18
|
+
//
|
|
19
|
+
// SynaBun's own copies are the pinned binary or they are not run: each is
|
|
20
|
+
// compared with its pinned SHA-256 every time a tunnel is about to be started
|
|
21
|
+
// with it (for a look at the state of things: the first time this process
|
|
22
|
+
// meets it, and again whenever the file changes). A copy in the data home that
|
|
23
|
+
// fails is removed; the one in an installer is left where it is and never
|
|
24
|
+
// used. A cloudflared on PATH is the user's own installation and is run as it
|
|
25
|
+
// is.
|
|
26
|
+
//
|
|
27
|
+
// Where a copy of SynaBun's own lies is part of its check (install.js
|
|
28
|
+
// unsafePlace): the copy and its folder up to SynaBun's own cloudflared folder
|
|
29
|
+
// must not be a link, nor writable by anybody but their owner. The data home's
|
|
30
|
+
// folder is 0700. The file is hashed by its real path, through one handle, and
|
|
31
|
+
// confirm() checks, as the tunnel is about to start it, that it is still that
|
|
32
|
+
// very file (same device, inode, size and times): one that changed since its
|
|
33
|
+
// check is never run. What is left: Node starts a program by its path, not by
|
|
34
|
+
// an open file, so between confirm() and the start (no await between them)
|
|
35
|
+
// another process of the same user could still swap the file. Whoever can do
|
|
36
|
+
// that can change the data home, and SynaBun with it, anyway.
|
|
37
|
+
//
|
|
38
|
+
// A cloudflared on PATH that is really one of SynaBun's own copies (a link or a
|
|
39
|
+
// second name for one, or a file inside one of its folders, compared by what
|
|
40
|
+
// the file is, never by how a path spells it) is never the user's: it is not
|
|
41
|
+
// run, not even for `--version`. Nor is a second name of a copy SynaBun
|
|
42
|
+
// refused in this process, for whatever reason (where it lies, its bytes, a
|
|
43
|
+
// change before its start): what the refusal saw (the hashing handle, or the
|
|
44
|
+
// lstat that judged where it lies) is kept before the copy goes.
|
|
45
|
+
//
|
|
46
|
+
// One download at a time: everybody who asks while one runs waits for that
|
|
47
|
+
// one. A failed one is not remembered as a verdict: the next start tries again.
|
|
48
|
+
//
|
|
49
|
+
// inspect() what is there now; never downloads (GET /api/tunnel/status)
|
|
50
|
+
// ensure() the copy to run, downloading it when there is none ({ signal }: stop on request)
|
|
51
|
+
// confirm() the copy ensure() gave, checked once more right before it is started: the path to start
|
|
52
|
+
// cancel() stop the download that is running
|
|
53
|
+
// status() the last known state, without touching the disk
|
|
54
|
+
// killNow() kill a `cloudflared --version` that is still running (the server's exit)
|
|
55
|
+
|
|
56
|
+
import { spawn } from 'node:child_process';
|
|
57
|
+
import { accessSync, chmodSync, constants, lstatSync, readdirSync, realpathSync, rmdirSync, rmSync, statSync, unlinkSync, utimesSync } from 'node:fs';
|
|
58
|
+
import { delimiter, dirname, join, resolve } from 'node:path';
|
|
59
|
+
import { getDataHome } from '../../../lib/paths.js';
|
|
60
|
+
import { packagedRuntime } from '../../../lib/packaged-runtime.js';
|
|
61
|
+
import { getAugmentedPath } from '../augmented-path.js';
|
|
62
|
+
import { CloudflaredError, checkCloudflared, fileIdentity, installCloudflared, lookAtPlace, sameFile } from './install.js';
|
|
63
|
+
import { bundledCloudflaredPath, cloudflaredPinFor, readCloudflaredPins } from './pins.js';
|
|
64
|
+
import { endChild } from './tunnel.js';
|
|
65
|
+
|
|
66
|
+
/** A version folder nobody used for this long is removed when a newer cloudflared is put in place. */
|
|
67
|
+
const UNUSED_VERSION_MS = 30 * 24 * 60 * 60 * 1000;
|
|
68
|
+
const VERSION_FOLDER = /^\d{4}\.\d{1,2}\.\d{1,3}$/;
|
|
69
|
+
|
|
70
|
+
/** `DATA_HOME/runtime/cloudflared`: where a downloaded cloudflared lives. */
|
|
71
|
+
export function cloudflaredRoot(dataHome = getDataHome()) {
|
|
72
|
+
return join(dataHome, 'runtime', 'cloudflared');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** What a file or folder is, whatever path names it (links followed): its device and inode. null when it is not there. */
|
|
76
|
+
function identityOf(path) {
|
|
77
|
+
try {
|
|
78
|
+
const info = statSync(path, { bigint: true });
|
|
79
|
+
// A file system without inode numbers (FAT) gives every file the same one: there its real path stands in.
|
|
80
|
+
return info.ino ? `${info.dev}:${info.ino}` : `${info.dev}:${realpathSync(path)}`;
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* identityOf() of the file a fileIdentity() describes, from what was seen then, without asking its path again. null
|
|
88
|
+
* without an inode number: a file system that has none has no second names either.
|
|
89
|
+
*/
|
|
90
|
+
const identityKey = (file) => (file?.ino ? `${file.dev}:${file.ino}` : null);
|
|
91
|
+
|
|
92
|
+
/** fileIdentity() of what is at `path`, its last part not followed; null when nothing is there. */
|
|
93
|
+
function lstatIdentity(path) {
|
|
94
|
+
try { return fileIdentity(lstatSync(path, { bigint: true })); } catch { return null; }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Is the file at the real path `real` inside one of `folders` (identityOf of each)? Asked folder by folder, by what each is. */
|
|
98
|
+
function insideAny(real, folders) {
|
|
99
|
+
if (!folders.length) return false;
|
|
100
|
+
for (let dir = dirname(real); ; dir = dirname(dir)) {
|
|
101
|
+
if (folders.includes(identityOf(dir))) return true;
|
|
102
|
+
if (dirname(dir) === dir) return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* A cloudflared the user installed: the first one on PATH (with the folders installers use: lib/augmented-path.js),
|
|
108
|
+
* then the folders of Cloudflare's Windows installer, as its real path (a link on PATH is followed here, and what is
|
|
109
|
+
* run is the file that was looked at). Never one of SynaBun's own: a file inside one of the `exclude` folders, or
|
|
110
|
+
* one `isOwn` says is SynaBun's (manager.js: its copies, and every one it refused), whatever link or
|
|
111
|
+
* second name leads to it. Folders and files are compared by what they are, not by how a path spells them.
|
|
112
|
+
* Null when there is none.
|
|
113
|
+
*/
|
|
114
|
+
export function findSystemCloudflared({ platform = process.platform, env = process.env, pathValue = getAugmentedPath(), exclude = [], isOwn = () => false } = {}) {
|
|
115
|
+
const name = platform === 'win32' ? 'cloudflared.exe' : 'cloudflared';
|
|
116
|
+
const folders = String(pathValue || '').split(delimiter).filter(Boolean);
|
|
117
|
+
if (platform === 'win32') {
|
|
118
|
+
for (const base of [env.LOCALAPPDATA, env.ProgramFiles, env['ProgramFiles(x86)']]) if (base) folders.push(join(base, 'cloudflared'));
|
|
119
|
+
}
|
|
120
|
+
const ours = exclude.filter(Boolean).map(identityOf).filter(Boolean);
|
|
121
|
+
for (const folder of folders) {
|
|
122
|
+
try {
|
|
123
|
+
const candidate = resolve(folder, name);
|
|
124
|
+
if (!statSync(candidate).isFile()) continue;
|
|
125
|
+
if (platform !== 'win32') accessSync(candidate, constants.X_OK);
|
|
126
|
+
const real = realpathSync(candidate);
|
|
127
|
+
if (insideAny(real, ours) || isOwn(real)) continue;
|
|
128
|
+
return real;
|
|
129
|
+
} catch { /* not here */ }
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** `cloudflared --version` runs that have not exited yet: killNow() kills them at once. */
|
|
135
|
+
const probing = new Set();
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* What `cloudflared --version` says ("2026.10.0"), "unknown" when it answers something else, null when it does not
|
|
139
|
+
* run, does not answer within `timeoutMs`, or `signal` stops it. It settles by then, whatever the program does: one
|
|
140
|
+
* that is still there is asked to end, and killed when it is still there `killAfterMs` later (tunnel.js endChild).
|
|
141
|
+
*/
|
|
142
|
+
export function probeCloudflaredVersion(path, { timeoutMs = 8000, killAfterMs = 1000, signal } = {}) {
|
|
143
|
+
return new Promise((done) => {
|
|
144
|
+
let child = null;
|
|
145
|
+
let out = '';
|
|
146
|
+
let settled = false;
|
|
147
|
+
let timer = null;
|
|
148
|
+
const settle = (value) => {
|
|
149
|
+
if (settled) return;
|
|
150
|
+
settled = true;
|
|
151
|
+
clearTimeout(timer);
|
|
152
|
+
signal?.removeEventListener('abort', giveUp);
|
|
153
|
+
done(value);
|
|
154
|
+
};
|
|
155
|
+
function giveUp() {
|
|
156
|
+
settle(null);
|
|
157
|
+
if (child) endChild(child, { killAfterMs });
|
|
158
|
+
}
|
|
159
|
+
if (signal?.aborted) { settle(null); return; }
|
|
160
|
+
try {
|
|
161
|
+
child = spawn(path, ['--version'], { stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
|
|
162
|
+
} catch {
|
|
163
|
+
settle(null);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (child.pid) { probing.add(child); child.once('exit', () => probing.delete(child)); }
|
|
167
|
+
timer = setTimeout(giveUp, timeoutMs);
|
|
168
|
+
signal?.addEventListener('abort', giveUp, { once: true });
|
|
169
|
+
child.stdout.setEncoding('utf8');
|
|
170
|
+
child.stdout.on('data', (chunk) => { if (out.length < 4096) out += chunk; });
|
|
171
|
+
child.on('error', () => settle(null));
|
|
172
|
+
child.on('close', (code) => settle(code === 0 ? (/cloudflared version (\d[\w.-]*)/i.exec(out)?.[1] || 'unknown') : null));
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const describe = (error) => ({
|
|
177
|
+
code: error instanceof CloudflaredError ? error.code : 'unknown',
|
|
178
|
+
message: error?.message || String(error),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const cancelled = () => new CloudflaredError('cancelled', 'Stopped.');
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* @param {object} [o]
|
|
185
|
+
* @param {string} [o.dataHome]
|
|
186
|
+
* @param {{ resources: string } | null} [o.packaged] lib/packaged-runtime.js packagedRuntime(); null: not a packaged application
|
|
187
|
+
* @param {object} [o.pins] pins.js readCloudflaredPins() (read here when it is left out; unreadable pins never throw)
|
|
188
|
+
* @param {typeof fetch} [o.fetchImpl]
|
|
189
|
+
* @param {() => (string | null)} [o.findSystem]
|
|
190
|
+
* @param {string} [o.pathValue] the PATH the user's own cloudflared is looked for in (findSystemCloudflared)
|
|
191
|
+
* @param {(path: string, o?: { signal?: AbortSignal }) => Promise<string | null>} [o.probeVersion]
|
|
192
|
+
* @param {(line: string) => void} [o.log]
|
|
193
|
+
*/
|
|
194
|
+
export function createCloudflaredManager({
|
|
195
|
+
dataHome = getDataHome(),
|
|
196
|
+
packaged = packagedRuntime(),
|
|
197
|
+
pins,
|
|
198
|
+
platform = process.platform,
|
|
199
|
+
arch = process.arch,
|
|
200
|
+
fetchImpl,
|
|
201
|
+
findSystem,
|
|
202
|
+
pathValue,
|
|
203
|
+
probeVersion = probeCloudflaredVersion,
|
|
204
|
+
stallMs,
|
|
205
|
+
log = () => {},
|
|
206
|
+
now = () => Date.now(),
|
|
207
|
+
} = {}) {
|
|
208
|
+
// The pins ship with the application. Should they ever be unreadable (a damaged install), the server still
|
|
209
|
+
// starts and everything else works: nothing is downloaded by guesswork, and only a cloudflared the user
|
|
210
|
+
// installed can run, as on a system no file is pinned for. The log says what is wrong.
|
|
211
|
+
if (!pins) {
|
|
212
|
+
try {
|
|
213
|
+
pins = readCloudflaredPins();
|
|
214
|
+
} catch (error) {
|
|
215
|
+
pins = Object.freeze({ version: null, platforms: Object.freeze({}) });
|
|
216
|
+
log(`[cloudflared] ${error?.message || error} SynaBun downloads no tunnel program until this is repaired; a cloudflared on PATH is used when there is one.`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const root = cloudflaredRoot(dataHome);
|
|
220
|
+
const pin = cloudflaredPinFor(pins, platform, arch);
|
|
221
|
+
const bundledPath = packaged?.resources ? bundledCloudflaredPath(packaged.resources, platform) : null;
|
|
222
|
+
const bundledFolder = bundledPath ? dirname(bundledPath) : null;
|
|
223
|
+
const downloadedPath = pin ? join(root, pin.version, pin.binary.name) : null;
|
|
224
|
+
const system = findSystem || (() => findSystemCloudflared({ platform, pathValue, exclude: [root, bundledFolder], isOwn: isOwnCopy }));
|
|
225
|
+
|
|
226
|
+
const checked = new Map(); // real path → { stat, verdict }: a copy is hashed once, and again when it changes
|
|
227
|
+
const marks = new Map(); // real path → { file, area }: what the last check of a copy read, and the folder it was checked in
|
|
228
|
+
const rejected = new Set(); // identityOf() of every copy of SynaBun's own that was refused, for whatever reason: never run under another name
|
|
229
|
+
const versions = new Map(); // a system copy's real path → { mtimeMs, version }
|
|
230
|
+
let download = null; // { promise, stop, progress } while one runs
|
|
231
|
+
let current = null; // { path, source, version }: the copy found last
|
|
232
|
+
let lastError = null; // why the last attempt gave no copy
|
|
233
|
+
let fallback = null; // why the copy in use is not a pinned one
|
|
234
|
+
let touched = false;
|
|
235
|
+
let unread = false; // the last look found a downloaded copy it could not read (see local)
|
|
236
|
+
const noted = new Set();
|
|
237
|
+
|
|
238
|
+
const once = (key, line) => { if (!noted.has(key)) { noted.add(key); log(line); } };
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Is the file at `path` (a real path) the pinned binary? For a look at the state of things it is hashed once per
|
|
242
|
+
* file as it is on disk (its identity: inode, size, times). `again` hashes it whatever is remembered: that is how
|
|
243
|
+
* the copy a tunnel is about to be started with is checked, so a file changed under the same size and time is
|
|
244
|
+
* still never run.
|
|
245
|
+
*/
|
|
246
|
+
async function isPinned(path, again = false, signal) {
|
|
247
|
+
let info;
|
|
248
|
+
try { info = fileIdentity(statSync(path, { bigint: true })); } catch { checked.delete(path); return { ok: false, reason: 'missing' }; }
|
|
249
|
+
const known = checked.get(path);
|
|
250
|
+
if (!again && known && sameFile(known.stat, info)) return known.verdict;
|
|
251
|
+
const verdict = await checkCloudflared(path, pin, { signal });
|
|
252
|
+
// A file that could not be read just now, or that changed while it was read, is not remembered as anything:
|
|
253
|
+
// the next look reads it again.
|
|
254
|
+
if (verdict.reason === 'unreadable' || verdict.reason === 'changed') checked.delete(path);
|
|
255
|
+
else checked.set(path, { stat: info, verdict });
|
|
256
|
+
return verdict;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* A copy of SynaBun's own was refused, and `file` (fileIdentity) is what the refusal saw of it: the bytes it is are
|
|
261
|
+
* never run under another name (a link or a second name on PATH) for the rest of this process. Called before the
|
|
262
|
+
* copy goes, with the identity the refusal itself read, never with a later look at its path.
|
|
263
|
+
*/
|
|
264
|
+
function refuse(file) {
|
|
265
|
+
const key = identityKey(file);
|
|
266
|
+
if (key) rejected.add(key);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* One of SynaBun's own copies, at `path` in its folder `area`: where it lies first (no link, nobody else may
|
|
271
|
+
* write: install.js lookAtPlace), then its bytes, by its real path. `{ ok, reason, path }` (the real path). One
|
|
272
|
+
* that is refused is known by what refused it: the file the hash read, or the one the look at its place saw.
|
|
273
|
+
*/
|
|
274
|
+
async function ownCopy(path, area, again, signal) {
|
|
275
|
+
const place = lookAtPlace(path, area);
|
|
276
|
+
if (place.unsafe) { refuse(place.file); return { ok: false, reason: place.unsafe.reason }; }
|
|
277
|
+
let real;
|
|
278
|
+
let realArea;
|
|
279
|
+
try { real = realpathSync(path); realArea = realpathSync(area); } catch { return { ok: false, reason: 'missing' }; }
|
|
280
|
+
const verdict = await isPinned(real, again, signal);
|
|
281
|
+
if (verdict.ok) marks.set(real, { file: verdict.file ?? checked.get(real)?.stat, area: realArea });
|
|
282
|
+
else if (verdict.reason !== 'missing') refuse(verdict.file ?? place.file);
|
|
283
|
+
return { ok: verdict.ok, reason: verdict.reason, path: real };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Why SynaBun's own folder in the data home cannot hold its cloudflared, or null (also when it is not there yet):
|
|
288
|
+
* `link`, `not-a-folder`, `owner` (another user's). One of this user's that lets others in is set back to 0700.
|
|
289
|
+
*/
|
|
290
|
+
function unsafeRoot() {
|
|
291
|
+
let info;
|
|
292
|
+
try { info = lstatSync(root); } catch { return null; }
|
|
293
|
+
if (info.isSymbolicLink()) return 'link';
|
|
294
|
+
if (!info.isDirectory()) return 'not-a-folder';
|
|
295
|
+
if (process.platform === 'win32') return null;
|
|
296
|
+
const uid = process.getuid?.();
|
|
297
|
+
if (uid !== undefined && info.uid !== uid) return 'owner';
|
|
298
|
+
if (info.mode & 0o077) { try { chmodSync(root, 0o700); } catch { return 'writable'; } }
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** What stands in the version's place in SynaBun's own folder goes: the copy, or a link there (the link itself, never what it points to). */
|
|
303
|
+
function removeDownloaded() {
|
|
304
|
+
const entry = join(root, pin.version);
|
|
305
|
+
try {
|
|
306
|
+
const info = lstatSync(entry);
|
|
307
|
+
if (info.isSymbolicLink() || !info.isDirectory()) { try { unlinkSync(entry); } catch { rmdirSync(entry); } }
|
|
308
|
+
else rmSync(downloadedPath, { force: true, recursive: true });
|
|
309
|
+
} catch { /* not there, or the next look removes it */ }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The pinned copy that is on this disk already, or null. Removes a downloaded copy that is not the pinned
|
|
314
|
+
* binary, or that is not kept as it should be. `again`: compare with the pin now, whatever an earlier look found
|
|
315
|
+
* (see isPinned).
|
|
316
|
+
*/
|
|
317
|
+
async function local(again = false, signal) {
|
|
318
|
+
unread = false;
|
|
319
|
+
if (!pin) return null;
|
|
320
|
+
if (bundledPath) {
|
|
321
|
+
const have = await ownCopy(bundledPath, bundledFolder, again, signal);
|
|
322
|
+
if (have.ok) return { path: have.path, source: 'bundled', version: pin.version };
|
|
323
|
+
if (have.reason !== 'missing') once(`bundled:${have.reason}`, `[cloudflared] The copy inside the application is not the pinned cloudflared ${pin.version}, or not kept where only its owner can change it (${have.reason}); it is not used.`);
|
|
324
|
+
}
|
|
325
|
+
// SynaBun's own folder in the data home: a link, or another user's folder, is never used or written through.
|
|
326
|
+
const unsafe = unsafeRoot();
|
|
327
|
+
if (unsafe) {
|
|
328
|
+
once(`root:${unsafe}`, `[cloudflared] ${root} is not a folder of SynaBun's own (${unsafe}): nothing in it is run, and nothing is downloaded into it.`);
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
const have = await ownCopy(downloadedPath, root, again, signal);
|
|
332
|
+
if (have.ok) {
|
|
333
|
+
if (!touched) { touched = true; try { const at = new Date(now()); utimesSync(dirname(have.path), at, at); } catch { /* a mark for housekeeping only */ } }
|
|
334
|
+
return { path: have.path, source: 'downloaded', version: pin.version };
|
|
335
|
+
}
|
|
336
|
+
if (have.reason === 'unreadable' || have.reason === 'changed') {
|
|
337
|
+
// It could not be read to be compared (too many open files, a file another program holds), or it changed
|
|
338
|
+
// while it was read: that says nothing about what it is. Not run, and not removed either.
|
|
339
|
+
unread = true;
|
|
340
|
+
once('unreadable', '[cloudflared] The copy in the data folder could not be read to be checked; it is not used until it can be.');
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
if (have.reason !== 'missing' && !download) {
|
|
344
|
+
// Never run, never kept: whatever is there instead of the pinned binary, kept as it should be, goes.
|
|
345
|
+
removeDownloaded();
|
|
346
|
+
log(`[cloudflared] The copy in the data folder was not the pinned cloudflared ${pin.version} (${have.reason}); it was removed.`);
|
|
347
|
+
}
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Version folders of the data home that nothing used for a month. The one in use is never among them. */
|
|
352
|
+
function pruneUnusedVersions() {
|
|
353
|
+
let names = [];
|
|
354
|
+
try { names = readdirSync(root); } catch { return; }
|
|
355
|
+
for (const name of names) {
|
|
356
|
+
if (!VERSION_FOLDER.test(name) || name === pin?.version) continue;
|
|
357
|
+
try {
|
|
358
|
+
if (now() - statSync(join(root, name)).mtimeMs > UNUSED_VERSION_MS) rmSync(join(root, name), { recursive: true, force: true });
|
|
359
|
+
} catch { /* in use, or gone */ }
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** The one download: whoever asks while it runs gets the same one. */
|
|
364
|
+
function fetchOnce() {
|
|
365
|
+
// One that was stopped is still winding down (removing what it had): whoever asks now did not ask for that
|
|
366
|
+
// one. A new download begins when it is gone, never beside it.
|
|
367
|
+
if (download?.stop.signal.aborted) return download.promise.catch(() => {}).then(() => fetchOnce());
|
|
368
|
+
if (download) return download.promise;
|
|
369
|
+
const stop = new AbortController();
|
|
370
|
+
const progress = { received: 0, total: pin.size };
|
|
371
|
+
log(`[cloudflared] Downloading cloudflared ${pin.version} for ${pin.key} (${(pin.size / 1024 / 1024).toFixed(1)} MB) from ${pin.url}`);
|
|
372
|
+
const promise = installCloudflared({
|
|
373
|
+
pin, root, fetchImpl, stallMs, platform, signal: stop.signal,
|
|
374
|
+
onProgress: ({ received }) => { progress.received = received; },
|
|
375
|
+
}).then((placed) => {
|
|
376
|
+
log(`[cloudflared] cloudflared ${pin.version} is in place, checksum verified: ${placed.path}`);
|
|
377
|
+
pruneUnusedVersions();
|
|
378
|
+
return placed;
|
|
379
|
+
}, (error) => {
|
|
380
|
+
log(`[cloudflared] The download of cloudflared ${pin.version} did not complete (${describe(error).code}): ${error?.message || error}`);
|
|
381
|
+
throw error;
|
|
382
|
+
}).finally(() => { if (download?.promise === promise) download = null; });
|
|
383
|
+
download = { promise, stop, progress };
|
|
384
|
+
return promise;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Is the file at `path` one of SynaBun's own copies: one in place (in the application, in the data home), one that
|
|
389
|
+
* was refused here, or a file inside one of SynaBun's folders for cloudflared? Compared by what the file is
|
|
390
|
+
* (its device and inode, links followed), never by how a path spells it. What cannot be looked at counts as one.
|
|
391
|
+
*/
|
|
392
|
+
function isOwnCopy(path) {
|
|
393
|
+
const id = identityOf(path);
|
|
394
|
+
if (!id || rejected.has(id)) return true;
|
|
395
|
+
if ([bundledPath, downloadedPath].some((own) => own && identityOf(own) === id)) return true;
|
|
396
|
+
let real;
|
|
397
|
+
try { real = realpathSync(path); } catch { return true; }
|
|
398
|
+
return insideAny(real, [root, bundledFolder].filter(Boolean).map(identityOf).filter(Boolean));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** The real path of a cloudflared on PATH, or null when it is SynaBun's own copy by another name (said once). */
|
|
402
|
+
function foreign(path) {
|
|
403
|
+
let real;
|
|
404
|
+
try { real = realpathSync(path); } catch { return null; }
|
|
405
|
+
if (!isOwnCopy(real)) return real;
|
|
406
|
+
once(`alias:${real}`, `[cloudflared] ${path} on PATH is SynaBun's own copy of cloudflared, not one installed on this computer: it is not run.`);
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** The user's own cloudflared, with the version it reports; null when there is none that runs. */
|
|
411
|
+
async function systemCopy(signal) {
|
|
412
|
+
let path = null;
|
|
413
|
+
try { path = system(); } catch { path = null; }
|
|
414
|
+
if (!path) return null;
|
|
415
|
+
// Looked at before it is run, even for --version: never one of SynaBun's own copies, whatever name leads to it.
|
|
416
|
+
const real = foreign(path);
|
|
417
|
+
if (!real) return null;
|
|
418
|
+
let mtimeMs = 0;
|
|
419
|
+
try { mtimeMs = statSync(real).mtimeMs; } catch { /* asked below */ }
|
|
420
|
+
const known = versions.get(real);
|
|
421
|
+
const version = known && known.mtimeMs === mtimeMs ? known.version : await probeVersion(real, { signal });
|
|
422
|
+
if (signal?.aborted) throw cancelled();
|
|
423
|
+
versions.set(real, { mtimeMs, version });
|
|
424
|
+
return version ? { path: real, source: 'system', version } : null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** No pinned copy could be had (`reason`): the user's own cloudflared when there is one, the reason when not. */
|
|
428
|
+
async function withoutDownload(reason, signal) {
|
|
429
|
+
const copy = await systemCopy(signal);
|
|
430
|
+
if (!copy) {
|
|
431
|
+
current = null;
|
|
432
|
+
fallback = null;
|
|
433
|
+
lastError = describe(reason);
|
|
434
|
+
throw reason;
|
|
435
|
+
}
|
|
436
|
+
once(`system:${copy.path}`, `[cloudflared] Using the cloudflared on PATH (${copy.path}, ${copy.version}): ${reason.message}`);
|
|
437
|
+
current = copy;
|
|
438
|
+
fallback = describe(reason);
|
|
439
|
+
lastError = null;
|
|
440
|
+
return { ...copy };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const found = (copy) => {
|
|
444
|
+
current = copy;
|
|
445
|
+
fallback = null;
|
|
446
|
+
lastError = null;
|
|
447
|
+
return { ...copy };
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
async function ensure({ signal } = {}) {
|
|
451
|
+
if (signal?.aborted) throw cancelled();
|
|
452
|
+
// The copy that is about to run is compared with its pin now, not by what an earlier look remembered.
|
|
453
|
+
const have = await local(true, signal);
|
|
454
|
+
if (have) return found(have);
|
|
455
|
+
if (signal?.aborted) throw cancelled();
|
|
456
|
+
if (!pin) return withoutDownload(new CloudflaredError('unsupported', `Cloudflare publishes no cloudflared for this system (${platform}-${arch}).`), signal);
|
|
457
|
+
lastError = null;
|
|
458
|
+
try {
|
|
459
|
+
await fetchOnce();
|
|
460
|
+
} catch (error) {
|
|
461
|
+
if (error instanceof CloudflaredError && error.code === 'cancelled') { current = null; throw error; }
|
|
462
|
+
if (signal?.aborted) throw cancelled();
|
|
463
|
+
return withoutDownload(error instanceof CloudflaredError ? error : new CloudflaredError('disk', error?.message || String(error), { cause: error }), signal);
|
|
464
|
+
}
|
|
465
|
+
if (signal?.aborted) throw cancelled();
|
|
466
|
+
// What was just put in place is met like any copy: compared with its pin before it is handed out.
|
|
467
|
+
const fresh = await local(true, signal);
|
|
468
|
+
if (fresh) return found(fresh);
|
|
469
|
+
return withoutDownload(unread
|
|
470
|
+
? new CloudflaredError('disk', 'The downloaded cloudflared could not be read back to be checked.')
|
|
471
|
+
: new CloudflaredError('checksum', 'The downloaded cloudflared did not pass its check and was removed.'), signal);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* The copy ensure() handed out, checked once more as it is about to be started (tunnel.js calls this and spawns
|
|
476
|
+
* the path it returns at once, with nothing in between). SynaBun's own copy must still lie where only its owner
|
|
477
|
+
* can change it and be the very file whose bytes ensure() hashed (same device, inode, size and times); a
|
|
478
|
+
* cloudflared on PATH must still not be one of SynaBun's own. Returns the real path to start; throws a
|
|
479
|
+
* CloudflaredError (nothing is started) otherwise. A downloaded copy that changed is removed: the next start
|
|
480
|
+
* fetches the pinned one again.
|
|
481
|
+
*/
|
|
482
|
+
function confirm(copy) {
|
|
483
|
+
if (copy?.source === 'system') {
|
|
484
|
+
const real = foreign(copy.path);
|
|
485
|
+
if (!real) throw new CloudflaredError('launch', 'The cloudflared on PATH is SynaBun\'s own copy by another name; it was not started.');
|
|
486
|
+
return real;
|
|
487
|
+
}
|
|
488
|
+
const mark = marks.get(copy?.path);
|
|
489
|
+
// Where it lies and which file it is, in one look (its last part not followed).
|
|
490
|
+
const place = mark ? lookAtPlace(copy.path, mark.area) : null;
|
|
491
|
+
if (place && !place.unsafe && sameFile(mark.file, place.file)) return copy.path;
|
|
492
|
+
// Another file, other bytes, a link, a folder others may write: what was checked is not what is there. What is
|
|
493
|
+
// there is never run, by this name or another one.
|
|
494
|
+
refuse(place ? place.file : lstatIdentity(copy?.path));
|
|
495
|
+
marks.delete(copy?.path);
|
|
496
|
+
checked.delete(copy?.path);
|
|
497
|
+
const remove = copy?.source === 'downloaded' && !!pin && !unsafeRoot();
|
|
498
|
+
if (remove) removeDownloaded();
|
|
499
|
+
log(`[cloudflared] cloudflared changed after it was checked (${copy?.path}); it was not started${remove ? ' and was removed' : ''}.`);
|
|
500
|
+
throw new CloudflaredError('checksum', 'cloudflared changed after it was checked against its pinned checksum; it was not started.');
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function inspect() {
|
|
504
|
+
if (download) return status();
|
|
505
|
+
const have = await local();
|
|
506
|
+
if (have) { found(have); return status(); }
|
|
507
|
+
// The user's own cloudflared is what runs only where no pinned one can be had: on a system Cloudflare
|
|
508
|
+
// publishes none for, or after a download that failed. Otherwise a start downloads the pinned one.
|
|
509
|
+
current = current?.source === 'system' || !pin ? await systemCopy() : null;
|
|
510
|
+
return status();
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function status() {
|
|
514
|
+
const base = {
|
|
515
|
+
pinned: pins.version,
|
|
516
|
+
supported: !!pin,
|
|
517
|
+
download: pin ? { size: pin.size } : null,
|
|
518
|
+
progress: null,
|
|
519
|
+
source: null,
|
|
520
|
+
version: null,
|
|
521
|
+
error: null,
|
|
522
|
+
fallback: null,
|
|
523
|
+
};
|
|
524
|
+
if (download) {
|
|
525
|
+
const { received, total } = download.progress;
|
|
526
|
+
return { ...base, state: 'downloading', progress: { received, total, percent: total ? Math.min(100, Math.floor((received / total) * 100)) : 0 } };
|
|
527
|
+
}
|
|
528
|
+
if (current) return { ...base, state: 'ready', source: current.source, version: current.version, fallback: current.source === 'system' ? fallback : null };
|
|
529
|
+
if (lastError) return { ...base, state: 'failed', error: lastError };
|
|
530
|
+
return { ...base, state: pin ? 'missing' : 'unsupported' };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function cancel() {
|
|
534
|
+
if (!download) return false;
|
|
535
|
+
download.stop.abort();
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** At the server's exit: a `cloudflared --version` that is still running is killed now. */
|
|
540
|
+
function killNow() {
|
|
541
|
+
for (const child of probing) { try { child.kill('SIGKILL'); } catch { /* gone already */ } }
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return { ensure, confirm, inspect, status, cancel, killNow, root, pin };
|
|
545
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun — cloudflared: which one
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// The one place that knows cloudflared, the program behind the public address
|
|
6
|
+
// (invite links, remote MCP): pins.json beside this file holds the version,
|
|
7
|
+
// Cloudflare's own release file for each platform, the SHA-256 of that file
|
|
8
|
+
// and of the binary inside it, and where each checksum comes from. The running
|
|
9
|
+
// application reads it here (manager.js downloads what it names on first use)
|
|
10
|
+
// and so does an application build (packaging/lib/stage.mjs and verify.mjs
|
|
11
|
+
// import this module from the application they stage), so an installer never
|
|
12
|
+
// carries a cloudflared the application would not have fetched itself.
|
|
13
|
+
//
|
|
14
|
+
// To move to another release: change `version`, `baseUrl` and `release`,
|
|
15
|
+
// download each file, and replace `sha256` / `size` with what GitHub publishes
|
|
16
|
+
// for the asset and `binary` with the checksum in the release's notes (for an
|
|
17
|
+
// archive: of the cloudflared inside it). tests/cloudflared-pins.test.mjs
|
|
18
|
+
// checks the shape; nothing checks the values but a download.
|
|
19
|
+
//
|
|
20
|
+
// Only node:fs, node:path and node:url here: a build imports this on the Node
|
|
21
|
+
// of the build machine, before the application has a dependency installed.
|
|
22
|
+
|
|
23
|
+
import { readFileSync } from 'node:fs';
|
|
24
|
+
import { join } from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
export const PINS_PATH = fileURLToPath(new URL('./pins.json', import.meta.url));
|
|
28
|
+
|
|
29
|
+
/** Where every pinned file is: Cloudflare's releases on GitHub, over HTTPS, and nowhere else. */
|
|
30
|
+
export const RELEASE_BASE = 'https://github.com/cloudflare/cloudflared/releases/download/';
|
|
31
|
+
|
|
32
|
+
/** The folder of a packaged application that holds its cloudflared, beside `runtime/` and `app/`. */
|
|
33
|
+
export const BUNDLED_FOLDER = 'cloudflared';
|
|
34
|
+
|
|
35
|
+
/** Which native format each operating system runs (the names packaging/lib/util.mjs gives them). */
|
|
36
|
+
export const PLATFORM_FORMAT = Object.freeze({ darwin: 'macho', linux: 'elf', win32: 'pe' });
|
|
37
|
+
|
|
38
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
39
|
+
const VERSION = /^\d{4}\.\d{1,2}\.\d{1,3}$/;
|
|
40
|
+
const FILE = /^cloudflared-[a-z0-9]+-[a-z0-9]+(?:\.[a-z0-9]+)?$/;
|
|
41
|
+
const KEY = /^(darwin|linux|win32)-(arm64|x64)$/;
|
|
42
|
+
const isSize = (value) => Number.isInteger(value) && value > 0 && value < 512 * 1024 * 1024;
|
|
43
|
+
|
|
44
|
+
/** The file name cloudflared has once it is in place. */
|
|
45
|
+
export function binaryName(platform = process.platform) {
|
|
46
|
+
return platform === 'win32' ? 'cloudflared.exe' : 'cloudflared';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The cloudflared of a packaged application: `<resources>/cloudflared/cloudflared[.exe]`. */
|
|
50
|
+
export function bundledCloudflaredPath(resources, platform = process.platform) {
|
|
51
|
+
return join(resources, BUNDLED_FOLDER, binaryName(platform));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The pins as an object every reader can rely on, or an Error that lists what is wrong with them. A pin is
|
|
56
|
+
* `{ key, platform, arch, version, file, url, sha256, size, archive, member, binary: { name, sha256, size, format } }`:
|
|
57
|
+
* `archive` is null for a release file that is the binary itself, and then `binary` repeats the file's checksum.
|
|
58
|
+
* @param {object} raw pins.json, parsed
|
|
59
|
+
*/
|
|
60
|
+
export function validateCloudflaredPins(raw) {
|
|
61
|
+
const problems = [];
|
|
62
|
+
const version = String(raw?.version || '');
|
|
63
|
+
if (!VERSION.test(version)) problems.push(`version "${version}" is not a cloudflared release (YYYY.M.N)`);
|
|
64
|
+
const baseUrl = String(raw?.baseUrl || '');
|
|
65
|
+
if (baseUrl !== `${RELEASE_BASE}${version}/`) problems.push(`baseUrl must be ${RELEASE_BASE}${version}/`);
|
|
66
|
+
if (typeof raw?.origin !== 'string' || !raw.origin.trim()) problems.push('origin (where the checksums come from) is missing');
|
|
67
|
+
const platforms = {};
|
|
68
|
+
for (const [key, entry] of Object.entries(raw?.platforms || {})) {
|
|
69
|
+
const at = KEY.exec(key);
|
|
70
|
+
if (!at) { problems.push(`"${key}" is not a platform (darwin|linux|win32)-(arm64|x64)`); continue; }
|
|
71
|
+
const [, platform, arch] = at;
|
|
72
|
+
const file = String(entry?.file || '');
|
|
73
|
+
const archive = entry?.archive ?? null;
|
|
74
|
+
const binary = entry?.binary || {};
|
|
75
|
+
const wrong = [];
|
|
76
|
+
if (!FILE.test(file)) wrong.push(`file "${file}" is not a cloudflared release file`);
|
|
77
|
+
if (!SHA256.test(String(entry?.sha256 || ''))) wrong.push('sha256 is not 64 hex digits');
|
|
78
|
+
if (!isSize(entry?.size)) wrong.push('size is not a number of bytes');
|
|
79
|
+
if (archive !== null && archive !== 'tgz') wrong.push(`archive "${archive}" is not a kind this reads (tgz)`);
|
|
80
|
+
if (archive && entry?.member !== 'cloudflared') wrong.push('member (the file inside the archive) must be "cloudflared"');
|
|
81
|
+
if (!SHA256.test(String(binary.sha256 || ''))) wrong.push('binary.sha256 is not 64 hex digits');
|
|
82
|
+
if (!isSize(binary.size)) wrong.push('binary.size is not a number of bytes');
|
|
83
|
+
if (binary.format !== PLATFORM_FORMAT[platform]) wrong.push(`binary.format must be "${PLATFORM_FORMAT[platform]}" for ${platform}`);
|
|
84
|
+
if (!archive && (binary.sha256 !== entry?.sha256 || binary.size !== entry?.size)) wrong.push('the file is the binary: binary must repeat its sha256 and size');
|
|
85
|
+
if (wrong.length) { problems.push(...wrong.map((each) => `${key}: ${each}`)); continue; }
|
|
86
|
+
platforms[key] = Object.freeze({
|
|
87
|
+
key, platform, arch, version, file,
|
|
88
|
+
url: `${baseUrl}${file}`,
|
|
89
|
+
sha256: entry.sha256,
|
|
90
|
+
size: entry.size,
|
|
91
|
+
archive,
|
|
92
|
+
member: archive ? entry.member : null,
|
|
93
|
+
binary: Object.freeze({ name: binaryName(platform), sha256: binary.sha256, size: binary.size, format: binary.format }),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (!Object.keys(platforms).length && !problems.length) problems.push('no platform is pinned');
|
|
97
|
+
if (problems.length) throw new Error(`The cloudflared pins are not usable: ${problems.join('; ')}`);
|
|
98
|
+
return Object.freeze({
|
|
99
|
+
version, baseUrl,
|
|
100
|
+
release: String(raw.release || ''),
|
|
101
|
+
license: String(raw.license || ''),
|
|
102
|
+
origin: raw.origin,
|
|
103
|
+
platforms: Object.freeze(platforms),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** pins.json, read and checked. Throws when the file is missing or wrong: nothing is downloaded by guesswork. */
|
|
108
|
+
export function readCloudflaredPins(path = PINS_PATH) {
|
|
109
|
+
return validateCloudflaredPins(JSON.parse(readFileSync(path, 'utf8')));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The pin of a platform, or null when Cloudflare publishes no file for it (or none is pinned here). */
|
|
113
|
+
export function cloudflaredPinFor(pins, platform = process.platform, arch = process.arch) {
|
|
114
|
+
return pins?.platforms?.[`${platform}-${arch}`] || null;
|
|
115
|
+
}
|