225 lines
6.2 KiB
TypeScript
225 lines
6.2 KiB
TypeScript
import type { ClawdbotConfig } from "../config/config.js";
|
|
import { resolveGatewayPort } from "../config/config.js";
|
|
import { findTailscaleBinary } from "../infra/tailscale.js";
|
|
import type { RuntimeEnv } from "../runtime.js";
|
|
import { note } from "../terminal/note.js";
|
|
import { buildGatewayAuthConfig } from "./configure.gateway-auth.js";
|
|
import { confirm, select, text } from "./configure.shared.js";
|
|
import { guardCancel, randomToken } from "./onboard-helpers.js";
|
|
|
|
type GatewayAuthChoice = "off" | "token" | "password";
|
|
|
|
export async function promptGatewayConfig(
|
|
cfg: ClawdbotConfig,
|
|
runtime: RuntimeEnv,
|
|
): Promise<{
|
|
config: ClawdbotConfig;
|
|
port: number;
|
|
token?: string;
|
|
}> {
|
|
const portRaw = guardCancel(
|
|
await text({
|
|
message: "Gateway port",
|
|
initialValue: String(resolveGatewayPort(cfg)),
|
|
validate: (value) => (Number.isFinite(Number(value)) ? undefined : "Invalid port"),
|
|
}),
|
|
runtime,
|
|
);
|
|
const port = Number.parseInt(String(portRaw), 10);
|
|
|
|
let bind = guardCancel(
|
|
await select({
|
|
message: "Gateway bind mode",
|
|
options: [
|
|
{
|
|
value: "auto",
|
|
label: "Auto (Tailnet → LAN)",
|
|
hint: "Prefer Tailnet IP, fall back to all interfaces if unavailable",
|
|
},
|
|
{
|
|
value: "lan",
|
|
label: "LAN (All interfaces)",
|
|
hint: "Bind to 0.0.0.0 - accessible from anywhere on your network",
|
|
},
|
|
{
|
|
value: "loopback",
|
|
label: "Loopback (Local only)",
|
|
hint: "Bind to 127.0.0.1 - secure, local-only access",
|
|
},
|
|
{
|
|
value: "custom",
|
|
label: "Custom IP",
|
|
hint: "Specify a specific IP address, with 0.0.0.0 fallback if unavailable",
|
|
},
|
|
],
|
|
}),
|
|
runtime,
|
|
) as "auto" | "lan" | "loopback" | "custom";
|
|
|
|
let customBindHost: string | undefined;
|
|
if (bind === "custom") {
|
|
const input = guardCancel(
|
|
await text({
|
|
message: "Custom IP address",
|
|
placeholder: "192.168.1.100",
|
|
validate: (value) => {
|
|
if (!value) return "IP address is required for custom bind mode";
|
|
const trimmed = value.trim();
|
|
const parts = trimmed.split(".");
|
|
if (parts.length !== 4) return "Invalid IPv4 address (e.g., 192.168.1.100)";
|
|
if (
|
|
parts.every((part) => {
|
|
const n = parseInt(part, 10);
|
|
return !Number.isNaN(n) && n >= 0 && n <= 255 && part === String(n);
|
|
})
|
|
)
|
|
return undefined;
|
|
return "Invalid IPv4 address (each octet must be 0-255)";
|
|
},
|
|
}),
|
|
runtime,
|
|
);
|
|
customBindHost = typeof input === "string" ? input : undefined;
|
|
}
|
|
|
|
let authMode = guardCancel(
|
|
await select({
|
|
message: "Gateway auth",
|
|
options: [
|
|
{
|
|
value: "off",
|
|
label: "Off (loopback only)",
|
|
hint: "Not recommended unless you fully trust local processes",
|
|
},
|
|
{ value: "token", label: "Token", hint: "Recommended default" },
|
|
{ value: "password", label: "Password" },
|
|
],
|
|
initialValue: "token",
|
|
}),
|
|
runtime,
|
|
) as GatewayAuthChoice;
|
|
|
|
const tailscaleMode = guardCancel(
|
|
await select({
|
|
message: "Tailscale exposure",
|
|
options: [
|
|
{ value: "off", label: "Off", hint: "No Tailscale exposure" },
|
|
{
|
|
value: "serve",
|
|
label: "Serve",
|
|
hint: "Private HTTPS for your tailnet (devices on Tailscale)",
|
|
},
|
|
{
|
|
value: "funnel",
|
|
label: "Funnel",
|
|
hint: "Public HTTPS via Tailscale Funnel (internet)",
|
|
},
|
|
],
|
|
}),
|
|
runtime,
|
|
) as "off" | "serve" | "funnel";
|
|
|
|
// Detect Tailscale binary before proceeding with serve/funnel setup.
|
|
if (tailscaleMode !== "off") {
|
|
const tailscaleBin = await findTailscaleBinary();
|
|
if (!tailscaleBin) {
|
|
note(
|
|
[
|
|
"Tailscale binary not found in PATH or /Applications.",
|
|
"Ensure Tailscale is installed from:",
|
|
" https://tailscale.com/download/mac",
|
|
"",
|
|
"You can continue setup, but serve/funnel will fail at runtime.",
|
|
].join("\n"),
|
|
"Tailscale Warning",
|
|
);
|
|
}
|
|
}
|
|
|
|
let tailscaleResetOnExit = false;
|
|
if (tailscaleMode !== "off") {
|
|
note(
|
|
["Docs:", "https://docs.clawd.bot/gateway/tailscale", "https://docs.clawd.bot/web"].join(
|
|
"\n",
|
|
),
|
|
"Tailscale",
|
|
);
|
|
tailscaleResetOnExit = Boolean(
|
|
guardCancel(
|
|
await confirm({
|
|
message: "Reset Tailscale serve/funnel on exit?",
|
|
initialValue: false,
|
|
}),
|
|
runtime,
|
|
),
|
|
);
|
|
}
|
|
|
|
if (tailscaleMode !== "off" && bind !== "loopback") {
|
|
note("Tailscale requires bind=loopback. Adjusting bind to loopback.", "Note");
|
|
bind = "loopback";
|
|
}
|
|
|
|
if (authMode === "off" && bind !== "loopback") {
|
|
note("Non-loopback bind requires auth. Switching to token auth.", "Note");
|
|
authMode = "token";
|
|
}
|
|
|
|
if (tailscaleMode === "funnel" && authMode !== "password") {
|
|
note("Tailscale funnel requires password auth.", "Note");
|
|
authMode = "password";
|
|
}
|
|
|
|
let gatewayToken: string | undefined;
|
|
let gatewayPassword: string | undefined;
|
|
let next = cfg;
|
|
|
|
if (authMode === "token") {
|
|
const tokenInput = guardCancel(
|
|
await text({
|
|
message: "Gateway token (blank to generate)",
|
|
initialValue: randomToken(),
|
|
}),
|
|
runtime,
|
|
);
|
|
gatewayToken = String(tokenInput).trim() || randomToken();
|
|
}
|
|
|
|
if (authMode === "password") {
|
|
const password = guardCancel(
|
|
await text({
|
|
message: "Gateway password",
|
|
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
}),
|
|
runtime,
|
|
);
|
|
gatewayPassword = String(password).trim();
|
|
}
|
|
|
|
const authConfig = buildGatewayAuthConfig({
|
|
existing: next.gateway?.auth,
|
|
mode: authMode,
|
|
token: gatewayToken,
|
|
password: gatewayPassword,
|
|
});
|
|
|
|
next = {
|
|
...next,
|
|
gateway: {
|
|
...next.gateway,
|
|
mode: "local",
|
|
port,
|
|
bind,
|
|
auth: authConfig,
|
|
...(customBindHost && { customBindHost }),
|
|
tailscale: {
|
|
...next.gateway?.tailscale,
|
|
mode: tailscaleMode,
|
|
resetOnExit: tailscaleResetOnExit,
|
|
},
|
|
},
|
|
};
|
|
|
|
return { config: next, port, token: gatewayToken };
|
|
}
|