mission-control/src/lib/gateway-runtime.ts

111 lines
4.0 KiB
TypeScript

import fs from 'node:fs'
import { config } from '@/lib/config'
import { logger } from '@/lib/logger'
interface OpenClawGatewayConfig {
gateway?: {
auth?: {
mode?: 'token' | 'password'
token?: string
password?: string
}
port?: number
controlUi?: {
allowedOrigins?: string[]
}
}
}
function readOpenClawConfig(): OpenClawGatewayConfig | null {
const configPath = config.openclawConfigPath
if (!configPath || !fs.existsSync(configPath)) return null
try {
const raw = fs.readFileSync(configPath, 'utf8')
return JSON.parse(raw) as OpenClawGatewayConfig
} catch {
return null
}
}
export function registerMcAsDashboard(mcUrl: string): { registered: boolean; alreadySet: boolean } {
const configPath = config.openclawConfigPath
if (!configPath || !fs.existsSync(configPath)) {
return { registered: false, alreadySet: false }
}
try {
const raw = fs.readFileSync(configPath, 'utf8')
const parsed = JSON.parse(raw) as Record<string, any>
// Ensure nested structure
if (!parsed.gateway) parsed.gateway = {}
if (!parsed.gateway.controlUi) parsed.gateway.controlUi = {}
const origin = new URL(mcUrl).origin
const origins: string[] = parsed.gateway.controlUi.allowedOrigins || []
const alreadyInOrigins = origins.includes(origin)
const deviceAuthAlreadyDisabled = parsed.gateway.controlUi.dangerouslyDisableDeviceAuth === true
if (alreadyInOrigins && deviceAuthAlreadyDisabled) {
return { registered: false, alreadySet: true }
}
// Add MC origin to allowedOrigins and disable device auth
// (MC authenticates via gateway token — device pairing is unnecessary)
if (!alreadyInOrigins) {
origins.push(origin)
parsed.gateway.controlUi.allowedOrigins = origins
}
parsed.gateway.controlUi.dangerouslyDisableDeviceAuth = true
fs.writeFileSync(configPath, JSON.stringify(parsed, null, 2) + '\n')
logger.info({ origin }, 'Registered MC origin in gateway config')
return { registered: true, alreadySet: false }
} catch (err: any) {
// Read-only filesystem (e.g. Docker read_only: true, or intentional mount) —
// treat as a non-fatal skip rather than an error.
if (err?.code === 'EROFS' || err?.code === 'EACCES' || err?.code === 'EPERM') {
logger.warn(
{ err, configPath },
'Gateway config is read-only — skipping MC origin registration. ' +
'To enable auto-registration, mount openclaw.json with write access or ' +
'add the MC origin to gateway.controlUi.allowedOrigins manually.',
)
return { registered: false, alreadySet: false }
}
logger.error({ err }, 'Failed to register MC in gateway config')
return { registered: false, alreadySet: false }
}
}
/**
* Returns the gateway auth credential (token or password) for Bearer/WS auth.
* Env overrides: OPENCLAW_GATEWAY_TOKEN, GATEWAY_TOKEN, OPENCLAW_GATEWAY_PASSWORD, GATEWAY_PASSWORD.
* From config: uses gateway.auth.token when mode is "token", gateway.auth.password when mode is "password".
*/
export function getDetectedGatewayToken(): string {
const envToken = (process.env.OPENCLAW_GATEWAY_TOKEN || process.env.GATEWAY_TOKEN || '').trim()
if (envToken) return envToken
const envPassword = (process.env.OPENCLAW_GATEWAY_PASSWORD || process.env.GATEWAY_PASSWORD || '').trim()
if (envPassword) return envPassword
const parsed = readOpenClawConfig()
const auth = parsed?.gateway?.auth
const mode = auth?.mode === 'password' ? 'password' : 'token'
const credential =
mode === 'password'
? String(auth?.password ?? '').trim()
: String(auth?.token ?? '').trim()
return credential
}
export function getDetectedGatewayPort(): number | null {
const envPort = Number(process.env.OPENCLAW_GATEWAY_PORT || process.env.GATEWAY_PORT || '')
if (Number.isFinite(envPort) && envPort > 0) return envPort
const parsed = readOpenClawConfig()
const cfgPort = Number(parsed?.gateway?.port || 0)
return Number.isFinite(cfgPort) && cfgPort > 0 ? cfgPort : null
}