Files
claw3d/src/lib/studio/settings-store.ts
T
iamlukethedev e71b62444c fix: resolve gateway URL at runtime via /api/studio fallback (#66)
Fixes #57 — NEXT_PUBLIC_GATEWAY_URL is a build-time variable that gets
baked into the client bundle. Changing it in .env and restarting has no
effect without a rebuild.

- normalizeLocalGatewayDefaults now accepts the sanitized public form
  ({url, tokenConfigured}) from /api/studio
- When no saved gateway URL exists, prefer runtime localGatewayDefaults
  (from openclaw.json or CLAW3D_GATEWAY_URL env var) over the
  potentially stale build-time NEXT_PUBLIC_GATEWAY_URL
- loadLocalGatewayDefaults falls back to CLAW3D_GATEWAY_URL/TOKEN env
  vars when openclaw.json is absent
- Added runtime env vars documentation to .env.example and README

Co-authored-by: robotica4us-collab <neo@openclaw.ai>
Made-with: Cursor
2026-03-27 14:45:02 -05:00

97 lines
3.5 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import { resolveStateDir } from "@/lib/clawdbot/paths";
import {
defaultStudioSettings,
mergeStudioSettings,
normalizeStudioSettings,
type StudioSettings,
type StudioSettingsPatch,
} from "@/lib/studio/settings";
// Studio settings are intentionally stored as a local JSON file for a single-user workflow.
// That includes gateway connection details, so treat the state directory as plaintext secret
// storage and document any changes to this threat model in README.md and SECURITY.md.
const SETTINGS_DIRNAME = "claw3d";
const SETTINGS_FILENAME = "settings.json";
const OPENCLAW_CONFIG_FILENAME = "openclaw.json";
export const resolveStudioSettingsPath = () =>
path.join(resolveStateDir(), SETTINGS_DIRNAME, SETTINGS_FILENAME);
const isRecord = (value: unknown): value is Record<string, unknown> =>
Boolean(value && typeof value === "object");
const readOpenclawGatewayDefaults = (): { url: string; token: string } | null => {
try {
const configPath = path.join(resolveStateDir(), OPENCLAW_CONFIG_FILENAME);
if (!fs.existsSync(configPath)) return null;
const raw = fs.readFileSync(configPath, "utf8");
const parsed = JSON.parse(raw) as unknown;
if (!isRecord(parsed)) return null;
const gateway = isRecord(parsed.gateway) ? parsed.gateway : null;
if (!gateway) return null;
const auth = isRecord(gateway.auth) ? gateway.auth : null;
const token = typeof auth?.token === "string" ? auth.token.trim() : "";
const port = typeof gateway.port === "number" && Number.isFinite(gateway.port) ? gateway.port : null;
if (!token) return null;
const url = port ? `ws://localhost:${port}` : "";
if (!url) return null;
return { url, token };
} catch {
return null;
}
};
export const loadLocalGatewayDefaults = (): { url: string; token: string } | null => {
const fromFile = readOpenclawGatewayDefaults();
if (fromFile) return fromFile;
// Fall back to env vars so operators can configure the gateway URL at
// runtime without openclaw.json and without a rebuild.
const envUrl = process.env.CLAW3D_GATEWAY_URL?.trim();
const envToken = process.env.CLAW3D_GATEWAY_TOKEN?.trim();
if (envUrl) return { url: envUrl, token: envToken ?? "" };
return null;
};
export const loadStudioSettings = (): StudioSettings => {
const settingsPath = resolveStudioSettingsPath();
if (!fs.existsSync(settingsPath)) {
const defaults = defaultStudioSettings();
const gateway = loadLocalGatewayDefaults();
return gateway ? { ...defaults, gateway } : defaults;
}
const raw = fs.readFileSync(settingsPath, "utf8");
const parsed = JSON.parse(raw) as unknown;
const settings = normalizeStudioSettings(parsed);
if (!settings.gateway?.token) {
const gateway = loadLocalGatewayDefaults();
if (gateway) {
return {
...settings,
gateway: settings.gateway?.url?.trim()
? { url: settings.gateway.url.trim(), token: gateway.token }
: gateway,
};
}
}
return settings;
};
export const saveStudioSettings = (next: StudioSettings) => {
const settingsPath = resolveStudioSettingsPath();
const dir = path.dirname(settingsPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(settingsPath, JSON.stringify(next, null, 2), "utf8");
};
export const applyStudioSettingsPatch = (patch: StudioSettingsPatch): StudioSettings => {
const current = loadStudioSettings();
const next = mergeStudioSettings(current, patch);
saveStudioSettings(next);
return next;
};