feat: connection candidates refresh + relay identity hardening
Candidates refresh (server + mobile + desktop clients):
- GET /api/client-auth/connection/candidates returns the server's current
LAN URLs, relay candidate, and serverId for already-paired devices
- /health and /api/version expose serverId so clients can verify a learned
address belongs to the expected server before sending their bearer token
- mobile: refresh saved candidates over the live transport after every
connect/wake, hot-switch relay->LAN when a fresh address is reachable;
serverId gate on direct probes; token no longer sent to /health
- desktop: refresh stored host apiUrl after a relay connect and hot-switch
back to direct; electron probe verifies serverId before authenticated fetch
Fixes found while debugging a dead pairing:
- settings: strict reader that throws on corrupt/unreadable file instead of
returning {}; relay signing/encryption key generation is now gated on it,
so a swallowed read failure can no longer mint a new server identity and
orphan every paired device (loud log when a keypair IS generated)
- SessionAuthGate: bounded auto-retry for transient session-check failures
(initial request racing the relay tunnel's first WS attempt, startup 5xx)
This commit is contained in:
@@ -438,6 +438,30 @@ export const createSettingsRuntime = (deps) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Strict variant for callers that REGENERATE persisted identity when a key is
|
||||
// absent (relay signing/encryption keys). The lenient reader above maps every
|
||||
// failure — corrupt JSON, EACCES, transient I/O — to `{}`, which such callers
|
||||
// cannot distinguish from "first run": they would mint a NEW identity, orphan
|
||||
// every paired device and push binding, and overwrite the settings file with
|
||||
// the empty spread. Here only a genuinely missing file means "no settings";
|
||||
// any other failure (including a non-object payload) throws.
|
||||
const readSettingsFromDiskStrict = async () => {
|
||||
let raw;
|
||||
try {
|
||||
raw = await fsPromises.readFile(SETTINGS_FILE_PATH, 'utf8');
|
||||
} catch (error) {
|
||||
if (error && typeof error === 'object' && error.code === 'ENOENT') {
|
||||
return {};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
throw new Error('Settings file is malformed (non-object payload)');
|
||||
}
|
||||
return parsed;
|
||||
};
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const isTransientWindowsReplaceError = (error) => {
|
||||
@@ -870,6 +894,7 @@ export const createSettingsRuntime = (deps) => {
|
||||
|
||||
return {
|
||||
readSettingsFromDisk,
|
||||
readSettingsFromDiskStrict,
|
||||
readSettingsFromDiskMigrated,
|
||||
writeSettingsToDisk,
|
||||
persistSettings,
|
||||
|
||||
Reference in New Issue
Block a user