fix(cli): atomic settings writes and gate relay key regeneration in connect-url
The CLI's settings accessors wrote settings.json directly with writeFile and
read it leniently, with no strict-reader gate on relay identity. Running
'openchamber connect-url' while the desktop app is up could:
- tear the file for a concurrent reader in the app, tripping the relay
service's read and mapping it to {} (first-run);
- then regenerate the relay signing/encryption keys, changing serverId and
orphaning every paired device and push binding.
Move the accessors into a dedicated module that mirrors the settings
runtime's guarantees: atomic tmp+rename writes (with the Windows fallback)
so no reader can observe a partial file, and a strict reader that throws on
corrupt/unreadable payloads so identity regeneration is gated exactly like
the server runtime. Wire the strict reader into the CLI relay identity path.
Adds unit tests covering atomic writes under concurrent readers, strict-read
behavior, and that a corrupt settings file makes getRelayIdentity fail
instead of minting a replacement keypair.
This commit is contained in:
@@ -17,6 +17,7 @@ import { createClientPairingRuntime } from '../../server/lib/client-auth/pairing
|
||||
import { createRelayIdentityRuntime } from '../../server/lib/relay/identity.js';
|
||||
import { DEFAULT_RELAY_URL } from '../../server/lib/relay/service.js';
|
||||
import { bytesToBase64Url } from '../../server/lib/relay/e2ee.js';
|
||||
import { createSettingsAccessors as createSettingsAccessorsModule } from './cli-settings-accessors.js';
|
||||
import {
|
||||
intro as clackIntro,
|
||||
outro as clackOutro,
|
||||
@@ -28,7 +29,6 @@ import {
|
||||
} from '../cli-output.js';
|
||||
|
||||
const REMOTE_CLIENTS_FILE_NAME = 'remote-clients.json';
|
||||
const SETTINGS_FILE_NAME = 'settings.json';
|
||||
const PAIRING_SESSIONS_FILE_NAME = 'client-pairing-sessions.json';
|
||||
|
||||
function isValidRelayUrl(value) {
|
||||
@@ -55,20 +55,18 @@ function resolveRelayUrl(settings) {
|
||||
// Minimal settings.json read/write for the relay identity runtime. It reads the
|
||||
// whole object and writes it back with the relay keys added, so other settings
|
||||
// are preserved. Enough for the CLI without wiring the full settings runtime.
|
||||
//
|
||||
// Mirrors the settings runtime's guarantees: atomic writes (tmp + rename) so
|
||||
// concurrent readers in the running app never observe a half-written file, and
|
||||
// a STRICT reader gating relay identity regeneration so a swallowed read
|
||||
// failure can never mint a new serverId and orphan paired devices.
|
||||
function createSettingsAccessors() {
|
||||
const settingsPath = path.join(getOpenChamberDataDir(), SETTINGS_FILE_NAME);
|
||||
const readSettingsFromDiskMigrated = async () => {
|
||||
try {
|
||||
return JSON.parse(await fs.promises.readFile(settingsPath, 'utf8'));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
const writeSettingsToDisk = async (settings) => {
|
||||
await fs.promises.mkdir(path.dirname(settingsPath), { recursive: true });
|
||||
await fs.promises.writeFile(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
||||
};
|
||||
return { readSettingsFromDiskMigrated, writeSettingsToDisk };
|
||||
return createSettingsAccessorsModule({
|
||||
fsPromises: fs.promises,
|
||||
path,
|
||||
dataDir: getOpenChamberDataDir(),
|
||||
settingsFileName: 'settings.json',
|
||||
});
|
||||
}
|
||||
|
||||
// Resolves the instance's relay identity (serverId + encryption public key,
|
||||
|
||||
Reference in New Issue
Block a user