#!/usr/bin/env node // Operator-bound, application-scoped repair. No restart and no shared service changes. import { internalRoutingConfig } from './lib/admin-internal-routing.mjs'; import { withGateway } from '../perf/speed-round3-2026-11-10/server-util.mjs'; const mode = process.argv[2] || '--check'; if (!['--check', '--apply', '--rollback'].includes(mode)) throw new Error('Use --check, --apply EXPECTED_SHA256, or --rollback EXPECTED_SHA256 BACKUP_PATH'); const expected = process.argv[3] || ''; if (mode !== '--check' && !/^[a-f0-9]{64}$/.test(expected)) throw new Error('Apply requires the checked configuration SHA-256'); if (mode === '--rollback' && !process.argv[4]) throw new Error('Rollback requires its exact backup path'); async function run(input) { const fs = await import('node:fs/promises'), crypto = await import('node:crypto'); const path = '/opt/future-server/config/config.xiaoshu.json'; const original = await fs.readFile(path, 'utf8'), before = JSON.parse(original); let next = internalRoutingConfig(before); const sha = value => crypto.createHash('sha256').update(value).digest('hex'); if (input.rollback) { if (!/^\/opt\/future-server\/config\/\.xiaoshu-routing-backups\/[0-9]+\.json$/.test(input.rollback)) throw new Error('Invalid rollback path'); const saved = JSON.parse(await fs.readFile(input.rollback, 'utf8')); if (JSON.stringify(internalRoutingConfig(saved)) !== JSON.stringify(internalRoutingConfig(before))) throw new Error('Backup contains unrelated configuration changes'); next = saved; } if (Parse.applicationId !== before.parse.appId || global.Config?.parse?.appId !== before.parse.appId) throw new Error('Unexpected runtime application'); if (![before.parse.serverURL, next.parse.serverURL].includes(Parse.serverURL)) throw new Error('Unexpected SDK routing'); // Verify the target app through its own loopback listener before any change. const response = await fetch(internalRoutingConfig(before).parse.serverURL + '/classes/Company?limit=0&count=1', { headers: { 'X-Parse-Application-Id': Parse.applicationId, 'X-Parse-Master-Key': Parse.masterKey }, signal: AbortSignal.timeout(5000), }); const data = await response.json(); if (!response.ok || !Number.isInteger(data.count)) throw new Error('Loopback application check failed'); let backup = null; if (input.apply) { if (sha(original) !== input.expected) throw new Error('Configuration changed since check'); if (next.parse.serverURL !== before.parse.serverURL) { const dir = '/opt/future-server/config/.xiaoshu-routing-backups'; await fs.mkdir(dir, { recursive: true, mode: 0o700 }); backup = dir + '/' + Date.now() + '.json'; await fs.writeFile(backup, original, { mode: 0o600, flag: 'wx' }); const temp = path + '.routing-' + crypto.randomUUID(); await fs.writeFile(temp, JSON.stringify(next, null, 2) + '\n', { mode: 0o600, flag: 'wx' }); try { if (sha(await fs.readFile(path, 'utf8')) !== input.expected) throw new Error('Configuration changed before replacement'); await fs.rename(temp, path); } finally { await fs.unlink(temp).catch(() => {}); } } // All functions in this Xiaoshu process share this SDK; active requests keep running. Parse.serverURL = next.parse.serverURL; global.Config.parse.serverURL = next.parse.serverURL; } const stored = JSON.parse(await fs.readFile(path, 'utf8')); return { applied: Boolean(input.apply), beforeSha256: sha(original), afterSha256: sha(await fs.readFile(path, 'utf8')), serverURL: stored.parse.serverURL, publicServerURL: stored.parse.publicServerURL, runtimeServerURL: Parse.serverURL, appId: Parse.applicationId, backup, pid: process.pid }; } await withGateway(internalRoutingConfig.toString() + '\n' + run.toString(), async call => { console.log(JSON.stringify(await call({ apply: mode !== '--check', expected, rollback: mode === '--rollback' ? process.argv[4] : undefined }))); });