Backups push automatically; the token is only for restore

Matches the master's tokenless-push change: pushBackup no longer waits
for master_backup_token — every instance with a uuid backs up on its
own, and the 5-minute tick force-pushes so a poisoned master copy
self-heals. The token now only gates pulling a backup back (restore),
so setup-linux.sh asks for it only when restoring and the Settings
help text says so.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-09-22 14:08:03 -05:00
co-authored by Claude Fable 5
parent 8154f25bb9
commit ba8bd50da0
4 changed files with 27 additions and 22 deletions
+17 -14
View File
@@ -5,9 +5,11 @@
* instance's uuid.
*
* This is the deliberate exception to "credentials never leave the machine":
* the payload goes over HTTPS to the bearer-gated /api/instances/backup and is
* AES-encrypted at rest on the master. It never rides the unauthenticated
* control socket.
* the payload goes over HTTPS to /api/instances/backup and is AES-encrypted
* at rest on the master. It never rides the control socket. Pushing needs no
* token — every instance backs up automatically once it has its uuid; only
* PULLING a backup back out (lib/backup-restore.ts) is token-gated, because
* that is the direction credentials come out in the clear.
*
* Push policy: on bootstrap, then every POLL_INTERVAL, but only when the
* content actually changed (hash comparison) — plus an immediate debounced
@@ -85,19 +87,20 @@ function contentHash(payload: BackupPayload): string {
.digest('hex');
}
function preconditions(): { masterUrl: string; id: string; token: string } | null {
function preconditions(): { masterUrl: string; id: string } | null {
const masterUrl = getSetting('master_dashboard_url');
const token = getSetting('master_backup_token');
// No uuid yet = the master has never said welcome; there is nothing stable
// to key the backup by, so wait for the first hello to complete.
// to key the backup by, so wait for the first hello to complete. No token
// needed: pushes are open on the master (only PULLING a backup is
// token-gated), so every instance backs up automatically.
const id = getSetting('instance_id');
if (!masterUrl || !token || !id) return null;
return { masterUrl, id, token };
if (!masterUrl || !id) return null;
return { masterUrl, id };
}
export async function pushBackup(force = false): Promise<{ ok: boolean; skipped?: boolean; error?: string }> {
const pre = preconditions();
if (!pre) return { ok: false, skipped: true, error: 'master url, backup token or instance uuid not set' };
if (!pre) return { ok: false, skipped: true, error: 'master url or instance uuid not set' };
const payload = collectBackup();
const hash = contentHash(payload);
@@ -113,10 +116,7 @@ export async function pushBackup(force = false): Promise<{ ok: boolean; skipped?
try {
const res = await fetch(url, {
method: 'PUT',
headers: {
'content-type': 'application/json',
authorization: `Bearer ${pre.token}`,
},
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
id: pre.id,
name: getSetting('instance_name') ?? '',
@@ -151,7 +151,10 @@ export function scheduleBackupPush(): void {
export function startBackupSync(): void {
stopBackupSync();
const tick = async () => {
await pushBackup();
// Forced: pushes need no token, so anyone who reaches the master and
// knows this uuid could overwrite our backup there. Re-asserting the
// real state every tick bounds that poisoning window to 5 minutes.
await pushBackup(true);
// Jitter so a fleet restarting together doesn't push in lockstep.
g.__backupSyncTimer = setTimeout(tick, POLL_INTERVAL_MS + Math.floor(Math.random() * 30_000));
};
+3 -2
View File
@@ -197,8 +197,9 @@ seedSetting.run('twocaptcha_api_key', '');
// Blank falls back to the proxy hardcoded in app/api/autobuyer/captcha; `none`
// solves without one.
seedSetting.run('twocaptcha_proxy', '');
// Bearer for pushing/pulling the encrypted settings+credentials backup on the
// master (its INSTANCE_BACKUP_TOKEN). Blank = backups off.
// Bearer for PULLING an encrypted settings+credentials backup back from the
// master (its INSTANCE_BACKUP_TOKEN) — pushing needs no token and happens
// automatically. Blank = restore unavailable from this machine's UI.
seedSetting.run('master_backup_token', '');
// Routes broker traffic (TopstepX, Tradovate, Yahoo quotes) through a proxy:
// http(s):// or socks5(h)://, user:pass ok. The master link stays direct on