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,533 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun — cloudflared: fetch, check, unpack, put in place
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// How a pinned cloudflared (pins.js) gets onto a disk. One sequence, used by
|
|
6
|
+
// the running application (manager.js, into the data home) and, from the
|
|
7
|
+
// unpacking on, by an application build (packaging/lib/stage.mjs):
|
|
8
|
+
//
|
|
9
|
+
// downloadPinned the release file, over HTTPS and nothing else (every
|
|
10
|
+
// redirect too), into a file of its own; stopped at the
|
|
11
|
+
// pinned size, then compared with the pinned SHA-256
|
|
12
|
+
// binaryFromRelease the cloudflared of that file: the file itself, or the
|
|
13
|
+
// one member of the archive, read out by this module
|
|
14
|
+
// (no tar program is started, no name in the archive is
|
|
15
|
+
// ever used as a path); compared with the pinned SHA-256
|
|
16
|
+
// of the binary, then made executable
|
|
17
|
+
// installCloudflared both, in a folder of its own beside the destination,
|
|
18
|
+
// and one rename at the end: the destination holds the
|
|
19
|
+
// whole checked binary or nothing
|
|
20
|
+
//
|
|
21
|
+
// A file that fails a check is removed in the same step and never renamed into
|
|
22
|
+
// place; whatever a stopped process left behind is removed by the next attempt
|
|
23
|
+
// (sweepStaleWork). Every failure is a CloudflaredError with a `code` the page
|
|
24
|
+
// has words for.
|
|
25
|
+
//
|
|
26
|
+
// SynaBun's own folder for its copy (DATA_HOME/runtime/cloudflared) is one only
|
|
27
|
+
// its owner can open (0700, ownFolder); nothing is put in it through a link. A
|
|
28
|
+
// copy that is about to run is checked where it lies (unsafePlace: no link, no
|
|
29
|
+
// folder others may write) and hashed through one handle opened without
|
|
30
|
+
// following a link (checkCloudflared), which says what file it read
|
|
31
|
+
// (fileIdentity): manager.js compares that with the file once more as it is
|
|
32
|
+
// started.
|
|
33
|
+
//
|
|
34
|
+
// Only node: modules here (see pins.js).
|
|
35
|
+
|
|
36
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
37
|
+
import { chmodSync, constants, createReadStream, existsSync, lstatSync, mkdirSync, readdirSync, renameSync, rmdirSync, rmSync, statSync, unlinkSync } from 'node:fs';
|
|
38
|
+
import { copyFile, lstat, open } from 'node:fs/promises';
|
|
39
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
40
|
+
import { createGunzip } from 'node:zlib';
|
|
41
|
+
|
|
42
|
+
/** What can go wrong, by the name the page shows a sentence for. */
|
|
43
|
+
export const CLOUDFLARED_ERROR_CODES = Object.freeze([
|
|
44
|
+
'unsupported', // Cloudflare publishes no cloudflared for this system
|
|
45
|
+
'offline', // the download server was not reached
|
|
46
|
+
'http', // it answered, but not with the file
|
|
47
|
+
'interrupted', // the transfer ended before the file did
|
|
48
|
+
'timeout', // nothing arrived for too long
|
|
49
|
+
'checksum', // what arrived is not the pinned file
|
|
50
|
+
'unpack', // the archive does not hold the pinned binary
|
|
51
|
+
'disk', // the data home could not be written (no room, no permission)
|
|
52
|
+
'cancelled', // stopped on request
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
export class CloudflaredError extends Error {
|
|
56
|
+
/** @param {string} code one of CLOUDFLARED_ERROR_CODES */
|
|
57
|
+
constructor(code, message, { cause, detail } = {}) {
|
|
58
|
+
super(message, cause ? { cause } : undefined);
|
|
59
|
+
this.name = 'CloudflaredError';
|
|
60
|
+
this.code = code;
|
|
61
|
+
if (detail) this.detail = detail;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const WORK_PREFIX = '.work-';
|
|
66
|
+
const STALE_WORK_MS = 60 * 60 * 1000;
|
|
67
|
+
const MAX_REDIRECTS = 5;
|
|
68
|
+
const DISK_CODES = new Set(['ENOSPC', 'EACCES', 'EPERM', 'EROFS', 'EDQUOT', 'EMFILE', 'ENFILE', 'ENOTDIR', 'EISDIR', 'EEXIST']);
|
|
69
|
+
const NETWORK_CODES = new Set(['ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED', 'ENETUNREACH', 'EHOSTUNREACH', 'ETIMEDOUT', 'UND_ERR_CONNECT_TIMEOUT', 'CERT_HAS_EXPIRED', 'DEPTH_ZERO_SELF_SIGNED_CERT', 'SELF_SIGNED_CERT_IN_CHAIN', 'UNABLE_TO_VERIFY_LEAF_SIGNATURE', 'ERR_TLS_CERT_ALTNAME_INVALID']);
|
|
70
|
+
|
|
71
|
+
const codeOf = (error) => error?.code || error?.cause?.code || error?.cause?.cause?.code || '';
|
|
72
|
+
|
|
73
|
+
/** A failure as a CloudflaredError: what it already is, or what its cause says it is, or `fallback`. */
|
|
74
|
+
function asCloudflaredError(error, fallback, message) {
|
|
75
|
+
if (error instanceof CloudflaredError) return error;
|
|
76
|
+
const code = codeOf(error);
|
|
77
|
+
if (DISK_CODES.has(code)) return new CloudflaredError('disk', `The data folder could not be written (${code}).`, { cause: error, detail: code });
|
|
78
|
+
if (NETWORK_CODES.has(code)) return new CloudflaredError('offline', `The download server could not be reached (${code}).`, { cause: error, detail: code });
|
|
79
|
+
return new CloudflaredError(fallback, message, { cause: error, detail: code || error?.message || String(error) });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** SHA-256 and size of a file, read as a stream. */
|
|
83
|
+
export function hashFile(path, { signal } = {}) {
|
|
84
|
+
return new Promise((done, fail) => {
|
|
85
|
+
const hash = createHash('sha256');
|
|
86
|
+
let size = 0;
|
|
87
|
+
const stream = createReadStream(path, { signal });
|
|
88
|
+
stream.on('data', (chunk) => { hash.update(chunk); size += chunk.length; });
|
|
89
|
+
stream.on('error', fail);
|
|
90
|
+
stream.on('end', () => done({ sha256: hash.digest('hex'), size }));
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** SHA-256 and size of what an open file holds, read from its start. */
|
|
95
|
+
async function hashHandle(handle, { signal } = {}) {
|
|
96
|
+
const hash = createHash('sha256');
|
|
97
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
98
|
+
let size = 0;
|
|
99
|
+
for (;;) {
|
|
100
|
+
if (signal?.aborted) throw new CloudflaredError('cancelled', 'Stopped.');
|
|
101
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, size);
|
|
102
|
+
if (!bytesRead) break;
|
|
103
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
104
|
+
size += bytesRead;
|
|
105
|
+
}
|
|
106
|
+
return { sha256: hash.digest('hex'), size };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Opening a file without following a link at its last part, where the system has that (not Windows: asked with lstat first). */
|
|
110
|
+
const NO_FOLLOW = constants.O_NOFOLLOW ?? 0;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* What a file is, from a stat taken with `{ bigint: true }`: the same identity is the same file with the same bytes
|
|
114
|
+
* (any write changes its change time, and no program can set that back).
|
|
115
|
+
*/
|
|
116
|
+
export function fileIdentity(info) {
|
|
117
|
+
return { dev: info.dev, ino: info.ino, size: info.size, mtimeNs: info.mtimeNs, ctimeNs: info.ctimeNs };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Are two fileIdentity() the same file, unchanged? */
|
|
121
|
+
export function sameFile(a, b) {
|
|
122
|
+
return !!a && !!b && a.dev === b.dev && a.ino === b.ino && a.size === b.size && a.mtimeNs === b.mtimeNs && a.ctimeNs === b.ctimeNs;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Is the file at `path` the pinned binary? Its size first, then its SHA-256, read through one handle opened without
|
|
127
|
+
* following a link at `path` (a link there is `link`, never what it points to). The file is compared before and
|
|
128
|
+
* after the read, and with what `path` names once it is read: `changed` when either differs. `file` is the
|
|
129
|
+
* identity (fileIdentity) of the file the handle opened, whose bytes were read (or could not be: `unreadable`).
|
|
130
|
+
* @returns {Promise<{ ok: boolean, reason: 'missing' | 'link' | 'not-a-file' | 'size' | 'sha256' | 'changed' | 'unreadable' | null, file?: object }>}
|
|
131
|
+
*/
|
|
132
|
+
export async function checkCloudflared(path, pin, { signal } = {}) {
|
|
133
|
+
let handle;
|
|
134
|
+
try {
|
|
135
|
+
if (!NO_FOLLOW && (await lstat(path)).isSymbolicLink()) return { ok: false, reason: 'link' };
|
|
136
|
+
handle = await open(path, constants.O_RDONLY | NO_FOLLOW);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
const code = error?.code;
|
|
139
|
+
if (code === 'ENOENT' || code === 'ENOTDIR') return { ok: false, reason: 'missing' };
|
|
140
|
+
if (code === 'ELOOP' || code === 'EMLINK') return { ok: false, reason: 'link' };
|
|
141
|
+
if (code === 'EISDIR') return { ok: false, reason: 'not-a-file' };
|
|
142
|
+
return { ok: false, reason: 'unreadable' };
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const before = await handle.stat({ bigint: true });
|
|
146
|
+
if (!before.isFile()) return { ok: false, reason: 'not-a-file' };
|
|
147
|
+
const file = fileIdentity(before);
|
|
148
|
+
if (before.size !== BigInt(pin.binary.size)) return { ok: false, reason: 'size', file };
|
|
149
|
+
let sha256;
|
|
150
|
+
try {
|
|
151
|
+
({ sha256 } = await hashHandle(handle, { signal }));
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (signal?.aborted) throw new CloudflaredError('cancelled', 'Stopped.', { cause: error });
|
|
154
|
+
return { ok: false, reason: 'unreadable', file };
|
|
155
|
+
}
|
|
156
|
+
// Written to while it was read, or no longer the file `path` names: what was read is not what is there.
|
|
157
|
+
const after = fileIdentity(await handle.stat({ bigint: true }));
|
|
158
|
+
let named = null;
|
|
159
|
+
try { named = fileIdentity(await lstat(path, { bigint: true })); } catch { /* gone meanwhile */ }
|
|
160
|
+
if (!sameFile(file, after) || !sameFile(file, named)) return { ok: false, reason: 'changed', file };
|
|
161
|
+
return sha256 === pin.binary.sha256 ? { ok: true, reason: null, file } : { ok: false, reason: 'sha256', file };
|
|
162
|
+
} finally {
|
|
163
|
+
await handle.close().catch(() => {});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Is the file at `path` the pinned binary? checkCloudflared, without the identity.
|
|
169
|
+
* @returns {Promise<{ ok: boolean, reason: 'missing' | 'link' | 'not-a-file' | 'size' | 'sha256' | 'changed' | 'unreadable' | null }>}
|
|
170
|
+
*/
|
|
171
|
+
export async function verifyCloudflared(path, pin, options) {
|
|
172
|
+
const { ok, reason } = await checkCloudflared(path, pin, options);
|
|
173
|
+
return { ok, reason };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Is `path`, a cloudflared in SynaBun's own folder `area`, kept where nobody else can change it? Each step from the
|
|
178
|
+
* file up to `area` (both included) is looked at without following a link. null when every one is safe; otherwise
|
|
179
|
+
* the first that is not, `{ reason, at }`: `missing`, `link`, `not-a-file`, `not-a-folder`, `writable` (its group
|
|
180
|
+
* or everybody may write it), `owner` (it belongs to a user other than this one and root), `outside` (`path` is not
|
|
181
|
+
* inside `area`). Permissions are POSIX ones: on Windows only links are looked for. Nothing above `area` is asked:
|
|
182
|
+
* that is the data home, or the installed application, which have to be the user's anyway.
|
|
183
|
+
*/
|
|
184
|
+
export function unsafePlace(path, area, options) {
|
|
185
|
+
return lookAtPlace(path, area, options).unsafe;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* unsafePlace, with what that look saw of the file itself: `{ unsafe, file }`, `unsafe` what unsafePlace says and
|
|
190
|
+
* `file` the identity (fileIdentity) of what is at `path`, its last part not followed, from the very lstat that
|
|
191
|
+
* judged it; null when nothing is there. A copy refused for where it lies is known by it (manager.js), never by a
|
|
192
|
+
* second look at its path.
|
|
193
|
+
*/
|
|
194
|
+
export function lookAtPlace(path, area, { platform = process.platform, uid = process.getuid?.() } = {}) {
|
|
195
|
+
const top = resolve(area);
|
|
196
|
+
const file = resolve(path);
|
|
197
|
+
const inside = relative(top, file);
|
|
198
|
+
if (!inside || inside === '..' || inside.startsWith(`..${sep}`) || isAbsolute(inside)) return { unsafe: { reason: 'outside', at: file }, file: null };
|
|
199
|
+
let seen = null;
|
|
200
|
+
const refused = (reason, at) => ({ unsafe: { reason, at }, file: seen });
|
|
201
|
+
for (let at = file; ;) {
|
|
202
|
+
let info;
|
|
203
|
+
try { info = lstatSync(at, { bigint: true }); } catch { return refused('missing', at); }
|
|
204
|
+
if (at === file) seen = fileIdentity(info);
|
|
205
|
+
if (info.isSymbolicLink()) return refused('link', at);
|
|
206
|
+
if (at === file ? !info.isFile() : !info.isDirectory()) return refused(at === file ? 'not-a-file' : 'not-a-folder', at);
|
|
207
|
+
if (platform !== 'win32') {
|
|
208
|
+
if (Number(info.mode) & 0o022) return refused('writable', at);
|
|
209
|
+
const owner = Number(info.uid);
|
|
210
|
+
if (uid !== undefined && owner !== uid && owner !== 0) return refused('owner', at);
|
|
211
|
+
}
|
|
212
|
+
if (at === top) return { unsafe: null, file: seen };
|
|
213
|
+
const up = dirname(at);
|
|
214
|
+
if (up === at) return refused('outside', at);
|
|
215
|
+
at = up;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* `path` as one of SynaBun's own folders for cloudflared, which only its owner can open (0700): made when it is not
|
|
221
|
+
* there, set back to 0700 when it is this user's and lets anybody else in. A link, something that is not a folder,
|
|
222
|
+
* or another user's folder is refused (`disk`): nothing is put through it, and nothing in it is touched.
|
|
223
|
+
*/
|
|
224
|
+
export function ownFolder(path, { platform = process.platform, uid = process.getuid?.() } = {}) {
|
|
225
|
+
try { mkdirSync(path, { mode: 0o700 }); } catch (error) { if (error?.code !== 'EEXIST') throw error; }
|
|
226
|
+
const info = lstatSync(path);
|
|
227
|
+
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
228
|
+
throw new CloudflaredError('disk', `${path} is ${info.isSymbolicLink() ? 'a link' : 'not a folder'}, not a folder of SynaBun's own; nothing was put there.`, { detail: info.isSymbolicLink() ? 'ELOOP' : 'ENOTDIR' });
|
|
229
|
+
}
|
|
230
|
+
if (platform === 'win32') return path;
|
|
231
|
+
if (uid !== undefined && info.uid !== uid) throw new CloudflaredError('disk', `${path} belongs to another user; nothing was put there.`, { detail: 'EPERM' });
|
|
232
|
+
if (info.mode & 0o077) chmodSync(path, 0o700);
|
|
233
|
+
return path;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function writeAll(handle, buffer) {
|
|
237
|
+
let at = 0;
|
|
238
|
+
while (at < buffer.length) at += (await handle.write(buffer, at, buffer.length - at)).bytesWritten;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** The next address of a redirect, which has to be HTTPS like the first one. */
|
|
242
|
+
function redirectTarget(response, from) {
|
|
243
|
+
const location = response.headers.get('location');
|
|
244
|
+
let next;
|
|
245
|
+
try { next = new URL(location, from); } catch { next = null; }
|
|
246
|
+
if (!location || !next) throw new CloudflaredError('http', 'The download server sent a redirect without an address.', { detail: String(response.status) });
|
|
247
|
+
if (next.protocol !== 'https:') throw new CloudflaredError('http', 'The download server redirected to an address that is not HTTPS; it was not followed.', { detail: next.protocol });
|
|
248
|
+
return next;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Download a pinned release file into `destination` (a new file: it must not exist) and check it.
|
|
253
|
+
* Resolves `{ sha256, size }` of what is then there, which is the pinned file; anything else rejects, and
|
|
254
|
+
* `destination` is removed before it does.
|
|
255
|
+
*
|
|
256
|
+
* @param {object} o
|
|
257
|
+
* @param {object} o.pin a pin of pins.js
|
|
258
|
+
* @param {string} o.destination
|
|
259
|
+
* @param {typeof fetch} [o.fetchImpl] the transport (tests stand a local server in for GitHub through it)
|
|
260
|
+
* @param {AbortSignal} [o.signal] stops the download: `cancelled`
|
|
261
|
+
* @param {(progress: { received: number, total: number }) => void} [o.onProgress]
|
|
262
|
+
* @param {number} [o.stallMs] how long nothing may arrive, headers included
|
|
263
|
+
*/
|
|
264
|
+
export async function downloadPinned({ pin, destination, fetchImpl = fetch, signal, onProgress = () => {}, stallMs = 45_000 }) {
|
|
265
|
+
if (!pin?.url?.startsWith('https://')) throw new CloudflaredError('http', 'The pinned address is not HTTPS; nothing was downloaded.');
|
|
266
|
+
const stop = new AbortController();
|
|
267
|
+
const onAbort = () => stop.abort(new CloudflaredError('cancelled', 'The download was stopped.'));
|
|
268
|
+
if (signal?.aborted) onAbort(); else signal?.addEventListener('abort', onAbort, { once: true });
|
|
269
|
+
let timer = null;
|
|
270
|
+
const alive = () => {
|
|
271
|
+
clearTimeout(timer);
|
|
272
|
+
timer = setTimeout(() => stop.abort(new CloudflaredError('timeout', `Nothing arrived from the download server for ${Math.round(stallMs / 1000)} seconds.`)), stallMs);
|
|
273
|
+
};
|
|
274
|
+
let handle = null;
|
|
275
|
+
let made = false; // the destination is this call's own file: only then is it removed on a failure
|
|
276
|
+
let answered = false; // the server sent the file's headers: what fails after that is the transfer
|
|
277
|
+
let received = 0;
|
|
278
|
+
const hash = createHash('sha256');
|
|
279
|
+
try {
|
|
280
|
+
alive();
|
|
281
|
+
let url = new URL(pin.url);
|
|
282
|
+
let response = null;
|
|
283
|
+
for (let hop = 0; ; hop++) {
|
|
284
|
+
// identity: the file's bytes as they are, so that their number is the pinned size
|
|
285
|
+
response = await fetchImpl(url, { redirect: 'manual', signal: stop.signal, headers: { accept: 'application/octet-stream', 'accept-encoding': 'identity', 'user-agent': 'synabun' } });
|
|
286
|
+
if (response.status < 300 || response.status >= 400) break;
|
|
287
|
+
const next = redirectTarget(response, url);
|
|
288
|
+
try { await response.body?.cancel(); } catch { /* nothing to drain */ }
|
|
289
|
+
if (hop >= MAX_REDIRECTS) throw new CloudflaredError('http', 'The download server redirected too many times.');
|
|
290
|
+
url = next;
|
|
291
|
+
alive();
|
|
292
|
+
}
|
|
293
|
+
if (response.status !== 200 || !response.body) {
|
|
294
|
+
try { await response.body?.cancel(); } catch { /* nothing to drain */ }
|
|
295
|
+
throw new CloudflaredError('http', `The download server answered ${response.status} instead of the file.`, { detail: String(response.status) });
|
|
296
|
+
}
|
|
297
|
+
const announced = Number(response.headers.get('content-length'));
|
|
298
|
+
if (Number.isFinite(announced) && announced > 0 && announced !== pin.size) {
|
|
299
|
+
try { await response.body.cancel(); } catch { /* nothing to drain */ }
|
|
300
|
+
throw new CloudflaredError('checksum', `The download server offered a file of ${announced} bytes; the pinned one has ${pin.size}. It was not downloaded.`);
|
|
301
|
+
}
|
|
302
|
+
answered = true;
|
|
303
|
+
handle = await open(destination, 'wx', 0o600);
|
|
304
|
+
made = true;
|
|
305
|
+
onProgress({ received: 0, total: pin.size });
|
|
306
|
+
for await (const chunk of response.body) {
|
|
307
|
+
alive();
|
|
308
|
+
received += chunk.length;
|
|
309
|
+
if (received > pin.size) throw new CloudflaredError('checksum', 'The download is larger than the pinned file. It was stopped and removed.');
|
|
310
|
+
hash.update(chunk);
|
|
311
|
+
await writeAll(handle, chunk);
|
|
312
|
+
onProgress({ received, total: pin.size });
|
|
313
|
+
}
|
|
314
|
+
if (received !== pin.size) throw new CloudflaredError('interrupted', `The download ended after ${received} of ${pin.size} bytes.`);
|
|
315
|
+
const sha256 = hash.digest('hex');
|
|
316
|
+
if (sha256 !== pin.sha256) throw new CloudflaredError('checksum', 'The downloaded file does not match its pinned checksum. It was removed and will never be run.', { detail: sha256 });
|
|
317
|
+
await handle.sync();
|
|
318
|
+
await handle.close();
|
|
319
|
+
handle = null;
|
|
320
|
+
return { sha256, size: received };
|
|
321
|
+
} catch (error) {
|
|
322
|
+
try { await handle?.close(); } catch { /* already closed */ }
|
|
323
|
+
handle = null;
|
|
324
|
+
if (made) rmSync(destination, { force: true });
|
|
325
|
+
// An abort arrives as whatever the transport makes of it: the reason it was given is the answer.
|
|
326
|
+
if (stop.signal.aborted && stop.signal.reason instanceof CloudflaredError) throw stop.signal.reason;
|
|
327
|
+
if (error instanceof CloudflaredError) throw error;
|
|
328
|
+
if (DISK_CODES.has(codeOf(error))) throw asCloudflaredError(error, 'disk', 'The data folder could not be written.');
|
|
329
|
+
if (answered) throw new CloudflaredError('interrupted', `The download ended after ${received} of ${pin.size} bytes.`, { cause: error, detail: codeOf(error) || error?.message });
|
|
330
|
+
throw asCloudflaredError(error, 'offline', 'The download server could not be reached.');
|
|
331
|
+
} finally {
|
|
332
|
+
clearTimeout(timer);
|
|
333
|
+
signal?.removeEventListener('abort', onAbort);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** One header block of a tar archive: the entry's name, the size of its data, and whether it is a plain file. */
|
|
338
|
+
function readTarHeader(block) {
|
|
339
|
+
const text = (from, to) => { const end = block.indexOf(0, from); return block.toString('utf8', from, end === -1 || end > to ? to : end); };
|
|
340
|
+
const stored = parseInt(text(148, 156).trim(), 8);
|
|
341
|
+
let sum = 0;
|
|
342
|
+
for (let i = 0; i < 512; i++) sum += i >= 148 && i < 156 ? 32 : block[i];
|
|
343
|
+
if (stored !== sum) throw new CloudflaredError('unpack', 'The archive is not a tar archive.');
|
|
344
|
+
let size = 0;
|
|
345
|
+
if (block[124] & 0x80) for (let i = 125; i < 136; i++) size = size * 256 + block[i]; // base-256, for sizes octal cannot hold
|
|
346
|
+
else size = parseInt(text(124, 136).trim() || '0', 8);
|
|
347
|
+
if (!Number.isSafeInteger(size) || size < 0) throw new CloudflaredError('unpack', 'The archive names an entry without a size.');
|
|
348
|
+
const prefix = text(345, 500);
|
|
349
|
+
const name = `${prefix ? `${prefix}/` : ''}${text(0, 100)}`.replace(/^\.\//, '');
|
|
350
|
+
const flag = block[156];
|
|
351
|
+
return { name, size, file: flag === 0 || flag === 48 }; // "\0" and "0": a regular file
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Read one member out of a gzipped tar archive into `destination` (a new file), without a tar program and
|
|
356
|
+
* without ever using a name of the archive as a path: the entry called `member` that is a regular file of
|
|
357
|
+
* exactly `size` bytes is copied out, and nothing else is looked at. Resolves `{ sha256, size }` of what was
|
|
358
|
+
* written; rejects `unpack` (and removes `destination`) when the archive holds no such entry or ends inside it.
|
|
359
|
+
*/
|
|
360
|
+
export async function extractTarMember({ archive, member, size, destination, signal }) {
|
|
361
|
+
const hash = createHash('sha256');
|
|
362
|
+
let source = null;
|
|
363
|
+
let gunzip = null;
|
|
364
|
+
let handle = null;
|
|
365
|
+
let made = false;
|
|
366
|
+
let pending = Buffer.alloc(0);
|
|
367
|
+
let mode = 'header'; // header | member | skip | done | end
|
|
368
|
+
let left = 0; // bytes of the current entry still to come (its padding included, when skipping)
|
|
369
|
+
let padding = 0;
|
|
370
|
+
let written = 0;
|
|
371
|
+
try {
|
|
372
|
+
handle = await open(destination, 'wx', 0o600);
|
|
373
|
+
made = true;
|
|
374
|
+
// Made here, with nothing awaited before the loop that reads them: an archive that cannot be opened fails
|
|
375
|
+
// the loop. (The listener on gunzip is for the instant before the loop has its own: an 'error' nobody
|
|
376
|
+
// listens for would be an uncaught exception of the whole server.)
|
|
377
|
+
source = createReadStream(archive);
|
|
378
|
+
gunzip = createGunzip();
|
|
379
|
+
gunzip.on('error', () => {});
|
|
380
|
+
source.on('error', (error) => gunzip.destroy(error));
|
|
381
|
+
for await (const chunk of source.pipe(gunzip)) {
|
|
382
|
+
if (signal?.aborted) throw new CloudflaredError('cancelled', 'Stopped.');
|
|
383
|
+
pending = pending.length ? Buffer.concat([pending, chunk]) : chunk;
|
|
384
|
+
while (pending.length && mode !== 'done') {
|
|
385
|
+
if (mode === 'header') {
|
|
386
|
+
if (pending.length < 512) break;
|
|
387
|
+
const block = pending.subarray(0, 512);
|
|
388
|
+
pending = pending.subarray(512);
|
|
389
|
+
if (block.every((byte) => byte === 0)) { mode = 'end'; break; }
|
|
390
|
+
const entry = readTarHeader(block);
|
|
391
|
+
padding = (512 - (entry.size % 512)) % 512;
|
|
392
|
+
if (entry.file && entry.name === member) {
|
|
393
|
+
if (entry.size !== size) throw new CloudflaredError('unpack', `The archive's ${member} has ${entry.size} bytes; the pinned one has ${size}.`);
|
|
394
|
+
mode = 'member';
|
|
395
|
+
left = entry.size;
|
|
396
|
+
} else {
|
|
397
|
+
mode = entry.size + padding ? 'skip' : 'header';
|
|
398
|
+
left = entry.size + padding;
|
|
399
|
+
}
|
|
400
|
+
} else {
|
|
401
|
+
const take = Math.min(left, pending.length);
|
|
402
|
+
if (mode === 'member') {
|
|
403
|
+
const part = pending.subarray(0, take);
|
|
404
|
+
hash.update(part);
|
|
405
|
+
await writeAll(handle, part);
|
|
406
|
+
written += take;
|
|
407
|
+
}
|
|
408
|
+
pending = pending.subarray(take);
|
|
409
|
+
left -= take;
|
|
410
|
+
if (left === 0) mode = mode === 'member' ? 'done' : 'header';
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (mode === 'done' || mode === 'end') break;
|
|
414
|
+
}
|
|
415
|
+
if (mode !== 'done') throw new CloudflaredError('unpack', mode === 'member' ? `The archive ends inside its ${member}.` : `The archive holds no ${member}.`);
|
|
416
|
+
await handle.sync();
|
|
417
|
+
await handle.close();
|
|
418
|
+
handle = null;
|
|
419
|
+
return { sha256: hash.digest('hex'), size: written };
|
|
420
|
+
} catch (error) {
|
|
421
|
+
try { await handle?.close(); } catch { /* already closed */ }
|
|
422
|
+
handle = null;
|
|
423
|
+
if (made) rmSync(destination, { force: true });
|
|
424
|
+
throw asCloudflaredError(error, 'unpack', 'The archive could not be read.');
|
|
425
|
+
} finally {
|
|
426
|
+
source?.destroy();
|
|
427
|
+
gunzip?.destroy();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* The cloudflared of a release file that was already checked, as a new executable file at `destination`:
|
|
433
|
+
* the file itself (moved there with `move`, copied otherwise), or the member of the archive. What ends up
|
|
434
|
+
* there is compared with the pinned SHA-256 of the binary; a file that is not it is removed and `checksum`
|
|
435
|
+
* (or `unpack`) is thrown.
|
|
436
|
+
*/
|
|
437
|
+
export async function binaryFromRelease({ pin, releaseFile, destination, move = false, signal, platform = process.platform }) {
|
|
438
|
+
let made = false; // the destination is this call's own file from here on: only then is it removed on a failure
|
|
439
|
+
try {
|
|
440
|
+
if (existsSync(destination)) throw new CloudflaredError('disk', 'Something is already where cloudflared was to be put; it was left as it is.', { detail: 'EEXIST' });
|
|
441
|
+
if (pin.archive === 'tgz') {
|
|
442
|
+
const unpacked = await extractTarMember({ archive: releaseFile, member: pin.member, size: pin.binary.size, destination, signal });
|
|
443
|
+
made = true;
|
|
444
|
+
if (unpacked.sha256 !== pin.binary.sha256) throw new CloudflaredError('checksum', 'The cloudflared inside the archive does not match its pinned checksum. It was removed and will never be run.', { detail: unpacked.sha256 });
|
|
445
|
+
} else {
|
|
446
|
+
if (move) renameSync(releaseFile, destination);
|
|
447
|
+
else await copyFile(releaseFile, destination, constants.COPYFILE_EXCL);
|
|
448
|
+
made = true;
|
|
449
|
+
const check = await verifyCloudflared(destination, pin, { signal });
|
|
450
|
+
if (!check.ok) throw new CloudflaredError('checksum', 'The file does not match the pinned checksum of cloudflared. It was removed and will never be run.', { detail: check.reason });
|
|
451
|
+
}
|
|
452
|
+
if (platform !== 'win32') chmodSync(destination, 0o755);
|
|
453
|
+
return destination;
|
|
454
|
+
} catch (error) {
|
|
455
|
+
if (made) rmSync(destination, { force: true });
|
|
456
|
+
throw asCloudflaredError(error, 'disk', 'cloudflared could not be put in place.');
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const processAlive = (pid) => {
|
|
461
|
+
try { process.kill(pid, 0); return true; } catch (error) { return error?.code === 'EPERM'; }
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Remove the work folders an earlier attempt left in `root` (a process that was stopped mid-download cannot
|
|
466
|
+
* clean up after itself): every one whose process is gone or this process itself, and any older than an hour.
|
|
467
|
+
* @returns {string[]} what was removed
|
|
468
|
+
*/
|
|
469
|
+
export function sweepStaleWork(root, { now = Date.now(), alive = processAlive, pid = process.pid } = {}) {
|
|
470
|
+
const removed = [];
|
|
471
|
+
let names = [];
|
|
472
|
+
try { names = readdirSync(root); } catch { return removed; }
|
|
473
|
+
for (const name of names) {
|
|
474
|
+
if (!name.startsWith(WORK_PREFIX)) continue;
|
|
475
|
+
const owner = Number(name.slice(WORK_PREFIX.length).split('-')[0]);
|
|
476
|
+
const path = join(root, name);
|
|
477
|
+
let age = Infinity;
|
|
478
|
+
try { age = now - statSync(path).mtimeMs; } catch { /* gone meanwhile */ }
|
|
479
|
+
if (owner === pid || !Number.isInteger(owner) || !alive(owner) || age > STALE_WORK_MS) {
|
|
480
|
+
try { rmSync(path, { recursive: true, force: true }); removed.push(name); } catch { /* in use: the next attempt asks again */ }
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return removed;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Download the pinned cloudflared and put it at `<root>/<version>/cloudflared[.exe]`. Everything happens in a
|
|
488
|
+
* work folder of this attempt inside `root`, which is removed whatever the outcome; the binary is renamed into
|
|
489
|
+
* place only after both of its checks, so its place holds the whole checked file or nothing. `root` and the
|
|
490
|
+
* version's folder are SynaBun's own, 0700 (ownFolder): a link or another user's folder there stops the install.
|
|
491
|
+
* One attempt at a time per process is the caller's business (manager.js); two processes that download at once
|
|
492
|
+
* each rename a checked file into the same place, and the place holds one of them, whole.
|
|
493
|
+
*
|
|
494
|
+
* @returns {Promise<{ path: string, version: string }>}
|
|
495
|
+
*/
|
|
496
|
+
export async function installCloudflared({ pin, root, fetchImpl, signal, onProgress, stallMs, platform = process.platform }) {
|
|
497
|
+
const directory = join(root, pin.version);
|
|
498
|
+
const destination = join(directory, pin.binary.name);
|
|
499
|
+
let work = null;
|
|
500
|
+
let placed = false;
|
|
501
|
+
try {
|
|
502
|
+
mkdirSync(dirname(root), { recursive: true });
|
|
503
|
+
ownFolder(root);
|
|
504
|
+
sweepStaleWork(root);
|
|
505
|
+
work = join(root, `${WORK_PREFIX}${process.pid}-${randomBytes(6).toString('hex')}`);
|
|
506
|
+
mkdirSync(work, { mode: 0o700 });
|
|
507
|
+
const releaseFile = join(work, 'release.part');
|
|
508
|
+
await downloadPinned({ pin, destination: releaseFile, fetchImpl, signal, onProgress, stallMs });
|
|
509
|
+
const binary = join(work, 'binary.part');
|
|
510
|
+
await binaryFromRelease({ pin, releaseFile, destination: binary, move: true, signal, platform });
|
|
511
|
+
if (signal?.aborted) throw new CloudflaredError('cancelled', 'Stopped.');
|
|
512
|
+
// A link or a file where the version's folder belongs is taken away (the link itself, never what it points to).
|
|
513
|
+
let there = null;
|
|
514
|
+
try { there = lstatSync(directory); } catch { /* not there yet */ }
|
|
515
|
+
if (there && (there.isSymbolicLink() || !there.isDirectory())) { try { unlinkSync(directory); } catch { rmdirSync(directory); } }
|
|
516
|
+
ownFolder(directory);
|
|
517
|
+
try {
|
|
518
|
+
renameSync(binary, destination);
|
|
519
|
+
} catch (error) {
|
|
520
|
+
// Windows does not replace a file that is running: when what is there is the pinned binary, it is the result.
|
|
521
|
+
if (!existsSync(destination) || !(await verifyCloudflared(destination, pin)).ok) throw error;
|
|
522
|
+
}
|
|
523
|
+
placed = true;
|
|
524
|
+
return { path: destination, version: pin.version };
|
|
525
|
+
} catch (error) {
|
|
526
|
+
throw asCloudflaredError(error, 'disk', 'cloudflared could not be put in place.');
|
|
527
|
+
} finally {
|
|
528
|
+
if (work) { try { rmSync(work, { recursive: true, force: true }); } catch { /* the next attempt sweeps it */ } }
|
|
529
|
+
// A failed attempt leaves no folder of its own either (rmdir only ever removes an empty one). A `root` that was
|
|
530
|
+
// not SynaBun's own (no work folder was made in it) is left as it is.
|
|
531
|
+
if (!placed && work) for (const folder of [directory, root]) { try { rmdirSync(folder); } catch { /* not empty, or not there */ } }
|
|
532
|
+
}
|
|
533
|
+
}
|