openvibers/openvibe.events Commits

rotate-subscription-secret: --consumer rotates every first-party subscription of a service to one new secret (they share one env variable)

OP OpenVibers committed 24 Sept 2026 · 9f51514286 · 1 file, +12 −8


Changes

ddiff --git a/scripts/rotate-subscription-secret.js b/scripts/rotate-subscription-secret.js
iindex f636823..e070914 100755
---- a/scripts/rotate-subscription-secret.js
++++ b/scripts/rotate-subscription-secret.js
@@@ -3,7 +3,7 @@
/**
* Operator rotation of a first-party subscription's secret, without the value ever being printed.
*
-- * node scripts/rotate-subscription-secret.js --db <events.db> --subscription <sub_…>
++ * node scripts/rotate-subscription-secret.js --db <events.db> (--subscription <sub_…> | --consumer <service>)
* --env-file </etc/openvibe/<consumer>.env> --env-var <NAME> [--overlap-s 86400] [--apply]
*
* With --apply it (1) sets a new random secret on the subscription, keeping the old one signing next to it
@@@ -22,34 +22,38 @@ const opt = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] :
const apply = args.includes('--apply');
const dbPath = opt('--db');
const subId = opt('--subscription');
++const consumer = opt('--consumer'); // every first-party subscription of that service, to ONE new secret (they share one env variable)
const envFile = opt('--env-file');
const envVar = opt('--env-var');
const overlapS = Number(opt('--overlap-s', '86400'));
function fail(msg) { console.error(`rotate-subscription-secret: ${msg}`); process.exit(1); }
--if (!dbPath || !subId || !envFile || !envVar) fail('--db, --subscription, --env-file and --env-var are required');
++if (!dbPath || !(subId || consumer) || !envFile || !envVar) fail('--db, --subscription or --consumer, --env-file and --env-var are required');
if (!/^[A-Z][A-Z0-9_]{1,63}$/.test(envVar)) fail('--env-var must be an env variable name');
if (!Number.isFinite(overlapS) || overlapS < 0 || overlapS > 7 * 86400) fail('--overlap-s is 0..604800');
if (!fs.existsSync(envFile)) fail(`${envFile} does not exist`);
const db = new Database(dbPath, { fileMustExist: true });
--const sub = db.prepare('SELECT id, consumer, topic_pattern, project_id FROM subscriptions WHERE id = ?').get(subId);
--if (!sub) fail(`no subscription ${subId}`);
--if (sub.project_id) fail('developer-app subscriptions rotate through the API (POST /api/v1/subscriptions/:id/rotate-secret)');
++const subs = consumer
++ ? db.prepare('SELECT id, consumer, topic_pattern, project_id FROM subscriptions WHERE consumer = ? AND project_id IS NULL').all(consumer)
++ : db.prepare('SELECT id, consumer, topic_pattern, project_id FROM subscriptions WHERE id = ?').all(subId);
++if (!subs.length) fail(`no subscription for ${consumer || subId}`);
++if (subs.some((x) => x.project_id)) fail('developer-app subscriptions rotate through the API (POST /api/v1/subscriptions/:id/rotate-secret)');
++const sub = subs[0];
const cols = new Set(db.prepare('PRAGMA table_info(subscriptions)').all().map((c) => c.name));
if (!cols.has('previous_secret')) fail('this Events database predates rotation; deploy Events first (it adds the columns at boot)');
const lines = fs.readFileSync(envFile, 'utf8').split('\n');
const has = lines.some((l) => l.startsWith(`${envVar}=`));
--console.log(`subscription ${sub.id} (${sub.consumer}, ${sub.topic_pattern}); ${envFile}: ${envVar} ${has ? 'will be replaced' : 'will be appended'}; overlap ${overlapS}s`);
++console.log(`${subs.length} subscription(s) of ${sub.consumer} (${subs.map((x) => x.topic_pattern).join(', ')}); ${envFile}: ${envVar} ${has ? 'will be replaced' : 'will be appended'}; overlap ${overlapS}s`);
if (!apply) { console.log('dry run: nothing changed (add --apply)'); process.exit(0); }
const secret = `whsec_${crypto.randomBytes(32).toString('hex')}`;
const now = Date.now();
fs.copyFileSync(envFile, `${envFile}.bak-${new Date(now).toISOString().replace(/[:.]/g, '-')}`);
db.transaction(() => {
-- db.prepare('UPDATE subscriptions SET previous_secret = secret, previous_secret_until = ?, secret = ?, updated_at = ? WHERE id = ?')
-- .run(now + overlapS * 1000, secret, now, sub.id);
++ const up = db.prepare('UPDATE subscriptions SET previous_secret = secret, previous_secret_until = ?, secret = ?, updated_at = ? WHERE id = ?');
++ for (const x of subs) up.run(now + overlapS * 1000, secret, now, x.id);
})();
let out;
if (has) out = lines.map((l) => (l.startsWith(`${envVar}=`) ? `${envVar}=${secret}` : l));