mirror of
https://github.com/lotsoftick/openclaw_client.git
synced 2026-08-14 00:48:07 +00:00
cors fix
This commit is contained in:
@@ -110,22 +110,33 @@ openclaw_client restart # production (installed via `npm start`)
|
||||
npm run dev # development
|
||||
```
|
||||
|
||||
`api/.env`, the Vite dev/preview server, the built `serve.mjs`, `ALLOWED_DOMAIN`, `API_PUBLIC_URL`, and the bundled client's `VITE_API_BASE_URL` are all derived from this file, so both dev and production stay consistent.
|
||||
`api/.env`, the Vite dev/preview server, and the built `serve.mjs` all read this file so both dev and production stay consistent. The API derives `API_PUBLIC_URL` from each request's `Host` header (so workspace URLs match the hostname the user is actually browsing) and ships a permissive CORS default; see the **CORS / remote access** note below for how to lock it down.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Generated automatically on first run in `api/.env` (see `api/.env.example` for reference):
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ---------------- | ------------------------ | ----------------------------------------------------- |
|
||||
| `NODE_ENV` | `development` | Environment mode |
|
||||
| `JWT_SECRET` | _(random)_ | Secret for JWT signing |
|
||||
| `DB_PATH` | `./data/openclaw.sqlite` | Path to SQLite database file |
|
||||
| `PORT` | _(API_PORT)_ | API listen port (driven by `~/.openclaw_client/.env`) |
|
||||
| `ALLOWED_DOMAIN` | _(CLIENT origin)_ | CORS allowed origin(s), comma-separated |
|
||||
| `API_PUBLIC_URL` | _(API origin)_ | Public base URL used for generated workspace URLs |
|
||||
| Variable | Default | Description |
|
||||
| ---------------------- | -------------------------- | ----------------------------------------------------------------- |
|
||||
| `NODE_ENV` | `development` | Environment mode |
|
||||
| `JWT_SECRET` | _(random)_ | Secret for JWT signing |
|
||||
| `DB_PATH` | `./data/openclaw.sqlite` | Path to SQLite database file |
|
||||
| `PORT` | _(API_PORT)_ | API listen port (driven by `~/.openclaw_client/.env`) |
|
||||
| `ALLOWED_DOMAIN` | _(unset — allow all)_ | CORS allowlist, comma-separated; only enforced when `OPENCLAW_STRICT_CORS=1` |
|
||||
| `OPENCLAW_STRICT_CORS` | _(off)_ | Set to `1` to reject any origin not in `ALLOWED_DOMAIN` |
|
||||
| `API_PUBLIC_URL` | _(derived from request)_ | Optional fallback origin for workspace URLs when no `Host` header |
|
||||
|
||||
The client reads `VITE_API_BASE_URL` at build time; it is set automatically to match `API_PORT`. Override by setting it in `client/.env` only if you deploy behind a custom host.
|
||||
> **CORS / remote access.** OpenClaw Client is a single-user local app. By
|
||||
> default the API allows every origin and the client derives the API URL
|
||||
> from `window.location`, so the same install works on `localhost`, on a
|
||||
> LAN IP, and over Tailscale without any extra configuration. To lock it
|
||||
> down to a fixed allowlist, set `ALLOWED_DOMAIN` and `OPENCLAW_STRICT_CORS=1`
|
||||
> in `api/.env` (or `~/.openclaw_client/api/.env` for production installs).
|
||||
|
||||
The client picks its API origin at runtime: `__OPENCLAW_CONFIG__.apiBaseUrl`
|
||||
(injected by the production static server from the request host) ▸
|
||||
`VITE_API_BASE_URL` (build-time override) ▸ derived from
|
||||
`window.location` + `VITE_API_PORT`.
|
||||
|
||||
To regenerate secrets, delete `api/.env` and run `npm run dev` or `npm run setup` again.
|
||||
|
||||
|
||||
+16
-4
@@ -1,8 +1,20 @@
|
||||
NODE_ENV=development
|
||||
JWT_SECRET=your-secret-here
|
||||
DB_PATH=./data/openclaw.sqlite
|
||||
# The following three are normally driven by ~/.openclaw_client/.env
|
||||
# (API_PORT / CLIENT_PORT). Listed here for reference.
|
||||
# Normally driven by ~/.openclaw_client/.env (API_PORT / CLIENT_PORT). Listed
|
||||
# here for reference.
|
||||
PORT=18802
|
||||
ALLOWED_DOMAIN=http://localhost:18800
|
||||
API_PUBLIC_URL=http://localhost:18802
|
||||
|
||||
# --- CORS ---
|
||||
# OpenClaw Client is a single-user local app that you typically reach from
|
||||
# multiple devices on your LAN / Tailscale, so the default policy is
|
||||
# "allow every origin". Uncomment the two lines below to lock it down to
|
||||
# a strict allowlist.
|
||||
# ALLOWED_DOMAIN=http://localhost:18800,http://my-host.tail-net.ts.net:18800
|
||||
# OPENCLAW_STRICT_CORS=1
|
||||
|
||||
# --- API public URL ---
|
||||
# Optional fallback used when the API can't read `Host` from the
|
||||
# request (server-to-server callers). Normal browser traffic always
|
||||
# uses the request host so workspace URLs match wherever the client is.
|
||||
# API_PUBLIC_URL=http://localhost:18802
|
||||
|
||||
Vendored
+2
-1
@@ -5,7 +5,8 @@ declare global {
|
||||
interface ProcessEnv {
|
||||
NODE_ENV: 'test' | 'development' | 'production';
|
||||
JWT_SECRET: string;
|
||||
ALLOWED_DOMAIN: string;
|
||||
ALLOWED_DOMAIN?: string;
|
||||
OPENCLAW_STRICT_CORS?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+48
-11
@@ -1,21 +1,58 @@
|
||||
import cors from 'cors';
|
||||
|
||||
/**
|
||||
* CORS policy for the OpenClaw Client API.
|
||||
*
|
||||
* This is a single-user local desktop app (not a public multi-tenant
|
||||
* service). Auth is JWT-bearer in the `Authorization` header — no
|
||||
* cookies, so CSRF surface is essentially zero — and the whole point of
|
||||
* exposing it on `0.0.0.0` is so the user can reach it from their other
|
||||
* devices on the same LAN or over Tailscale. A strict origin allowlist
|
||||
* is therefore the wrong default: it breaks every legitimate access
|
||||
* pattern beyond "browser open on the install host" while protecting
|
||||
* nothing of value.
|
||||
*
|
||||
* Policy:
|
||||
* - In `development`, allow every origin (was already the case).
|
||||
* - In production, allow every origin **by default** so installs
|
||||
* accessed via Tailscale/LAN/IP just work.
|
||||
* - If `ALLOWED_DOMAIN` is set (comma-separated), it acts as an
|
||||
* allowlist *augmenting* the permissive default — and if
|
||||
* `OPENCLAW_STRICT_CORS=1` is also set, the allowlist becomes the
|
||||
* final word and everything else is rejected.
|
||||
*
|
||||
* Operators who want the old strict behaviour:
|
||||
* `ALLOWED_DOMAIN=https://openclaw.example.com OPENCLAW_STRICT_CORS=1`
|
||||
*/
|
||||
const STRICT = ['1', 'true', 'yes'].includes(
|
||||
String(process.env.OPENCLAW_STRICT_CORS || '').toLowerCase()
|
||||
);
|
||||
|
||||
const ALLOWLIST = (process.env.ALLOWED_DOMAIN || '')
|
||||
.split(',')
|
||||
.map((d) => d.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
export default cors({
|
||||
exposedHeaders: 'access-token',
|
||||
origin: (origin, next) => {
|
||||
if (!origin || process.env.NODE_ENV === 'development') return next(null, true);
|
||||
// Same-origin / non-browser callers (curl, server-to-server) don't
|
||||
// send Origin and shouldn't be blocked.
|
||||
if (!origin) return next(null, true);
|
||||
|
||||
const allowed = process.env.ALLOWED_DOMAIN
|
||||
? process.env.ALLOWED_DOMAIN.split(',')
|
||||
.map((d) => d.trim())
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
if (process.env.NODE_ENV === 'development') return next(null, true);
|
||||
|
||||
if (allowed.includes(origin)) return next(null, true);
|
||||
if (ALLOWLIST.includes(origin)) return next(null, true);
|
||||
|
||||
return next(
|
||||
new Error('The CORS policy for this site does not allow access from the specified Origin.'),
|
||||
false
|
||||
);
|
||||
if (STRICT) {
|
||||
return next(
|
||||
new Error(
|
||||
`CORS policy: origin ${origin} is not in ALLOWED_DOMAIN and OPENCLAW_STRICT_CORS is on.`
|
||||
),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
return next(null, true);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -14,7 +14,37 @@ import {
|
||||
} from '../../@types/message';
|
||||
import * as ocService from '../../services/openclaw';
|
||||
|
||||
const API_PUBLIC_URL = process.env.API_PUBLIC_URL || 'http://localhost:18802';
|
||||
/**
|
||||
* Resolve the public origin to use when minting URLs back to the client.
|
||||
*
|
||||
* OpenClaw Client is deployed in two patterns:
|
||||
* 1. Local-only: browser on the install host. `Host` header reads
|
||||
* `localhost:<port>` and the API_PUBLIC_URL env (default
|
||||
* `http://localhost:18802`) was historically hardcoded — fine.
|
||||
* 2. LAN/Tailscale/IP: browser on a different device. `Host` reads
|
||||
* `<remote-host>:<port>`. A hardcoded localhost URL would point
|
||||
* the remote browser at *its own machine*, breaking workspace
|
||||
* file previews and downloads silently.
|
||||
*
|
||||
* So we prefer `req.headers.host` (already validated by Express + the
|
||||
* cors middleware) and only fall back to the env override / default
|
||||
* for non-HTTP callers. `x-forwarded-host` is honoured for users
|
||||
* running behind a reverse proxy.
|
||||
*/
|
||||
const apiPublicUrl = (req: {
|
||||
headers: Record<string, string | string[] | undefined>;
|
||||
protocol?: string;
|
||||
}): string => {
|
||||
const envOverride = process.env.API_PUBLIC_URL;
|
||||
const xfHost = req.headers['x-forwarded-host'];
|
||||
const host = (Array.isArray(xfHost) ? xfHost[0] : xfHost) || req.headers.host;
|
||||
if (host) {
|
||||
const xfProto = req.headers['x-forwarded-proto'];
|
||||
const proto = (Array.isArray(xfProto) ? xfProto[0] : xfProto) || req.protocol || 'http';
|
||||
return `${proto}://${host}`;
|
||||
}
|
||||
return envOverride || 'http://localhost:18802';
|
||||
};
|
||||
|
||||
function stripWrapperTags(text: string): string {
|
||||
return text
|
||||
@@ -105,9 +135,11 @@ const chat: Chat = async (req, res, next) => {
|
||||
const agent = await agentRepo.findOneBy({ _id: conv.agentId });
|
||||
const agentIdForFiles = agent?.openclawAgentId || 'main';
|
||||
|
||||
const publicUrl = apiPublicUrl(req);
|
||||
|
||||
const msgCount = await msgRepo.count({ where: { conversationId: conv._id } });
|
||||
if (msgCount === 0) {
|
||||
ocService.appendBootstrapImageRule(agentIdForFiles, conv.agentId, API_PUBLIC_URL);
|
||||
ocService.appendBootstrapImageRule(agentIdForFiles, conv.agentId, publicUrl);
|
||||
}
|
||||
|
||||
const filePaths = uploadedFiles.map((uf) =>
|
||||
@@ -121,7 +153,7 @@ const chat: Chat = async (req, res, next) => {
|
||||
originalName: f.originalname,
|
||||
mimetype: f.mimetype,
|
||||
size: f.size,
|
||||
url: `${API_PUBLIC_URL}/api/agent/${conv.agentId}/workspace/uploads/${encodeURIComponent(savedName)}`,
|
||||
url: `${publicUrl}/api/agent/${conv.agentId}/workspace/uploads/${encodeURIComponent(savedName)}`,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,44 @@ import { Button, TextField, Card, Typography, Box, CircularProgress, Alert } fro
|
||||
import { useFormik, FormikProvider, Form } from 'formik';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useLoginMutation } from '../../features/auth';
|
||||
import { API_BASE_URL } from '../../shared/api/baseApi';
|
||||
|
||||
/**
|
||||
* Translate an RTK Query error into a message that actually helps the
|
||||
* user. Previously we collapsed every error to "Login failed. Please
|
||||
* check your credentials." which made network/CORS failures look like
|
||||
* bad passwords — sent us on a goose chase the first time the app was
|
||||
* accessed over Tailscale.
|
||||
*/
|
||||
function describeLoginError(error: unknown): string {
|
||||
if (!error || typeof error !== 'object') return 'Login failed. Please try again.';
|
||||
const e = error as { status?: number | string; data?: unknown; error?: string };
|
||||
if (e.status === 401) return 'Login failed. Please check your credentials.';
|
||||
if (e.status === 'FETCH_ERROR') {
|
||||
return (
|
||||
`Could not reach the API at ${API_BASE_URL}. ` +
|
||||
'If you opened this page from another device, make sure the API ' +
|
||||
'is reachable on the same hostname (and that any firewall / ' +
|
||||
'reverse proxy forwards both the client and API ports).'
|
||||
);
|
||||
}
|
||||
if (e.status === 'PARSING_ERROR') {
|
||||
return 'API responded with something that is not JSON. The server may have crashed mid-request — check the API logs.';
|
||||
}
|
||||
if (typeof e.status === 'number' && e.status >= 500) {
|
||||
return `Server error (${e.status}). Check the API logs.`;
|
||||
}
|
||||
if (
|
||||
e.data &&
|
||||
typeof e.data === 'object' &&
|
||||
'message' in e.data &&
|
||||
typeof (e.data as { message: unknown }).message === 'string'
|
||||
) {
|
||||
return (e.data as { message: string }).message;
|
||||
}
|
||||
if (typeof e.error === 'string') return e.error;
|
||||
return 'Login failed. Please try again.';
|
||||
}
|
||||
|
||||
export default function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
@@ -53,7 +91,7 @@ export default function LoginPage() {
|
||||
<Form>
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ marginBottom: 2 }}>
|
||||
Login failed. Please check your credentials.
|
||||
{describeLoginError(error)}
|
||||
</Alert>
|
||||
)}
|
||||
<TextField
|
||||
|
||||
@@ -12,11 +12,33 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
const runtimeApiBase =
|
||||
typeof window !== 'undefined' ? window.__OPENCLAW_CONFIG__?.apiBaseUrl : undefined;
|
||||
/**
|
||||
* Pick the API origin that lets the bundle work on whichever host the
|
||||
* page was loaded from (localhost on the install machine, a Tailscale
|
||||
* MagicDNS name, a LAN IP, etc.).
|
||||
*
|
||||
* Resolution order:
|
||||
* 1. `window.__OPENCLAW_CONFIG__.apiBaseUrl` — the production static
|
||||
* server (`client/serve.mjs`) injects this from `req.headers.host`
|
||||
* so the URL always matches the page's hostname.
|
||||
* 2. `VITE_API_BASE_URL` — explicit build-time override; respected if
|
||||
* the operator wants to pin a fixed URL.
|
||||
* 3. Derived from `window.location` — same protocol + hostname as the
|
||||
* page, with the API port (`__OPENCLAW_CONFIG__.apiPort` ▸
|
||||
* `VITE_API_PORT` ▸ `18802`). This is the fallback dev mode lands
|
||||
* on when accessed over Tailscale or a LAN IP without any custom
|
||||
* env wiring.
|
||||
*/
|
||||
function resolveApiBaseUrl(): string {
|
||||
if (typeof window === 'undefined') return 'http://localhost:18802/api';
|
||||
const cfg = window.__OPENCLAW_CONFIG__;
|
||||
if (cfg?.apiBaseUrl) return cfg.apiBaseUrl;
|
||||
if (import.meta.env.VITE_API_BASE_URL) return import.meta.env.VITE_API_BASE_URL as string;
|
||||
const apiPort = cfg?.apiPort || Number(import.meta.env.VITE_API_PORT) || 18802;
|
||||
return `${window.location.protocol}//${window.location.hostname}:${apiPort}/api`;
|
||||
}
|
||||
|
||||
export const API_BASE_URL =
|
||||
runtimeApiBase || import.meta.env.VITE_API_BASE_URL || 'http://localhost:18802/api';
|
||||
export const API_BASE_URL = resolveApiBaseUrl();
|
||||
|
||||
const rawBaseQuery = fetchBaseQuery({
|
||||
baseUrl: API_BASE_URL,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openclaw-client",
|
||||
"version": "2.4.6",
|
||||
"version": "2.4.7",
|
||||
"description": "Web-based chat interface for OpenClaw AI agents",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
+26
-18
@@ -107,7 +107,9 @@ export function deploy() {
|
||||
|
||||
process.stdout.write('🔨 Building...\n');
|
||||
run(NPM_BIN, ['run', 'build'], API_SRC);
|
||||
// VITE_API_BASE_URL is embedded into the bundle at build time
|
||||
// VITE_API_PORT is embedded into the bundle as a fallback; the
|
||||
// runtime resolves the actual API origin from the page's hostname so
|
||||
// the same build works on localhost, LAN, and Tailscale.
|
||||
try {
|
||||
execFileSync(NPM_BIN, ['run', 'build'], { cwd: CLIENT_SRC, stdio: 'pipe', env: buildEnv });
|
||||
} catch (err) {
|
||||
@@ -141,29 +143,35 @@ export function deploy() {
|
||||
const canonicalDbPath = path.join(dataDir, 'openclaw.sqlite');
|
||||
|
||||
const envDist = path.join(apiDist, '.env');
|
||||
const allowedDomain = `http://localhost:${clientPort}`;
|
||||
const apiPublicUrl = `http://localhost:${apiPort}`;
|
||||
|
||||
// We seed only what the runtime can't figure out on its own.
|
||||
// - DB_PATH and PORT must match where the install actually lives.
|
||||
// - JWT_SECRET must persist across reinstalls or every login token
|
||||
// gets invalidated, so we generate it once.
|
||||
// - ALLOWED_DOMAIN / API_PUBLIC_URL are deliberately omitted: the
|
||||
// API has a permissive CORS default and derives public URLs from
|
||||
// the request host. Users who want strict CORS set
|
||||
// `ALLOWED_DOMAIN=...` and `OPENCLAW_STRICT_CORS=1` themselves.
|
||||
const seedDefaults = {
|
||||
NODE_ENV: 'production',
|
||||
JWT_SECRET: crypto.randomBytes(32).toString('hex'),
|
||||
DB_PATH: canonicalDbPath,
|
||||
PORT: String(apiPort),
|
||||
};
|
||||
const overrides = {
|
||||
DB_PATH: canonicalDbPath,
|
||||
PORT: String(apiPort),
|
||||
};
|
||||
|
||||
if (!existsSync(envDist)) {
|
||||
writeFileSync(
|
||||
envDist,
|
||||
[
|
||||
'NODE_ENV=production',
|
||||
`JWT_SECRET=${crypto.randomBytes(32).toString('hex')}`,
|
||||
`DB_PATH=${canonicalDbPath}`,
|
||||
`PORT=${apiPort}`,
|
||||
`ALLOWED_DOMAIN=${allowedDomain}`,
|
||||
`API_PUBLIC_URL=${apiPublicUrl}`,
|
||||
'',
|
||||
].join('\n')
|
||||
Object.entries(seedDefaults)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.concat('')
|
||||
.join('\n')
|
||||
);
|
||||
} else {
|
||||
const overrides = {
|
||||
DB_PATH: canonicalDbPath,
|
||||
PORT: String(apiPort),
|
||||
ALLOWED_DOMAIN: allowedDomain,
|
||||
API_PUBLIC_URL: apiPublicUrl,
|
||||
};
|
||||
const seen = new Set();
|
||||
const lines = readFileSync(envDist, 'utf-8').split('\n');
|
||||
const updated = lines.map((line) => {
|
||||
|
||||
+25
-6
@@ -64,9 +64,30 @@ export function readPorts() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Derived env vars expected by API and Client code.
|
||||
* Use these when spawning child processes so a single user-level .env is
|
||||
* the source of truth.
|
||||
* Derived env vars for spawning child processes.
|
||||
*
|
||||
* Note we deliberately do NOT export `ALLOWED_DOMAIN`, `API_PUBLIC_URL`,
|
||||
* or `VITE_API_BASE_URL` here, even though the API and client both read
|
||||
* those vars. Pinning them to `http://localhost:${port}` would block
|
||||
* legitimate access from other devices on the user's LAN / Tailscale —
|
||||
* the very deployment pattern this app is designed for. Instead:
|
||||
*
|
||||
* - `ALLOWED_DOMAIN` is unset by default; the API ships a permissive
|
||||
* CORS policy and an `OPENCLAW_STRICT_CORS=1` opt-in for the strict
|
||||
* allowlist behaviour.
|
||||
* - `API_PUBLIC_URL` is derived per-request from the `Host` header
|
||||
* (with `x-forwarded-*` honoured) — see `routes/message/controller`.
|
||||
* - `VITE_API_BASE_URL` is left empty so the bundle isn't built with
|
||||
* a baked-in `http://localhost:...` URL; the client derives the
|
||||
* API origin at runtime from `__OPENCLAW_CONFIG__` or
|
||||
* `window.location` — see `client/src/shared/api/baseApi`.
|
||||
*
|
||||
* Users who want strict mode can still set any of those keys in
|
||||
* `~/.openclaw_client/.env` or `api/.env`; nothing here overwrites them.
|
||||
*
|
||||
* `VITE_API_PORT` is exported so the build can embed a sensible default
|
||||
* for the runtime URL derivation when the user installs across a
|
||||
* non-standard port.
|
||||
*/
|
||||
export function portEnv() {
|
||||
const { apiPort, clientPort } = readPorts();
|
||||
@@ -74,8 +95,6 @@ export function portEnv() {
|
||||
API_PORT: String(apiPort),
|
||||
CLIENT_PORT: String(clientPort),
|
||||
PORT: String(apiPort),
|
||||
ALLOWED_DOMAIN: `http://localhost:${clientPort}`,
|
||||
API_PUBLIC_URL: `http://localhost:${apiPort}`,
|
||||
VITE_API_BASE_URL: `http://localhost:${apiPort}/api`,
|
||||
VITE_API_PORT: String(apiPort),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,14 +35,17 @@ function parseEnvFile(file) {
|
||||
const userEnv = parseEnvFile(USER_ENV);
|
||||
const apiPort = Number(userEnv.API_PORT) || 18802;
|
||||
const clientPort = Number(userEnv.CLIENT_PORT) || 18800;
|
||||
// We deliberately do not export ALLOWED_DOMAIN / API_PUBLIC_URL here:
|
||||
// the API has a permissive CORS default and derives public URLs from
|
||||
// the request host, so the same install works on localhost, LAN, and
|
||||
// Tailscale. Users who want strict CORS set ALLOWED_DOMAIN and
|
||||
// OPENCLAW_STRICT_CORS=1 in ~/.openclaw_client/api/.env themselves.
|
||||
const childEnv = {
|
||||
...process.env,
|
||||
NODE_ENV: 'production',
|
||||
API_PORT: String(apiPort),
|
||||
CLIENT_PORT: String(clientPort),
|
||||
PORT: String(apiPort),
|
||||
ALLOWED_DOMAIN: `http://localhost:${clientPort}`,
|
||||
API_PUBLIC_URL: `http://localhost:${apiPort}`,
|
||||
};
|
||||
|
||||
const children = [];
|
||||
|
||||
+5
-2
@@ -17,6 +17,11 @@ if (fs.existsSync(API_ENV)) {
|
||||
|
||||
const JWT_SECRET = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
// ALLOWED_DOMAIN / API_PUBLIC_URL are deliberately omitted: the API has
|
||||
// a permissive CORS default and derives public URLs from the request
|
||||
// host so the same install works on localhost, LAN, and Tailscale.
|
||||
// Users who want strict CORS can set `ALLOWED_DOMAIN=...` and
|
||||
// `OPENCLAW_STRICT_CORS=1` themselves.
|
||||
fs.writeFileSync(
|
||||
API_ENV,
|
||||
[
|
||||
@@ -24,8 +29,6 @@ fs.writeFileSync(
|
||||
`JWT_SECRET=${JWT_SECRET}`,
|
||||
`DB_PATH=./data/openclaw.sqlite`,
|
||||
`PORT=${apiPort}`,
|
||||
`ALLOWED_DOMAIN=http://localhost:${clientPort}`,
|
||||
`API_PUBLIC_URL=http://localhost:${apiPort}`,
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user