@xopcai/xopc 0.0.84 → 0.0.85
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/dist/browser-ext/manifest.json +1 -1
- package/dist/extensions/feishu/src/outbound/media-load.js +1 -1
- package/dist/extensions/feishu/src/plugin.d.ts +2 -0
- package/dist/extensions/feishu/src/plugin.js +10 -0
- package/dist/extensions/feishu/src/plugin.js.map +1 -1
- package/dist/extensions/feishu/src/workflow-progress.d.ts +27 -0
- package/dist/extensions/feishu/src/workflow-progress.js +99 -0
- package/dist/extensions/feishu/src/workflow-progress.js.map +1 -0
- package/dist/extensions/telegram/src/plugin.d.ts +2 -0
- package/dist/extensions/telegram/src/plugin.js +11 -1
- package/dist/extensions/telegram/src/plugin.js.map +1 -1
- package/dist/extensions/telegram/src/routing-integration.js +2 -2
- package/dist/extensions/telegram/src/workflow-progress.d.ts +24 -0
- package/dist/extensions/telegram/src/workflow-progress.js +73 -0
- package/dist/extensions/telegram/src/workflow-progress.js.map +1 -0
- package/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/extensions/weixin/src/__tests__/workflow-progress.test.js +158 -0
- package/dist/extensions/weixin/src/__tests__/workflow-progress.test.js.map +1 -0
- package/dist/extensions/weixin/src/api/api.js +2 -2
- package/dist/extensions/weixin/src/auth/accounts.js +1 -1
- package/dist/extensions/weixin/src/cdn/upload.js +1 -1
- package/dist/extensions/weixin/src/media/data-url.js +1 -1
- package/dist/extensions/weixin/src/messaging/debug-mode.js +1 -1
- package/dist/extensions/weixin/src/messaging/inbound.js +1 -1
- package/dist/extensions/weixin/src/messaging/process-message.js +1 -1
- package/dist/extensions/weixin/src/plugin.d.ts +2 -0
- package/dist/extensions/weixin/src/plugin.js +11 -1
- package/dist/extensions/weixin/src/plugin.js.map +1 -1
- package/dist/extensions/weixin/src/storage/sync-buf.js +1 -1
- package/dist/extensions/weixin/src/workflow-progress.d.ts +26 -0
- package/dist/extensions/weixin/src/workflow-progress.js +99 -0
- package/dist/extensions/weixin/src/workflow-progress.js.map +1 -0
- package/dist/gateway/static/root/assets/agents-D3_-kNlZ.js +222 -0
- package/dist/gateway/static/root/assets/apps-page-D7v7649T.js +1 -0
- package/dist/gateway/static/root/assets/channels-settings-nCaMb0a7.js +1 -0
- package/dist/gateway/static/root/assets/channels-status-swr-C1gZBcJV.js +8 -0
- package/dist/gateway/static/root/assets/createLucideIcon-DPHK1VkS.js +1 -0
- package/dist/gateway/static/root/assets/cron-api-CoYK0hlm.js +1 -0
- package/dist/gateway/static/root/assets/cron-page-DeGo-Vjc.js +1 -0
- package/dist/gateway/static/root/assets/dist-BTWC-BTN.js +45 -0
- package/dist/gateway/static/root/assets/{dist-CqNMNhJM.js → dist-DaK4dsss.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-gf2L0kY_.js → extension-debug-page-BZngZWbO.js} +1 -1
- package/dist/gateway/static/root/assets/extension-page-D6JSyV27.js +1 -0
- package/dist/gateway/static/root/assets/extension-settings-page-8PZcmWI7.js +1 -0
- package/dist/gateway/static/root/assets/fetch-B2MYHbWg.js +1 -0
- package/dist/gateway/static/root/assets/{field-primitives-DTtlp-l8.js → field-primitives-Zzl22MvN.js} +1 -1
- package/dist/gateway/static/root/assets/heartbeat-config-api-BtIcpG0O.js +1 -0
- package/dist/gateway/static/root/assets/index-D4vM3-P7.js +4700 -0
- package/dist/gateway/static/root/assets/index-ew_2L2We.css +1 -0
- package/dist/gateway/static/root/assets/logs-page-_d4UJ-qQ.js +1 -0
- package/dist/gateway/static/root/assets/sessions-page-5N4aF2Wk.js +1 -0
- package/dist/gateway/static/root/assets/settings-form-section-D_tgb8r2.js +1 -0
- package/dist/gateway/static/root/assets/settings-page-C18xBt4X.js +3 -0
- package/dist/gateway/static/root/assets/share-preview-page-D4EG_vM1.js +2 -0
- package/dist/gateway/static/root/assets/skills-page-sPAXhh8w.js +2 -0
- package/dist/gateway/static/root/assets/theme-store-DryYl3qD.js +1 -0
- package/dist/gateway/static/root/assets/url-BwNL6Rgk.js +3 -0
- package/dist/gateway/static/root/assets/utils-CYO9eTCM.js +1 -0
- package/dist/gateway/static/root/assets/voice-api-key-field-Ds51havm.js +1 -0
- package/dist/gateway/static/root/index.html +7 -6
- package/dist/package.js +1 -1
- package/dist/src/agent/agent-manager.js +7 -7
- package/dist/src/agent/bootstrap/load-bootstrap-files.js +1 -1
- package/dist/src/agent/context/workspace-seed.js +3 -3
- package/dist/src/agent/embedded/map-stream-events.js +6 -0
- package/dist/src/agent/embedded/map-stream-events.js.map +1 -1
- package/dist/src/agent/embedded/subscribe-session.js +24 -0
- package/dist/src/agent/embedded/subscribe-session.js.map +1 -1
- package/dist/src/agent/embedded/types.d.ts +19 -0
- package/dist/src/agent/goals/goal-locale.js +2 -2
- package/dist/src/agent/goals/goal-run-store.js +4 -4
- package/dist/src/agent/goals/persistent-goal-service.js +1 -1
- package/dist/src/agent/goals/post-turn.js +2 -2
- package/dist/src/agent/image/load-image-media.js +2 -2
- package/dist/src/agent/ipc/bus.js +1 -1
- package/dist/src/agent/ipc/inbox.js +2 -2
- package/dist/src/agent/ipc/socket.js +1 -1
- package/dist/src/agent/memory/builtin-memory-store.js +1 -1
- package/dist/src/agent/memory/dreaming/deep-promotion.js +1 -1
- package/dist/src/agent/memory/dreaming/events.js +1 -1
- package/dist/src/agent/memory/dreaming/last-run.js +1 -1
- package/dist/src/agent/memory/dreaming/light-sweep.js +1 -1
- package/dist/src/agent/memory/dreaming/preview.js +1 -1
- package/dist/src/agent/memory/dreaming/rem-patterns.js +1 -1
- package/dist/src/agent/memory/dreaming/short-term-store.js +1 -1
- package/dist/src/agent/memory/dreaming/utils.js +1 -1
- package/dist/src/agent/memory/plugin-discovery.js +1 -1
- package/dist/src/agent/models/manager.js +1 -1
- package/dist/src/agent/prompt/service-prompt-builder.js +2 -2
- package/dist/src/agent/reply/post-compaction-context.js +1 -1
- package/dist/src/agent/reply/startup-context.d.ts +3 -0
- package/dist/src/agent/reply/startup-context.js +25 -2
- package/dist/src/agent/reply/startup-context.js.map +1 -1
- package/dist/src/agent/reply/workspace-boundary-read.js +1 -1
- package/dist/src/agent/sandbox/path-policy.js +2 -2
- package/dist/src/agent/service/build-direct-message-content.js +1 -1
- package/dist/src/agent/service.d.ts +1 -0
- package/dist/src/agent/service.js +10 -4
- package/dist/src/agent/service.js.map +1 -1
- package/dist/src/agent/session/session-inspector.js +1 -1
- package/dist/src/agent/skills/config.js +1 -1
- package/dist/src/agent/skills/hub-hash.js +2 -2
- package/dist/src/agent/skills/hub-lock.js +1 -1
- package/dist/src/agent/skills/hub-pull.js +3 -3
- package/dist/src/agent/skills/index.js +1 -1
- package/dist/src/agent/skills/managed-store.js +1 -1
- package/dist/src/agent/skills/scanner.js +1 -1
- package/dist/src/agent/skills/skill-manage-ops.js +1 -1
- package/dist/src/agent/skills/skill-manager.js +1 -1
- package/dist/src/agent/tools/create-share-tool.d.ts +27 -0
- package/dist/src/agent/tools/create-share-tool.js +237 -0
- package/dist/src/agent/tools/create-share-tool.js.map +1 -0
- package/dist/src/agent/tools/dreaming-tool.js +1 -1
- package/dist/src/agent/tools/factory.js +35 -1
- package/dist/src/agent/tools/factory.js.map +1 -1
- package/dist/src/agent/tools/image-generate-tool.js +1 -1
- package/dist/src/agent/tools/index.d.ts +2 -0
- package/dist/src/agent/tools/index.js +3 -1
- package/dist/src/agent/tools/send-media.js +1 -1
- package/dist/src/agent/tools/skill-manage-tool.js +1 -1
- package/dist/src/agent/tools/workflow-tool.d.ts +41 -0
- package/dist/src/agent/tools/workflow-tool.js +271 -0
- package/dist/src/agent/tools/workflow-tool.js.map +1 -0
- package/dist/src/agent/tools/write.js +1 -1
- package/dist/src/agent/workflow/builtins/audit-repo.d.ts +9 -0
- package/dist/src/agent/workflow/builtins/audit-repo.js +115 -0
- package/dist/src/agent/workflow/builtins/audit-repo.js.map +1 -0
- package/dist/src/agent/workflow/builtins/index.d.ts +15 -0
- package/dist/src/agent/workflow/builtins/index.js +28 -0
- package/dist/src/agent/workflow/builtins/index.js.map +1 -0
- package/dist/src/agent/workflow/builtins/multi-perspective-review.d.ts +9 -0
- package/dist/src/agent/workflow/builtins/multi-perspective-review.js +113 -0
- package/dist/src/agent/workflow/builtins/multi-perspective-review.js.map +1 -0
- package/dist/src/agent/workflow/builtins/research.d.ts +9 -0
- package/dist/src/agent/workflow/builtins/research.js +129 -0
- package/dist/src/agent/workflow/builtins/research.js.map +1 -0
- package/dist/src/agent/workflow/catalog.d.ts +51 -0
- package/dist/src/agent/workflow/catalog.js +155 -0
- package/dist/src/agent/workflow/catalog.js.map +1 -0
- package/dist/src/agent/workflow/channel-capability.d.ts +76 -0
- package/dist/src/agent/workflow/channel-capability.js +1 -0
- package/dist/src/agent/workflow/index.d.ts +11 -0
- package/dist/src/agent/workflow/index.js +10 -0
- package/dist/src/agent/workflow/last-run-memory.d.ts +42 -0
- package/dist/src/agent/workflow/last-run-memory.js +60 -0
- package/dist/src/agent/workflow/last-run-memory.js.map +1 -0
- package/dist/src/agent/workflow/parser.d.ts +20 -0
- package/dist/src/agent/workflow/parser.js +137 -0
- package/dist/src/agent/workflow/parser.js.map +1 -0
- package/dist/src/agent/workflow/progress-broker.d.ts +80 -0
- package/dist/src/agent/workflow/progress-broker.js +263 -0
- package/dist/src/agent/workflow/progress-broker.js.map +1 -0
- package/dist/src/agent/workflow/runtime.d.ts +31 -0
- package/dist/src/agent/workflow/runtime.js +301 -0
- package/dist/src/agent/workflow/runtime.js.map +1 -0
- package/dist/src/agent/workflow/snapshot.d.ts +18 -0
- package/dist/src/agent/workflow/snapshot.js +144 -0
- package/dist/src/agent/workflow/snapshot.js.map +1 -0
- package/dist/src/agent/workflow/structured-output-tool.d.ts +33 -0
- package/dist/src/agent/workflow/structured-output-tool.js +58 -0
- package/dist/src/agent/workflow/structured-output-tool.js.map +1 -0
- package/dist/src/agent/workflow/subagent-runner.d.ts +42 -0
- package/dist/src/agent/workflow/subagent-runner.js +104 -0
- package/dist/src/agent/workflow/subagent-runner.js.map +1 -0
- package/dist/src/agent/workflow/types.d.ts +137 -0
- package/dist/src/agent/workflow/types.js +1 -0
- package/dist/src/auth/credentials.js +3 -3
- package/dist/src/auth/profiles/store.js +1 -1
- package/dist/src/auth/sync-provider-auth.js +1 -1
- package/dist/src/browser/cache-dir-policy.js +1 -1
- package/dist/src/browser/cdp-local-launcher.js +2 -2
- package/dist/src/browser/providers/browser-ext-install.js +4 -4
- package/dist/src/browser/providers/cloakbrowser.js +4 -4
- package/dist/src/browser/providers/playwright-doctor.js +1 -1
- package/dist/src/browser/stealth.js +1 -1
- package/dist/src/channels/attachments/inbound-persist.js +1 -1
- package/dist/src/channels/attachments/outbound-tts-persist.js +1 -1
- package/dist/src/channels/outbound/persist-store.js +1 -1
- package/dist/src/channels/pairing/allow-from-file.js +1 -1
- package/dist/src/channels/pairing/pairing-store.js +2 -2
- package/dist/src/chat-commands/builtins/config.js +2 -2
- package/dist/src/chat-commands/builtins/model.js +40 -23
- package/dist/src/chat-commands/builtins/model.js.map +1 -1
- package/dist/src/chat-commands/builtins/system.js +30 -15
- package/dist/src/chat-commands/builtins/system.js.map +1 -1
- package/dist/src/chat-commands/builtins/workflow.d.ts +18 -0
- package/dist/src/chat-commands/builtins/workflow.js +167 -0
- package/dist/src/chat-commands/builtins/workflow.js.map +1 -0
- package/dist/src/chat-commands/context.js +1 -1
- package/dist/src/chat-commands/format-output.d.ts +28 -0
- package/dist/src/chat-commands/format-output.js +45 -0
- package/dist/src/chat-commands/format-output.js.map +1 -0
- package/dist/src/chat-commands/index.d.ts +1 -0
- package/dist/src/chat-commands/index.js +3 -1
- package/dist/src/chat-commands/index.js.map +1 -1
- package/dist/src/cli/commands/config.js +2 -2
- package/dist/src/cli/commands/doctor/checks/config-health.js +1 -1
- package/dist/src/cli/commands/doctor/checks/provider-auth.js +1 -1
- package/dist/src/cli/commands/doctor/checks/session-integrity.js +1 -1
- package/dist/src/cli/commands/doctor/checks/state-integrity.js +1 -1
- package/dist/src/cli/commands/doctor/checks/workspace-status.js +1 -1
- package/dist/src/cli/commands/extension-dev.js +1 -1
- package/dist/src/cli/commands/extension-marketplace.js +1 -1
- package/dist/src/cli/commands/extension-pack.js +1 -1
- package/dist/src/cli/commands/gateway/lifecycle.js +10 -4
- package/dist/src/cli/commands/gateway/lifecycle.js.map +1 -1
- package/dist/src/cli/commands/gateway/shared.js +1 -1
- package/dist/src/cli/commands/image.js +1 -1
- package/dist/src/cli/commands/init.js +4 -4
- package/dist/src/cli/commands/onboard.js +2 -2
- package/dist/src/cli/commands/tunnel.js +2 -2
- package/dist/src/cli/utils/gateway-client.js +1 -1
- package/dist/src/cli/utils/init-workspace-core.js +2 -2
- package/dist/src/config/agent-profile.js +1 -1
- package/dist/src/config/gateway-bind.js +1 -1
- package/dist/src/config/index.js +5 -5
- package/dist/src/config/loader.js +2 -2
- package/dist/src/config/models-json.js +2 -2
- package/dist/src/config/paths-state.js +1 -1
- package/dist/src/config/profile.js +2 -2
- package/dist/src/config/public-url.d.ts +28 -0
- package/dist/src/config/public-url.js +103 -0
- package/dist/src/config/public-url.js.map +1 -0
- package/dist/src/config/schema.d.ts +82 -0
- package/dist/src/config/schema.js +130 -1
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/config/workspace-path.js +1 -1
- package/dist/src/cron/executor.js +2 -2
- package/dist/src/cron/persistence.js +1 -1
- package/dist/src/cron/run-log-store.js +1 -1
- package/dist/src/daemon/constants.js +1 -1
- package/dist/src/daemon/install-plan.js +3 -3
- package/dist/src/daemon/install-plan.js.map +1 -1
- package/dist/src/daemon/launchd.js +2 -2
- package/dist/src/daemon/schtasks.js +38 -1
- package/dist/src/daemon/schtasks.js.map +1 -1
- package/dist/src/daemon/systemd.js +2 -2
- package/dist/src/extensions/bundle-mcp.js +1 -1
- package/dist/src/extensions/discover-extensions.js +1 -1
- package/dist/src/extensions/health.js +1 -1
- package/dist/src/extensions/loader.js +1 -1
- package/dist/src/extensions/lockfile.js +2 -2
- package/dist/src/gateway/agents-admin.js +2 -2
- package/dist/src/gateway/file-path-classifier.js +2 -2
- package/dist/src/gateway/hono/app.js +33 -2
- package/dist/src/gateway/hono/app.js.map +1 -1
- package/dist/src/gateway/hono/lib/config-payload.js +1 -1
- package/dist/src/gateway/hono/lib/extension-store.js +2 -2
- package/dist/src/gateway/hono/lib/static-ui.js +2 -2
- package/dist/src/gateway/hono/oauth.js +1 -1
- package/dist/src/gateway/hono/routes/agents.js +1 -1
- package/dist/src/gateway/hono/routes/auth-registry-extensions.js +1 -1
- package/dist/src/gateway/hono/routes/config-patch/misc.js +1 -1
- package/dist/src/gateway/hono/routes/dreaming.js +1 -1
- package/dist/src/gateway/hono/routes/host-fs.js +2 -2
- package/dist/src/gateway/hono/routes/lazy-bundles.js +8 -0
- package/dist/src/gateway/hono/routes/lazy-bundles.js.map +1 -1
- package/dist/src/gateway/hono/routes/models.js +1 -1
- package/dist/src/gateway/hono/routes/shares.js +631 -34
- package/dist/src/gateway/hono/routes/shares.js.map +1 -1
- package/dist/src/gateway/hono/routes/site-shares.d.ts +3 -0
- package/dist/src/gateway/hono/routes/site-shares.js +228 -0
- package/dist/src/gateway/hono/routes/site-shares.js.map +1 -0
- package/dist/src/gateway/hono/routes/tunnel.js +97 -8
- package/dist/src/gateway/hono/routes/tunnel.js.map +1 -1
- package/dist/src/gateway/hono/routes/workspace.js +5 -5
- package/dist/src/gateway/hono/sse.js +2 -2
- package/dist/src/gateway/host.d.ts +3 -1
- package/dist/src/gateway/host.js +3 -1
- package/dist/src/gateway/host.js.map +1 -1
- package/dist/src/gateway/lock.js +3 -3
- package/dist/src/gateway/ports.d.ts +6 -0
- package/dist/src/gateway/ports.js +38 -2
- package/dist/src/gateway/ports.js.map +1 -1
- package/dist/src/gateway/public-url.d.ts +8 -0
- package/dist/src/gateway/public-url.js +10 -0
- package/dist/src/gateway/public-url.js.map +1 -0
- package/dist/src/gateway/security/origin-check.d.ts +9 -1
- package/dist/src/gateway/security/origin-check.js +4 -0
- package/dist/src/gateway/security/origin-check.js.map +1 -1
- package/dist/src/gateway/server.js +15 -0
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/gateway/service/agent-runner.js +2 -2
- package/dist/src/gateway/service/marketplace-service.js +2 -2
- package/dist/src/gateway/service/run-gateway-agent.js +2 -2
- package/dist/src/gateway/service.js +3 -2
- package/dist/src/gateway/service.js.map +1 -1
- package/dist/src/gateway/workspace-fs-file-list.js +1 -1
- package/dist/src/i18n/goals-bundle.js +1 -1
- package/dist/src/i18n/index.d.ts +1 -0
- package/dist/src/i18n/index.js +2 -1
- package/dist/src/i18n/locales/share-tool.en.js +15 -0
- package/dist/src/i18n/locales/share-tool.en.js.map +1 -0
- package/dist/src/i18n/locales/share-tool.zh.js +15 -0
- package/dist/src/i18n/locales/share-tool.zh.js.map +1 -0
- package/dist/src/i18n/share-tool-bundle.d.ts +20 -0
- package/dist/src/i18n/share-tool-bundle.js +56 -0
- package/dist/src/i18n/share-tool-bundle.js.map +1 -0
- package/dist/src/infra/gateway-processes.js +1 -0
- package/dist/src/infra/gateway-processes.js.map +1 -1
- package/dist/src/infra/restart.js +2 -2
- package/dist/src/infra/update-check.js +1 -1
- package/dist/src/infra/update-lock.js +3 -3
- package/dist/src/infra/update-runner.js +1 -1
- package/dist/src/infra/update-startup.js +2 -2
- package/dist/src/infra/write-file-atomic.js +2 -2
- package/dist/src/providers/auth-runtime/auth-profile-store.js +1 -1
- package/dist/src/providers/index.js +2 -2
- package/dist/src/providers/model-registry.js +1 -1
- package/dist/src/session/config-store.js +2 -2
- package/dist/src/session/parity/jsonl-transcript-io.js +2 -2
- package/dist/src/session/parity/sessions-json-file.js +1 -1
- package/dist/src/session/parity/transcript-file-lock.js +2 -2
- package/dist/src/session/parity/transcript-paths.js +1 -1
- package/dist/src/session/search-index-cache.js +1 -1
- package/dist/src/session/search-index.js +1 -1
- package/dist/src/session/session-title.js +3 -2
- package/dist/src/session/session-title.js.map +1 -1
- package/dist/src/session/store.js +5 -5
- package/dist/src/share/share-auto.d.ts +74 -0
- package/dist/src/share/share-auto.js +247 -0
- package/dist/src/share/share-auto.js.map +1 -0
- package/dist/src/share/share-config.js +63 -4
- package/dist/src/share/share-config.js.map +1 -1
- package/dist/src/share/share-landing.d.ts +28 -2
- package/dist/src/share/share-landing.js +155 -34
- package/dist/src/share/share-landing.js.map +1 -1
- package/dist/src/share/share-store.d.ts +48 -4
- package/dist/src/share/share-store.js +322 -51
- package/dist/src/share/share-store.js.map +1 -1
- package/dist/src/share/share-thumbnail.d.ts +35 -0
- package/dist/src/share/share-thumbnail.js +277 -0
- package/dist/src/share/share-thumbnail.js.map +1 -0
- package/dist/src/share/share-types.d.ts +68 -10
- package/dist/src/share/share-types.js +18 -1
- package/dist/src/share/share-types.js.map +1 -1
- package/dist/src/share/share-url.js +1 -1
- package/dist/src/share/share-zip.d.ts +35 -0
- package/dist/src/share/share-zip.js +303 -0
- package/dist/src/share/share-zip.js.map +1 -0
- package/dist/src/share/site-proxy.d.ts +35 -0
- package/dist/src/share/site-proxy.js +234 -0
- package/dist/src/share/site-proxy.js.map +1 -0
- package/dist/src/share/site-share-config.d.ts +11 -0
- package/dist/src/share/site-share-config.js +103 -0
- package/dist/src/share/site-share-config.js.map +1 -0
- package/dist/src/share/site-share-router.d.ts +23 -0
- package/dist/src/share/site-share-router.js +147 -0
- package/dist/src/share/site-share-router.js.map +1 -0
- package/dist/src/share/site-share-store.d.ts +53 -0
- package/dist/src/share/site-share-store.js +400 -0
- package/dist/src/share/site-share-store.js.map +1 -0
- package/dist/src/share/site-share-types.d.ts +103 -0
- package/dist/src/share/site-share-types.js +41 -0
- package/dist/src/share/site-share-types.js.map +1 -0
- package/dist/src/share/site-static-serve.d.ts +10 -0
- package/dist/src/share/site-static-serve.js +145 -0
- package/dist/src/share/site-static-serve.js.map +1 -0
- package/dist/src/tui/clipboard-image.js +3 -3
- package/dist/src/tui/theme-manager.js +1 -1
- package/dist/src/tui/tui-commands.js +18 -0
- package/dist/src/tui/tui-commands.js.map +1 -1
- package/dist/src/tui/tui-keybindings-file.js +1 -1
- package/dist/src/tui/tui-scoped-models.js +2 -2
- package/dist/src/tui/tui-settings.js +1 -1
- package/dist/src/tui/tui-workflow-slash.d.ts +32 -0
- package/dist/src/tui/tui-workflow-slash.js +63 -0
- package/dist/src/tui/tui-workflow-slash.js.map +1 -0
- package/dist/src/tui/tui.js +2 -2
- package/dist/src/tunnel/enable-lan-pairing.js +1 -1
- package/dist/src/tunnel/frpc-binary.js +3 -3
- package/dist/src/tunnel/frpc-config.js +1 -1
- package/dist/src/tunnel/frpc-extract.js +1 -1
- package/dist/src/tunnel/index.js +2 -2
- package/dist/src/tunnel/pair-context.d.ts +7 -1
- package/dist/src/tunnel/pair-context.js +25 -9
- package/dist/src/tunnel/pair-context.js.map +1 -1
- package/dist/src/tunnel/pair-url.d.ts +14 -1
- package/dist/src/tunnel/pair-url.js +14 -1
- package/dist/src/tunnel/pair-url.js.map +1 -1
- package/dist/src/tunnel/tunnel-service.js +2 -2
- package/dist/src/tunnel/tunnel-state.js +1 -1
- package/dist/src/utils/logger/audit.js +1 -1
- package/dist/src/utils/logger/log-store.js +1 -1
- package/dist/src/utils/logger/rotation.js +1 -1
- package/dist/src/voice/tts/audio.js +1 -1
- package/dist/src/voice/tts/providers/edge-speech.js +2 -2
- package/package.json +3 -2
- package/dist/gateway/static/root/assets/agents-tR-nNP04.js +0 -222
- package/dist/gateway/static/root/assets/apps-page-BDw6SP-d.js +0 -1
- package/dist/gateway/static/root/assets/button-KafIU8dx.js +0 -1
- package/dist/gateway/static/root/assets/channels-settings-DEFd-jj1.js +0 -1
- package/dist/gateway/static/root/assets/channels-status-swr-DI5FHdGe.js +0 -8
- package/dist/gateway/static/root/assets/cron-api-BSqY8LwW.js +0 -1
- package/dist/gateway/static/root/assets/cron-page-D7lVDjcR.js +0 -1
- package/dist/gateway/static/root/assets/dist-C57OMHW8.js +0 -48
- package/dist/gateway/static/root/assets/extension-page-CQo2Xsmg.js +0 -1
- package/dist/gateway/static/root/assets/extension-settings-page-CZf0WoZg.js +0 -1
- package/dist/gateway/static/root/assets/fetch-2iRFmd3n.js +0 -3
- package/dist/gateway/static/root/assets/heartbeat-config-api-B0drdQEJ.js +0 -1
- package/dist/gateway/static/root/assets/index-0Gt3TG4j.js +0 -4693
- package/dist/gateway/static/root/assets/index-BuFldCsB.css +0 -1
- package/dist/gateway/static/root/assets/logs-page-DMuORLfC.js +0 -1
- package/dist/gateway/static/root/assets/sessions-page-_UO8g6NN.js +0 -1
- package/dist/gateway/static/root/assets/settings-form-section-DkmHkknc.js +0 -1
- package/dist/gateway/static/root/assets/settings-page-Cz8FoW_A.js +0 -3
- package/dist/gateway/static/root/assets/skills-page-HrUOxF7H.js +0 -2
- package/dist/gateway/static/root/assets/theme-store-D01dJt95.js +0 -1
- package/dist/gateway/static/root/assets/utils-BFwcR6pL.js +0 -1
- package/dist/gateway/static/root/assets/voice-api-key-field-JF8-aqc5.js +0 -1
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
import { init_public_url, validatePublicUrl } from "../../../config/public-url.js";
|
|
1
2
|
import { resolveGatewayEffectiveHost } from "../../../config/gateway-bind.js";
|
|
3
|
+
import { loadTunnelState } from "../../../tunnel/tunnel-state.js";
|
|
2
4
|
import { extractToken } from "../../auth.js";
|
|
3
5
|
import { TUNNEL_CONSENT_REQUIRED_CODE, TunnelConsentError, assertTunnelMayStart, getTunnelConsentState } from "../../../tunnel/consent.js";
|
|
4
6
|
import { getTunnelRegistrationSecretMeta, readTunnelRegistrationSecretFromConfigOnly, resolveTunnelBrokerUrl } from "../../../tunnel/env.js";
|
|
5
7
|
import { createPairingSecret, exchangePairingSecretOnce, getCachedPairingExchange } from "../../../tunnel/pairing.js";
|
|
6
|
-
import { loadTunnelState } from "../../../tunnel/tunnel-state.js";
|
|
7
8
|
import { logTunnelAudit } from "../../../tunnel/tunnel-audit.js";
|
|
8
9
|
import { getTunnelService, hashGatewayToken } from "../../../tunnel/tunnel-service.js";
|
|
9
10
|
import { configureTunnelFromGatewayConfig } from "../../../tunnel/gateway-lifecycle.js";
|
|
10
11
|
import { applyTunnelConsentToConfig, setTunnelEnabledInConfig } from "../../../tunnel/tunnel-config.js";
|
|
11
12
|
import { getClientIpFromHeaders } from "../../security/loopback.js";
|
|
13
|
+
import { resolveReverseProxyPublicUrl } from "../../public-url.js";
|
|
12
14
|
import { applyLanPairingGatewayPatch } from "../../../tunnel/enable-lan-pairing.js";
|
|
13
15
|
import { consumeTunnelMutationLimit } from "../../../tunnel/tunnel-rate-limit.js";
|
|
14
16
|
import "../../../tunnel/index.js";
|
|
@@ -16,6 +18,7 @@ import { buildMobileConnectUrlOrder, resolveMobilePairLanUrl, validateMobilePair
|
|
|
16
18
|
import { buildMobilePairContext } from "../../../tunnel/pair-context.js";
|
|
17
19
|
import { consumePairingExchangeFailLimit } from "../../../tunnel/pairing-rate-limit.js";
|
|
18
20
|
//#region src/gateway/hono/routes/tunnel.ts
|
|
21
|
+
init_public_url();
|
|
19
22
|
async function configureTunnelFromService(deps, opts) {
|
|
20
23
|
await configureTunnelFromGatewayConfig(deps.service.currentConfig, opts);
|
|
21
24
|
}
|
|
@@ -62,7 +65,8 @@ function registerTunnelPublicRoutes(app, service) {
|
|
|
62
65
|
const context = buildMobilePairContext({
|
|
63
66
|
config,
|
|
64
67
|
tunnelPublicUrl: status.publicUrl,
|
|
65
|
-
tunnelConnected: status.state === "connected"
|
|
68
|
+
tunnelConnected: status.state === "connected",
|
|
69
|
+
reverseProxyPublicUrl: resolveReverseProxyPublicUrl(config)
|
|
66
70
|
});
|
|
67
71
|
return c.json({
|
|
68
72
|
ok: true,
|
|
@@ -74,6 +78,7 @@ function registerTunnelPublicRoutes(app, service) {
|
|
|
74
78
|
pairingReady: context.pairingReady,
|
|
75
79
|
blockReason: context.blockReason ?? null,
|
|
76
80
|
tunnelConnected: status.state === "connected",
|
|
81
|
+
reverseProxyConfigured: Boolean(resolveReverseProxyPublicUrl(config)),
|
|
77
82
|
connectUrls: context.connectUrls
|
|
78
83
|
});
|
|
79
84
|
});
|
|
@@ -121,15 +126,19 @@ function registerTunnelPublicRoutes(app, service) {
|
|
|
121
126
|
if (!token) return c.json({ error: "Gateway token not configured" }, 500);
|
|
122
127
|
const persisted = loadTunnelState();
|
|
123
128
|
const config = service.currentConfig;
|
|
124
|
-
const
|
|
129
|
+
const tunnelUrl = persisted?.publicUrl?.trim() || null;
|
|
130
|
+
const reverseProxyUrl = resolveReverseProxyPublicUrl(config);
|
|
125
131
|
const lanUrl = resolveMobilePairLanUrl(config);
|
|
126
132
|
const connectUrls = buildMobileConnectUrlOrder({
|
|
127
|
-
|
|
128
|
-
|
|
133
|
+
reverseProxyUrl,
|
|
134
|
+
baseUrl: reverseProxyUrl ?? tunnelUrl,
|
|
135
|
+
lanUrl,
|
|
136
|
+
tunnelUrl
|
|
129
137
|
});
|
|
138
|
+
const advertisedBaseUrl = reverseProxyUrl ?? tunnelUrl;
|
|
130
139
|
const payload = await exchangePairingSecretOnce(pairingSecret, () => ({
|
|
131
140
|
token,
|
|
132
|
-
baseUrl:
|
|
141
|
+
baseUrl: advertisedBaseUrl,
|
|
133
142
|
lanUrl,
|
|
134
143
|
connectUrls
|
|
135
144
|
}));
|
|
@@ -166,7 +175,8 @@ function registerTunnelRoutes(authenticated, deps) {
|
|
|
166
175
|
const context = buildMobilePairContext({
|
|
167
176
|
config,
|
|
168
177
|
tunnelPublicUrl: status.publicUrl,
|
|
169
|
-
tunnelConnected: status.state === "connected"
|
|
178
|
+
tunnelConnected: status.state === "connected",
|
|
179
|
+
reverseProxyPublicUrl: resolveReverseProxyPublicUrl(config)
|
|
170
180
|
});
|
|
171
181
|
return c.json(context);
|
|
172
182
|
});
|
|
@@ -197,7 +207,8 @@ function registerTunnelRoutes(authenticated, deps) {
|
|
|
197
207
|
let context = buildMobilePairContext({
|
|
198
208
|
config: deps.service.currentConfig,
|
|
199
209
|
tunnelPublicUrl: status.publicUrl,
|
|
200
|
-
tunnelConnected: status.state === "connected"
|
|
210
|
+
tunnelConnected: status.state === "connected",
|
|
211
|
+
reverseProxyPublicUrl: resolveReverseProxyPublicUrl(deps.service.currentConfig)
|
|
201
212
|
});
|
|
202
213
|
if (patchResult.changed) context = {
|
|
203
214
|
...context,
|
|
@@ -224,6 +235,84 @@ function registerTunnelRoutes(authenticated, deps) {
|
|
|
224
235
|
expiresAt: expiresAt.toISOString()
|
|
225
236
|
});
|
|
226
237
|
});
|
|
238
|
+
/**
|
|
239
|
+
* Probe a candidate reverse-proxy URL before persisting it. The check round-trips
|
|
240
|
+
* a `GET /api/tunnel/pair/ping` and validates the response identifies as
|
|
241
|
+
* `service: 'xopc-gateway'`. Surface-area errors are mapped to stable codes so
|
|
242
|
+
* the UI can render targeted hints (TLS / DNS / wrong service / blocked path).
|
|
243
|
+
*/
|
|
244
|
+
authenticated.post("/api/tunnel/pair/probe-public", tunnelMutationLimit, async (c) => {
|
|
245
|
+
if (!requireGatewayToken(c)) return c.json({ error: "Gateway token required" }, 401);
|
|
246
|
+
let body;
|
|
247
|
+
try {
|
|
248
|
+
body = await c.req.json();
|
|
249
|
+
} catch {
|
|
250
|
+
return c.json({
|
|
251
|
+
ok: false,
|
|
252
|
+
code: "INVALID_JSON",
|
|
253
|
+
message: "Invalid JSON body"
|
|
254
|
+
}, 400);
|
|
255
|
+
}
|
|
256
|
+
const validation = validatePublicUrl(typeof body.url === "string" ? body.url : "");
|
|
257
|
+
if (validation.ok === false) return c.json({
|
|
258
|
+
ok: false,
|
|
259
|
+
code: validation.code,
|
|
260
|
+
message: validation.message
|
|
261
|
+
});
|
|
262
|
+
const pingUrl = `${validation.url}/api/tunnel/pair/ping`;
|
|
263
|
+
const startedAt = Date.now();
|
|
264
|
+
let response;
|
|
265
|
+
try {
|
|
266
|
+
response = await fetch(pingUrl, {
|
|
267
|
+
method: "GET",
|
|
268
|
+
headers: { Accept: "application/json" },
|
|
269
|
+
signal: AbortSignal.timeout(5e3)
|
|
270
|
+
});
|
|
271
|
+
} catch (error) {
|
|
272
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
273
|
+
const lower = message.toLowerCase();
|
|
274
|
+
let code = "NETWORK_ERROR";
|
|
275
|
+
if (lower.includes("timeout") || lower.includes("aborted")) code = "TIMEOUT";
|
|
276
|
+
else if (lower.includes("certificate") || lower.includes("cert") || lower.includes("tls") || lower.includes("ssl")) code = "TLS_INVALID";
|
|
277
|
+
else if (lower.includes("enotfound") || lower.includes("econnrefused") || lower.includes("eai_again")) code = "DNS_OR_CONN_REFUSED";
|
|
278
|
+
return c.json({
|
|
279
|
+
ok: false,
|
|
280
|
+
code,
|
|
281
|
+
message
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
if (response.status === 401 || response.status === 403) return c.json({
|
|
285
|
+
ok: false,
|
|
286
|
+
code: "AUTH_BLOCKED",
|
|
287
|
+
message: `Reverse proxy returned ${response.status}`
|
|
288
|
+
});
|
|
289
|
+
if (!response.ok) return c.json({
|
|
290
|
+
ok: false,
|
|
291
|
+
code: "HTTP_ERROR",
|
|
292
|
+
message: `HTTP ${response.status}`
|
|
293
|
+
});
|
|
294
|
+
let parsed = null;
|
|
295
|
+
try {
|
|
296
|
+
parsed = await response.json();
|
|
297
|
+
} catch {
|
|
298
|
+
return c.json({
|
|
299
|
+
ok: false,
|
|
300
|
+
code: "NOT_XOPC_GATEWAY",
|
|
301
|
+
message: "Response was not JSON"
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
if (!parsed || parsed.service !== "xopc-gateway") return c.json({
|
|
305
|
+
ok: false,
|
|
306
|
+
code: "NOT_XOPC_GATEWAY",
|
|
307
|
+
message: "Endpoint did not identify as an xopc gateway"
|
|
308
|
+
});
|
|
309
|
+
return c.json({
|
|
310
|
+
ok: true,
|
|
311
|
+
url: validation.url,
|
|
312
|
+
latencyMs: Date.now() - startedAt,
|
|
313
|
+
mobilePairing: parsed.mobilePairing === true
|
|
314
|
+
});
|
|
315
|
+
});
|
|
227
316
|
authenticated.get("/api/tunnel/status", async (c) => {
|
|
228
317
|
await configureTunnelFromService(deps);
|
|
229
318
|
const config = deps.service.currentConfig;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel.js","names":[],"sources":["../../../../../src/gateway/hono/routes/tunnel.ts"],"sourcesContent":["import type { Hono, MiddlewareHandler } from 'hono';\n\nimport type { Config } from '../../../config/schema.js';\nimport { resolveGatewayEffectiveHost } from '../../../config/gateway-bind.js';\nimport { extractToken } from '../../auth.js';\nimport {\n assertTunnelMayStart,\n getTunnelConsentState,\n TUNNEL_CONSENT_REQUIRED_CODE,\n TunnelConsentError,\n} from '../../../tunnel/consent.js';\nimport { hashGatewayToken } from '../../../tunnel/tunnel-service.js';\nimport { configureTunnelFromGatewayConfig } from '../../../tunnel/gateway-lifecycle.js';\nimport {\n getTunnelRegistrationSecretMeta,\n readTunnelRegistrationSecretFromConfigOnly,\n resolveTunnelBrokerUrl,\n} from '../../../tunnel/env.js';\nimport { getTunnelService } from '../../../tunnel/index.js';\nimport { createPairingSecret, exchangePairingSecretOnce, getCachedPairingExchange } from '../../../tunnel/pairing.js';\nimport { buildMobilePairContext } from '../../../tunnel/pair-context.js';\nimport { applyLanPairingGatewayPatch } from '../../../tunnel/enable-lan-pairing.js';\nimport {\n buildMobileConnectUrlOrder,\n resolveMobilePairLanUrl,\n validateMobilePairBaseUrl,\n} from '../../../tunnel/pair-url.js';\nimport { consumePairingExchangeFailLimit } from '../../../tunnel/pairing-rate-limit.js';\nimport { loadTunnelState } from '../../../tunnel/tunnel-state.js';\nimport { logTunnelAudit } from '../../../tunnel/tunnel-audit.js';\nimport {\n applyTunnelConsentToConfig,\n setTunnelEnabledInConfig,\n} from '../../../tunnel/tunnel-config.js';\nimport { consumeTunnelMutationLimit } from '../../../tunnel/tunnel-rate-limit.js';\nimport type { AuthenticatedRouteDeps } from './deps.js';\nimport type { GatewayService } from '../../service.js';\nimport { getClientIpFromHeaders } from '../../security/loopback.js';\n\nasync function configureTunnelFromService(\n deps: AuthenticatedRouteDeps,\n opts?: { force?: boolean },\n): Promise<void> {\n await configureTunnelFromGatewayConfig(deps.service.currentConfig, opts);\n}\n\nfunction enrichTunnelStatus(config: Config, status: ReturnType<ReturnType<typeof getTunnelService>['getStatus']>) {\n const consent = getTunnelConsentState(config);\n const brokerUrl = resolveTunnelBrokerUrl(config.tunnel?.brokerUrl);\n const registrationSecret = getTunnelRegistrationSecretMeta(config, process.env, brokerUrl);\n return {\n ...status,\n consentRequired: consent.consentRequired,\n consent: {\n currentVersion: consent.currentVersion,\n acceptedVersion: consent.acceptedVersion,\n acceptedAt: consent.acceptedAt,\n valid: consent.valid,\n },\n canAutoStart: consent.canAutoStart,\n registrationSecret,\n };\n}\n\nfunction requireGatewayToken(c: { req: { header: (name: string) => string | undefined } }): string | null {\n return (\n extractToken({\n authorization: c.req.header('authorization') ?? undefined,\n }) ?? null\n );\n}\n\nfunction createTunnelMutationRateLimitMiddleware(): MiddlewareHandler {\n return async (c, next) => {\n const token = requireGatewayToken(c);\n if (!token) {\n return c.json({ error: 'Gateway token required' }, 401);\n }\n const result = consumeTunnelMutationLimit(token);\n if (!result.allowed) {\n c.header('Retry-After', String(Math.ceil(result.retryAfterMs / 1000)));\n return c.json(\n {\n error: 'Too many tunnel operations. Try again later.',\n code: 'TUNNEL_RATE_LIMITED',\n retryAfterMs: result.retryAfterMs,\n },\n 429,\n );\n }\n await next();\n };\n}\n\nexport function registerTunnelPublicRoutes(app: Hono, service: GatewayService): void {\n app.get('/api/tunnel/pair/ping', async (c) => {\n const config = service.currentConfig as Config;\n const tunnel = getTunnelService();\n const status = tunnel.getStatus();\n const context = buildMobilePairContext({\n config,\n tunnelPublicUrl: status.publicUrl,\n tunnelConnected: status.state === 'connected',\n });\n return c.json({\n ok: true,\n service: 'xopc-gateway',\n mobilePairing: true,\n port: context.port,\n bindMode: context.bindMode,\n listenHost: context.listenHost,\n pairingReady: context.pairingReady,\n blockReason: context.blockReason ?? null,\n tunnelConnected: status.state === 'connected',\n connectUrls: context.connectUrls,\n });\n });\n\n app.post('/api/tunnel/pair/validate-url', async (c) => {\n let body: { baseUrl?: unknown };\n try {\n body = (await c.req.json()) as { baseUrl?: unknown };\n } catch {\n return c.json({ error: 'Invalid JSON body' }, 400);\n }\n const baseUrl = typeof body.baseUrl === 'string' ? body.baseUrl : '';\n const result = validateMobilePairBaseUrl(baseUrl);\n if (result.ok === false) {\n return c.json({\n ok: false,\n code: result.code,\n message: result.message,\n });\n }\n return c.json({\n ok: true,\n url: result.url,\n loopback: false,\n probePath: '/api/tunnel/pair/ping',\n });\n });\n\n app.post('/api/tunnel/exchange-token', async (c) => {\n const clientIp =\n getClientIpFromHeaders({\n get: (name: string) => c.req.header(name) ?? undefined,\n }) ?? 'unknown';\n\n let body: { pairingSecret?: unknown };\n try {\n body = (await c.req.json()) as { pairingSecret?: unknown };\n } catch {\n return c.json({ error: 'Invalid JSON body' }, 400);\n }\n\n const pairingSecret = typeof body.pairingSecret === 'string' ? body.pairingSecret.trim() : '';\n if (!pairingSecret) {\n return c.json({ error: 'pairingSecret required' }, 400);\n }\n\n const cached = getCachedPairingExchange(pairingSecret);\n if (cached) {\n logTunnelAudit(\n 'tunnel.exchange_token',\n { ok: true, clientIp, phase: 'pairing_exchange', replay: true },\n 'Pairing secret replayed (duplicate mobile exchange)',\n );\n return c.json(cached);\n }\n\n const token = service.getAuthToken();\n if (!token) {\n return c.json({ error: 'Gateway token not configured' }, 500);\n }\n\n const persisted = loadTunnelState();\n const config = service.currentConfig as Config;\n const publicUrl = persisted?.publicUrl?.trim() || null;\n const lanUrl = resolveMobilePairLanUrl(config);\n const connectUrls = buildMobileConnectUrlOrder({ baseUrl: publicUrl, lanUrl });\n\n const payload = await exchangePairingSecretOnce(pairingSecret, () => ({\n token,\n baseUrl: publicUrl,\n lanUrl,\n connectUrls,\n }));\n\n if (!payload) {\n const limited = consumePairingExchangeFailLimit(clientIp);\n if (!limited.allowed) {\n c.header('Retry-After', String(Math.ceil(limited.retryAfterMs / 1000)));\n }\n logTunnelAudit(\n 'tunnel.exchange_token',\n { ok: false, clientIp, phase: 'pairing_exchange' },\n 'Pairing exchange denied: invalid or expired secret',\n );\n return c.json({ error: 'Invalid or expired pairing secret', code: 'PAIRING_INVALID' }, 401);\n }\n\n logTunnelAudit(\n 'tunnel.exchange_token',\n { ok: true, clientIp, subdomain: persisted?.subdomain ?? null, phase: 'pairing_exchange' },\n 'Pairing secret exchanged for gateway token',\n );\n return c.json(payload);\n });\n}\n\nexport function registerTunnelRoutes(authenticated: Hono, deps: AuthenticatedRouteDeps): void {\n const { strictRateLimitMiddleware } = deps;\n const tunnel = getTunnelService();\n const tunnelMutationLimit = createTunnelMutationRateLimitMiddleware();\n\n authenticated.get('/api/tunnel/pair/context', async (c) => {\n await configureTunnelFromService(deps);\n const config = deps.service.currentConfig as Config;\n const status = tunnel.getStatus();\n const context = buildMobilePairContext({\n config,\n tunnelPublicUrl: status.publicUrl,\n tunnelConnected: status.state === 'connected',\n });\n return c.json(context);\n });\n\n authenticated.post('/api/tunnel/pair/enable-lan', tunnelMutationLimit, async (c) => {\n const token = requireGatewayToken(c);\n if (!token) return c.json({ error: 'Gateway token required' }, 401);\n\n const config = deps.service.currentConfig as Config;\n const patchResult = applyLanPairingGatewayPatch(config);\n if (patchResult.ok === false) {\n return c.json({ ok: false, error: { message: patchResult.message, code: 'LAN_PAIRING_CONFIG' } }, 400);\n }\n\n if (patchResult.changed) {\n const saveResult = await deps.service.saveConfig(config);\n if (!saveResult.saved) {\n return c.json(\n { ok: false, error: { message: saveResult.error ?? 'Failed to save config', code: 'SAVE_FAILED' } },\n 500,\n );\n }\n logTunnelAudit(\n 'tunnel.enable_lan_pairing',\n { gatewayTokenHash: hashGatewayToken(token).slice(0, 12) },\n 'Gateway bind switched to LAN for mobile pairing',\n );\n }\n\n const status = tunnel.getStatus();\n let context = buildMobilePairContext({\n config: deps.service.currentConfig as Config,\n tunnelPublicUrl: status.publicUrl,\n tunnelConnected: status.state === 'connected',\n });\n\n if (patchResult.changed) {\n context = {\n ...context,\n pairingReady: false,\n blockReason: 'GATEWAY_LOOPBACK_ONLY',\n };\n }\n\n return c.json({\n ok: true,\n requiresRestart: patchResult.changed,\n context,\n });\n });\n\n authenticated.post('/api/tunnel/pair', tunnelMutationLimit, async (c) => {\n await configureTunnelFromService(deps);\n const token = requireGatewayToken(c);\n if (!token) return c.json({ error: 'Gateway token required' }, 401);\n\n const { secret, expiresAt } = createPairingSecret();\n logTunnelAudit(\n 'tunnel.pair',\n {\n expiresAt: expiresAt.toISOString(),\n gatewayTokenHash: hashGatewayToken(token).slice(0, 12),\n },\n 'Mobile pairing session created',\n );\n return c.json({ pairingSecret: secret, expiresAt: expiresAt.toISOString() });\n });\n\n authenticated.get('/api/tunnel/status', async (c) => {\n await configureTunnelFromService(deps);\n const config = deps.service.currentConfig as Config;\n return c.json(enrichTunnelStatus(config, tunnel.getStatus()));\n });\n\n authenticated.post('/api/tunnel/consent', tunnelMutationLimit, async (c) => {\n const token = requireGatewayToken(c);\n if (!token) return c.json({ error: 'Gateway token required' }, 401);\n\n const config = deps.service.currentConfig as Config;\n applyTunnelConsentToConfig(config);\n const result = await deps.service.saveConfig(config);\n if (!result.saved) {\n return c.json({ ok: false, error: result.error ?? 'Failed to save config' }, 500);\n }\n const consent = getTunnelConsentState(config);\n logTunnelAudit(\n 'tunnel.consent',\n {\n consentVersion: consent.currentVersion,\n gatewayTokenHash: hashGatewayToken(token).slice(0, 12),\n },\n 'Remote access security consent recorded',\n );\n return c.json({\n ok: true,\n consent: {\n currentVersion: consent.currentVersion,\n acceptedVersion: consent.acceptedVersion,\n acceptedAt: consent.acceptedAt,\n valid: consent.valid,\n },\n });\n });\n\n authenticated.post('/api/tunnel/start', tunnelMutationLimit, async (c) => {\n await configureTunnelFromService(deps, { force: true });\n const config = deps.service.currentConfig as Config;\n const token = requireGatewayToken(c);\n if (!token) return c.json({ error: 'Gateway token required' }, 401);\n\n try {\n assertTunnelMayStart(config);\n } catch (err) {\n if (err instanceof TunnelConsentError) {\n logTunnelAudit(\n 'tunnel.start_denied',\n { reason: TUNNEL_CONSENT_REQUIRED_CODE, gatewayTokenHash: hashGatewayToken(token).slice(0, 12) },\n 'Tunnel start denied: consent required',\n );\n return c.json({ error: err.message, code: TUNNEL_CONSENT_REQUIRED_CODE }, 403);\n }\n throw err;\n }\n\n const gateway = config.gateway;\n const port = gateway.port ?? 18790;\n try {\n const qr = await tunnel.start(port, token);\n setTunnelEnabledInConfig(config, true);\n await deps.service.saveConfig(config);\n const status = tunnel.getStatus();\n return c.json({\n publicUrl: qr.publicUrl,\n subdomain: status.subdomain,\n qrPayload: qr.qrPayload,\n lanUrl: qr.lanUrl,\n });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return c.json({ error: message }, 500);\n }\n });\n\n authenticated.post('/api/tunnel/stop', tunnelMutationLimit, async (c) => {\n await configureTunnelFromService(deps);\n const config = deps.service.currentConfig as Config;\n let release = false;\n try {\n const body = (await c.req.json().catch(() => ({}))) as { release?: unknown };\n release = body.release === true;\n } catch {\n release = false;\n }\n const { released } = await tunnel.stop({ release });\n setTunnelEnabledInConfig(config, false);\n await deps.service.saveConfig(config);\n return c.json({ ok: true, released });\n });\n\n authenticated.get('/api/tunnel/qr', async (c) => {\n await configureTunnelFromService(deps);\n const gateway = deps.service.currentConfig.gateway;\n const port = gateway.port ?? 18790;\n const host = resolveGatewayEffectiveHost(deps.service.currentConfig);\n const token = requireGatewayToken(c);\n if (!token) return c.json({ error: 'Gateway token required' }, 401);\n const qr = await tunnel.buildQr(port, host);\n return c.json(qr);\n });\n\n authenticated.get('/api/tunnel/transport-status', async (c) => {\n await configureTunnelFromService(deps);\n return c.json({\n transport: { tls: 'broker_terminated' as const },\n });\n });\n\n /**\n * POST /api/tunnel/reveal-registration-secret — plaintext only when stored in config file.\n */\n authenticated.post('/api/tunnel/reveal-registration-secret', strictRateLimitMiddleware, async (c) => {\n const config = deps.service.currentConfig as Config;\n const registrationSecret = readTunnelRegistrationSecretFromConfigOnly(config);\n return c.json({\n ok: true,\n payload: {\n registrationSecret,\n source: registrationSecret ? ('config' as const) : ('none' as const),\n },\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAuCA,eAAe,2BACb,MACA,MACe;AACf,OAAM,iCAAiC,KAAK,QAAQ,eAAe,KAAK;;AAG1E,SAAS,mBAAmB,QAAgB,QAAsE;CAChH,MAAM,UAAU,sBAAsB,OAAO;CAC7C,MAAM,YAAY,uBAAuB,OAAO,QAAQ,UAAU;CAClE,MAAM,qBAAqB,gCAAgC,QAAQ,QAAQ,KAAK,UAAU;AAC1F,QAAO;EACL,GAAG;EACH,iBAAiB,QAAQ;EACzB,SAAS;GACP,gBAAgB,QAAQ;GACxB,iBAAiB,QAAQ;GACzB,YAAY,QAAQ;GACpB,OAAO,QAAQ;GAChB;EACD,cAAc,QAAQ;EACtB;EACD;;AAGH,SAAS,oBAAoB,GAA6E;AACxG,QACE,aAAa,EACX,eAAe,EAAE,IAAI,OAAO,gBAAgB,IAAI,KAAA,GACjD,CAAC,IAAI;;AAIV,SAAS,0CAA6D;AACpE,QAAO,OAAO,GAAG,SAAS;EACxB,MAAM,QAAQ,oBAAoB,EAAE;AACpC,MAAI,CAAC,MACH,QAAO,EAAE,KAAK,EAAE,OAAO,0BAA0B,EAAE,IAAI;EAEzD,MAAM,SAAS,2BAA2B,MAAM;AAChD,MAAI,CAAC,OAAO,SAAS;AACnB,KAAE,OAAO,eAAe,OAAO,KAAK,KAAK,OAAO,eAAe,IAAK,CAAC,CAAC;AACtE,UAAO,EAAE,KACP;IACE,OAAO;IACP,MAAM;IACN,cAAc,OAAO;IACtB,EACD,IACD;;AAEH,QAAM,MAAM;;;AAIhB,SAAgB,2BAA2B,KAAW,SAA+B;AACnF,KAAI,IAAI,yBAAyB,OAAO,MAAM;EAC5C,MAAM,SAAS,QAAQ;EAEvB,MAAM,SADS,kBACM,CAAC,WAAW;EACjC,MAAM,UAAU,uBAAuB;GACrC;GACA,iBAAiB,OAAO;GACxB,iBAAiB,OAAO,UAAU;GACnC,CAAC;AACF,SAAO,EAAE,KAAK;GACZ,IAAI;GACJ,SAAS;GACT,eAAe;GACf,MAAM,QAAQ;GACd,UAAU,QAAQ;GAClB,YAAY,QAAQ;GACpB,cAAc,QAAQ;GACtB,aAAa,QAAQ,eAAe;GACpC,iBAAiB,OAAO,UAAU;GAClC,aAAa,QAAQ;GACtB,CAAC;GACF;AAEF,KAAI,KAAK,iCAAiC,OAAO,MAAM;EACrD,IAAI;AACJ,MAAI;AACF,UAAQ,MAAM,EAAE,IAAI,MAAM;UACpB;AACN,UAAO,EAAE,KAAK,EAAE,OAAO,qBAAqB,EAAE,IAAI;;EAGpD,MAAM,SAAS,0BADC,OAAO,KAAK,YAAY,WAAW,KAAK,UAAU,GACjB;AACjD,MAAI,OAAO,OAAO,MAChB,QAAO,EAAE,KAAK;GACZ,IAAI;GACJ,MAAM,OAAO;GACb,SAAS,OAAO;GACjB,CAAC;AAEJ,SAAO,EAAE,KAAK;GACZ,IAAI;GACJ,KAAK,OAAO;GACZ,UAAU;GACV,WAAW;GACZ,CAAC;GACF;AAEF,KAAI,KAAK,8BAA8B,OAAO,MAAM;EAClD,MAAM,WACJ,uBAAuB,EACrB,MAAM,SAAiB,EAAE,IAAI,OAAO,KAAK,IAAI,KAAA,GAC9C,CAAC,IAAI;EAER,IAAI;AACJ,MAAI;AACF,UAAQ,MAAM,EAAE,IAAI,MAAM;UACpB;AACN,UAAO,EAAE,KAAK,EAAE,OAAO,qBAAqB,EAAE,IAAI;;EAGpD,MAAM,gBAAgB,OAAO,KAAK,kBAAkB,WAAW,KAAK,cAAc,MAAM,GAAG;AAC3F,MAAI,CAAC,cACH,QAAO,EAAE,KAAK,EAAE,OAAO,0BAA0B,EAAE,IAAI;EAGzD,MAAM,SAAS,yBAAyB,cAAc;AACtD,MAAI,QAAQ;AACV,kBACE,yBACA;IAAE,IAAI;IAAM;IAAU,OAAO;IAAoB,QAAQ;IAAM,EAC/D,sDACD;AACD,UAAO,EAAE,KAAK,OAAO;;EAGvB,MAAM,QAAQ,QAAQ,cAAc;AACpC,MAAI,CAAC,MACH,QAAO,EAAE,KAAK,EAAE,OAAO,gCAAgC,EAAE,IAAI;EAG/D,MAAM,YAAY,iBAAiB;EACnC,MAAM,SAAS,QAAQ;EACvB,MAAM,YAAY,WAAW,WAAW,MAAM,IAAI;EAClD,MAAM,SAAS,wBAAwB,OAAO;EAC9C,MAAM,cAAc,2BAA2B;GAAE,SAAS;GAAW;GAAQ,CAAC;EAE9E,MAAM,UAAU,MAAM,0BAA0B,sBAAsB;GACpE;GACA,SAAS;GACT;GACA;GACD,EAAE;AAEH,MAAI,CAAC,SAAS;GACZ,MAAM,UAAU,gCAAgC,SAAS;AACzD,OAAI,CAAC,QAAQ,QACX,GAAE,OAAO,eAAe,OAAO,KAAK,KAAK,QAAQ,eAAe,IAAK,CAAC,CAAC;AAEzE,kBACE,yBACA;IAAE,IAAI;IAAO;IAAU,OAAO;IAAoB,EAClD,qDACD;AACD,UAAO,EAAE,KAAK;IAAE,OAAO;IAAqC,MAAM;IAAmB,EAAE,IAAI;;AAG7F,iBACE,yBACA;GAAE,IAAI;GAAM;GAAU,WAAW,WAAW,aAAa;GAAM,OAAO;GAAoB,EAC1F,6CACD;AACD,SAAO,EAAE,KAAK,QAAQ;GACtB;;AAGJ,SAAgB,qBAAqB,eAAqB,MAAoC;CAC5F,MAAM,EAAE,8BAA8B;CACtC,MAAM,SAAS,kBAAkB;CACjC,MAAM,sBAAsB,yCAAyC;AAErE,eAAc,IAAI,4BAA4B,OAAO,MAAM;AACzD,QAAM,2BAA2B,KAAK;EACtC,MAAM,SAAS,KAAK,QAAQ;EAC5B,MAAM,SAAS,OAAO,WAAW;EACjC,MAAM,UAAU,uBAAuB;GACrC;GACA,iBAAiB,OAAO;GACxB,iBAAiB,OAAO,UAAU;GACnC,CAAC;AACF,SAAO,EAAE,KAAK,QAAQ;GACtB;AAEF,eAAc,KAAK,+BAA+B,qBAAqB,OAAO,MAAM;EAClF,MAAM,QAAQ,oBAAoB,EAAE;AACpC,MAAI,CAAC,MAAO,QAAO,EAAE,KAAK,EAAE,OAAO,0BAA0B,EAAE,IAAI;EAEnE,MAAM,SAAS,KAAK,QAAQ;EAC5B,MAAM,cAAc,4BAA4B,OAAO;AACvD,MAAI,YAAY,OAAO,MACrB,QAAO,EAAE,KAAK;GAAE,IAAI;GAAO,OAAO;IAAE,SAAS,YAAY;IAAS,MAAM;IAAsB;GAAE,EAAE,IAAI;AAGxG,MAAI,YAAY,SAAS;GACvB,MAAM,aAAa,MAAM,KAAK,QAAQ,WAAW,OAAO;AACxD,OAAI,CAAC,WAAW,MACd,QAAO,EAAE,KACP;IAAE,IAAI;IAAO,OAAO;KAAE,SAAS,WAAW,SAAS;KAAyB,MAAM;KAAe;IAAE,EACnG,IACD;AAEH,kBACE,6BACA,EAAE,kBAAkB,iBAAiB,MAAM,CAAC,MAAM,GAAG,GAAG,EAAE,EAC1D,kDACD;;EAGH,MAAM,SAAS,OAAO,WAAW;EACjC,IAAI,UAAU,uBAAuB;GACnC,QAAQ,KAAK,QAAQ;GACrB,iBAAiB,OAAO;GACxB,iBAAiB,OAAO,UAAU;GACnC,CAAC;AAEF,MAAI,YAAY,QACd,WAAU;GACR,GAAG;GACH,cAAc;GACd,aAAa;GACd;AAGH,SAAO,EAAE,KAAK;GACZ,IAAI;GACJ,iBAAiB,YAAY;GAC7B;GACD,CAAC;GACF;AAEF,eAAc,KAAK,oBAAoB,qBAAqB,OAAO,MAAM;AACvE,QAAM,2BAA2B,KAAK;EACtC,MAAM,QAAQ,oBAAoB,EAAE;AACpC,MAAI,CAAC,MAAO,QAAO,EAAE,KAAK,EAAE,OAAO,0BAA0B,EAAE,IAAI;EAEnE,MAAM,EAAE,QAAQ,cAAc,qBAAqB;AACnD,iBACE,eACA;GACE,WAAW,UAAU,aAAa;GAClC,kBAAkB,iBAAiB,MAAM,CAAC,MAAM,GAAG,GAAG;GACvD,EACD,iCACD;AACD,SAAO,EAAE,KAAK;GAAE,eAAe;GAAQ,WAAW,UAAU,aAAa;GAAE,CAAC;GAC5E;AAEF,eAAc,IAAI,sBAAsB,OAAO,MAAM;AACnD,QAAM,2BAA2B,KAAK;EACtC,MAAM,SAAS,KAAK,QAAQ;AAC5B,SAAO,EAAE,KAAK,mBAAmB,QAAQ,OAAO,WAAW,CAAC,CAAC;GAC7D;AAEF,eAAc,KAAK,uBAAuB,qBAAqB,OAAO,MAAM;EAC1E,MAAM,QAAQ,oBAAoB,EAAE;AACpC,MAAI,CAAC,MAAO,QAAO,EAAE,KAAK,EAAE,OAAO,0BAA0B,EAAE,IAAI;EAEnE,MAAM,SAAS,KAAK,QAAQ;AAC5B,6BAA2B,OAAO;EAClC,MAAM,SAAS,MAAM,KAAK,QAAQ,WAAW,OAAO;AACpD,MAAI,CAAC,OAAO,MACV,QAAO,EAAE,KAAK;GAAE,IAAI;GAAO,OAAO,OAAO,SAAS;GAAyB,EAAE,IAAI;EAEnF,MAAM,UAAU,sBAAsB,OAAO;AAC7C,iBACE,kBACA;GACE,gBAAgB,QAAQ;GACxB,kBAAkB,iBAAiB,MAAM,CAAC,MAAM,GAAG,GAAG;GACvD,EACD,0CACD;AACD,SAAO,EAAE,KAAK;GACZ,IAAI;GACJ,SAAS;IACP,gBAAgB,QAAQ;IACxB,iBAAiB,QAAQ;IACzB,YAAY,QAAQ;IACpB,OAAO,QAAQ;IAChB;GACF,CAAC;GACF;AAEF,eAAc,KAAK,qBAAqB,qBAAqB,OAAO,MAAM;AACxE,QAAM,2BAA2B,MAAM,EAAE,OAAO,MAAM,CAAC;EACvD,MAAM,SAAS,KAAK,QAAQ;EAC5B,MAAM,QAAQ,oBAAoB,EAAE;AACpC,MAAI,CAAC,MAAO,QAAO,EAAE,KAAK,EAAE,OAAO,0BAA0B,EAAE,IAAI;AAEnE,MAAI;AACF,wBAAqB,OAAO;WACrB,KAAK;AACZ,OAAI,eAAe,oBAAoB;AACrC,mBACE,uBACA;KAAE,QAAQ;KAA8B,kBAAkB,iBAAiB,MAAM,CAAC,MAAM,GAAG,GAAG;KAAE,EAChG,wCACD;AACD,WAAO,EAAE,KAAK;KAAE,OAAO,IAAI;KAAS,MAAM;KAA8B,EAAE,IAAI;;AAEhF,SAAM;;EAIR,MAAM,OADU,OAAO,QACF,QAAQ;AAC7B,MAAI;GACF,MAAM,KAAK,MAAM,OAAO,MAAM,MAAM,MAAM;AAC1C,4BAAyB,QAAQ,KAAK;AACtC,SAAM,KAAK,QAAQ,WAAW,OAAO;GACrC,MAAM,SAAS,OAAO,WAAW;AACjC,UAAO,EAAE,KAAK;IACZ,WAAW,GAAG;IACd,WAAW,OAAO;IAClB,WAAW,GAAG;IACd,QAAQ,GAAG;IACZ,CAAC;WACK,KAAK;GACZ,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAChE,UAAO,EAAE,KAAK,EAAE,OAAO,SAAS,EAAE,IAAI;;GAExC;AAEF,eAAc,KAAK,oBAAoB,qBAAqB,OAAO,MAAM;AACvE,QAAM,2BAA2B,KAAK;EACtC,MAAM,SAAS,KAAK,QAAQ;EAC5B,IAAI,UAAU;AACd,MAAI;AAEF,cAAU,MADU,EAAE,IAAI,MAAM,CAAC,aAAa,EAAE,EAAE,EACnC,YAAY;UACrB;AACN,aAAU;;EAEZ,MAAM,EAAE,aAAa,MAAM,OAAO,KAAK,EAAE,SAAS,CAAC;AACnD,2BAAyB,QAAQ,MAAM;AACvC,QAAM,KAAK,QAAQ,WAAW,OAAO;AACrC,SAAO,EAAE,KAAK;GAAE,IAAI;GAAM;GAAU,CAAC;GACrC;AAEF,eAAc,IAAI,kBAAkB,OAAO,MAAM;AAC/C,QAAM,2BAA2B,KAAK;EAEtC,MAAM,OADU,KAAK,QAAQ,cAAc,QACtB,QAAQ;EAC7B,MAAM,OAAO,4BAA4B,KAAK,QAAQ,cAAc;AAEpE,MAAI,CADU,oBAAoB,EACxB,CAAE,QAAO,EAAE,KAAK,EAAE,OAAO,0BAA0B,EAAE,IAAI;EACnE,MAAM,KAAK,MAAM,OAAO,QAAQ,MAAM,KAAK;AAC3C,SAAO,EAAE,KAAK,GAAG;GACjB;AAEF,eAAc,IAAI,gCAAgC,OAAO,MAAM;AAC7D,QAAM,2BAA2B,KAAK;AACtC,SAAO,EAAE,KAAK,EACZ,WAAW,EAAE,KAAK,qBAA8B,EACjD,CAAC;GACF;;;;AAKF,eAAc,KAAK,0CAA0C,2BAA2B,OAAO,MAAM;EACnG,MAAM,SAAS,KAAK,QAAQ;EAC5B,MAAM,qBAAqB,2CAA2C,OAAO;AAC7E,SAAO,EAAE,KAAK;GACZ,IAAI;GACJ,SAAS;IACP;IACA,QAAQ,qBAAsB,WAAsB;IACrD;GACF,CAAC;GACF"}
|
|
1
|
+
{"version":3,"file":"tunnel.js","names":[],"sources":["../../../../../src/gateway/hono/routes/tunnel.ts"],"sourcesContent":["import type { Hono, MiddlewareHandler } from 'hono';\n\nimport type { Config } from '../../../config/schema.js';\nimport { resolveGatewayEffectiveHost } from '../../../config/gateway-bind.js';\nimport { extractToken } from '../../auth.js';\nimport {\n assertTunnelMayStart,\n getTunnelConsentState,\n TUNNEL_CONSENT_REQUIRED_CODE,\n TunnelConsentError,\n} from '../../../tunnel/consent.js';\nimport { hashGatewayToken } from '../../../tunnel/tunnel-service.js';\nimport { configureTunnelFromGatewayConfig } from '../../../tunnel/gateway-lifecycle.js';\nimport {\n getTunnelRegistrationSecretMeta,\n readTunnelRegistrationSecretFromConfigOnly,\n resolveTunnelBrokerUrl,\n} from '../../../tunnel/env.js';\nimport { getTunnelService } from '../../../tunnel/index.js';\nimport { createPairingSecret, exchangePairingSecretOnce, getCachedPairingExchange } from '../../../tunnel/pairing.js';\nimport { buildMobilePairContext } from '../../../tunnel/pair-context.js';\nimport { applyLanPairingGatewayPatch } from '../../../tunnel/enable-lan-pairing.js';\nimport {\n buildMobileConnectUrlOrder,\n resolveMobilePairLanUrl,\n validateMobilePairBaseUrl,\n} from '../../../tunnel/pair-url.js';\nimport { consumePairingExchangeFailLimit } from '../../../tunnel/pairing-rate-limit.js';\nimport { loadTunnelState } from '../../../tunnel/tunnel-state.js';\nimport { logTunnelAudit } from '../../../tunnel/tunnel-audit.js';\nimport { resolveReverseProxyPublicUrl } from '../../public-url.js';\nimport { validatePublicUrl } from '../../../config/public-url.js';\nimport {\n applyTunnelConsentToConfig,\n setTunnelEnabledInConfig,\n} from '../../../tunnel/tunnel-config.js';\nimport { consumeTunnelMutationLimit } from '../../../tunnel/tunnel-rate-limit.js';\nimport type { AuthenticatedRouteDeps } from './deps.js';\nimport type { GatewayService } from '../../service.js';\nimport { getClientIpFromHeaders } from '../../security/loopback.js';\n\nasync function configureTunnelFromService(\n deps: AuthenticatedRouteDeps,\n opts?: { force?: boolean },\n): Promise<void> {\n await configureTunnelFromGatewayConfig(deps.service.currentConfig, opts);\n}\n\nfunction enrichTunnelStatus(config: Config, status: ReturnType<ReturnType<typeof getTunnelService>['getStatus']>) {\n const consent = getTunnelConsentState(config);\n const brokerUrl = resolveTunnelBrokerUrl(config.tunnel?.brokerUrl);\n const registrationSecret = getTunnelRegistrationSecretMeta(config, process.env, brokerUrl);\n return {\n ...status,\n consentRequired: consent.consentRequired,\n consent: {\n currentVersion: consent.currentVersion,\n acceptedVersion: consent.acceptedVersion,\n acceptedAt: consent.acceptedAt,\n valid: consent.valid,\n },\n canAutoStart: consent.canAutoStart,\n registrationSecret,\n };\n}\n\nfunction requireGatewayToken(c: { req: { header: (name: string) => string | undefined } }): string | null {\n return (\n extractToken({\n authorization: c.req.header('authorization') ?? undefined,\n }) ?? null\n );\n}\n\nfunction createTunnelMutationRateLimitMiddleware(): MiddlewareHandler {\n return async (c, next) => {\n const token = requireGatewayToken(c);\n if (!token) {\n return c.json({ error: 'Gateway token required' }, 401);\n }\n const result = consumeTunnelMutationLimit(token);\n if (!result.allowed) {\n c.header('Retry-After', String(Math.ceil(result.retryAfterMs / 1000)));\n return c.json(\n {\n error: 'Too many tunnel operations. Try again later.',\n code: 'TUNNEL_RATE_LIMITED',\n retryAfterMs: result.retryAfterMs,\n },\n 429,\n );\n }\n await next();\n };\n}\n\nexport function registerTunnelPublicRoutes(app: Hono, service: GatewayService): void {\n app.get('/api/tunnel/pair/ping', async (c) => {\n const config = service.currentConfig as Config;\n const tunnel = getTunnelService();\n const status = tunnel.getStatus();\n const context = buildMobilePairContext({\n config,\n tunnelPublicUrl: status.publicUrl,\n tunnelConnected: status.state === 'connected',\n reverseProxyPublicUrl: resolveReverseProxyPublicUrl(config),\n });\n return c.json({\n ok: true,\n service: 'xopc-gateway',\n mobilePairing: true,\n port: context.port,\n bindMode: context.bindMode,\n listenHost: context.listenHost,\n pairingReady: context.pairingReady,\n blockReason: context.blockReason ?? null,\n tunnelConnected: status.state === 'connected',\n reverseProxyConfigured: Boolean(resolveReverseProxyPublicUrl(config)),\n connectUrls: context.connectUrls,\n });\n });\n\n app.post('/api/tunnel/pair/validate-url', async (c) => {\n let body: { baseUrl?: unknown };\n try {\n body = (await c.req.json()) as { baseUrl?: unknown };\n } catch {\n return c.json({ error: 'Invalid JSON body' }, 400);\n }\n const baseUrl = typeof body.baseUrl === 'string' ? body.baseUrl : '';\n const result = validateMobilePairBaseUrl(baseUrl);\n if (result.ok === false) {\n return c.json({\n ok: false,\n code: result.code,\n message: result.message,\n });\n }\n return c.json({\n ok: true,\n url: result.url,\n loopback: false,\n probePath: '/api/tunnel/pair/ping',\n });\n });\n\n app.post('/api/tunnel/exchange-token', async (c) => {\n const clientIp =\n getClientIpFromHeaders({\n get: (name: string) => c.req.header(name) ?? undefined,\n }) ?? 'unknown';\n\n let body: { pairingSecret?: unknown };\n try {\n body = (await c.req.json()) as { pairingSecret?: unknown };\n } catch {\n return c.json({ error: 'Invalid JSON body' }, 400);\n }\n\n const pairingSecret = typeof body.pairingSecret === 'string' ? body.pairingSecret.trim() : '';\n if (!pairingSecret) {\n return c.json({ error: 'pairingSecret required' }, 400);\n }\n\n const cached = getCachedPairingExchange(pairingSecret);\n if (cached) {\n logTunnelAudit(\n 'tunnel.exchange_token',\n { ok: true, clientIp, phase: 'pairing_exchange', replay: true },\n 'Pairing secret replayed (duplicate mobile exchange)',\n );\n return c.json(cached);\n }\n\n const token = service.getAuthToken();\n if (!token) {\n return c.json({ error: 'Gateway token not configured' }, 500);\n }\n\n const persisted = loadTunnelState();\n const config = service.currentConfig as Config;\n const tunnelUrl = persisted?.publicUrl?.trim() || null;\n const reverseProxyUrl = resolveReverseProxyPublicUrl(config);\n const lanUrl = resolveMobilePairLanUrl(config);\n const connectUrls = buildMobileConnectUrlOrder({\n reverseProxyUrl,\n baseUrl: reverseProxyUrl ?? tunnelUrl,\n lanUrl,\n tunnelUrl,\n });\n // Mobile prefers HTTPS user-deployed URL over FRP broker when both exist.\n const advertisedBaseUrl = reverseProxyUrl ?? tunnelUrl;\n\n const payload = await exchangePairingSecretOnce(pairingSecret, () => ({\n token,\n baseUrl: advertisedBaseUrl,\n lanUrl,\n connectUrls,\n }));\n\n if (!payload) {\n const limited = consumePairingExchangeFailLimit(clientIp);\n if (!limited.allowed) {\n c.header('Retry-After', String(Math.ceil(limited.retryAfterMs / 1000)));\n }\n logTunnelAudit(\n 'tunnel.exchange_token',\n { ok: false, clientIp, phase: 'pairing_exchange' },\n 'Pairing exchange denied: invalid or expired secret',\n );\n return c.json({ error: 'Invalid or expired pairing secret', code: 'PAIRING_INVALID' }, 401);\n }\n\n logTunnelAudit(\n 'tunnel.exchange_token',\n { ok: true, clientIp, subdomain: persisted?.subdomain ?? null, phase: 'pairing_exchange' },\n 'Pairing secret exchanged for gateway token',\n );\n return c.json(payload);\n });\n}\n\nexport function registerTunnelRoutes(authenticated: Hono, deps: AuthenticatedRouteDeps): void {\n const { strictRateLimitMiddleware } = deps;\n const tunnel = getTunnelService();\n const tunnelMutationLimit = createTunnelMutationRateLimitMiddleware();\n\n authenticated.get('/api/tunnel/pair/context', async (c) => {\n await configureTunnelFromService(deps);\n const config = deps.service.currentConfig as Config;\n const status = tunnel.getStatus();\n const context = buildMobilePairContext({\n config,\n tunnelPublicUrl: status.publicUrl,\n tunnelConnected: status.state === 'connected',\n reverseProxyPublicUrl: resolveReverseProxyPublicUrl(config),\n });\n return c.json(context);\n });\n\n authenticated.post('/api/tunnel/pair/enable-lan', tunnelMutationLimit, async (c) => {\n const token = requireGatewayToken(c);\n if (!token) return c.json({ error: 'Gateway token required' }, 401);\n\n const config = deps.service.currentConfig as Config;\n const patchResult = applyLanPairingGatewayPatch(config);\n if (patchResult.ok === false) {\n return c.json({ ok: false, error: { message: patchResult.message, code: 'LAN_PAIRING_CONFIG' } }, 400);\n }\n\n if (patchResult.changed) {\n const saveResult = await deps.service.saveConfig(config);\n if (!saveResult.saved) {\n return c.json(\n { ok: false, error: { message: saveResult.error ?? 'Failed to save config', code: 'SAVE_FAILED' } },\n 500,\n );\n }\n logTunnelAudit(\n 'tunnel.enable_lan_pairing',\n { gatewayTokenHash: hashGatewayToken(token).slice(0, 12) },\n 'Gateway bind switched to LAN for mobile pairing',\n );\n }\n\n const status = tunnel.getStatus();\n let context = buildMobilePairContext({\n config: deps.service.currentConfig as Config,\n tunnelPublicUrl: status.publicUrl,\n tunnelConnected: status.state === 'connected',\n reverseProxyPublicUrl: resolveReverseProxyPublicUrl(deps.service.currentConfig as Config),\n });\n\n if (patchResult.changed) {\n context = {\n ...context,\n pairingReady: false,\n blockReason: 'GATEWAY_LOOPBACK_ONLY',\n };\n }\n\n return c.json({\n ok: true,\n requiresRestart: patchResult.changed,\n context,\n });\n });\n\n authenticated.post('/api/tunnel/pair', tunnelMutationLimit, async (c) => {\n await configureTunnelFromService(deps);\n const token = requireGatewayToken(c);\n if (!token) return c.json({ error: 'Gateway token required' }, 401);\n\n const { secret, expiresAt } = createPairingSecret();\n logTunnelAudit(\n 'tunnel.pair',\n {\n expiresAt: expiresAt.toISOString(),\n gatewayTokenHash: hashGatewayToken(token).slice(0, 12),\n },\n 'Mobile pairing session created',\n );\n return c.json({ pairingSecret: secret, expiresAt: expiresAt.toISOString() });\n });\n\n /**\n * Probe a candidate reverse-proxy URL before persisting it. The check round-trips\n * a `GET /api/tunnel/pair/ping` and validates the response identifies as\n * `service: 'xopc-gateway'`. Surface-area errors are mapped to stable codes so\n * the UI can render targeted hints (TLS / DNS / wrong service / blocked path).\n */\n authenticated.post('/api/tunnel/pair/probe-public', tunnelMutationLimit, async (c) => {\n const token = requireGatewayToken(c);\n if (!token) return c.json({ error: 'Gateway token required' }, 401);\n\n let body: { url?: unknown };\n try {\n body = (await c.req.json()) as { url?: unknown };\n } catch {\n return c.json({ ok: false, code: 'INVALID_JSON', message: 'Invalid JSON body' }, 400);\n }\n const raw = typeof body.url === 'string' ? body.url : '';\n const validation = validatePublicUrl(raw);\n if (validation.ok === false) {\n return c.json({ ok: false, code: validation.code, message: validation.message });\n }\n\n const pingUrl = `${validation.url}/api/tunnel/pair/ping`;\n const startedAt = Date.now();\n let response: Response;\n try {\n response = await fetch(pingUrl, {\n method: 'GET',\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(5000),\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n // Node's fetch surfaces TLS errors as TypeError with cause; map heuristically.\n const lower = message.toLowerCase();\n let code: 'TIMEOUT' | 'TLS_INVALID' | 'DNS_OR_CONN_REFUSED' | 'NETWORK_ERROR' = 'NETWORK_ERROR';\n if (lower.includes('timeout') || lower.includes('aborted')) code = 'TIMEOUT';\n else if (lower.includes('certificate') || lower.includes('cert') || lower.includes('tls') || lower.includes('ssl')) {\n code = 'TLS_INVALID';\n } else if (lower.includes('enotfound') || lower.includes('econnrefused') || lower.includes('eai_again')) {\n code = 'DNS_OR_CONN_REFUSED';\n }\n return c.json({ ok: false, code, message });\n }\n\n if (response.status === 401 || response.status === 403) {\n return c.json({ ok: false, code: 'AUTH_BLOCKED', message: `Reverse proxy returned ${response.status}` });\n }\n if (!response.ok) {\n return c.json({ ok: false, code: 'HTTP_ERROR', message: `HTTP ${response.status}` });\n }\n let parsed: { service?: unknown; mobilePairing?: unknown } | null = null;\n try {\n parsed = (await response.json()) as { service?: unknown; mobilePairing?: unknown };\n } catch {\n return c.json({ ok: false, code: 'NOT_XOPC_GATEWAY', message: 'Response was not JSON' });\n }\n if (!parsed || parsed.service !== 'xopc-gateway') {\n return c.json({\n ok: false,\n code: 'NOT_XOPC_GATEWAY',\n message: 'Endpoint did not identify as an xopc gateway',\n });\n }\n return c.json({\n ok: true,\n url: validation.url,\n latencyMs: Date.now() - startedAt,\n mobilePairing: parsed.mobilePairing === true,\n });\n });\n\n authenticated.get('/api/tunnel/status', async (c) => {\n await configureTunnelFromService(deps);\n const config = deps.service.currentConfig as Config;\n return c.json(enrichTunnelStatus(config, tunnel.getStatus()));\n });\n\n authenticated.post('/api/tunnel/consent', tunnelMutationLimit, async (c) => {\n const token = requireGatewayToken(c);\n if (!token) return c.json({ error: 'Gateway token required' }, 401);\n\n const config = deps.service.currentConfig as Config;\n applyTunnelConsentToConfig(config);\n const result = await deps.service.saveConfig(config);\n if (!result.saved) {\n return c.json({ ok: false, error: result.error ?? 'Failed to save config' }, 500);\n }\n const consent = getTunnelConsentState(config);\n logTunnelAudit(\n 'tunnel.consent',\n {\n consentVersion: consent.currentVersion,\n gatewayTokenHash: hashGatewayToken(token).slice(0, 12),\n },\n 'Remote access security consent recorded',\n );\n return c.json({\n ok: true,\n consent: {\n currentVersion: consent.currentVersion,\n acceptedVersion: consent.acceptedVersion,\n acceptedAt: consent.acceptedAt,\n valid: consent.valid,\n },\n });\n });\n\n authenticated.post('/api/tunnel/start', tunnelMutationLimit, async (c) => {\n await configureTunnelFromService(deps, { force: true });\n const config = deps.service.currentConfig as Config;\n const token = requireGatewayToken(c);\n if (!token) return c.json({ error: 'Gateway token required' }, 401);\n\n try {\n assertTunnelMayStart(config);\n } catch (err) {\n if (err instanceof TunnelConsentError) {\n logTunnelAudit(\n 'tunnel.start_denied',\n { reason: TUNNEL_CONSENT_REQUIRED_CODE, gatewayTokenHash: hashGatewayToken(token).slice(0, 12) },\n 'Tunnel start denied: consent required',\n );\n return c.json({ error: err.message, code: TUNNEL_CONSENT_REQUIRED_CODE }, 403);\n }\n throw err;\n }\n\n const gateway = config.gateway;\n const port = gateway.port ?? 18790;\n try {\n const qr = await tunnel.start(port, token);\n setTunnelEnabledInConfig(config, true);\n await deps.service.saveConfig(config);\n const status = tunnel.getStatus();\n return c.json({\n publicUrl: qr.publicUrl,\n subdomain: status.subdomain,\n qrPayload: qr.qrPayload,\n lanUrl: qr.lanUrl,\n });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return c.json({ error: message }, 500);\n }\n });\n\n authenticated.post('/api/tunnel/stop', tunnelMutationLimit, async (c) => {\n await configureTunnelFromService(deps);\n const config = deps.service.currentConfig as Config;\n let release = false;\n try {\n const body = (await c.req.json().catch(() => ({}))) as { release?: unknown };\n release = body.release === true;\n } catch {\n release = false;\n }\n const { released } = await tunnel.stop({ release });\n setTunnelEnabledInConfig(config, false);\n await deps.service.saveConfig(config);\n return c.json({ ok: true, released });\n });\n\n authenticated.get('/api/tunnel/qr', async (c) => {\n await configureTunnelFromService(deps);\n const gateway = deps.service.currentConfig.gateway;\n const port = gateway.port ?? 18790;\n const host = resolveGatewayEffectiveHost(deps.service.currentConfig);\n const token = requireGatewayToken(c);\n if (!token) return c.json({ error: 'Gateway token required' }, 401);\n const qr = await tunnel.buildQr(port, host);\n return c.json(qr);\n });\n\n authenticated.get('/api/tunnel/transport-status', async (c) => {\n await configureTunnelFromService(deps);\n return c.json({\n transport: { tls: 'broker_terminated' as const },\n });\n });\n\n /**\n * POST /api/tunnel/reveal-registration-secret — plaintext only when stored in config file.\n */\n authenticated.post('/api/tunnel/reveal-registration-secret', strictRateLimitMiddleware, async (c) => {\n const config = deps.service.currentConfig as Config;\n const registrationSecret = readTunnelRegistrationSecretFromConfigOnly(config);\n return c.json({\n ok: true,\n payload: {\n registrationSecret,\n source: registrationSecret ? ('config' as const) : ('none' as const),\n },\n });\n });\n}\n"],"mappings":"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"}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { createLogger } from "../../../utils/logger/index.js";
|
|
2
|
-
import { init_logger } from "../../../utils/logger.js";
|
|
3
1
|
import { init_agent_scope, listAgentEntries, normalizeAgentId, resolveAgentHomeDir, resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../../agent/agent-scope.js";
|
|
4
2
|
import { extractProfileAgentId } from "../../../config/agent-profile.js";
|
|
3
|
+
import { createLogger } from "../../../utils/logger/index.js";
|
|
4
|
+
import { init_logger } from "../../../utils/logger.js";
|
|
5
5
|
import { validateWritePath } from "../../../agent/sandbox/path-policy.js";
|
|
6
|
+
import { isPathUnderWorkspace, resolveWorkspaceSafePath, toWorkspaceRelativePosix } from "../../workspace-editor-path.js";
|
|
6
7
|
import { resolveSafeInboundFilePath } from "../../../channels/attachments/inbound-persist.js";
|
|
7
8
|
import { getWorkspacePath } from "../../../config/workspace-path-helpers.js";
|
|
8
|
-
import { isPathUnderWorkspace, resolveWorkspaceSafePath, toWorkspaceRelativePosix } from "../../workspace-editor-path.js";
|
|
9
9
|
import { resolveSafeTtsFilePath } from "../../../channels/attachments/outbound-tts-persist.js";
|
|
10
10
|
import { buildFilePathClassifierContext, classifyFileLocation, displayNameForPath, fileRefSessionKeysMatch, resolveFileReferenceCandidate } from "../../file-path-classifier.js";
|
|
11
11
|
import { fileReferenceRegistry } from "../../file-reference-registry.js";
|
|
12
12
|
import { resolveHeartbeatMdPath } from "../../workspace-heartbeat-path.js";
|
|
13
13
|
import { listWorkspaceRelativeFilesFsFallback } from "../../workspace-fs-file-list.js";
|
|
14
14
|
import { runRipgrepInDirectory, runRipgrepListFiles } from "../../workspace-ripgrep.js";
|
|
15
|
-
import {
|
|
15
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
16
16
|
import { constants } from "node:fs";
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
17
18
|
import { copyFile, link, mkdir, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
18
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
19
19
|
//#region src/gateway/hono/routes/workspace.ts
|
|
20
20
|
init_agent_scope();
|
|
21
21
|
init_logger();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { buildSessionKey, init_session_key, parseSessionKey } from "../../routing/session-key.js";
|
|
2
|
+
import { getDefaultAgentId, init_resolve_route } from "../../routing/resolve-route.js";
|
|
1
3
|
import { updateAsyncLogContext } from "../../utils/logger/context.js";
|
|
2
4
|
import { createLogger } from "../../utils/logger/index.js";
|
|
3
5
|
import { init_logger } from "../../utils/logger.js";
|
|
4
|
-
import { buildSessionKey, init_session_key, parseSessionKey } from "../../routing/session-key.js";
|
|
5
|
-
import { getDefaultAgentId, init_resolve_route } from "../../routing/resolve-route.js";
|
|
6
6
|
import { MAX_WEBCHAT_ATTACHMENT_FILE_BYTES } from "../chat-limits.js";
|
|
7
7
|
import { stringifySSEData } from "./sse-json.js";
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
@@ -34,10 +34,12 @@ export declare function resolveGatewayCorsOrigins(params: {
|
|
|
34
34
|
}): string[];
|
|
35
35
|
/** Browser origin (`https://host`) from a gateway public/tunnel root URL. */
|
|
36
36
|
export declare function originFromGatewayPublicUrl(publicUrl: string | null | undefined): string | null;
|
|
37
|
-
/** CORS + CSRF allowlist including active FRP tunnel
|
|
37
|
+
/** CORS + CSRF allowlist including active FRP tunnel + reverse-proxy origins. */
|
|
38
38
|
export declare function resolveAllowedBrowserOrigins(params: {
|
|
39
39
|
configuredOrigins?: string[];
|
|
40
40
|
port: number;
|
|
41
41
|
bindHost?: string;
|
|
42
42
|
tunnelPublicUrl?: string | null;
|
|
43
|
+
/** User-configured reverse-proxy public URL (gateway.publicUrl). */
|
|
44
|
+
reverseProxyPublicUrl?: string | null;
|
|
43
45
|
}): string[];
|
package/dist/src/gateway/host.js
CHANGED
|
@@ -56,7 +56,7 @@ function originFromGatewayPublicUrl(publicUrl) {
|
|
|
56
56
|
return null;
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
-
/** CORS + CSRF allowlist including active FRP tunnel
|
|
59
|
+
/** CORS + CSRF allowlist including active FRP tunnel + reverse-proxy origins. */
|
|
60
60
|
function resolveAllowedBrowserOrigins(params) {
|
|
61
61
|
const origins = resolveGatewayCorsOrigins({
|
|
62
62
|
configuredOrigins: params.configuredOrigins,
|
|
@@ -65,6 +65,8 @@ function resolveAllowedBrowserOrigins(params) {
|
|
|
65
65
|
});
|
|
66
66
|
const tunnelOrigin = originFromGatewayPublicUrl(params.tunnelPublicUrl);
|
|
67
67
|
if (tunnelOrigin && !origins.includes(tunnelOrigin)) origins.push(tunnelOrigin);
|
|
68
|
+
const reverseProxyOrigin = originFromGatewayPublicUrl(params.reverseProxyPublicUrl);
|
|
69
|
+
if (reverseProxyOrigin && !origins.includes(reverseProxyOrigin)) origins.push(reverseProxyOrigin);
|
|
68
70
|
return origins;
|
|
69
71
|
}
|
|
70
72
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host.js","names":[],"sources":["../../../src/gateway/host.ts"],"sourcesContent":["import { DEFAULT_GATEWAY_PORT } from '../daemon/constants.js';\n\n/** True when the bind address is local-only (127.x, localhost, ::1). */\nexport function isLoopbackHost(host: string | undefined): boolean {\n if (!host) {\n return true;\n }\n const normalized = host.trim().toLowerCase();\n return (\n normalized === '127.0.0.1' ||\n normalized === 'localhost' ||\n normalized === '::1' ||\n normalized === '0:0:0:0:0:0:0:1'\n );\n}\n\n/** True when the gateway listens on all interfaces. */\nexport function isAllInterfacesHost(host: string | undefined): boolean {\n if (!host) {\n return false;\n }\n const normalized = host.trim();\n return normalized === '0.0.0.0' || normalized === '::' || normalized === '*';\n}\n\n/** Vite dev server origins for the gateway console (`web/` defaults to port 3000). */\nexport const GATEWAY_DEV_CONSOLE_ORIGINS = [\n 'http://localhost:3000',\n 'http://127.0.0.1:3000',\n] as const;\n\n/** Effective HTTP listen port: CLI `--port` override wins over config (default 18790). */\nexport function resolveEffectiveGatewayPort(\n config: { gateway?: { port?: number } },\n listenPortOverride?: number,\n): number {\n if (typeof listenPortOverride === 'number' && Number.isFinite(listenPortOverride)) {\n return listenPortOverride;\n }\n return config.gateway?.port ?? DEFAULT_GATEWAY_PORT;\n}\n\n/** Resolve listen port from a gateway service (supports partial test mocks without `getEffectiveListenPort`). */\nexport function resolveGatewayServiceListenPort(service: {\n currentConfig: { gateway?: { port?: number } };\n getEffectiveListenPort?: () => number;\n}): number {\n if (typeof service.getEffectiveListenPort === 'function') {\n return service.getEffectiveListenPort();\n }\n return resolveEffectiveGatewayPort(service.currentConfig);\n}\n\nexport function buildDefaultCorsOrigins(params: { port: number; bindHost?: string }): string[] {\n const origins = new Set<string>([\n `http://localhost:${params.port}`,\n `http://127.0.0.1:${params.port}`,\n ...GATEWAY_DEV_CONSOLE_ORIGINS,\n ]);\n const bindHost = params.bindHost?.trim();\n if (bindHost && !isLoopbackHost(bindHost) && !isAllInterfacesHost(bindHost)) {\n origins.add(`http://${bindHost}:${params.port}`);\n }\n return [...origins];\n}\n\n/**\n * Effective browser origins for CORS and CSRF checks.\n * Custom `gateway.corsOrigins` (e.g. after LAN pairing) still merge loopback Vite dev origins.\n */\nexport function resolveGatewayCorsOrigins(params: {\n configuredOrigins?: string[];\n port: number;\n bindHost?: string;\n}): string[] {\n const configured = (params.configuredOrigins ?? [])\n .map((origin) => origin.trim())\n .filter(Boolean);\n if (configured.length === 0) {\n return buildDefaultCorsOrigins({ port: params.port, bindHost: params.bindHost });\n }\n return [...new Set([...configured, ...GATEWAY_DEV_CONSOLE_ORIGINS])];\n}\n\n/** Browser origin (`https://host`) from a gateway public/tunnel root URL. */\nexport function originFromGatewayPublicUrl(publicUrl: string | null | undefined): string | null {\n const trimmed = publicUrl?.trim();\n if (!trimmed) return null;\n try {\n return new URL(trimmed).origin.toLowerCase();\n } catch {\n return null;\n }\n}\n\n/** CORS + CSRF allowlist including active FRP tunnel
|
|
1
|
+
{"version":3,"file":"host.js","names":[],"sources":["../../../src/gateway/host.ts"],"sourcesContent":["import { DEFAULT_GATEWAY_PORT } from '../daemon/constants.js';\n\n/** True when the bind address is local-only (127.x, localhost, ::1). */\nexport function isLoopbackHost(host: string | undefined): boolean {\n if (!host) {\n return true;\n }\n const normalized = host.trim().toLowerCase();\n return (\n normalized === '127.0.0.1' ||\n normalized === 'localhost' ||\n normalized === '::1' ||\n normalized === '0:0:0:0:0:0:0:1'\n );\n}\n\n/** True when the gateway listens on all interfaces. */\nexport function isAllInterfacesHost(host: string | undefined): boolean {\n if (!host) {\n return false;\n }\n const normalized = host.trim();\n return normalized === '0.0.0.0' || normalized === '::' || normalized === '*';\n}\n\n/** Vite dev server origins for the gateway console (`web/` defaults to port 3000). */\nexport const GATEWAY_DEV_CONSOLE_ORIGINS = [\n 'http://localhost:3000',\n 'http://127.0.0.1:3000',\n] as const;\n\n/** Effective HTTP listen port: CLI `--port` override wins over config (default 18790). */\nexport function resolveEffectiveGatewayPort(\n config: { gateway?: { port?: number } },\n listenPortOverride?: number,\n): number {\n if (typeof listenPortOverride === 'number' && Number.isFinite(listenPortOverride)) {\n return listenPortOverride;\n }\n return config.gateway?.port ?? DEFAULT_GATEWAY_PORT;\n}\n\n/** Resolve listen port from a gateway service (supports partial test mocks without `getEffectiveListenPort`). */\nexport function resolveGatewayServiceListenPort(service: {\n currentConfig: { gateway?: { port?: number } };\n getEffectiveListenPort?: () => number;\n}): number {\n if (typeof service.getEffectiveListenPort === 'function') {\n return service.getEffectiveListenPort();\n }\n return resolveEffectiveGatewayPort(service.currentConfig);\n}\n\nexport function buildDefaultCorsOrigins(params: { port: number; bindHost?: string }): string[] {\n const origins = new Set<string>([\n `http://localhost:${params.port}`,\n `http://127.0.0.1:${params.port}`,\n ...GATEWAY_DEV_CONSOLE_ORIGINS,\n ]);\n const bindHost = params.bindHost?.trim();\n if (bindHost && !isLoopbackHost(bindHost) && !isAllInterfacesHost(bindHost)) {\n origins.add(`http://${bindHost}:${params.port}`);\n }\n return [...origins];\n}\n\n/**\n * Effective browser origins for CORS and CSRF checks.\n * Custom `gateway.corsOrigins` (e.g. after LAN pairing) still merge loopback Vite dev origins.\n */\nexport function resolveGatewayCorsOrigins(params: {\n configuredOrigins?: string[];\n port: number;\n bindHost?: string;\n}): string[] {\n const configured = (params.configuredOrigins ?? [])\n .map((origin) => origin.trim())\n .filter(Boolean);\n if (configured.length === 0) {\n return buildDefaultCorsOrigins({ port: params.port, bindHost: params.bindHost });\n }\n return [...new Set([...configured, ...GATEWAY_DEV_CONSOLE_ORIGINS])];\n}\n\n/** Browser origin (`https://host`) from a gateway public/tunnel root URL. */\nexport function originFromGatewayPublicUrl(publicUrl: string | null | undefined): string | null {\n const trimmed = publicUrl?.trim();\n if (!trimmed) return null;\n try {\n return new URL(trimmed).origin.toLowerCase();\n } catch {\n return null;\n }\n}\n\n/** CORS + CSRF allowlist including active FRP tunnel + reverse-proxy origins. */\nexport function resolveAllowedBrowserOrigins(params: {\n configuredOrigins?: string[];\n port: number;\n bindHost?: string;\n tunnelPublicUrl?: string | null;\n /** User-configured reverse-proxy public URL (gateway.publicUrl). */\n reverseProxyPublicUrl?: string | null;\n}): string[] {\n const origins = resolveGatewayCorsOrigins({\n configuredOrigins: params.configuredOrigins,\n port: params.port,\n bindHost: params.bindHost,\n });\n const tunnelOrigin = originFromGatewayPublicUrl(params.tunnelPublicUrl);\n if (tunnelOrigin && !origins.includes(tunnelOrigin)) {\n origins.push(tunnelOrigin);\n }\n const reverseProxyOrigin = originFromGatewayPublicUrl(params.reverseProxyPublicUrl);\n if (reverseProxyOrigin && !origins.includes(reverseProxyOrigin)) {\n origins.push(reverseProxyOrigin);\n }\n return origins;\n}\n"],"mappings":";;;AAGA,SAAgB,eAAe,MAAmC;AAChE,KAAI,CAAC,KACH,QAAO;CAET,MAAM,aAAa,KAAK,MAAM,CAAC,aAAa;AAC5C,QACE,eAAe,eACf,eAAe,eACf,eAAe,SACf,eAAe;;;AAKnB,SAAgB,oBAAoB,MAAmC;AACrE,KAAI,CAAC,KACH,QAAO;CAET,MAAM,aAAa,KAAK,MAAM;AAC9B,QAAO,eAAe,aAAa,eAAe,QAAQ,eAAe;;;AAI3E,MAAa,8BAA8B,CACzC,yBACA,wBACD;;AAGD,SAAgB,4BACd,QACA,oBACQ;AACR,KAAI,OAAO,uBAAuB,YAAY,OAAO,SAAS,mBAAmB,CAC/E,QAAO;AAET,QAAO,OAAO,SAAS,QAAA;;;AAIzB,SAAgB,gCAAgC,SAGrC;AACT,KAAI,OAAO,QAAQ,2BAA2B,WAC5C,QAAO,QAAQ,wBAAwB;AAEzC,QAAO,4BAA4B,QAAQ,cAAc;;AAG3D,SAAgB,wBAAwB,QAAuD;CAC7F,MAAM,UAAU,IAAI,IAAY;EAC9B,oBAAoB,OAAO;EAC3B,oBAAoB,OAAO;EAC3B,GAAG;EACJ,CAAC;CACF,MAAM,WAAW,OAAO,UAAU,MAAM;AACxC,KAAI,YAAY,CAAC,eAAe,SAAS,IAAI,CAAC,oBAAoB,SAAS,CACzE,SAAQ,IAAI,UAAU,SAAS,GAAG,OAAO,OAAO;AAElD,QAAO,CAAC,GAAG,QAAQ;;;;;;AAOrB,SAAgB,0BAA0B,QAI7B;CACX,MAAM,cAAc,OAAO,qBAAqB,EAAE,EAC/C,KAAK,WAAW,OAAO,MAAM,CAAC,CAC9B,OAAO,QAAQ;AAClB,KAAI,WAAW,WAAW,EACxB,QAAO,wBAAwB;EAAE,MAAM,OAAO;EAAM,UAAU,OAAO;EAAU,CAAC;AAElF,QAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,YAAY,GAAG,4BAA4B,CAAC,CAAC;;;AAItE,SAAgB,2BAA2B,WAAqD;CAC9F,MAAM,UAAU,WAAW,MAAM;AACjC,KAAI,CAAC,QAAS,QAAO;AACrB,KAAI;AACF,SAAO,IAAI,IAAI,QAAQ,CAAC,OAAO,aAAa;SACtC;AACN,SAAO;;;;AAKX,SAAgB,6BAA6B,QAOhC;CACX,MAAM,UAAU,0BAA0B;EACxC,mBAAmB,OAAO;EAC1B,MAAM,OAAO;EACb,UAAU,OAAO;EAClB,CAAC;CACF,MAAM,eAAe,2BAA2B,OAAO,gBAAgB;AACvE,KAAI,gBAAgB,CAAC,QAAQ,SAAS,aAAa,CACjD,SAAQ,KAAK,aAAa;CAE5B,MAAM,qBAAqB,2BAA2B,OAAO,sBAAsB;AACnF,KAAI,sBAAsB,CAAC,QAAQ,SAAS,mBAAmB,CAC7D,SAAQ,KAAK,mBAAmB;AAElC,QAAO"}
|
package/dist/src/gateway/lock.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import net from "node:net";
|
|
2
3
|
import fsSync from "node:fs";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
3
5
|
import fs from "node:fs/promises";
|
|
4
|
-
import path from "node:path";
|
|
5
6
|
import { homedir } from "os";
|
|
6
|
-
import net from "node:net";
|
|
7
7
|
//#region src/gateway/lock.ts
|
|
8
8
|
/**
|
|
9
9
|
* Gateway Lock - Prevents multiple gateway instances from running simultaneously
|
|
@@ -11,6 +11,12 @@ export type ForceFreePortResult = {
|
|
|
11
11
|
escalatedToSigkill: boolean;
|
|
12
12
|
};
|
|
13
13
|
export declare function parseLsofOutput(output: string): PortProcess[];
|
|
14
|
+
/**
|
|
15
|
+
* Parse `netstat -ano` output (Windows) to find PIDs listening on a given port.
|
|
16
|
+
* Example line:
|
|
17
|
+
* TCP 0.0.0.0:18790 0.0.0.0:0 LISTENING 1234
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseNetstatOutput(output: string, port: number): PortProcess[];
|
|
14
20
|
export declare function listPortListeners(port: number): PortProcess[];
|
|
15
21
|
export declare function forceFreePortAndWait(port: number, opts?: {
|
|
16
22
|
timeoutMs?: number;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createLogger } from "../utils/logger/index.js";
|
|
2
2
|
import { init_logger } from "../utils/logger.js";
|
|
3
|
-
import fsSync from "node:fs";
|
|
4
3
|
import net from "node:net";
|
|
4
|
+
import fsSync from "node:fs";
|
|
5
5
|
import { execFileSync } from "node:child_process";
|
|
6
6
|
//#region src/gateway/ports.ts
|
|
7
7
|
/**
|
|
@@ -105,7 +105,43 @@ function listPortListenersViaProc(port) {
|
|
|
105
105
|
}
|
|
106
106
|
return results;
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Parse `netstat -ano` output (Windows) to find PIDs listening on a given port.
|
|
110
|
+
* Example line:
|
|
111
|
+
* TCP 0.0.0.0:18790 0.0.0.0:0 LISTENING 1234
|
|
112
|
+
*/
|
|
113
|
+
function parseNetstatOutput(output, port) {
|
|
114
|
+
const portSuffix = `:${port}`;
|
|
115
|
+
const results = [];
|
|
116
|
+
for (const line of output.split(/\r?\n/)) {
|
|
117
|
+
if (!line.includes("LISTENING")) continue;
|
|
118
|
+
const parts = line.trim().split(/\s+/);
|
|
119
|
+
if (parts.length < 5) continue;
|
|
120
|
+
if (!parts[1].endsWith(portSuffix)) continue;
|
|
121
|
+
const pid = parseInt(parts[parts.length - 1], 10);
|
|
122
|
+
if (Number.isFinite(pid) && pid > 0 && !results.some((p) => p.pid === pid)) results.push({ pid });
|
|
123
|
+
}
|
|
124
|
+
return results;
|
|
125
|
+
}
|
|
126
|
+
function listPortListenersViaNetstat(port) {
|
|
127
|
+
let out;
|
|
128
|
+
try {
|
|
129
|
+
out = execFileSync("netstat", [
|
|
130
|
+
"-ano",
|
|
131
|
+
"-p",
|
|
132
|
+
"TCP"
|
|
133
|
+
], {
|
|
134
|
+
encoding: "utf-8",
|
|
135
|
+
shell: true,
|
|
136
|
+
timeout: 5e3
|
|
137
|
+
});
|
|
138
|
+
} catch {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
return parseNetstatOutput(out, port);
|
|
142
|
+
}
|
|
108
143
|
function listPortListeners(port) {
|
|
144
|
+
if (process.platform === "win32") return listPortListenersViaNetstat(port);
|
|
109
145
|
try {
|
|
110
146
|
return parseLsofOutput(execFileSync("lsof", [
|
|
111
147
|
"-nP",
|
|
@@ -194,6 +230,6 @@ async function checkPortAvailable(port, host = "0.0.0.0") {
|
|
|
194
230
|
});
|
|
195
231
|
}
|
|
196
232
|
//#endregion
|
|
197
|
-
export { checkPortAvailable, forceFreePortAndWait, listPortListeners, parseLsofOutput };
|
|
233
|
+
export { checkPortAvailable, forceFreePortAndWait, listPortListeners, parseLsofOutput, parseNetstatOutput };
|
|
198
234
|
|
|
199
235
|
//# sourceMappingURL=ports.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ports.js","names":["execErr","fs"],"sources":["../../../src/gateway/ports.ts"],"sourcesContent":["/**\n * Ports Management - Port management utilities\n */\n\nimport { execFileSync } from \"node:child_process\";\nimport fs from \"node:fs\";\nimport net from \"node:net\";\nimport { createLogger } from \"../utils/logger.js\";\n\nconst log = createLogger(\"Ports\");\n\nexport type PortProcess = { pid: number; command?: string };\n\nexport type ForceFreePortResult = {\n killed: PortProcess[];\n waitedMs: number;\n escalatedToSigkill: boolean;\n};\n\n// Parse lsof output\nexport function parseLsofOutput(output: string): PortProcess[] {\n const lines = output.split(/\\r?\\n/).filter(Boolean);\n const results: PortProcess[] = [];\n let current: Partial<PortProcess> = {};\n\n for (const line of lines) {\n if (line.startsWith(\"p\")) {\n if (current.pid) {\n results.push(current as PortProcess);\n }\n current = { pid: parseInt(line.slice(1), 10) };\n } else if (line.startsWith(\"c\")) {\n current.command = line.slice(1);\n }\n }\n\n if (current.pid) {\n results.push(current as PortProcess);\n }\n\n return results;\n}\n\n/**\n * Parse `ss -tlnp` output to find PIDs listening on a given port.\n * Example line:\n * LISTEN 0 128 0.0.0.0:3000 0.0.0.0:* users:((\"node\",pid=1234,fd=18))\n */\nfunction listPortListenersViaSs(port: number): PortProcess[] {\n let out: string;\n try {\n out = execFileSync(\"ss\", [\"-tlnp\", `sport = :${port}`], { encoding: \"utf-8\" });\n } catch (err: unknown) {\n const execErr = err as { status?: number; code?: string };\n if (execErr.status === 1) {\n return []; // No matching sockets\n }\n throw err instanceof Error ? err : new Error(String(err));\n }\n const results: PortProcess[] = [];\n\n for (const line of out.split(/\\r?\\n/)) {\n if (!line.includes(\"LISTEN\")) continue;\n for (const match of line.matchAll(/pid=(\\d+)/g)) {\n const pid = parseInt(match[1], 10);\n if (!results.some((p) => p.pid === pid)) {\n results.push({ pid });\n }\n }\n }\n\n return results;\n}\n\n/**\n * Read /proc/net/tcp (and /proc/net/tcp6) to find PIDs listening on a given port.\n * Falls back to an empty list if /proc is unavailable (non-Linux).\n */\nfunction listPortListenersViaProc(port: number): PortProcess[] {\n const hexPort = port.toString(16).toUpperCase().padStart(4, \"0\");\n const results: PortProcess[] = [];\n const inodeSet = new Set<string>();\n\n for (const procFile of [\"/proc/net/tcp\", \"/proc/net/tcp6\"]) {\n let content: string;\n try {\n content = fs.readFileSync(procFile, \"utf-8\");\n } catch {\n continue;\n }\n\n for (const line of content.split(\"\\n\").slice(1)) {\n const parts = line.trim().split(/\\s+/);\n // state 0A = TCP_LISTEN\n if (parts.length < 10 || parts[3] !== \"0A\") continue;\n const localAddress = parts[1];\n const portHex = localAddress.split(\":\")[1];\n if (portHex?.toUpperCase() !== hexPort) continue;\n inodeSet.add(parts[9]);\n }\n }\n\n if (inodeSet.size === 0) return results;\n\n // Walk /proc/<pid>/fd to match socket inodes to PIDs\n let pidDirs: string[];\n try {\n pidDirs = fs.readdirSync(\"/proc\").filter((name) => /^\\d+$/.test(name));\n } catch {\n return results;\n }\n\n for (const pidStr of pidDirs) {\n const fdDir = `/proc/${pidStr}/fd`;\n let fds: string[];\n try {\n fds = fs.readdirSync(fdDir);\n } catch {\n continue;\n }\n\n for (const fd of fds) {\n let target: string;\n try {\n target = fs.readlinkSync(`${fdDir}/${fd}`);\n } catch {\n continue;\n }\n\n // symlink target looks like \"socket:[12345]\"\n const inodeMatch = /^socket:\\[(\\d+)\\]$/.exec(target);\n if (!inodeMatch || !inodeSet.has(inodeMatch[1])) continue;\n\n const pid = parseInt(pidStr, 10);\n if (!results.some((p) => p.pid === pid)) {\n let command: string | undefined;\n try {\n command = fs.readFileSync(`/proc/${pidStr}/comm`, \"utf-8\").trim();\n } catch {\n // comm not readable — leave undefined\n }\n results.push({ pid, command });\n }\n break;\n }\n }\n\n return results;\n}\n\n// List processes listening on port\nexport function listPortListeners(port: number): PortProcess[] {\n // Try lsof first (macOS + most Linux distros)\n try {\n const out = execFileSync(\"lsof\", [\"-nP\", `-iTCP:${port}`, \"-sTCP:LISTEN\", \"-FpFc\"], {\n encoding: \"utf-8\",\n });\n return parseLsofOutput(out);\n } catch (err: unknown) {\n const execErr = err as { status?: number; code?: string };\n\n if (execErr.code !== \"ENOENT\") {\n if (execErr.status === 1) return []; // No listeners\n throw err instanceof Error ? err : new Error(String(err));\n }\n // lsof not available — fall through to Linux alternatives\n log.debug({ port }, \"lsof not found; trying ss fallback\");\n }\n\n // Try ss (iproute2, available on most modern Linux systems)\n try {\n return listPortListenersViaSs(port);\n } catch (err: unknown) {\n const execErr = err as { code?: string };\n if (execErr.code !== \"ENOENT\") {\n throw err instanceof Error ? err : new Error(String(err));\n }\n log.debug({ port }, \"ss not found; trying /proc/net/tcp fallback\");\n }\n\n // Last resort: parse /proc/net/tcp directly (no external tools required)\n return listPortListenersViaProc(port);\n}\n\n// Force free port\nexport async function forceFreePortAndWait(\n port: number,\n opts: {\n timeoutMs?: number;\n intervalMs?: number;\n sigtermTimeoutMs?: number;\n } = {}\n): Promise<ForceFreePortResult> {\n const timeoutMs = Math.max(opts.timeoutMs ?? 2000, 0);\n const intervalMs = Math.max(opts.intervalMs ?? 100, 1);\n const sigtermTimeoutMs = Math.min(Math.max(opts.sigtermTimeoutMs ?? 700, 0), timeoutMs);\n\n // 1. Get listener list\n const listeners = listPortListeners(port);\n const killed: PortProcess[] = [...listeners];\n\n // 2. Send SIGTERM\n for (const proc of listeners) {\n try {\n process.kill(proc.pid, \"SIGTERM\");\n log.info({ pid: proc.pid }, \"Sent SIGTERM\");\n } catch (err) {\n log.warn({ pid: proc.pid, err }, \"Failed to send SIGTERM\");\n }\n }\n\n // 3. Wait for processes to exit\n let waitedMs = 0;\n const checkInterval = () => new Promise<void>((r) => setTimeout(r, intervalMs));\n\n // Wait for SIGTERM to take effect\n const sigtermTries = Math.ceil(sigtermTimeoutMs / intervalMs);\n for (let i = 0; i < sigtermTries; i++) {\n await checkInterval();\n waitedMs += intervalMs;\n\n const remaining = listPortListeners(port);\n if (remaining.length === 0) {\n return { killed, waitedMs, escalatedToSigkill: false };\n }\n }\n\n // 4. SIGTERM timeout, send SIGKILL\n const remaining = listPortListeners(port);\n for (const proc of remaining) {\n try {\n process.kill(proc.pid, \"SIGKILL\");\n log.info({ pid: proc.pid }, \"Sent SIGKILL\");\n } catch (err) {\n log.warn({ pid: proc.pid, err }, \"Failed to send SIGKILL\");\n }\n }\n\n // 5. Wait for SIGKILL to take effect\n const remainingBudget = Math.max(timeoutMs - waitedMs, 0);\n const sigkillTries = Math.ceil(remainingBudget / intervalMs);\n\n for (let i = 0; i < sigkillTries; i++) {\n await checkInterval();\n waitedMs += intervalMs;\n\n const stillRemaining = listPortListeners(port);\n if (stillRemaining.length === 0) {\n return { killed, waitedMs, escalatedToSigkill: true };\n }\n }\n\n throw new Error(`Port ${port} still has listeners after force free`);\n}\n\n// Check if port is available\nexport async function checkPortAvailable(port: number, host = \"0.0.0.0\"): Promise<boolean> {\n return new Promise((resolve) => {\n const server = net.createServer();\n\n server.once(\"error\", (err: NodeJS.ErrnoException) => {\n if (err.code === \"EADDRINUSE\") {\n resolve(false);\n } else {\n resolve(true);\n }\n });\n\n server.once(\"listening\", () => {\n server.close();\n resolve(true);\n });\n\n server.listen(port, host);\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ports.js","names":["execErr","fs"],"sources":["../../../src/gateway/ports.ts"],"sourcesContent":["/**\n * Ports Management - Port management utilities\n */\n\nimport { execFileSync } from \"node:child_process\";\nimport fs from \"node:fs\";\nimport net from \"node:net\";\nimport { createLogger } from \"../utils/logger.js\";\n\nconst log = createLogger(\"Ports\");\n\nexport type PortProcess = { pid: number; command?: string };\n\nexport type ForceFreePortResult = {\n killed: PortProcess[];\n waitedMs: number;\n escalatedToSigkill: boolean;\n};\n\n// Parse lsof output\nexport function parseLsofOutput(output: string): PortProcess[] {\n const lines = output.split(/\\r?\\n/).filter(Boolean);\n const results: PortProcess[] = [];\n let current: Partial<PortProcess> = {};\n\n for (const line of lines) {\n if (line.startsWith(\"p\")) {\n if (current.pid) {\n results.push(current as PortProcess);\n }\n current = { pid: parseInt(line.slice(1), 10) };\n } else if (line.startsWith(\"c\")) {\n current.command = line.slice(1);\n }\n }\n\n if (current.pid) {\n results.push(current as PortProcess);\n }\n\n return results;\n}\n\n/**\n * Parse `ss -tlnp` output to find PIDs listening on a given port.\n * Example line:\n * LISTEN 0 128 0.0.0.0:3000 0.0.0.0:* users:((\"node\",pid=1234,fd=18))\n */\nfunction listPortListenersViaSs(port: number): PortProcess[] {\n let out: string;\n try {\n out = execFileSync(\"ss\", [\"-tlnp\", `sport = :${port}`], { encoding: \"utf-8\" });\n } catch (err: unknown) {\n const execErr = err as { status?: number; code?: string };\n if (execErr.status === 1) {\n return []; // No matching sockets\n }\n throw err instanceof Error ? err : new Error(String(err));\n }\n const results: PortProcess[] = [];\n\n for (const line of out.split(/\\r?\\n/)) {\n if (!line.includes(\"LISTEN\")) continue;\n for (const match of line.matchAll(/pid=(\\d+)/g)) {\n const pid = parseInt(match[1], 10);\n if (!results.some((p) => p.pid === pid)) {\n results.push({ pid });\n }\n }\n }\n\n return results;\n}\n\n/**\n * Read /proc/net/tcp (and /proc/net/tcp6) to find PIDs listening on a given port.\n * Falls back to an empty list if /proc is unavailable (non-Linux).\n */\nfunction listPortListenersViaProc(port: number): PortProcess[] {\n const hexPort = port.toString(16).toUpperCase().padStart(4, \"0\");\n const results: PortProcess[] = [];\n const inodeSet = new Set<string>();\n\n for (const procFile of [\"/proc/net/tcp\", \"/proc/net/tcp6\"]) {\n let content: string;\n try {\n content = fs.readFileSync(procFile, \"utf-8\");\n } catch {\n continue;\n }\n\n for (const line of content.split(\"\\n\").slice(1)) {\n const parts = line.trim().split(/\\s+/);\n // state 0A = TCP_LISTEN\n if (parts.length < 10 || parts[3] !== \"0A\") continue;\n const localAddress = parts[1];\n const portHex = localAddress.split(\":\")[1];\n if (portHex?.toUpperCase() !== hexPort) continue;\n inodeSet.add(parts[9]);\n }\n }\n\n if (inodeSet.size === 0) return results;\n\n // Walk /proc/<pid>/fd to match socket inodes to PIDs\n let pidDirs: string[];\n try {\n pidDirs = fs.readdirSync(\"/proc\").filter((name) => /^\\d+$/.test(name));\n } catch {\n return results;\n }\n\n for (const pidStr of pidDirs) {\n const fdDir = `/proc/${pidStr}/fd`;\n let fds: string[];\n try {\n fds = fs.readdirSync(fdDir);\n } catch {\n continue;\n }\n\n for (const fd of fds) {\n let target: string;\n try {\n target = fs.readlinkSync(`${fdDir}/${fd}`);\n } catch {\n continue;\n }\n\n // symlink target looks like \"socket:[12345]\"\n const inodeMatch = /^socket:\\[(\\d+)\\]$/.exec(target);\n if (!inodeMatch || !inodeSet.has(inodeMatch[1])) continue;\n\n const pid = parseInt(pidStr, 10);\n if (!results.some((p) => p.pid === pid)) {\n let command: string | undefined;\n try {\n command = fs.readFileSync(`/proc/${pidStr}/comm`, \"utf-8\").trim();\n } catch {\n // comm not readable — leave undefined\n }\n results.push({ pid, command });\n }\n break;\n }\n }\n\n return results;\n}\n\n/**\n * Parse `netstat -ano` output (Windows) to find PIDs listening on a given port.\n * Example line:\n * TCP 0.0.0.0:18790 0.0.0.0:0 LISTENING 1234\n */\nexport function parseNetstatOutput(output: string, port: number): PortProcess[] {\n const portSuffix = `:${port}`;\n const results: PortProcess[] = [];\n\n for (const line of output.split(/\\r?\\n/)) {\n if (!line.includes(\"LISTENING\")) continue;\n const parts = line.trim().split(/\\s+/);\n // Format: TCP <local addr> <foreign addr> LISTENING <pid>\n if (parts.length < 5) continue;\n const localAddr = parts[1];\n if (!localAddr.endsWith(portSuffix)) continue;\n const pid = parseInt(parts[parts.length - 1], 10);\n if (Number.isFinite(pid) && pid > 0 && !results.some((p) => p.pid === pid)) {\n results.push({ pid });\n }\n }\n\n return results;\n}\n\nfunction listPortListenersViaNetstat(port: number): PortProcess[] {\n let out: string;\n try {\n out = execFileSync(\"netstat\", [\"-ano\", \"-p\", \"TCP\"], {\n encoding: \"utf-8\",\n shell: true,\n timeout: 5000,\n });\n } catch {\n return [];\n }\n\n return parseNetstatOutput(out, port);\n}\n\n// List processes listening on port\nexport function listPortListeners(port: number): PortProcess[] {\n // Windows: use netstat -ano\n if (process.platform === \"win32\") {\n return listPortListenersViaNetstat(port);\n }\n\n // Try lsof first (macOS + most Linux distros)\n try {\n const out = execFileSync(\"lsof\", [\"-nP\", `-iTCP:${port}`, \"-sTCP:LISTEN\", \"-FpFc\"], {\n encoding: \"utf-8\",\n });\n return parseLsofOutput(out);\n } catch (err: unknown) {\n const execErr = err as { status?: number; code?: string };\n\n if (execErr.code !== \"ENOENT\") {\n if (execErr.status === 1) return []; // No listeners\n throw err instanceof Error ? err : new Error(String(err));\n }\n // lsof not available — fall through to Linux alternatives\n log.debug({ port }, \"lsof not found; trying ss fallback\");\n }\n\n // Try ss (iproute2, available on most modern Linux systems)\n try {\n return listPortListenersViaSs(port);\n } catch (err: unknown) {\n const execErr = err as { code?: string };\n if (execErr.code !== \"ENOENT\") {\n throw err instanceof Error ? err : new Error(String(err));\n }\n log.debug({ port }, \"ss not found; trying /proc/net/tcp fallback\");\n }\n\n // Last resort: parse /proc/net/tcp directly (no external tools required)\n return listPortListenersViaProc(port);\n}\n\n// Force free port\nexport async function forceFreePortAndWait(\n port: number,\n opts: {\n timeoutMs?: number;\n intervalMs?: number;\n sigtermTimeoutMs?: number;\n } = {}\n): Promise<ForceFreePortResult> {\n const timeoutMs = Math.max(opts.timeoutMs ?? 2000, 0);\n const intervalMs = Math.max(opts.intervalMs ?? 100, 1);\n const sigtermTimeoutMs = Math.min(Math.max(opts.sigtermTimeoutMs ?? 700, 0), timeoutMs);\n\n // 1. Get listener list\n const listeners = listPortListeners(port);\n const killed: PortProcess[] = [...listeners];\n\n // 2. Send SIGTERM\n for (const proc of listeners) {\n try {\n process.kill(proc.pid, \"SIGTERM\");\n log.info({ pid: proc.pid }, \"Sent SIGTERM\");\n } catch (err) {\n log.warn({ pid: proc.pid, err }, \"Failed to send SIGTERM\");\n }\n }\n\n // 3. Wait for processes to exit\n let waitedMs = 0;\n const checkInterval = () => new Promise<void>((r) => setTimeout(r, intervalMs));\n\n // Wait for SIGTERM to take effect\n const sigtermTries = Math.ceil(sigtermTimeoutMs / intervalMs);\n for (let i = 0; i < sigtermTries; i++) {\n await checkInterval();\n waitedMs += intervalMs;\n\n const remaining = listPortListeners(port);\n if (remaining.length === 0) {\n return { killed, waitedMs, escalatedToSigkill: false };\n }\n }\n\n // 4. SIGTERM timeout, send SIGKILL\n const remaining = listPortListeners(port);\n for (const proc of remaining) {\n try {\n process.kill(proc.pid, \"SIGKILL\");\n log.info({ pid: proc.pid }, \"Sent SIGKILL\");\n } catch (err) {\n log.warn({ pid: proc.pid, err }, \"Failed to send SIGKILL\");\n }\n }\n\n // 5. Wait for SIGKILL to take effect\n const remainingBudget = Math.max(timeoutMs - waitedMs, 0);\n const sigkillTries = Math.ceil(remainingBudget / intervalMs);\n\n for (let i = 0; i < sigkillTries; i++) {\n await checkInterval();\n waitedMs += intervalMs;\n\n const stillRemaining = listPortListeners(port);\n if (stillRemaining.length === 0) {\n return { killed, waitedMs, escalatedToSigkill: true };\n }\n }\n\n throw new Error(`Port ${port} still has listeners after force free`);\n}\n\n// Check if port is available\nexport async function checkPortAvailable(port: number, host = \"0.0.0.0\"): Promise<boolean> {\n return new Promise((resolve) => {\n const server = net.createServer();\n\n server.once(\"error\", (err: NodeJS.ErrnoException) => {\n if (err.code === \"EADDRINUSE\") {\n resolve(false);\n } else {\n resolve(true);\n }\n });\n\n server.once(\"listening\", () => {\n server.close();\n resolve(true);\n });\n\n server.listen(port, host);\n });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime accessors for `gateway.publicUrl` (the user-deployed reverse-proxy
|
|
3
|
+
* URL). Centralizes the "is it configured?" + "normalized origin" lookup so
|
|
4
|
+
* callers (pair-context, exchange-token, CORS allowlist, UI status) all agree
|
|
5
|
+
* on the same value.
|
|
6
|
+
*/
|
|
7
|
+
import type { Config } from '../config/schema.js';
|
|
8
|
+
export declare function resolveReverseProxyPublicUrl(config: Config | undefined): string | null;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { init_public_url, normalizePublicUrlOrNull } from "../config/public-url.js";
|
|
2
|
+
//#region src/gateway/public-url.ts
|
|
3
|
+
init_public_url();
|
|
4
|
+
function resolveReverseProxyPublicUrl(config) {
|
|
5
|
+
return normalizePublicUrlOrNull(config?.gateway?.publicUrl);
|
|
6
|
+
}
|
|
7
|
+
//#endregion
|
|
8
|
+
export { resolveReverseProxyPublicUrl };
|
|
9
|
+
|
|
10
|
+
//# sourceMappingURL=public-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"public-url.js","names":[],"sources":["../../../src/gateway/public-url.ts"],"sourcesContent":["/**\n * Runtime accessors for `gateway.publicUrl` (the user-deployed reverse-proxy\n * URL). Centralizes the \"is it configured?\" + \"normalized origin\" lookup so\n * callers (pair-context, exchange-token, CORS allowlist, UI status) all agree\n * on the same value.\n */\n\nimport type { Config } from '../config/schema.js';\nimport { normalizePublicUrlOrNull } from '../config/public-url.js';\n\nexport function resolveReverseProxyPublicUrl(config: Config | undefined): string | null {\n return normalizePublicUrlOrNull(config?.gateway?.publicUrl);\n}\n"],"mappings":";;iBAQmE;AAEnE,SAAgB,6BAA6B,QAA2C;AACtF,QAAO,yBAAyB,QAAQ,SAAS,UAAU"}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
type OriginCheckResult = {
|
|
8
8
|
ok: true;
|
|
9
|
-
matchedBy: 'allowlist' | 'host-header-fallback' | 'local-loopback';
|
|
9
|
+
matchedBy: 'allowlist' | 'host-header-fallback' | 'local-loopback' | 'trusted-proxy-same-host';
|
|
10
10
|
} | {
|
|
11
11
|
ok: false;
|
|
12
12
|
reason: string;
|
|
@@ -17,5 +17,13 @@ export declare function checkBrowserOrigin(params: {
|
|
|
17
17
|
allowedOrigins?: string[];
|
|
18
18
|
allowHostHeaderOriginFallback?: boolean;
|
|
19
19
|
isLocalClient?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* When true, allow `Origin` whose host portion exactly equals the `Host`
|
|
22
|
+
* header. Only flip this on after verifying the TCP source is loopback or
|
|
23
|
+
* inside `gateway.trustedProxies` — otherwise an attacker who can set
|
|
24
|
+
* arbitrary Origin + Host (e.g. via an open SSRF) bypasses CSRF.
|
|
25
|
+
* Enables zero-config reverse-proxy access at the user's own domain.
|
|
26
|
+
*/
|
|
27
|
+
autoAllowSameHostFromTrustedProxy?: boolean;
|
|
20
28
|
}): OriginCheckResult;
|
|
21
29
|
export {};
|
|
@@ -37,6 +37,10 @@ function checkBrowserOrigin(params) {
|
|
|
37
37
|
matchedBy: "allowlist"
|
|
38
38
|
};
|
|
39
39
|
const requestHost = normalizeHostHeader(params.requestHost);
|
|
40
|
+
if (params.autoAllowSameHostFromTrustedProxy === true && requestHost && parsedOrigin.host === requestHost) return {
|
|
41
|
+
ok: true,
|
|
42
|
+
matchedBy: "trusted-proxy-same-host"
|
|
43
|
+
};
|
|
40
44
|
if (params.allowHostHeaderOriginFallback === true && requestHost && parsedOrigin.host === requestHost) return {
|
|
41
45
|
ok: true,
|
|
42
46
|
matchedBy: "host-header-fallback"
|