2026-01-19 11:32:15 +00:00
import type {
GatewayAuthConfig ,
GatewayBindMode ,
GatewayTailscaleConfig ,
loadConfig ,
} from "../config/config.js" ;
2026-01-14 09:11:21 +00:00
import {
assertGatewayAuthConfigured ,
type ResolvedGatewayAuth ,
resolveGatewayAuth ,
} from "./auth.js" ;
2026-01-22 23:41:28 +00:00
import { normalizeControlUiBasePath } from "./control-ui-shared.js" ;
2026-01-14 09:11:21 +00:00
import { resolveHooksConfig } from "./hooks.js" ;
2026-02-20 18:03:53 +00:00
import {
isLoopbackHost ,
isTrustedProxyAddress ,
isValidIPv4 ,
resolveGatewayBindHost ,
} from "./net.js" ;
2026-02-19 02:35:50 -05:00
import { mergeGatewayTailscaleConfig } from "./startup-auth.js" ;
2026-01-14 09:11:21 +00:00
export type GatewayRuntimeConfig = {
bindHost : string ;
controlUiEnabled : boolean ;
openAiChatCompletionsEnabled : boolean ;
2026-03-06 00:35:50 -05:00
openAiChatCompletionsConfig? : import ( "../config/types.gateway.js" ) . GatewayHttpChatCompletionsConfig ;
2026-01-19 10:44:48 +01:00
openResponsesEnabled : boolean ;
2026-01-20 07:35:29 +00:00
openResponsesConfig? : import ( "../config/types.gateway.js" ) . GatewayHttpResponsesConfig ;
2026-02-23 19:47:09 +00:00
strictTransportSecurityHeader? : string ;
2026-01-14 09:11:21 +00:00
controlUiBasePath : string ;
2026-02-03 13:56:20 -05:00
controlUiRoot? : string ;
2026-01-14 09:11:21 +00:00
resolvedAuth : ResolvedGatewayAuth ;
authMode : ResolvedGatewayAuth [ "mode" ] ;
tailscaleConfig : GatewayTailscaleConfig ;
tailscaleMode : "off" | "serve" | "funnel" ;
hooksConfig : ReturnType < typeof resolveHooksConfig > ;
canvasHostEnabled : boolean ;
} ;
export async function resolveGatewayRuntimeConfig ( params : {
cfg : ReturnType < typeof loadConfig > ;
port : number ;
2026-01-19 04:50:07 +00:00
bind? : GatewayBindMode ;
2026-01-14 09:11:21 +00:00
host? : string ;
controlUiEnabled? : boolean ;
openAiChatCompletionsEnabled? : boolean ;
2026-01-19 10:44:48 +01:00
openResponsesEnabled? : boolean ;
2026-01-14 09:11:21 +00:00
auth? : GatewayAuthConfig ;
tailscale? : GatewayTailscaleConfig ;
} ) : Promise < GatewayRuntimeConfig > {
const bindMode = params . bind ? ? params . cfg . gateway ? . bind ? ? "loopback" ;
const customBindHost = params . cfg . gateway ? . customBindHost ;
2026-01-14 14:31:43 +00:00
const bindHost = params . host ? ? ( await resolveGatewayBindHost ( bindMode , customBindHost ) ) ;
2026-02-19 14:36:39 +01:00
if ( bindMode === "loopback" && ! isLoopbackHost ( bindHost ) ) {
throw new Error (
` gateway bind=loopback resolved to non-loopback host ${ bindHost } ; refusing fallback to a network bind ` ,
) ;
}
if ( bindMode === "custom" ) {
const configuredCustomBindHost = customBindHost ? . trim ( ) ;
if ( ! configuredCustomBindHost ) {
throw new Error ( "gateway.bind=custom requires gateway.customBindHost" ) ;
}
if ( ! isValidIPv4 ( configuredCustomBindHost ) ) {
throw new Error (
` gateway.bind=custom requires a valid IPv4 customBindHost (got ${ configuredCustomBindHost } ) ` ,
) ;
}
if ( bindHost !== configuredCustomBindHost ) {
throw new Error (
` gateway bind=custom requested ${ configuredCustomBindHost } but resolved ${ bindHost } ; refusing fallback ` ,
) ;
}
}
2026-01-14 09:11:21 +00:00
const controlUiEnabled =
params . controlUiEnabled ? ? params . cfg . gateway ? . controlUi ? . enabled ? ? true ;
2026-03-06 00:35:50 -05:00
const openAiChatCompletionsConfig = params . cfg . gateway ? . http ? . endpoints ? . chatCompletions ;
2026-01-14 09:11:21 +00:00
const openAiChatCompletionsEnabled =
2026-03-06 00:35:50 -05:00
params . openAiChatCompletionsEnabled ? ? openAiChatCompletionsConfig ? . enabled ? ? false ;
2026-01-20 07:35:29 +00:00
const openResponsesConfig = params . cfg . gateway ? . http ? . endpoints ? . responses ;
const openResponsesEnabled = params . openResponsesEnabled ? ? openResponsesConfig ? . enabled ? ? false ;
2026-02-23 19:47:09 +00:00
const strictTransportSecurityConfig =
params . cfg . gateway ? . http ? . securityHeaders ? . strictTransportSecurity ;
const strictTransportSecurityHeader =
strictTransportSecurityConfig === false
? undefined
: typeof strictTransportSecurityConfig === "string" &&
strictTransportSecurityConfig . trim ( ) . length > 0
? strictTransportSecurityConfig . trim ( )
: undefined ;
2026-01-14 14:31:43 +00:00
const controlUiBasePath = normalizeControlUiBasePath ( params . cfg . gateway ? . controlUi ? . basePath ) ;
2026-02-03 13:56:20 -05:00
const controlUiRootRaw = params . cfg . gateway ? . controlUi ? . root ;
const controlUiRoot =
typeof controlUiRootRaw === "string" && controlUiRootRaw . trim ( ) . length > 0
? controlUiRootRaw . trim ( )
: undefined ;
2026-01-14 09:11:21 +00:00
const tailscaleBase = params . cfg . gateway ? . tailscale ? ? { } ;
const tailscaleOverrides = params . tailscale ? ? { } ;
2026-02-19 02:35:50 -05:00
const tailscaleConfig = mergeGatewayTailscaleConfig ( tailscaleBase , tailscaleOverrides ) ;
2026-01-14 09:11:21 +00:00
const tailscaleMode = tailscaleConfig . mode ? ? "off" ;
const resolvedAuth = resolveGatewayAuth ( {
2026-02-19 02:35:50 -05:00
authConfig : params.cfg.gateway?.auth ,
authOverride : params.auth ,
2026-01-14 09:11:21 +00:00
env : process.env ,
tailscaleMode ,
} ) ;
const authMode : ResolvedGatewayAuth [ "mode" ] = resolvedAuth . mode ;
2026-01-26 12:56:33 +00:00
const hasToken = typeof resolvedAuth . token === "string" && resolvedAuth . token . trim ( ) . length > 0 ;
const hasPassword =
typeof resolvedAuth . password === "string" && resolvedAuth . password . trim ( ) . length > 0 ;
const hasSharedSecret =
( authMode === "token" && hasToken ) || ( authMode === "password" && hasPassword ) ;
2026-01-14 09:11:21 +00:00
const hooksConfig = resolveHooksConfig ( params . cfg ) ;
const canvasHostEnabled =
2026-01-30 03:15:10 +01:00
process . env . OPENCLAW_SKIP_CANVAS_HOST !== "1" && params . cfg . canvasHost ? . enabled !== false ;
2026-01-14 09:11:21 +00:00
2026-02-14 06:32:17 -05:00
const trustedProxies = params . cfg . gateway ? . trustedProxies ? ? [ ] ;
2026-02-24 01:52:15 +00:00
const controlUiAllowedOrigins = ( params . cfg . gateway ? . controlUi ? . allowedOrigins ? ? [ ] )
. map ( ( value ) = > value . trim ( ) )
. filter ( Boolean ) ;
const dangerouslyAllowHostHeaderOriginFallback =
params . cfg . gateway ? . controlUi ? . dangerouslyAllowHostHeaderOriginFallback === true ;
2026-02-14 06:32:17 -05:00
2026-03-07 22:44:11 +00:00
assertGatewayAuthConfigured ( resolvedAuth , params . cfg . gateway ? . auth ) ;
2026-01-14 09:11:21 +00:00
if ( tailscaleMode === "funnel" && authMode !== "password" ) {
throw new Error (
2026-01-30 03:15:10 +01:00
"tailscale funnel requires gateway auth mode=password (set gateway.auth.password or OPENCLAW_GATEWAY_PASSWORD)" ,
2026-01-14 09:11:21 +00:00
) ;
}
if ( tailscaleMode !== "off" && ! isLoopbackHost ( bindHost ) ) {
2026-01-14 14:31:43 +00:00
throw new Error ( "tailscale serve/funnel requires gateway bind=loopback (127.0.0.1)" ) ;
2026-01-14 09:11:21 +00:00
}
2026-02-14 06:32:17 -05:00
if ( ! isLoopbackHost ( bindHost ) && ! hasSharedSecret && authMode !== "trusted-proxy" ) {
2026-01-14 09:11:21 +00:00
throw new Error (
2026-01-30 03:15:10 +01:00
` refusing to bind gateway to ${ bindHost } : ${ params . port } without auth (set gateway.auth.token/password, or set OPENCLAW_GATEWAY_TOKEN/OPENCLAW_GATEWAY_PASSWORD) ` ,
2026-01-14 09:11:21 +00:00
) ;
}
2026-02-24 01:52:15 +00:00
if (
controlUiEnabled &&
! isLoopbackHost ( bindHost ) &&
controlUiAllowedOrigins . length === 0 &&
! dangerouslyAllowHostHeaderOriginFallback
) {
throw new Error (
"non-loopback Control UI requires gateway.controlUi.allowedOrigins (set explicit origins), or set gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true to use Host-header origin fallback mode" ,
) ;
}
2026-01-14 09:11:21 +00:00
2026-02-14 06:32:17 -05:00
if ( authMode === "trusted-proxy" ) {
if ( trustedProxies . length === 0 ) {
throw new Error (
"gateway auth mode=trusted-proxy requires gateway.trustedProxies to be configured with at least one proxy IP" ,
) ;
}
2026-02-20 18:03:53 +00:00
if ( isLoopbackHost ( bindHost ) ) {
const hasLoopbackTrustedProxy =
isTrustedProxyAddress ( "127.0.0.1" , trustedProxies ) ||
isTrustedProxyAddress ( "::1" , trustedProxies ) ;
if ( ! hasLoopbackTrustedProxy ) {
throw new Error (
"gateway auth mode=trusted-proxy with bind=loopback requires gateway.trustedProxies to include 127.0.0.1, ::1, or a loopback CIDR" ,
) ;
}
}
2026-02-14 06:32:17 -05:00
}
2026-01-14 09:11:21 +00:00
return {
bindHost ,
controlUiEnabled ,
openAiChatCompletionsEnabled ,
2026-03-06 00:35:50 -05:00
openAiChatCompletionsConfig : openAiChatCompletionsConfig
? { . . . openAiChatCompletionsConfig , enabled : openAiChatCompletionsEnabled }
: undefined ,
2026-01-19 10:44:48 +01:00
openResponsesEnabled ,
2026-01-20 07:35:29 +00:00
openResponsesConfig : openResponsesConfig
? { . . . openResponsesConfig , enabled : openResponsesEnabled }
: undefined ,
2026-02-23 19:47:09 +00:00
strictTransportSecurityHeader ,
2026-01-14 09:11:21 +00:00
controlUiBasePath ,
2026-02-03 13:56:20 -05:00
controlUiRoot ,
2026-01-14 09:11:21 +00:00
resolvedAuth ,
authMode ,
tailscaleConfig ,
tailscaleMode ,
hooksConfig ,
canvasHostEnabled ,
} ;
}