openvibers/openvibe.run Commits

account-data: the consumer's route answers before the catch-all (#116)

OP OpenVibers committed 14 days ago · 1d255afce0 · 3 files, +17 −3

The account export and deletion consumer was mounted in server/index.js, after createApp() had
already registered the catch-all that answers every /internal/ request with a 404. In production
the route was shadowed: a delivery never reached the consumer. It is now mounted inside createApp,
before the catch-all, and the account-data test asserts that an unsigned delivery is refused (401)
rather than lost (404).

Changes

ddiff --git a/server/app.js b/server/app.js
iindex 08713ab..acf6672 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -157,6 +157,11 @@ function createApp(opts = {}) {
icons: [192, 512].flatMap((s) => [false, true].map((maskable) => ({ src: require('openvibe-shared/app-icons').url('run', s, { maskable }), sizes: `${s}x${s}`, type: 'image/png', purpose: maskable ? 'maskable' : 'any' }))),
})));
++ // The account export and deletion consumer (ADR-033, server/account-data.js): the two account topics, signed
++ // and loopback only. It answers the raw body (the v2 signature covers it), so no body parser may run before it:
++ // it is mounted here, before the catch-all below, which would otherwise answer every /internal/ request.
++ if (opts.accountData) app.post('/internal/events', opts.accountData.consumer({ secrets: [config.events.secret].filter((s) => s.length >= 32), send: opts.accountSend || null }));
++
app.use((req, res) => {
if (req.path.startsWith('/api/') || req.path.startsWith('/internal/')) return http.sendProblem(res, 404, 'not_found', { ctx: req.ov });
return res.status(404).type('text/plain').send('Not found\n');
ddiff --git a/server/index.js b/server/index.js
iindex 8b1b4a6..0717624 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -37,7 +37,7 @@ async function main() {
const accountData = accountDataLib.create({ db, log: console });
const accountSend = config.oauth.clientSecret ? createNetworkSender({ networkInternalUrl: config.network.internalUrl, clientId: config.oauth.clientId, clientSecret: config.oauth.clientSecret }) : null;
-- const app = createApp({ config, db, valkey, registry });
++ const app = createApp({ config, db, valkey, registry, accountData, accountSend });
const { keys, outbox, stream, tickets, dispatch, indexnow, sessions } = app.locals;
keys.start();
const ticketKey = tickets.start();
@@@ -52,7 +52,6 @@ async function main() {
sessions.start();
}
-- app.post('/internal/events', accountData.consumer({ secrets: [config.events.secret].filter((s) => s.length >= 32), send: accountSend }));
let accountSubscription = null;
const server = app.listen(config.port, config.host, () => {
console.log(`[Run] ${config.nodeEnv} on http://${config.host}:${config.port} → ${config.baseUrl} (store ${db.store}; valkey ${valkey ? 'on' : 'off: this process only'})`);
ddiff --git a/test/account-data.test.js b/test/account-data.test.js
iindex 7c6e9a0..42b8ffc 100644
---- a/test/account-data.test.js
++++ b/test/account-data.test.js
@@@ -3,10 +3,14 @@
* ADR-033: Run's part of an account export and deletion. A person's open browser sessions are exported (their
* settings and timing, never a ticket) and deleted; someone else's stay. The guard fails when a new table with a
* person column is neither covered nor exempted with a reason.
++ *
++ * The consumer's route is checked too: POST /internal/events answers the delivery (a bad signature is refused),
++ * so a route that never answers is caught here and not in production.
*/
const assert = require('assert');
const { uncovered } = require('openvibe-sdk/account-data');
const { testDb } = require('./helpers/db');
++const { boot } = require('./helpers/app');
const { TABLES, create } = require('../server/account-data');
const A = 'usr_01JZ0000000000000000000AAA';
@@@ -41,6 +45,12 @@ const B = 'usr_01JZ0000000000000000000BBB';
assert.strictEqual(await d.db.value('SELECT requester FROM run_jobs WHERE id = $1', ['job_01JZ0000000000000000000ADA']), 'user:deleted', 'the job stays, without its requester');
assert.strictEqual(await d.db.value('SELECT requester FROM run_jobs WHERE id = $1', ['job_01JZ0000000000000000000BQB']), `user:${B}`, 'Bo\'s job is untouched');
assert.strictEqual(counts.retained.run_jobs_tombstoned, 1);
-- console.log(' ✓ export, erase and the guard');
++
++ // The route answers: an unsigned delivery is refused (401), not lost (404) and not accepted.
++ const t = await boot({ db: d.db, env: { RUN_EVENTS_SECRET: 's'.repeat(40) }, appOpts: { accountData } });
++ const r = await t.call('POST', '/internal/events', { body: {}, token: null });
++ assert.strictEqual(r.status, 401, `the consumer refuses an unsigned delivery: ${r.text}`);
++ await t.close();
++ console.log(' ✓ export, erase, the guard and the route');
} finally { await d.close(); }
})();