@poolzin/pool-bot 2026.2.21 → 2026.2.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/dist/agents/api-key-rotation.js +47 -0
- package/dist/agents/apply-patch-update.js +19 -9
- package/dist/agents/apply-patch.js +72 -47
- package/dist/agents/bash-tools.exec.js +141 -559
- package/dist/agents/cli-backends.js +49 -6
- package/dist/agents/cli-runner/helpers.js +69 -152
- package/dist/agents/cli-runner.js +70 -19
- package/dist/agents/identity.js +20 -1
- package/dist/agents/image-sanitization.js +9 -0
- package/dist/agents/live-auth-keys.js +123 -26
- package/dist/agents/live-model-filter.js +13 -4
- package/dist/agents/model-catalog.js +40 -9
- package/dist/agents/model-forward-compat.js +60 -23
- package/dist/agents/model-selection.js +134 -41
- package/dist/agents/pi-auth-json.js +2 -2
- package/dist/agents/pi-embedded-helpers/bootstrap.js +65 -15
- package/dist/agents/pi-embedded-helpers/errors.js +140 -15
- package/dist/agents/pi-embedded-helpers/images.js +22 -12
- package/dist/agents/pi-embedded-helpers.js +2 -2
- package/dist/agents/pi-embedded-runner/abort.js +10 -3
- package/dist/agents/pi-embedded-runner/compact.js +230 -32
- package/dist/agents/pi-embedded-runner/extra-params.js +203 -12
- package/dist/agents/pi-embedded-runner/google.js +109 -19
- package/dist/agents/pi-embedded-runner/history.js +35 -17
- package/dist/agents/pi-embedded-runner/run/attempt.js +386 -95
- package/dist/agents/pi-embedded-runner/run/images.js +81 -55
- package/dist/agents/pi-embedded-runner/run/payloads.js +89 -39
- package/dist/agents/pi-embedded-runner/run.js +193 -25
- package/dist/agents/pi-embedded-runner/run.overflow-compaction.mocks.shared.js +2 -2
- package/dist/agents/pi-embedded-runner/runs.js +17 -8
- package/dist/agents/pi-embedded-runner/tool-result-context-guard.js +262 -0
- package/dist/agents/pi-embedded-runner.js +1 -1
- package/dist/agents/pi-embedded-subscribe.handlers.tools.js +180 -10
- package/dist/agents/pi-embedded-subscribe.js +37 -0
- package/dist/agents/pi-embedded-subscribe.tools.js +127 -30
- package/dist/agents/pi-model-discovery.js +9 -2
- package/dist/agents/pi-tool-definition-adapter.js +60 -8
- package/dist/agents/pi-tools.before-tool-call.js +1 -1
- package/dist/agents/pi-tools.js +113 -94
- package/dist/agents/pi-tools.read.js +337 -38
- package/dist/agents/poolbot-tools.js +14 -5
- package/dist/agents/sandbox/docker.js +10 -5
- package/dist/agents/sandbox/registry.js +96 -46
- package/dist/agents/sandbox/sanitize-env-vars.js +82 -0
- package/dist/agents/sandbox-paths.js +43 -10
- package/dist/agents/session-tool-result-guard-wrapper.js +23 -11
- package/dist/agents/session-tool-result-guard.js +39 -39
- package/dist/agents/session-transcript-repair.js +36 -33
- package/dist/agents/session-write-lock.js +62 -44
- package/dist/agents/skills/frontmatter.js +49 -88
- package/dist/agents/skills/workspace.js +335 -28
- package/dist/agents/subagent-announce.js +508 -174
- package/dist/agents/subagent-registry.js +45 -4
- package/dist/agents/subagent-spawn.js +16 -33
- package/dist/agents/system-prompt-report.js +27 -10
- package/dist/agents/system-prompt.js +26 -32
- package/dist/agents/tool-call-id.js +69 -17
- package/dist/agents/tool-display-common.js +1 -1
- package/dist/agents/tool-images.js +64 -31
- package/dist/agents/tools/canvas-tool.js +17 -11
- package/dist/agents/tools/common.js +37 -19
- package/dist/agents/tools/cron-tool.js +40 -38
- package/dist/agents/tools/gateway.js +70 -2
- package/dist/agents/tools/message-tool.js +181 -40
- package/dist/agents/tools/nodes-tool.js +128 -36
- package/dist/agents/tools/nodes-utils.js +12 -38
- package/dist/agents/tools/session-status-tool.js +24 -71
- package/dist/agents/tools/sessions-helpers.js +38 -210
- package/dist/agents/tools/sessions-spawn-tool.js +28 -198
- package/dist/agents/tools/telegram-actions.js +58 -7
- package/dist/agents/tools/web-fetch-utils.js +112 -7
- package/dist/agents/tools/web-fetch.js +279 -175
- package/dist/agents/tools/web-shared.js +71 -8
- package/dist/agents/usage.js +25 -16
- package/dist/auto-reply/commands-registry.data.js +85 -11
- package/dist/auto-reply/dispatch.js +40 -21
- package/dist/auto-reply/reply/abort.js +102 -33
- package/dist/auto-reply/reply/commands-core.js +82 -33
- package/dist/auto-reply/reply/commands-export-session.js +1 -1
- package/dist/auto-reply/reply/commands-info.js +41 -12
- package/dist/auto-reply/reply/commands-subagents.js +352 -100
- package/dist/auto-reply/reply/commands-system-prompt.js +2 -2
- package/dist/auto-reply/reply/dispatch-from-config.js +100 -29
- package/dist/auto-reply/reply/elevated-unavailable.js +1 -1
- package/dist/auto-reply/reply/inbound-meta.js +12 -1
- package/dist/auto-reply/reply/mentions.js +18 -11
- package/dist/auto-reply/reply/normalize-reply.js +17 -8
- package/dist/auto-reply/reply/reply-dispatcher.js +62 -10
- package/dist/auto-reply/reply/session.js +102 -21
- package/dist/auto-reply/reply/streaming-directives.js +16 -5
- package/dist/auto-reply/status.js +73 -50
- package/dist/browser/extension-relay.js +3 -3
- package/dist/browser/http-auth.js +1 -1
- package/dist/browser/paths.js +2 -2
- package/dist/build-info.json +3 -3
- package/dist/channels/allowlist-match.js +20 -0
- package/dist/channels/allowlists/resolve-utils.js +65 -2
- package/dist/channels/chat-type.js +8 -4
- package/dist/channels/dock.js +127 -35
- package/dist/channels/draft-stream-loop.js +6 -2
- package/dist/channels/plugins/actions/telegram.js +42 -18
- package/dist/channels/plugins/allowlist-match.js +1 -1
- package/dist/channels/plugins/group-mentions.js +51 -41
- package/dist/channels/plugins/message-action-names.js +2 -0
- package/dist/channels/plugins/message-actions.js +24 -5
- package/dist/channels/plugins/normalize/discord.js +26 -4
- package/dist/channels/plugins/normalize/signal.js +35 -22
- package/dist/channels/plugins/onboarding/helpers.js +8 -26
- package/dist/channels/plugins/outbound/imessage.js +15 -14
- package/dist/channels/registry.js +20 -7
- package/dist/cli/acp-cli.js +7 -5
- package/dist/cli/browser-cli-extension.js +25 -12
- package/dist/cli/browser-cli-state.cookies-storage.js +25 -6
- package/dist/cli/browser-cli-state.js +101 -145
- package/dist/cli/command-options.js +28 -0
- package/dist/cli/completion-cli.js +6 -6
- package/dist/cli/cron-cli/register.cron-add.js +25 -1
- package/dist/cli/cron-cli/register.cron-edit.js +44 -0
- package/dist/cli/cron-cli/shared.js +7 -1
- package/dist/cli/daemon-cli/lifecycle-core.js +23 -21
- package/dist/cli/daemon-cli/lifecycle.js +23 -247
- package/dist/cli/daemon-cli/register-service-commands.js +25 -4
- package/dist/cli/daemon-cli.js +1 -0
- package/dist/cli/devices-cli.js +33 -20
- package/dist/cli/gateway-cli/register.js +37 -105
- package/dist/cli/gateway-cli/run.js +49 -11
- package/dist/cli/nodes-camera.js +59 -4
- package/dist/cli/nodes-cli/register.camera.js +27 -24
- package/dist/cli/nodes-cli/rpc.js +21 -38
- package/dist/cli/qr-cli.js +2 -2
- package/dist/cli/skills-cli.format.js +2 -2
- package/dist/cli/update-cli/progress.js +2 -2
- package/dist/cli/update-cli/restart-helper.js +28 -7
- package/dist/cli/update-cli/shared.js +7 -7
- package/dist/cli/update-cli/status.js +1 -1
- package/dist/cli/update-cli/update-command.js +14 -8
- package/dist/cli/update-cli/wizard.js +2 -2
- package/dist/cli/update-cli.js +21 -1027
- package/dist/commands/auth-choice.apply.anthropic.js +10 -2
- package/dist/commands/channels/add-mutators.js +3 -35
- package/dist/commands/channels/add.js +39 -51
- package/dist/commands/config-validation.js +1 -1
- package/dist/commands/configure.gateway-auth.js +52 -15
- package/dist/commands/configure.gateway.js +84 -40
- package/dist/commands/doctor-completion.js +3 -3
- package/dist/commands/doctor-config-flow.js +536 -16
- package/dist/commands/doctor-gateway-services.js +103 -79
- package/dist/commands/doctor-memory-search.js +9 -9
- package/dist/commands/doctor-platform-notes.js +57 -30
- package/dist/commands/doctor-prompter.js +26 -15
- package/dist/commands/doctor-session-locks.js +1 -1
- package/dist/commands/doctor.js +21 -9
- package/dist/commands/model-picker.js +120 -95
- package/dist/commands/models/set.js +2 -21
- package/dist/commands/models/shared.js +65 -37
- package/dist/commands/onboard-helpers.js +81 -39
- package/dist/commands/openai-codex-oauth.js +1 -1
- package/dist/commands/sessions.js +52 -53
- package/dist/commands/status.summary.js +52 -34
- package/dist/commands/test-wizard-helpers.js +2 -2
- package/dist/config/defaults.js +79 -42
- package/dist/config/group-policy.js +50 -18
- package/dist/config/includes.js +37 -10
- package/dist/config/schema.help.js +5 -4
- package/dist/config/schema.hints.js +2 -2
- package/dist/config/schema.labels.js +1 -0
- package/dist/config/sessions/group.js +12 -11
- package/dist/config/sessions/paths.js +137 -11
- package/dist/config/sessions/store.js +185 -65
- package/dist/config/sessions/types.js +15 -1
- package/dist/config/sessions.js +1 -0
- package/dist/config/telegram-custom-commands.js +3 -2
- package/dist/config/types.js +2 -0
- package/dist/config/zod-schema.agent-defaults.js +6 -27
- package/dist/config/zod-schema.agent-runtime.js +171 -79
- package/dist/config/zod-schema.providers-core.js +138 -65
- package/dist/config/zod-schema.session.js +49 -22
- package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -1
- package/dist/cron/isolated-agent/run.js +224 -57
- package/dist/cron/normalize.js +48 -45
- package/dist/cron/run-log.js +14 -0
- package/dist/cron/service/jobs.js +190 -28
- package/dist/cron/service/normalize.js +29 -11
- package/dist/cron/service/store.js +30 -44
- package/dist/cron/service/timer.js +182 -96
- package/dist/cron/service.js +3 -0
- package/dist/cron/stagger.js +37 -0
- package/dist/daemon/inspect.js +132 -92
- package/dist/daemon/runtime-paths.js +25 -4
- package/dist/daemon/service-audit.js +47 -16
- package/dist/discord/accounts.js +23 -20
- package/dist/discord/monitor/agent-components.js +1115 -219
- package/dist/discord/monitor/allow-list.js +114 -34
- package/dist/discord/monitor/listeners.js +204 -97
- package/dist/discord/monitor/message-handler.js +21 -10
- package/dist/discord/monitor/message-handler.preflight.js +195 -101
- package/dist/discord/monitor/message-handler.process.js +384 -123
- package/dist/discord/monitor/message-utils.js +86 -23
- package/dist/discord/monitor/native-command.js +77 -57
- package/dist/discord/monitor/provider.js +122 -117
- package/dist/discord/monitor/reply-context.js +20 -16
- package/dist/discord/monitor/reply-delivery.js +40 -8
- package/dist/discord/monitor/rest-fetch.js +22 -0
- package/dist/discord/monitor/threading.js +117 -24
- package/dist/discord/send.js +2 -1
- package/dist/discord/send.outbound.js +124 -11
- package/dist/discord/send.shared.js +112 -72
- package/dist/discord/voice-message.js +3 -3
- package/dist/gateway/auth.js +119 -44
- package/dist/gateway/call.js +76 -34
- package/dist/gateway/channel-health-monitor.js +57 -50
- package/dist/gateway/client.js +63 -29
- package/dist/gateway/control-ui-contract.js +1 -1
- package/dist/gateway/gateway-config-prompts.shared.js +2 -2
- package/dist/gateway/net.js +109 -1
- package/dist/gateway/protocol/index.js +5 -8
- package/dist/gateway/protocol/schema/agent.js +19 -1
- package/dist/gateway/protocol/schema/channels.js +21 -0
- package/dist/gateway/protocol/schema/cron.js +43 -30
- package/dist/gateway/protocol/schema/protocol-schemas.js +6 -11
- package/dist/gateway/protocol/schema/sessions.js +5 -1
- package/dist/gateway/protocol/schema.js +0 -1
- package/dist/gateway/server/presence-events.js +12 -0
- package/dist/gateway/server/ws-connection/message-handler.js +203 -212
- package/dist/gateway/server/ws-connection.js +58 -21
- package/dist/gateway/server-broadcast.js +18 -13
- package/dist/gateway/server-cron.js +177 -10
- package/dist/gateway/server-methods/agent-job.js +131 -38
- package/dist/gateway/server-methods/send.js +60 -14
- package/dist/gateway/server-methods/sessions.js +160 -96
- package/dist/gateway/server-methods/system.js +5 -7
- package/dist/gateway/server-methods-list.js +8 -0
- package/dist/gateway/server-methods.js +24 -8
- package/dist/gateway/server-node-events.js +278 -68
- package/dist/gateway/session-utils.fs.js +316 -75
- package/dist/gateway/session-utils.js +224 -70
- package/dist/gateway/sessions-patch.js +63 -20
- package/dist/gateway/test-temp-config.js +1 -1
- package/dist/gateway/tools-invoke-http.js +118 -70
- package/dist/gateway/ws-log.js +135 -107
- package/dist/hooks/frontmatter.js +36 -82
- package/dist/hooks/install.js +149 -139
- package/dist/hooks/internal-hooks.js +29 -4
- package/dist/hooks/plugin-hooks.js +2 -1
- package/dist/imessage/monitor/deliver.js +10 -4
- package/dist/imessage/monitor/monitor-provider.js +138 -375
- package/dist/imessage/monitor/runtime.js +4 -8
- package/dist/imessage/send.js +65 -19
- package/dist/infra/exec-approvals-allowlist.js +7 -0
- package/dist/infra/exec-approvals.js +35 -920
- package/dist/infra/exec-safe-bin-trust.js +64 -0
- package/dist/infra/heartbeat-runner.js +207 -134
- package/dist/infra/heartbeat-wake.js +183 -22
- package/dist/infra/install-source-utils.js +47 -0
- package/dist/infra/net/ssrf.js +170 -36
- package/dist/infra/outbound/deliver.js +224 -58
- package/dist/infra/outbound/message-action-spec.js +12 -5
- package/dist/infra/outbound/outbound-session.js +27 -25
- package/dist/infra/poolbot-root.js +32 -22
- package/dist/infra/ports.js +14 -11
- package/dist/infra/skills-remote.js +48 -37
- package/dist/infra/system-events.js +25 -11
- package/dist/infra/system-presence.js +26 -33
- package/dist/infra/tmp-poolbot-dir.js +81 -2
- package/dist/infra/wsl.js +37 -1
- package/dist/line/bot-message-context.js +163 -191
- package/dist/logging/subsystem.js +59 -22
- package/dist/markdown/ir.js +124 -50
- package/dist/media/store.js +1 -1
- package/dist/media-understanding/runner.entries.js +42 -25
- package/dist/media-understanding/runner.js +53 -488
- package/dist/memory/embeddings-gemini.js +53 -38
- package/dist/memory/manager-embedding-ops.js +48 -69
- package/dist/pairing/pairing-store.js +178 -119
- package/dist/plugin-sdk/index.js +34 -6
- package/dist/plugins/hooks.js +135 -14
- package/dist/plugins/install.js +190 -152
- package/dist/polls.js +11 -0
- package/dist/routing/resolve-route.js +190 -56
- package/dist/routing/session-key.js +38 -22
- package/dist/runtime.js +35 -9
- package/dist/security/audit-channel.js +1 -1
- package/dist/sessions/session-key-utils.js +29 -11
- package/dist/shared/frontmatter.js +5 -5
- package/dist/shared/node-list-types.js +1 -0
- package/dist/shared/string-normalization.js +15 -0
- package/dist/signal/monitor/event-handler.js +68 -36
- package/dist/signal/send.js +29 -37
- package/dist/slack/monitor/allow-list.js +10 -11
- package/dist/slack/monitor/commands.js +14 -3
- package/dist/slack/monitor/events/interactions.js +4 -4
- package/dist/slack/monitor/media.js +224 -16
- package/dist/slack/monitor/message-handler/dispatch.js +247 -13
- package/dist/slack/monitor/message-handler/prepare.js +128 -45
- package/dist/slack/monitor/slash.js +357 -144
- package/dist/slack/streaming.js +77 -0
- package/dist/telegram/accounts.js +40 -13
- package/dist/telegram/allowed-updates.js +3 -0
- package/dist/telegram/bot/delivery.js +129 -66
- package/dist/telegram/bot/helpers.js +136 -122
- package/dist/telegram/bot-handlers.js +600 -339
- package/dist/telegram/bot-message-context.js +115 -73
- package/dist/telegram/bot-message-dispatch.js +235 -104
- package/dist/telegram/bot-native-command-menu.js +3 -1
- package/dist/telegram/bot-native-commands.js +213 -193
- package/dist/telegram/bot.js +24 -132
- package/dist/telegram/draft-stream.js +84 -75
- package/dist/telegram/format.js +150 -6
- package/dist/telegram/send.js +415 -255
- package/dist/telegram/targets.js +21 -2
- package/dist/telegram/update-offset-store.js +19 -3
- package/dist/terminal/restore.js +5 -2
- package/dist/test-utils/fetch-mock.js +5 -0
- package/dist/version.js +18 -5
- package/dist/web/auto-reply/monitor/broadcast.js +7 -3
- package/dist/web/auto-reply/monitor/on-message.js +6 -3
- package/dist/web/inbound/media.js +34 -8
- package/dist/web/inbound/monitor.js +34 -17
- package/dist/web/inbound/send-api.js +18 -17
- package/dist/web/outbound.js +12 -5
- package/dist/wizard/clack-prompter.js +40 -7
- package/extensions/bluebubbles/package.json +1 -1
- package/extensions/copilot-proxy/package.json +1 -1
- package/extensions/diagnostics-otel/package.json +1 -1
- package/extensions/discord/package.json +1 -1
- package/extensions/feishu/package.json +1 -1
- package/extensions/google-antigravity-auth/package.json +1 -1
- package/extensions/google-gemini-cli-auth/package.json +1 -1
- package/extensions/googlechat/package.json +1 -1
- package/extensions/imessage/package.json +1 -1
- package/extensions/irc/package.json +1 -1
- package/extensions/line/package.json +1 -1
- package/extensions/llm-task/package.json +1 -1
- package/extensions/lobster/package.json +1 -1
- package/extensions/matrix/CHANGELOG.md +5 -0
- package/extensions/matrix/package.json +1 -1
- package/extensions/mattermost/package.json +1 -1
- package/extensions/memory-core/package.json +1 -1
- package/extensions/memory-lancedb/package.json +1 -1
- package/extensions/minimax-portal-auth/package.json +1 -1
- package/extensions/msteams/CHANGELOG.md +5 -0
- package/extensions/msteams/package.json +1 -1
- package/extensions/nextcloud-talk/package.json +1 -1
- package/extensions/nostr/CHANGELOG.md +5 -0
- package/extensions/nostr/package.json +1 -1
- package/extensions/open-prose/package.json +1 -1
- package/extensions/openai-codex-auth/package.json +1 -1
- package/extensions/signal/package.json +1 -1
- package/extensions/slack/package.json +1 -1
- package/extensions/telegram/package.json +1 -1
- package/extensions/tlon/package.json +1 -1
- package/extensions/twitch/CHANGELOG.md +5 -0
- package/extensions/twitch/package.json +1 -1
- package/extensions/voice-call/CHANGELOG.md +5 -0
- package/extensions/voice-call/package.json +1 -1
- package/extensions/whatsapp/package.json +1 -1
- package/extensions/zalo/CHANGELOG.md +5 -0
- package/extensions/zalo/package.json +1 -1
- package/extensions/zalouser/CHANGELOG.md +5 -0
- package/extensions/zalouser/package.json +1 -1
- package/package.json +1 -1
- package/skills/apple-reminders/SKILL.md +100 -49
- package/skills/coding-agent/SKILL.md +34 -28
- package/skills/github/SKILL.md +131 -16
- package/skills/imsg/SKILL.md +112 -15
- package/skills/openhue/SKILL.md +101 -19
- package/skills/tmux/SKILL.md +111 -79
- package/skills/weather/SKILL.md +88 -25
package/dist/gateway/call.js
CHANGED
|
@@ -1,11 +1,35 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { loadConfig, resolveConfigPath, resolveGatewayPort, resolveStateDir, } from "../config/config.js";
|
|
3
|
-
import { pickPrimaryTailnetIPv4 } from "../infra/tailnet.js";
|
|
4
3
|
import { loadOrCreateDeviceIdentity } from "../infra/device-identity.js";
|
|
5
|
-
import {
|
|
4
|
+
import { pickPrimaryTailnetIPv4 } from "../infra/tailnet.js";
|
|
6
5
|
import { loadGatewayTlsRuntime } from "../infra/tls/gateway.js";
|
|
6
|
+
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES, } from "../utils/message-channel.js";
|
|
7
7
|
import { GatewayClient } from "./client.js";
|
|
8
|
+
import { pickPrimaryLanIPv4 } from "./net.js";
|
|
8
9
|
import { PROTOCOL_VERSION } from "./protocol/index.js";
|
|
10
|
+
export function resolveExplicitGatewayAuth(opts) {
|
|
11
|
+
const token = typeof opts?.token === "string" && opts.token.trim().length > 0 ? opts.token.trim() : undefined;
|
|
12
|
+
const password = typeof opts?.password === "string" && opts.password.trim().length > 0
|
|
13
|
+
? opts.password.trim()
|
|
14
|
+
: undefined;
|
|
15
|
+
return { token, password };
|
|
16
|
+
}
|
|
17
|
+
export function ensureExplicitGatewayAuth(params) {
|
|
18
|
+
if (!params.urlOverride) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (params.auth.token || params.auth.password) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const message = [
|
|
25
|
+
"gateway url override requires explicit credentials",
|
|
26
|
+
params.errorHint,
|
|
27
|
+
params.configPath ? `Config: ${params.configPath}` : undefined,
|
|
28
|
+
]
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.join("\n");
|
|
31
|
+
throw new Error(message);
|
|
32
|
+
}
|
|
9
33
|
export function buildGatewayConnectionDetails(options = {}) {
|
|
10
34
|
const config = options.config ?? loadConfig();
|
|
11
35
|
const configPath = options.configPath ?? resolveConfigPath(process.env, resolveStateDir(process.env));
|
|
@@ -16,10 +40,14 @@ export function buildGatewayConnectionDetails(options = {}) {
|
|
|
16
40
|
const tailnetIPv4 = pickPrimaryTailnetIPv4();
|
|
17
41
|
const bindMode = config.gateway?.bind ?? "loopback";
|
|
18
42
|
const preferTailnet = bindMode === "tailnet" && !!tailnetIPv4;
|
|
43
|
+
const preferLan = bindMode === "lan";
|
|
44
|
+
const lanIPv4 = preferLan ? pickPrimaryLanIPv4() : undefined;
|
|
19
45
|
const scheme = tlsEnabled ? "wss" : "ws";
|
|
20
46
|
const localUrl = preferTailnet && tailnetIPv4
|
|
21
47
|
? `${scheme}://${tailnetIPv4}:${localPort}`
|
|
22
|
-
:
|
|
48
|
+
: preferLan && lanIPv4
|
|
49
|
+
? `${scheme}://${lanIPv4}:${localPort}`
|
|
50
|
+
: `${scheme}://127.0.0.1:${localPort}`;
|
|
23
51
|
const urlOverride = typeof options.url === "string" && options.url.trim().length > 0
|
|
24
52
|
? options.url.trim()
|
|
25
53
|
: undefined;
|
|
@@ -34,7 +62,9 @@ export function buildGatewayConnectionDetails(options = {}) {
|
|
|
34
62
|
? "missing gateway.remote.url (fallback local)"
|
|
35
63
|
: preferTailnet && tailnetIPv4
|
|
36
64
|
? `local tailnet ${tailnetIPv4}`
|
|
37
|
-
:
|
|
65
|
+
: preferLan && lanIPv4
|
|
66
|
+
? `local lan ${lanIPv4}`
|
|
67
|
+
: "local loopback";
|
|
38
68
|
const remoteFallbackNote = remoteMisconfigured
|
|
39
69
|
? "Warn: gateway.mode=remote but gateway.remote.url is missing; set gateway.remote.url or switch gateway.mode=local."
|
|
40
70
|
: undefined;
|
|
@@ -57,11 +87,19 @@ export function buildGatewayConnectionDetails(options = {}) {
|
|
|
57
87
|
};
|
|
58
88
|
}
|
|
59
89
|
export async function callGateway(opts) {
|
|
60
|
-
const timeoutMs = opts.timeoutMs
|
|
90
|
+
const timeoutMs = typeof opts.timeoutMs === "number" && Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : 10_000;
|
|
91
|
+
const safeTimerTimeoutMs = Math.max(1, Math.min(Math.floor(timeoutMs), 2_147_483_647));
|
|
61
92
|
const config = opts.config ?? loadConfig();
|
|
62
93
|
const isRemoteMode = config.gateway?.mode === "remote";
|
|
63
94
|
const remote = isRemoteMode ? config.gateway?.remote : undefined;
|
|
64
95
|
const urlOverride = typeof opts.url === "string" && opts.url.trim().length > 0 ? opts.url.trim() : undefined;
|
|
96
|
+
const explicitAuth = resolveExplicitGatewayAuth({ token: opts.token, password: opts.password });
|
|
97
|
+
ensureExplicitGatewayAuth({
|
|
98
|
+
urlOverride,
|
|
99
|
+
auth: explicitAuth,
|
|
100
|
+
errorHint: "Fix: pass --token or --password (or gatewayToken in tools).",
|
|
101
|
+
configPath: opts.configPath ?? resolveConfigPath(process.env, resolveStateDir(process.env)),
|
|
102
|
+
});
|
|
65
103
|
const remoteUrl = typeof remote?.url === "string" && remote.url.trim().length > 0 ? remote.url.trim() : undefined;
|
|
66
104
|
if (isRemoteMode && !urlOverride && !remoteUrl) {
|
|
67
105
|
const configPath = opts.configPath ?? resolveConfigPath(process.env, resolveStateDir(process.env));
|
|
@@ -88,30 +126,30 @@ export async function callGateway(opts) {
|
|
|
88
126
|
const tlsFingerprint = overrideTlsFingerprint ||
|
|
89
127
|
remoteTlsFingerprint ||
|
|
90
128
|
(tlsRuntime?.enabled ? tlsRuntime.fingerprintSha256 : undefined);
|
|
91
|
-
const token =
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const password =
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
129
|
+
const token = explicitAuth.token ||
|
|
130
|
+
(!urlOverride
|
|
131
|
+
? isRemoteMode
|
|
132
|
+
? typeof remote?.token === "string" && remote.token.trim().length > 0
|
|
133
|
+
? remote.token.trim()
|
|
134
|
+
: undefined
|
|
135
|
+
: process.env.POOLBOT_GATEWAY_TOKEN?.trim() ||
|
|
136
|
+
process.env.CLAWDBOT_GATEWAY_TOKEN?.trim() ||
|
|
137
|
+
(typeof authToken === "string" && authToken.trim().length > 0
|
|
138
|
+
? authToken.trim()
|
|
139
|
+
: undefined)
|
|
140
|
+
: undefined);
|
|
141
|
+
const password = explicitAuth.password ||
|
|
142
|
+
(!urlOverride
|
|
143
|
+
? process.env.POOLBOT_GATEWAY_PASSWORD?.trim() ||
|
|
144
|
+
process.env.CLAWDBOT_GATEWAY_PASSWORD?.trim() ||
|
|
145
|
+
(isRemoteMode
|
|
146
|
+
? typeof remote?.password === "string" && remote.password.trim().length > 0
|
|
147
|
+
? remote.password.trim()
|
|
148
|
+
: undefined
|
|
149
|
+
: typeof authPassword === "string" && authPassword.trim().length > 0
|
|
150
|
+
? authPassword.trim()
|
|
151
|
+
: undefined)
|
|
152
|
+
: undefined);
|
|
115
153
|
const formatCloseError = (code, reason) => {
|
|
116
154
|
const reasonText = reason?.trim() || "no close reason";
|
|
117
155
|
const hint = code === 1006 ? "abnormal closure (no close frame)" : code === 1000 ? "normal closure" : "";
|
|
@@ -123,14 +161,17 @@ export async function callGateway(opts) {
|
|
|
123
161
|
let settled = false;
|
|
124
162
|
let ignoreClose = false;
|
|
125
163
|
const stop = (err, value) => {
|
|
126
|
-
if (settled)
|
|
164
|
+
if (settled) {
|
|
127
165
|
return;
|
|
166
|
+
}
|
|
128
167
|
settled = true;
|
|
129
168
|
clearTimeout(timer);
|
|
130
|
-
if (err)
|
|
169
|
+
if (err) {
|
|
131
170
|
reject(err);
|
|
132
|
-
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
133
173
|
resolve(value);
|
|
174
|
+
}
|
|
134
175
|
};
|
|
135
176
|
const client = new GatewayClient({
|
|
136
177
|
url,
|
|
@@ -164,8 +205,9 @@ export async function callGateway(opts) {
|
|
|
164
205
|
}
|
|
165
206
|
},
|
|
166
207
|
onClose: (code, reason) => {
|
|
167
|
-
if (settled || ignoreClose)
|
|
208
|
+
if (settled || ignoreClose) {
|
|
168
209
|
return;
|
|
210
|
+
}
|
|
169
211
|
ignoreClose = true;
|
|
170
212
|
client.stop();
|
|
171
213
|
stop(new Error(formatCloseError(code, reason)));
|
|
@@ -175,7 +217,7 @@ export async function callGateway(opts) {
|
|
|
175
217
|
ignoreClose = true;
|
|
176
218
|
client.stop();
|
|
177
219
|
stop(new Error(formatTimeoutError()));
|
|
178
|
-
},
|
|
220
|
+
}, safeTimerTimeoutMs);
|
|
179
221
|
client.start();
|
|
180
222
|
});
|
|
181
223
|
}
|
|
@@ -26,71 +26,78 @@ export function startChannelHealthMonitor(deps) {
|
|
|
26
26
|
const restartRecords = new Map();
|
|
27
27
|
const startedAt = Date.now();
|
|
28
28
|
let stopped = false;
|
|
29
|
+
let checkInFlight = false;
|
|
29
30
|
let timer = null;
|
|
30
31
|
const rKey = (channelId, accountId) => `${channelId}:${accountId}`;
|
|
31
32
|
function pruneOldRestarts(record, now) {
|
|
32
33
|
record.restartsThisHour = record.restartsThisHour.filter((r) => now - r.at < ONE_HOUR_MS);
|
|
33
34
|
}
|
|
34
35
|
async function runCheck() {
|
|
35
|
-
if (stopped) {
|
|
36
|
+
if (stopped || checkInFlight) {
|
|
36
37
|
return;
|
|
37
38
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
for (const [channelId, accounts] of Object.entries(snapshot.channelAccounts)) {
|
|
44
|
-
if (!accounts) {
|
|
45
|
-
continue;
|
|
39
|
+
checkInFlight = true;
|
|
40
|
+
try {
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
if (now - startedAt < startupGraceMs) {
|
|
43
|
+
return;
|
|
46
44
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
if (!isManagedAccount(status)) {
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
if (channelManager.isManuallyStopped(channelId, accountId)) {
|
|
45
|
+
const snapshot = channelManager.getRuntimeSnapshot();
|
|
46
|
+
for (const [channelId, accounts] of Object.entries(snapshot.channelAccounts)) {
|
|
47
|
+
if (!accounts) {
|
|
55
48
|
continue;
|
|
56
49
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
50
|
+
for (const [accountId, status] of Object.entries(accounts)) {
|
|
51
|
+
if (!status) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (!isManagedAccount(status)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (channelManager.isManuallyStopped(channelId, accountId)) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (isChannelHealthy(status)) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const key = rKey(channelId, accountId);
|
|
64
|
+
const record = restartRecords.get(key) ?? {
|
|
65
|
+
lastRestartAt: 0,
|
|
66
|
+
restartsThisHour: [],
|
|
67
|
+
};
|
|
68
|
+
if (now - record.lastRestartAt <= cooldownMs) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
pruneOldRestarts(record, now);
|
|
72
|
+
if (record.restartsThisHour.length >= maxRestartsPerHour) {
|
|
73
|
+
log.warn?.(`[${channelId}:${accountId}] health-monitor: hit ${maxRestartsPerHour} restarts/hour limit, skipping`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const reason = !status.running
|
|
77
|
+
? status.reconnectAttempts && status.reconnectAttempts >= 10
|
|
78
|
+
? "gave-up"
|
|
79
|
+
: "stopped"
|
|
80
|
+
: "stuck";
|
|
81
|
+
log.info?.(`[${channelId}:${accountId}] health-monitor: restarting (reason: ${reason})`);
|
|
82
|
+
try {
|
|
83
|
+
if (status.running) {
|
|
84
|
+
await channelManager.stopChannel(channelId, accountId);
|
|
85
|
+
}
|
|
86
|
+
channelManager.resetRestartAttempts(channelId, accountId);
|
|
87
|
+
await channelManager.startChannel(channelId, accountId);
|
|
88
|
+
record.lastRestartAt = now;
|
|
89
|
+
record.restartsThisHour.push({ at: now });
|
|
90
|
+
restartRecords.set(key, record);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
log.error?.(`[${channelId}:${accountId}] health-monitor: restart failed: ${String(err)}`);
|
|
82
94
|
}
|
|
83
|
-
channelManager.resetRestartAttempts(channelId, accountId);
|
|
84
|
-
await channelManager.startChannel(channelId, accountId);
|
|
85
|
-
record.lastRestartAt = now;
|
|
86
|
-
record.restartsThisHour.push({ at: now });
|
|
87
|
-
restartRecords.set(key, record);
|
|
88
|
-
}
|
|
89
|
-
catch (err) {
|
|
90
|
-
log.error?.(`[${channelId}:${accountId}] health-monitor: restart failed: ${String(err)}`);
|
|
91
95
|
}
|
|
92
96
|
}
|
|
93
97
|
}
|
|
98
|
+
finally {
|
|
99
|
+
checkInFlight = false;
|
|
100
|
+
}
|
|
94
101
|
}
|
|
95
102
|
function stop() {
|
|
96
103
|
stopped = true;
|
package/dist/gateway/client.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { WebSocket } from "ws";
|
|
3
|
+
import { clearDeviceAuthToken, loadDeviceAuthToken, storeDeviceAuthToken, } from "../infra/device-auth-store.js";
|
|
4
|
+
import { loadOrCreateDeviceIdentity, publicKeyRawBase64UrlFromPem, signDevicePayload, } from "../infra/device-identity.js";
|
|
3
5
|
import { normalizeFingerprint } from "../infra/tls/fingerprint.js";
|
|
4
6
|
import { rawDataToString } from "../infra/ws.js";
|
|
5
7
|
import { logDebug, logError } from "../logger.js";
|
|
6
|
-
import { loadOrCreateDeviceIdentity, publicKeyRawBase64UrlFromPem, signDevicePayload, } from "../infra/device-identity.js";
|
|
7
|
-
import { clearDeviceAuthToken, loadDeviceAuthToken, storeDeviceAuthToken, } from "../infra/device-auth-store.js";
|
|
8
8
|
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES, } from "../utils/message-channel.js";
|
|
9
9
|
import { buildDeviceAuthPayload } from "./device-auth.js";
|
|
10
10
|
import { PROTOCOL_VERSION, validateEventFrame, validateRequestFrame, validateResponseFrame, } from "./protocol/index.js";
|
|
@@ -38,8 +38,9 @@ export class GatewayClient {
|
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
start() {
|
|
41
|
-
if (this.closed)
|
|
41
|
+
if (this.closed) {
|
|
42
42
|
return;
|
|
43
|
+
}
|
|
43
44
|
const url = this.opts.url ?? "ws://127.0.0.1:18789";
|
|
44
45
|
if (this.opts.tlsFingerprint && !url.startsWith("wss://")) {
|
|
45
46
|
this.opts.onConnectError?.(new Error("gateway tls fingerprint requires wss:// gateway url"));
|
|
@@ -67,6 +68,7 @@ export class GatewayClient {
|
|
|
67
68
|
return new Error("gateway tls fingerprint mismatch");
|
|
68
69
|
}
|
|
69
70
|
return undefined;
|
|
71
|
+
// oxlint-disable-next-line typescript/no-explicit-any
|
|
70
72
|
});
|
|
71
73
|
}
|
|
72
74
|
this.ws = new WebSocket(url, wsOptions);
|
|
@@ -85,6 +87,19 @@ export class GatewayClient {
|
|
|
85
87
|
this.ws.on("close", (code, reason) => {
|
|
86
88
|
const reasonText = rawDataToString(reason);
|
|
87
89
|
this.ws = null;
|
|
90
|
+
// If closed due to device token mismatch, clear the stored token so next attempt can get a fresh one
|
|
91
|
+
if (code === 1008 &&
|
|
92
|
+
reasonText.toLowerCase().includes("device token mismatch") &&
|
|
93
|
+
this.opts.deviceIdentity) {
|
|
94
|
+
const role = this.opts.role ?? "operator";
|
|
95
|
+
try {
|
|
96
|
+
clearDeviceAuthToken({ deviceId: this.opts.deviceIdentity.deviceId, role });
|
|
97
|
+
logDebug(`cleared stale device-auth token for device ${this.opts.deviceIdentity.deviceId}`);
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
logDebug(`failed clearing stale device-auth token for device ${this.opts.deviceIdentity.deviceId}: ${String(err)}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
88
103
|
this.flushPendingErrors(new Error(`gateway closed (${code}): ${reasonText}`));
|
|
89
104
|
this.scheduleReconnect();
|
|
90
105
|
this.opts.onClose?.(code, reasonText);
|
|
@@ -107,8 +122,9 @@ export class GatewayClient {
|
|
|
107
122
|
this.flushPendingErrors(new Error("gateway client stopped"));
|
|
108
123
|
}
|
|
109
124
|
sendConnect() {
|
|
110
|
-
if (this.connectSent)
|
|
125
|
+
if (this.connectSent) {
|
|
111
126
|
return;
|
|
127
|
+
}
|
|
112
128
|
this.connectSent = true;
|
|
113
129
|
if (this.connectTimer) {
|
|
114
130
|
clearTimeout(this.connectTimer);
|
|
@@ -118,8 +134,9 @@ export class GatewayClient {
|
|
|
118
134
|
const storedToken = this.opts.deviceIdentity
|
|
119
135
|
? loadDeviceAuthToken({ deviceId: this.opts.deviceIdentity.deviceId, role })?.token
|
|
120
136
|
: null;
|
|
121
|
-
|
|
122
|
-
|
|
137
|
+
// Prefer explicitly provided credentials (e.g. CLI `--token`) over any persisted
|
|
138
|
+
// device-auth tokens. Persisted tokens are only used when no token is provided.
|
|
139
|
+
const authToken = this.opts.token ?? storedToken ?? undefined;
|
|
123
140
|
const auth = authToken || this.opts.password
|
|
124
141
|
? {
|
|
125
142
|
token: authToken,
|
|
@@ -130,8 +147,9 @@ export class GatewayClient {
|
|
|
130
147
|
const nonce = this.connectNonce ?? undefined;
|
|
131
148
|
const scopes = this.opts.scopes ?? ["operator.admin"];
|
|
132
149
|
const device = (() => {
|
|
133
|
-
if (!this.opts.deviceIdentity)
|
|
150
|
+
if (!this.opts.deviceIdentity) {
|
|
134
151
|
return undefined;
|
|
152
|
+
}
|
|
135
153
|
const payload = buildDeviceAuthPayload({
|
|
136
154
|
deviceId: this.opts.deviceIdentity.deviceId,
|
|
137
155
|
clientId: this.opts.clientName ?? GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT,
|
|
@@ -194,18 +212,14 @@ export class GatewayClient {
|
|
|
194
212
|
this.opts.onHelloOk?.(helloOk);
|
|
195
213
|
})
|
|
196
214
|
.catch((err) => {
|
|
197
|
-
if (canFallbackToShared && this.opts.deviceIdentity) {
|
|
198
|
-
clearDeviceAuthToken({
|
|
199
|
-
deviceId: this.opts.deviceIdentity.deviceId,
|
|
200
|
-
role,
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
215
|
this.opts.onConnectError?.(err instanceof Error ? err : new Error(String(err)));
|
|
204
216
|
const msg = `gateway connect failed: ${String(err)}`;
|
|
205
|
-
if (this.opts.mode === GATEWAY_CLIENT_MODES.PROBE)
|
|
217
|
+
if (this.opts.mode === GATEWAY_CLIENT_MODES.PROBE) {
|
|
206
218
|
logDebug(msg);
|
|
207
|
-
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
208
221
|
logError(msg);
|
|
222
|
+
}
|
|
209
223
|
this.ws?.close(1008, "connect failed");
|
|
210
224
|
});
|
|
211
225
|
}
|
|
@@ -238,8 +252,9 @@ export class GatewayClient {
|
|
|
238
252
|
}
|
|
239
253
|
if (validateResponseFrame(parsed)) {
|
|
240
254
|
const pending = this.pending.get(parsed.id);
|
|
241
|
-
if (!pending)
|
|
255
|
+
if (!pending) {
|
|
242
256
|
return;
|
|
257
|
+
}
|
|
243
258
|
// If the payload is an ack with status accepted, keep waiting for final.
|
|
244
259
|
const payload = parsed.payload;
|
|
245
260
|
const status = payload?.status;
|
|
@@ -247,10 +262,12 @@ export class GatewayClient {
|
|
|
247
262
|
return;
|
|
248
263
|
}
|
|
249
264
|
this.pending.delete(parsed.id);
|
|
250
|
-
if (parsed.ok)
|
|
265
|
+
if (parsed.ok) {
|
|
251
266
|
pending.resolve(parsed.payload);
|
|
252
|
-
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
253
269
|
pending.reject(new Error(parsed.error?.message ?? "unknown error"));
|
|
270
|
+
}
|
|
254
271
|
}
|
|
255
272
|
}
|
|
256
273
|
catch (err) {
|
|
@@ -260,15 +277,21 @@ export class GatewayClient {
|
|
|
260
277
|
queueConnect() {
|
|
261
278
|
this.connectNonce = null;
|
|
262
279
|
this.connectSent = false;
|
|
263
|
-
|
|
280
|
+
const rawConnectDelayMs = this.opts.connectDelayMs;
|
|
281
|
+
const connectDelayMs = typeof rawConnectDelayMs === "number" && Number.isFinite(rawConnectDelayMs)
|
|
282
|
+
? Math.max(0, Math.min(5_000, rawConnectDelayMs))
|
|
283
|
+
: 750;
|
|
284
|
+
if (this.connectTimer) {
|
|
264
285
|
clearTimeout(this.connectTimer);
|
|
286
|
+
}
|
|
265
287
|
this.connectTimer = setTimeout(() => {
|
|
266
288
|
this.sendConnect();
|
|
267
|
-
},
|
|
289
|
+
}, connectDelayMs);
|
|
268
290
|
}
|
|
269
291
|
scheduleReconnect() {
|
|
270
|
-
if (this.closed)
|
|
292
|
+
if (this.closed) {
|
|
271
293
|
return;
|
|
294
|
+
}
|
|
272
295
|
if (this.tickTimer) {
|
|
273
296
|
clearInterval(this.tickTimer);
|
|
274
297
|
this.tickTimer = null;
|
|
@@ -284,14 +307,21 @@ export class GatewayClient {
|
|
|
284
307
|
this.pending.clear();
|
|
285
308
|
}
|
|
286
309
|
startTickWatch() {
|
|
287
|
-
if (this.tickTimer)
|
|
310
|
+
if (this.tickTimer) {
|
|
288
311
|
clearInterval(this.tickTimer);
|
|
289
|
-
|
|
312
|
+
}
|
|
313
|
+
const rawMinInterval = this.opts.tickWatchMinIntervalMs;
|
|
314
|
+
const minInterval = typeof rawMinInterval === "number" && Number.isFinite(rawMinInterval)
|
|
315
|
+
? Math.max(1, Math.min(30_000, rawMinInterval))
|
|
316
|
+
: 1000;
|
|
317
|
+
const interval = Math.max(this.tickIntervalMs, minInterval);
|
|
290
318
|
this.tickTimer = setInterval(() => {
|
|
291
|
-
if (this.closed)
|
|
319
|
+
if (this.closed) {
|
|
292
320
|
return;
|
|
293
|
-
|
|
321
|
+
}
|
|
322
|
+
if (!this.lastTick) {
|
|
294
323
|
return;
|
|
324
|
+
}
|
|
295
325
|
const gap = Date.now() - this.lastTick;
|
|
296
326
|
if (gap > this.tickIntervalMs * 2) {
|
|
297
327
|
this.ws?.close(4000, "tick timeout");
|
|
@@ -299,21 +329,25 @@ export class GatewayClient {
|
|
|
299
329
|
}, interval);
|
|
300
330
|
}
|
|
301
331
|
validateTlsFingerprint() {
|
|
302
|
-
if (!this.opts.tlsFingerprint || !this.ws)
|
|
332
|
+
if (!this.opts.tlsFingerprint || !this.ws) {
|
|
303
333
|
return null;
|
|
334
|
+
}
|
|
304
335
|
const expected = normalizeFingerprint(this.opts.tlsFingerprint);
|
|
305
|
-
if (!expected)
|
|
336
|
+
if (!expected) {
|
|
306
337
|
return new Error("gateway tls fingerprint missing");
|
|
338
|
+
}
|
|
307
339
|
const socket = this.ws._socket;
|
|
308
340
|
if (!socket || typeof socket.getPeerCertificate !== "function") {
|
|
309
341
|
return new Error("gateway tls fingerprint unavailable");
|
|
310
342
|
}
|
|
311
343
|
const cert = socket.getPeerCertificate();
|
|
312
344
|
const fingerprint = normalizeFingerprint(cert?.fingerprint256 ?? "");
|
|
313
|
-
if (!fingerprint)
|
|
345
|
+
if (!fingerprint) {
|
|
314
346
|
return new Error("gateway tls fingerprint unavailable");
|
|
315
|
-
|
|
347
|
+
}
|
|
348
|
+
if (fingerprint !== expected) {
|
|
316
349
|
return new Error("gateway tls fingerprint mismatch");
|
|
350
|
+
}
|
|
317
351
|
return null;
|
|
318
352
|
}
|
|
319
353
|
async request(method, params, opts) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CONTROL_UI_BOOTSTRAP_CONFIG_PATH = "/
|
|
1
|
+
export const CONTROL_UI_BOOTSTRAP_CONFIG_PATH = "/__poolbot/control-ui-config.json";
|
|
@@ -20,6 +20,6 @@ export const TAILSCALE_MISSING_BIN_NOTE_LINES = [
|
|
|
20
20
|
];
|
|
21
21
|
export const TAILSCALE_DOCS_LINES = [
|
|
22
22
|
"Docs:",
|
|
23
|
-
"https://docs.
|
|
24
|
-
"https://docs.
|
|
23
|
+
"https://docs.poolbot.dev/gateway/tailscale",
|
|
24
|
+
"https://docs.poolbot.dev/web",
|
|
25
25
|
];
|
package/dist/gateway/net.js
CHANGED
|
@@ -23,6 +23,27 @@ export function pickPrimaryLanIPv4() {
|
|
|
23
23
|
}
|
|
24
24
|
return undefined;
|
|
25
25
|
}
|
|
26
|
+
export function normalizeHostHeader(hostHeader) {
|
|
27
|
+
return (hostHeader ?? "").trim().toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
export function resolveHostName(hostHeader) {
|
|
30
|
+
const host = normalizeHostHeader(hostHeader);
|
|
31
|
+
if (!host) {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
if (host.startsWith("[")) {
|
|
35
|
+
const end = host.indexOf("]");
|
|
36
|
+
if (end !== -1) {
|
|
37
|
+
return host.slice(1, end);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Unbracketed IPv6 host (e.g. "::1") has no port and should be returned as-is.
|
|
41
|
+
if (net.isIP(host) === 6) {
|
|
42
|
+
return host;
|
|
43
|
+
}
|
|
44
|
+
const [name] = host.split(":");
|
|
45
|
+
return name ?? "";
|
|
46
|
+
}
|
|
26
47
|
export function isLoopbackAddress(ip) {
|
|
27
48
|
if (!ip) {
|
|
28
49
|
return false;
|
|
@@ -41,6 +62,47 @@ export function isLoopbackAddress(ip) {
|
|
|
41
62
|
}
|
|
42
63
|
return false;
|
|
43
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Returns true if the IP belongs to a private or loopback network range.
|
|
67
|
+
* Private ranges: RFC1918, link-local, ULA IPv6, and CGNAT (100.64/10), plus loopback.
|
|
68
|
+
*/
|
|
69
|
+
export function isPrivateOrLoopbackAddress(ip) {
|
|
70
|
+
if (!ip) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
if (isLoopbackAddress(ip)) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
const normalized = normalizeIPv4MappedAddress(ip.trim().toLowerCase());
|
|
77
|
+
const family = net.isIP(normalized);
|
|
78
|
+
if (!family) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
if (family === 4) {
|
|
82
|
+
const octets = normalized.split(".").map((value) => Number.parseInt(value, 10));
|
|
83
|
+
if (octets.length !== 4 || octets.some((value) => Number.isNaN(value))) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const [o1, o2] = octets;
|
|
87
|
+
// RFC1918 IPv4 private ranges.
|
|
88
|
+
if (o1 === 10 || (o1 === 172 && o2 >= 16 && o2 <= 31) || (o1 === 192 && o2 === 168)) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
// IPv4 link-local and CGNAT (commonly used by Tailnet-like networks).
|
|
92
|
+
if ((o1 === 169 && o2 === 254) || (o1 === 100 && o2 >= 64 && o2 <= 127)) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
// IPv6 unique-local and link-local ranges.
|
|
98
|
+
if (normalized.startsWith("fc") || normalized.startsWith("fd")) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
if (/^fe[89ab]/.test(normalized)) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
44
106
|
function normalizeIPv4MappedAddress(ip) {
|
|
45
107
|
if (ip.startsWith("::ffff:")) {
|
|
46
108
|
return ip.slice("::ffff:".length);
|
|
@@ -87,12 +149,58 @@ function parseRealIp(realIp) {
|
|
|
87
149
|
}
|
|
88
150
|
return normalizeIp(stripOptionalPort(raw));
|
|
89
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Check if an IP address matches a CIDR block.
|
|
154
|
+
* Supports IPv4 CIDR notation (e.g., "10.42.0.0/24").
|
|
155
|
+
*
|
|
156
|
+
* @param ip - The IP address to check (e.g., "10.42.0.59")
|
|
157
|
+
* @param cidr - The CIDR block (e.g., "10.42.0.0/24")
|
|
158
|
+
* @returns True if the IP is within the CIDR block
|
|
159
|
+
*/
|
|
160
|
+
function ipMatchesCIDR(ip, cidr) {
|
|
161
|
+
// Handle exact IP match (no CIDR notation)
|
|
162
|
+
if (!cidr.includes("/")) {
|
|
163
|
+
return ip === cidr;
|
|
164
|
+
}
|
|
165
|
+
const [subnet, prefixLenStr] = cidr.split("/");
|
|
166
|
+
const prefixLen = parseInt(prefixLenStr, 10);
|
|
167
|
+
// Validate prefix length
|
|
168
|
+
if (Number.isNaN(prefixLen) || prefixLen < 0 || prefixLen > 32) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
// Convert IPs to 32-bit integers
|
|
172
|
+
const ipParts = ip.split(".").map((p) => parseInt(p, 10));
|
|
173
|
+
const subnetParts = subnet.split(".").map((p) => parseInt(p, 10));
|
|
174
|
+
// Validate IP format
|
|
175
|
+
if (ipParts.length !== 4 ||
|
|
176
|
+
subnetParts.length !== 4 ||
|
|
177
|
+
ipParts.some((p) => Number.isNaN(p) || p < 0 || p > 255) ||
|
|
178
|
+
subnetParts.some((p) => Number.isNaN(p) || p < 0 || p > 255)) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
const ipInt = (ipParts[0] << 24) | (ipParts[1] << 16) | (ipParts[2] << 8) | ipParts[3];
|
|
182
|
+
const subnetInt = (subnetParts[0] << 24) | (subnetParts[1] << 16) | (subnetParts[2] << 8) | subnetParts[3];
|
|
183
|
+
// Create mask and compare
|
|
184
|
+
const mask = prefixLen === 0 ? 0 : (-1 >>> (32 - prefixLen)) << (32 - prefixLen);
|
|
185
|
+
return (ipInt & mask) === (subnetInt & mask);
|
|
186
|
+
}
|
|
90
187
|
export function isTrustedProxyAddress(ip, trustedProxies) {
|
|
91
188
|
const normalized = normalizeIp(ip);
|
|
92
189
|
if (!normalized || !trustedProxies || trustedProxies.length === 0) {
|
|
93
190
|
return false;
|
|
94
191
|
}
|
|
95
|
-
return trustedProxies.some((proxy) =>
|
|
192
|
+
return trustedProxies.some((proxy) => {
|
|
193
|
+
const candidate = proxy.trim();
|
|
194
|
+
if (!candidate) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
// Handle CIDR notation
|
|
198
|
+
if (candidate.includes("/")) {
|
|
199
|
+
return ipMatchesCIDR(normalized, candidate);
|
|
200
|
+
}
|
|
201
|
+
// Exact IP match
|
|
202
|
+
return normalizeIp(candidate) === normalized;
|
|
203
|
+
});
|
|
96
204
|
}
|
|
97
205
|
export function resolveGatewayClientIp(params) {
|
|
98
206
|
const remote = normalizeIp(params.remoteAddr);
|