Verify off-box presence by reading the remote dir back
Exit 0 from the backup script is deliberately tied to ON-BOX durability, so it is not proof the off-box copy landed. GET /api/internal/backup/offbox runs rsync --list-only against BACKUP_REMOTE using the SAME pinned known_hosts as the push (checking never disabled) and returns the dumps actually present, with size and timestamp — so off-box presence is a verified fact rather than an inference from an exit code. Needed because the dev box cannot authenticate to the Storage Box: the authorized key installed there is Kev's ~/vyndr-backup-key, not the keypair generated in-session, so independent verification has to run from the container that does hold working credentials. Suite 280/3338 green, build exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SmNjJAwEnqHPtXbvSZR8kA
This commit is contained in:
+59
-1
@@ -174,7 +174,65 @@ function latestDump(dir = process.env.BACKUP_DIR || '/var/backups/vyndr', deps =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List the OFF-BOX copies actually present on the Storage Box.
|
||||||
|
*
|
||||||
|
* Exit 0 from the backup script is NOT proof the dump landed — the script
|
||||||
|
* deliberately keeps its exit code tied to on-box durability. This reads the
|
||||||
|
* remote directory back, so "the off-box copy exists" is a verified fact rather
|
||||||
|
* than an inference. Uses the SAME pinned known_hosts as the push; host-key
|
||||||
|
* checking is never disabled.
|
||||||
|
*/
|
||||||
|
function listOffbox(deps = {}) {
|
||||||
|
const spawn = deps.spawn || require('child_process').spawn;
|
||||||
|
const remote = process.env.BACKUP_REMOTE;
|
||||||
|
const port = process.env.BACKUP_SSH_PORT || '23';
|
||||||
|
const knownHosts = process.env.BACKUP_KNOWN_HOSTS
|
||||||
|
|| path.join(__dirname, '..', 'scripts', 'storagebox_known_hosts');
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (!remote) return resolve({ ok: false, error: 'BACKUP_REMOTE unset' });
|
||||||
|
if (!process.env.BACKUP_SSH_KEY) return resolve({ ok: false, error: 'BACKUP_SSH_KEY unset' });
|
||||||
|
const fs = require('fs');
|
||||||
|
const os = require('os');
|
||||||
|
let keyFile;
|
||||||
|
try {
|
||||||
|
// Same decode contract as backup-db.sh: base64 preferred, raw PEM fallback.
|
||||||
|
const raw = process.env.BACKUP_SSH_KEY;
|
||||||
|
let decoded;
|
||||||
|
try {
|
||||||
|
const d = Buffer.from(raw, 'base64').toString('utf8');
|
||||||
|
decoded = d.includes('PRIVATE KEY') ? d : raw.replace(/\\n/g, '\n');
|
||||||
|
} catch { decoded = raw.replace(/\\n/g, '\n'); }
|
||||||
|
keyFile = path.join(os.tmpdir(), `vyndr-offbox-${Date.now()}`);
|
||||||
|
fs.writeFileSync(keyFile, decoded.endsWith('\n') ? decoded : `${decoded}\n`, { mode: 0o600 });
|
||||||
|
} catch (e) {
|
||||||
|
return resolve({ ok: false, error: `key write failed: ${e.message}` });
|
||||||
|
}
|
||||||
|
const sshCmd = `ssh -p ${port} -o StrictHostKeyChecking=yes -o UserKnownHostsFile=${knownHosts} -o BatchMode=yes -i ${keyFile}`;
|
||||||
|
const child = spawn('rsync', ['--list-only', '-e', sshCmd, remote], { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||||
|
let out = '';
|
||||||
|
let err = '';
|
||||||
|
child.stdout.on('data', (b) => { out += b.toString(); });
|
||||||
|
child.stderr.on('data', (b) => { err = (err + b.toString()).slice(-1500); });
|
||||||
|
const done = (result) => {
|
||||||
|
try { require('fs').unlinkSync(keyFile); } catch { /* best effort */ }
|
||||||
|
resolve(result);
|
||||||
|
};
|
||||||
|
child.on('error', (e) => done({ ok: false, error: e.message }));
|
||||||
|
child.on('close', (code) => {
|
||||||
|
const files = out.split('\n')
|
||||||
|
.map((l) => l.trim())
|
||||||
|
.filter((l) => /vyndr-.*\.dump$/.test(l))
|
||||||
|
.map((l) => {
|
||||||
|
const parts = l.split(/\s+/);
|
||||||
|
return { size: Number(parts[1].replace(/,/g, '')) || null, date: parts[2], time: parts[3], file: parts[parts.length - 1] };
|
||||||
|
});
|
||||||
|
done({ ok: code === 0, code, count: files.length, files, error: code === 0 ? null : (err || `exit ${code}`) });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
startBackupScheduler, runBackup, shouldArm, durabilityWarning, SCRIPT,
|
startBackupScheduler, runBackup, shouldArm, durabilityWarning, SCRIPT,
|
||||||
countRowsInDump, latestDump,
|
countRowsInDump, latestDump, listOffbox,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -329,6 +329,22 @@ router.get('/backup/verify', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/internal/backup/offbox — list the dumps actually present ON the
|
||||||
|
* Storage Box. Exit 0 from the backup is not proof the file landed; this reads
|
||||||
|
* the remote directory back so off-box presence is verified, not inferred.
|
||||||
|
*/
|
||||||
|
router.get('/backup/offbox', async (req, res) => {
|
||||||
|
const { listOffbox } = require('../backupScheduler');
|
||||||
|
try {
|
||||||
|
const result = await listOffbox();
|
||||||
|
return res.json(result);
|
||||||
|
} catch (err) {
|
||||||
|
const message = err && err.message ? err.message : String(err);
|
||||||
|
return res.status(500).json({ ok: false, error: message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/internal/ledger/settle (Session 58, Phase 1) — settle the
|
* POST /api/internal/ledger/settle (Session 58, Phase 1) — settle the
|
||||||
* persistent ledger (outcome + actual_value + CLV) across every sport.
|
* persistent ledger (outcome + actual_value + CLV) across every sport.
|
||||||
|
|||||||
Reference in New Issue
Block a user