From e6d505ee8b905b04f9eb412493fda281e41cce5b Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Tue, 6 Oct 2026 15:00:38 +0800 Subject: [PATCH 1/4] feat(socket-auth): generate the socket auth key on install flb install-fleetbase now writes SOCKETCLUSTER_AUTH_KEY (crypto.randomBytes, 64 hex chars) and SOCKETCLUSTER_AUTH_MODE=enforce to the project-root .env, where docker-compose.yml reads them for the application, queue, scheduler and socket containers. An existing key (>= 32 chars) and mode are preserved on re-run. SOCKETCLUSTER_OPTIONS origins are still written to docker-compose.override.yml. --- README.md | 7 ++++++ index.js | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/README.md b/README.md index d52a3df..a15ab0a 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,13 @@ flb install-fleetbase The installer creates an empty `api/.env` (bind-mounted by `docker-compose.yml`) when one does not exist. +It also writes the realtime socket authentication settings to the project-root `.env` (next to `docker-compose.yml`), which Compose passes to the application, queue, scheduler and socket containers: + +- `SOCKETCLUSTER_AUTH_KEY`: shared secret between the API and the socket server (64 random hex characters). An existing key of 32+ characters is kept on re-runs, since changing it invalidates socket tokens already issued. +- `SOCKETCLUSTER_AUTH_MODE`: `off`, `log` or `enforce` (default `enforce`; an existing value is kept). + +The socket's allowed origins (`SOCKETCLUSTER_OPTIONS`) are still written to `docker-compose.override.yml`. Setting the auth key only in `api/.env` has no effect. + **Example:** ```bash flb install-fleetbase --host 0.0.0.0 --environment production --directory /opt/fleetbase diff --git a/index.js b/index.js index 4776753..698d8db 100755 --- a/index.js +++ b/index.js @@ -1142,6 +1142,68 @@ async function ensureApiEnvFile(directory) { console.log('✔ api/.env created'); } +/** + * Write the realtime socket auth settings to the project-root .env file. + * docker-compose.yml reads SOCKETCLUSTER_AUTH_KEY and SOCKETCLUSTER_AUTH_MODE from this + * file (Compose loads it automatically) and hands the same key to the application, queue, + * scheduler and socket containers; setting the key in api/.env has no effect. + * An existing key of at least 32 characters is kept across re-runs, because changing it + * would invalidate every socket token already handed out. An existing mode is kept too. + * Other lines in the file are left untouched. + * @param {string} directory resolved installation directory + * @returns {Promise<{ envPath: string, mode: string, generated: boolean }>} + */ +async function ensureSocketAuthEnv(directory) { + const crypto = require('crypto'); + const envPath = path.join(directory, '.env'); + + let lines = []; + if (await fs.pathExists(envPath)) { + const stat = await fs.stat(envPath); + if (stat.isDirectory()) { + console.error(`\n✖ ${envPath} is a directory; remove it and re-run the installer.`); + process.exit(1); + } + lines = (await fs.readFile(envPath, 'utf8')).split(/\r?\n/); + if (lines.length && lines[lines.length - 1] === '') lines.pop(); + } + + // Last KEY=value in the file wins (same as Compose), with surrounding quotes stripped. + const readValue = (key) => { + const pattern = new RegExp(`^\\s*${key}=(.*)$`); + let value = ''; + for (const line of lines) { + const match = line.match(pattern); + if (match) value = match[1].trim().replace(/^(['"])(.*)\1$/, '$2'); + } + return value; + }; + + let authKey = readValue('SOCKETCLUSTER_AUTH_KEY'); + let generated = false; + if (authKey.length >= 32) { + console.log('✔ Keeping the existing socket auth key from .env'); + } else { + if (authKey) { + console.warn(' ⚠ The socket auth key in .env is shorter than 32 characters; generating a new one.'); + } + authKey = crypto.randomBytes(32).toString('hex'); // 64 hex characters + generated = true; + console.log('✔ Socket auth key generated'); + } + const mode = readValue('SOCKETCLUSTER_AUTH_MODE') || 'enforce'; + + const managed = /^\s*SOCKETCLUSTER_AUTH_(KEY|MODE)=/; + const output = lines.filter(line => !managed.test(line)); + output.push(`SOCKETCLUSTER_AUTH_KEY=${authKey}`, `SOCKETCLUSTER_AUTH_MODE=${mode}`, ''); + + await fs.writeFile(envPath, output.join('\n'), { mode: 0o600 }); + try { await fs.chmod(envPath, 0o600); } catch { /* best effort, e.g. on Windows */ } + console.log(`✔ Socket auth written to .env (mode: ${mode})`); + + return { envPath, mode, generated }; +} + // Command to install Fleetbase via Docker async function installFleetbaseCommand(options) { const crypto = require('crypto'); @@ -1464,6 +1526,10 @@ async function installFleetbaseCommand(options) { const appKey = 'base64:' + crypto.randomBytes(32).toString('base64'); console.log('✔ APP_KEY generated'); + // ── Step 8b: Socket authentication key (project-root .env) ─────────── + console.log('\n⏳ Configuring socket authentication...'); + const socketAuth = await ensureSocketAuthEnv(directory); + // ── Step 9: Write docker-compose.override.yml ───────────────────────── console.log('⏳ Writing docker-compose.override.yml...'); @@ -1656,6 +1722,7 @@ ${buildEnvBlock(dbEnvVars)} mailSetup.configure ? `Mail (${mailConfig.mailMailer})` : null, storageChoice.driver !== 'public' ? `Storage (${storageChoice.driver.toUpperCase()})` : null, 'WebSocket security (origins restricted)', + `Socket authentication (${socketAuth.mode}; key in .env)`, thirdPartySetup.configure ? 'Third-party APIs' : null, ].filter(Boolean); From 2d824d45cd513381bef4fd9ce4d2e7f3131a98de Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Wed, 7 Oct 2026 14:25:20 +0800 Subject: [PATCH 2/4] feat(socket-auth): write SOCKETCLUSTER_AUTH_ENABLED=true for fresh installs The API now gates socket auth (token routes, authorize endpoint, signed HTTP publishing) behind SOCKETCLUSTER_AUTH_ENABLED, default false. With it off the API publishes over the legacy websocket path, which a socket server in enforce mode refuses. Fresh installs get true next to the key and mode; an existing value is kept on re-run, with a warning when enforce is paired with a switch that is not on. README documents the switch and the rollout order. --- README.md | 7 +++++-- index.js | 38 +++++++++++++++++++++++++++----------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index a15ab0a..d74da8a 100644 --- a/README.md +++ b/README.md @@ -167,9 +167,12 @@ The installer creates an empty `api/.env` (bind-mounted by `docker-compose.yml`) It also writes the realtime socket authentication settings to the project-root `.env` (next to `docker-compose.yml`), which Compose passes to the application, queue, scheduler and socket containers: - `SOCKETCLUSTER_AUTH_KEY`: shared secret between the API and the socket server (64 random hex characters). An existing key of 32+ characters is kept on re-runs, since changing it invalidates socket tokens already issued. -- `SOCKETCLUSTER_AUTH_MODE`: `off`, `log` or `enforce` (default `enforce`; an existing value is kept). +- `SOCKETCLUSTER_AUTH_ENABLED`: the API-side switch (default `true` for fresh installs; an existing value is kept). Socket token routes, the authorize endpoint and signed HTTP publishing are active only when this is `true` and the key is valid. With it off, the API publishes over the legacy websocket path. +- `SOCKETCLUSTER_AUTH_MODE`: `off`, `log` or `enforce` (default `enforce`; an existing value is kept). `enforce` requires `SOCKETCLUSTER_AUTH_ENABLED=true`, because the socket server refuses the legacy publish path; the installer warns when the two disagree. -The socket's allowed origins (`SOCKETCLUSTER_OPTIONS`) are still written to `docker-compose.override.yml`. Setting the auth key only in `api/.env` has no effect. +Upgrading an existing install with clients that do not fetch socket tokens yet? Roll out in this order: ship clients that fall back when the token route answers 404, then set `SOCKETCLUSTER_AUTH_ENABLED=true` with `SOCKETCLUSTER_AUTH_MODE=log`, then switch to `enforce`. + +The socket's allowed origins (`SOCKETCLUSTER_OPTIONS`) are still written to `docker-compose.override.yml`. Setting these only in `api/.env` has no effect. **Example:** ```bash diff --git a/index.js b/index.js index 698d8db..9b94c46 100755 --- a/index.js +++ b/index.js @@ -1144,14 +1144,18 @@ async function ensureApiEnvFile(directory) { /** * Write the realtime socket auth settings to the project-root .env file. - * docker-compose.yml reads SOCKETCLUSTER_AUTH_KEY and SOCKETCLUSTER_AUTH_MODE from this - * file (Compose loads it automatically) and hands the same key to the application, queue, - * scheduler and socket containers; setting the key in api/.env has no effect. + * docker-compose.yml reads SOCKETCLUSTER_AUTH_KEY, SOCKETCLUSTER_AUTH_ENABLED and + * SOCKETCLUSTER_AUTH_MODE from this file (Compose loads it automatically) and hands the + * same key to the application, queue, scheduler and socket containers; setting them in + * api/.env has no effect. + * SOCKETCLUSTER_AUTH_ENABLED is the API-side switch (token routes, the authorize endpoint + * and signed HTTP publishing). With it off the API publishes over the legacy websocket + * path, which a socket server in enforce mode refuses, so fresh installs get `true`. * An existing key of at least 32 characters is kept across re-runs, because changing it - * would invalidate every socket token already handed out. An existing mode is kept too. - * Other lines in the file are left untouched. + * would invalidate every socket token already handed out. An existing switch value and + * mode are kept too. Other lines in the file are left untouched. * @param {string} directory resolved installation directory - * @returns {Promise<{ envPath: string, mode: string, generated: boolean }>} + * @returns {Promise<{ envPath: string, enabled: string, mode: string, generated: boolean }>} */ async function ensureSocketAuthEnv(directory) { const crypto = require('crypto'); @@ -1191,17 +1195,29 @@ async function ensureSocketAuthEnv(directory) { generated = true; console.log('✔ Socket auth key generated'); } + const enabled = readValue('SOCKETCLUSTER_AUTH_ENABLED') || 'true'; const mode = readValue('SOCKETCLUSTER_AUTH_MODE') || 'enforce'; + if (mode === 'enforce' && !/^(true|1|on|yes)$/i.test(enabled)) { + console.warn( + ` ⚠ SOCKETCLUSTER_AUTH_MODE=enforce needs SOCKETCLUSTER_AUTH_ENABLED=true (found "${enabled}").\n` + + ' The socket server will refuse API broadcasts; set the switch to true or the mode to log.' + ); + } - const managed = /^\s*SOCKETCLUSTER_AUTH_(KEY|MODE)=/; + const managed = /^\s*SOCKETCLUSTER_AUTH_(KEY|ENABLED|MODE)=/; const output = lines.filter(line => !managed.test(line)); - output.push(`SOCKETCLUSTER_AUTH_KEY=${authKey}`, `SOCKETCLUSTER_AUTH_MODE=${mode}`, ''); + output.push( + `SOCKETCLUSTER_AUTH_KEY=${authKey}`, + `SOCKETCLUSTER_AUTH_ENABLED=${enabled}`, + `SOCKETCLUSTER_AUTH_MODE=${mode}`, + '' + ); await fs.writeFile(envPath, output.join('\n'), { mode: 0o600 }); try { await fs.chmod(envPath, 0o600); } catch { /* best effort, e.g. on Windows */ } - console.log(`✔ Socket auth written to .env (mode: ${mode})`); + console.log(`✔ Socket auth written to .env (enabled: ${enabled}, mode: ${mode})`); - return { envPath, mode, generated }; + return { envPath, enabled, mode, generated }; } // Command to install Fleetbase via Docker @@ -1722,7 +1738,7 @@ ${buildEnvBlock(dbEnvVars)} mailSetup.configure ? `Mail (${mailConfig.mailMailer})` : null, storageChoice.driver !== 'public' ? `Storage (${storageChoice.driver.toUpperCase()})` : null, 'WebSocket security (origins restricted)', - `Socket authentication (${socketAuth.mode}; key in .env)`, + `Socket authentication (enabled: ${socketAuth.enabled}, mode: ${socketAuth.mode}; key in .env)`, thirdPartySetup.configure ? 'Third-party APIs' : null, ].filter(Boolean); From d01bb9eec32c0ced8ba2e590a840499ba682f57e Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Wed, 7 Oct 2026 14:26:39 +0800 Subject: [PATCH 3/4] fix(socket-auth): fresh installs write the switch off and log mode, like docker-install.sh fleetbase/fleetbase 044911e49 made the socket server honour SOCKETCLUSTER_AUTH_ENABLED too, and scripts/docker-install.sh writes it false with mode log, so released mobile apps and integrations that don't fetch socket tokens keep working until the operator turns it on. Writing true + enforce from the CLI would refuse those clients on day one. Use the same defaults (existing values still kept) and report the state in the install summary. README: the switch, what it gates, enforce needing it, and the rollout order. --- README.md | 6 +++--- index.js | 27 +++++++++++++-------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index d74da8a..a6d602b 100644 --- a/README.md +++ b/README.md @@ -167,10 +167,10 @@ The installer creates an empty `api/.env` (bind-mounted by `docker-compose.yml`) It also writes the realtime socket authentication settings to the project-root `.env` (next to `docker-compose.yml`), which Compose passes to the application, queue, scheduler and socket containers: - `SOCKETCLUSTER_AUTH_KEY`: shared secret between the API and the socket server (64 random hex characters). An existing key of 32+ characters is kept on re-runs, since changing it invalidates socket tokens already issued. -- `SOCKETCLUSTER_AUTH_ENABLED`: the API-side switch (default `true` for fresh installs; an existing value is kept). Socket token routes, the authorize endpoint and signed HTTP publishing are active only when this is `true` and the key is valid. With it off, the API publishes over the legacy websocket path. -- `SOCKETCLUSTER_AUTH_MODE`: `off`, `log` or `enforce` (default `enforce`; an existing value is kept). `enforce` requires `SOCKETCLUSTER_AUTH_ENABLED=true`, because the socket server refuses the legacy publish path; the installer warns when the two disagree. +- `SOCKETCLUSTER_AUTH_ENABLED`: switches socket authentication on for the API and the socket server (default `false`; an existing value is kept). The API's socket token routes, its authorize endpoint and signed HTTP publishing are active only when this is `true` and the key is valid. While it is off, nothing is authenticated, the token routes answer `404`, and the API publishes over the legacy websocket path, so clients that don't fetch socket tokens yet keep working. +- `SOCKETCLUSTER_AUTH_MODE`: `off`, `log` or `enforce` (default `log`; an existing value is kept). It only takes effect once the switch is on. `enforce` requires `SOCKETCLUSTER_AUTH_ENABLED=true` on the API, since an enforcing socket server refuses the legacy publish path. -Upgrading an existing install with clients that do not fetch socket tokens yet? Roll out in this order: ship clients that fall back when the token route answers 404, then set `SOCKETCLUSTER_AUTH_ENABLED=true` with `SOCKETCLUSTER_AUTH_MODE=log`, then switch to `enforce`. +These are the same defaults as `scripts/docker-install.sh`. To turn socket authentication on, follow this order: ship clients that fall back to an anonymous connection when the token route answers `404`, then set `SOCKETCLUSTER_AUTH_ENABLED=true` with `SOCKETCLUSTER_AUTH_MODE=log`, and switch to `enforce` once the socket server's deny log only shows traffic you expect to lose. Restart the containers after editing `.env`. The socket's allowed origins (`SOCKETCLUSTER_OPTIONS`) are still written to `docker-compose.override.yml`. Setting these only in `api/.env` has no effect. diff --git a/index.js b/index.js index 9b94c46..ef373e2 100755 --- a/index.js +++ b/index.js @@ -1148,9 +1148,12 @@ async function ensureApiEnvFile(directory) { * SOCKETCLUSTER_AUTH_MODE from this file (Compose loads it automatically) and hands the * same key to the application, queue, scheduler and socket containers; setting them in * api/.env has no effect. - * SOCKETCLUSTER_AUTH_ENABLED is the API-side switch (token routes, the authorize endpoint - * and signed HTTP publishing). With it off the API publishes over the legacy websocket - * path, which a socket server in enforce mode refuses, so fresh installs get `true`. + * SOCKETCLUSTER_AUTH_ENABLED switches socket auth on for the API (token routes, the + * authorize endpoint, signed HTTP publishing) and the socket server. Fresh installs get + * `false` and mode `log`, the same as scripts/docker-install.sh: the key is provisioned, + * but clients that don't fetch socket tokens yet (released mobile apps, integrations) + * keep working until the switch is turned on. While it is off the API publishes over the + * legacy websocket path, so `enforce` needs the switch on. * An existing key of at least 32 characters is kept across re-runs, because changing it * would invalidate every socket token already handed out. An existing switch value and * mode are kept too. Other lines in the file are left untouched. @@ -1195,14 +1198,9 @@ async function ensureSocketAuthEnv(directory) { generated = true; console.log('✔ Socket auth key generated'); } - const enabled = readValue('SOCKETCLUSTER_AUTH_ENABLED') || 'true'; - const mode = readValue('SOCKETCLUSTER_AUTH_MODE') || 'enforce'; - if (mode === 'enforce' && !/^(true|1|on|yes)$/i.test(enabled)) { - console.warn( - ` ⚠ SOCKETCLUSTER_AUTH_MODE=enforce needs SOCKETCLUSTER_AUTH_ENABLED=true (found "${enabled}").\n` + - ' The socket server will refuse API broadcasts; set the switch to true or the mode to log.' - ); - } + const enabled = readValue('SOCKETCLUSTER_AUTH_ENABLED') || 'false'; + const mode = readValue('SOCKETCLUSTER_AUTH_MODE') || 'log'; + const switchedOn = /^(true|1|yes|on)$/i.test(enabled); const managed = /^\s*SOCKETCLUSTER_AUTH_(KEY|ENABLED|MODE)=/; const output = lines.filter(line => !managed.test(line)); @@ -1215,9 +1213,10 @@ async function ensureSocketAuthEnv(directory) { await fs.writeFile(envPath, output.join('\n'), { mode: 0o600 }); try { await fs.chmod(envPath, 0o600); } catch { /* best effort, e.g. on Windows */ } - console.log(`✔ Socket auth written to .env (enabled: ${enabled}, mode: ${mode})`); + const summary = switchedOn ? `on, mode: ${mode}` : 'off until SOCKETCLUSTER_AUTH_ENABLED=true'; + console.log(`✔ Socket auth written to .env (${summary})`); - return { envPath, enabled, mode, generated }; + return { envPath, enabled, mode, summary, generated }; } // Command to install Fleetbase via Docker @@ -1738,7 +1737,7 @@ ${buildEnvBlock(dbEnvVars)} mailSetup.configure ? `Mail (${mailConfig.mailMailer})` : null, storageChoice.driver !== 'public' ? `Storage (${storageChoice.driver.toUpperCase()})` : null, 'WebSocket security (origins restricted)', - `Socket authentication (enabled: ${socketAuth.enabled}, mode: ${socketAuth.mode}; key in .env)`, + `Socket authentication (${socketAuth.summary}; key in .env)`, thirdPartySetup.configure ? 'Third-party APIs' : null, ].filter(Boolean); From 3f63e15429aab70a85148014fe4daa0447713b14 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Thu, 8 Oct 2026 10:37:57 +0800 Subject: [PATCH 4/4] feat(socket-auth): write SOCKETCLUSTER_ORIGIN to the project .env The API's websocket publisher sends no Origin header, so with the restricted SOCKETCLUSTER_OPTIONS origins the installer writes, every broadcast is refused with 'Invalid origin: *'. Write the console origin (http://localhost:4200 on localhost, :// otherwise) next to the socket auth settings, keeping an existing value. --- README.md | 1 + index.js | 24 ++++++++++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a6d602b..2b126e6 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,7 @@ It also writes the realtime socket authentication settings to the project-root ` - `SOCKETCLUSTER_AUTH_KEY`: shared secret between the API and the socket server (64 random hex characters). An existing key of 32+ characters is kept on re-runs, since changing it invalidates socket tokens already issued. - `SOCKETCLUSTER_AUTH_ENABLED`: switches socket authentication on for the API and the socket server (default `false`; an existing value is kept). The API's socket token routes, its authorize endpoint and signed HTTP publishing are active only when this is `true` and the key is valid. While it is off, nothing is authenticated, the token routes answer `404`, and the API publishes over the legacy websocket path, so clients that don't fetch socket tokens yet keep working. +- `SOCKETCLUSTER_ORIGIN`: the `Origin` header the API sends when it publishes over the websocket, which it does while socket auth is off. Defaults to the console origin (`http://localhost:4200` for localhost installs, `://` otherwise), which the origins written to `SOCKETCLUSTER_OPTIONS` allow; an existing value is kept. Without it the socket server refuses every broadcast with `Invalid origin: *`. - `SOCKETCLUSTER_AUTH_MODE`: `off`, `log` or `enforce` (default `log`; an existing value is kept). It only takes effect once the switch is on. `enforce` requires `SOCKETCLUSTER_AUTH_ENABLED=true` on the API, since an enforcing socket server refuses the legacy publish path. These are the same defaults as `scripts/docker-install.sh`. To turn socket authentication on, follow this order: ship clients that fall back to an anonymous connection when the token route answers `404`, then set `SOCKETCLUSTER_AUTH_ENABLED=true` with `SOCKETCLUSTER_AUTH_MODE=log`, and switch to `enforce` once the socket server's deny log only shows traffic you expect to lose. Restart the containers after editing `.env`. diff --git a/index.js b/index.js index ef373e2..e8b56fb 100755 --- a/index.js +++ b/index.js @@ -1157,10 +1157,16 @@ async function ensureApiEnvFile(directory) { * An existing key of at least 32 characters is kept across re-runs, because changing it * would invalidate every socket token already handed out. An existing switch value and * mode are kept too. Other lines in the file are left untouched. - * @param {string} directory resolved installation directory - * @returns {Promise<{ envPath: string, enabled: string, mode: string, generated: boolean }>} + * SOCKETCLUSTER_ORIGIN is the Origin header the API's websocket publisher sends (the path + * used while the switch is off). Without it the socket server sees the origin as `*` and, + * with restricted SOCKETCLUSTER_OPTIONS origins, refuses every broadcast with + * "Invalid origin: *". It defaults to `defaultOrigin` (an origin the written origins + * allow); an existing value is kept. + * @param {string} directory resolved installation directory + * @param {string} defaultOrigin console origin allowed by SOCKETCLUSTER_OPTIONS + * @returns {Promise<{ envPath: string, enabled: string, mode: string, origin: string, generated: boolean }>} */ -async function ensureSocketAuthEnv(directory) { +async function ensureSocketAuthEnv(directory, defaultOrigin = '') { const crypto = require('crypto'); const envPath = path.join(directory, '.env'); @@ -1200,14 +1206,16 @@ async function ensureSocketAuthEnv(directory) { } const enabled = readValue('SOCKETCLUSTER_AUTH_ENABLED') || 'false'; const mode = readValue('SOCKETCLUSTER_AUTH_MODE') || 'log'; + const origin = readValue('SOCKETCLUSTER_ORIGIN') || defaultOrigin; const switchedOn = /^(true|1|yes|on)$/i.test(enabled); - const managed = /^\s*SOCKETCLUSTER_AUTH_(KEY|ENABLED|MODE)=/; + const managed = /^\s*SOCKETCLUSTER_(AUTH_KEY|AUTH_ENABLED|AUTH_MODE|ORIGIN)=/; const output = lines.filter(line => !managed.test(line)); output.push( `SOCKETCLUSTER_AUTH_KEY=${authKey}`, `SOCKETCLUSTER_AUTH_ENABLED=${enabled}`, `SOCKETCLUSTER_AUTH_MODE=${mode}`, + ...(origin ? [`SOCKETCLUSTER_ORIGIN=${origin}`] : []), '' ); @@ -1215,8 +1223,9 @@ async function ensureSocketAuthEnv(directory) { try { await fs.chmod(envPath, 0o600); } catch { /* best effort, e.g. on Windows */ } const summary = switchedOn ? `on, mode: ${mode}` : 'off until SOCKETCLUSTER_AUTH_ENABLED=true'; console.log(`✔ Socket auth written to .env (${summary})`); + if (origin) console.log(`✔ API publisher origin: ${origin}`); - return { envPath, enabled, mode, summary, generated }; + return { envPath, enabled, mode, origin, summary, generated }; } // Command to install Fleetbase via Docker @@ -1500,6 +1509,9 @@ async function installFleetbaseCommand(options) { ? 'http://localhost:*,https://localhost:*,ws://localhost:*,wss://localhost:*' : `${schemeConsole}://${host}:*,wss://${host}:*`; const socketClusterOptions = JSON.stringify({ origins: socketOrigins }); + // Origin the API publisher sends; the socket server matches hostname + port + // against the entries above (e.g. "localhost:*"), so it must fall inside them. + const socketPublisherOrigin = isLocalhost ? 'http://localhost:4200' : `${schemeConsole}://${host}`; console.log(`✔ SESSION_DOMAIN set to: ${sessionDomain}`); console.log(`✔ WebSocket origins restricted to: ${socketOrigins}`); @@ -1543,7 +1555,7 @@ async function installFleetbaseCommand(options) { // ── Step 8b: Socket authentication key (project-root .env) ─────────── console.log('\n⏳ Configuring socket authentication...'); - const socketAuth = await ensureSocketAuthEnv(directory); + const socketAuth = await ensureSocketAuthEnv(directory, socketPublisherOrigin); // ── Step 9: Write docker-compose.override.yml ───────────────────────── console.log('⏳ Writing docker-compose.override.yml...');