openvibers/openvibe.bot Commits
Bot: a machine revoked on OpenVibe.Network stops at once (network.node.revoked revokes its devices and closes the socket) (#112)
Changes
@@@ -131,7 +131,19 @@ function createApp(opts = {}) {const accountSend = opts.accountSend || (config.oauth.clientSecret? createNetworkSender({ networkInternalUrl: config.network.internalUrl, clientId: config.oauth.clientId, clientSecret: config.oauth.clientSecret, fetch: fetchImpl }): async () => { throw new Error('OV_OAUTH_CLIENT_SECRET is not set: Bot cannot answer account events'); });-- app.post('/internal/events', accountData.consumer({ secrets: config.events.secrets, send: accountSend, log }));++ // network.node.revoked (a person's paired machine revoked on Network): its devices are revoked and a live session is++ // closed at once, instead of at the machine's next reauth deadline.++ const onEvent = async (ev) => {++ if (ev && ev.event_type === 'network.node.revoked') {++ const principal = String((ev.payload && ev.payload.principal_id) || '');++ if (!/^nod_[0-9A-HJKMNP-TV-Z]{26}$/.test(principal)) return 'applied';++ const revoked = await domain.devices.revokeByNode(principal);++ for (const d of revoked) hub.closeDevice(d.id, 'machine revoked on OpenVibe.Network');++ if (revoked.length) log.log(`[Bot] node ${principal} revoked on Network: ${revoked.length} device(s) revoked`);++ }++ return 'applied';++ };++ app.post('/internal/events', accountData.consumer({ secrets: config.events.secrets, send: accountSend, onEvent, log }));// The OpenVibe Frame's scripts and the showcase stylesheet the front page links (content-hashed URLs).app.use('/shared', require('openvibe-shared/serve').handler());@@@ -601,6 +601,17 @@ function createDomain({ db, config, outbox, link = null, nodes = null, openre =return updated;}++ /**++ * Network revoked a node principal (network.node.revoked: its owner, the pairing service or an account deletion):++ * every live device bound to it is revoked here too, without asking Network again. → the revoked rows++ */++ async function revokeByNode(principalId) {++ const at = iso(now());++ const rows = await db.many('UPDATE devices SET revoked_at = $2, credential_prev_hash = NULL, prev_valid_until = NULL, updated_at = $2 WHERE node_principal = $1 AND revoked_at IS NULL RETURNING *', [String(principalId), at]);++ if (rows.length) changed();++ return rows;++ }++// ── Network-paired devices (node principals, T2 §9.2 B1) ──────────────────────────────────────/*** The live device bound to Network node principal `principalId`, creating it on first use. Idempotent:@@@ -1087,7 +1098,7 @@ function createDomain({ db, config, outbox, link = null, nodes = null, openre =localProfile, saveLocalProfile, useCatalogueProfile },members: { roleOf, add: addOperator, remove: removeOperator, list: listOperators },pairing: { create: createPairingCode, redeem, prune: prunePairingCodes, installerCommand, driverForProfile, whipUrl },-- devices: { byCredential, bindNode, issuePublishKey, revokeVideo, updateDeclared, revokeNode, ensureServerDevice, get: getDevice, listForRobot: listDevicesForRobot, rotate: rotateDevice, revoke: revokeDevice, touchSeen, setOnline, ownerOf, onChange },++ devices: { byCredential, bindNode, issuePublishKey, revokeVideo, updateDeclared, revokeNode, ensureServerDevice, get: getDevice, listForRobot: listDevicesForRobot, rotate: rotateDevice, revoke: revokeDevice, revokeByNode, touchSeen, setOnline, ownerOf, onChange },machines: { mint: mintMachine, list: listMachines, workerRef, setAgentComputer, setWatchProbe },streaming: { get: streaming, set: setStreaming },video: { live: liveVideo },@@@ -19,7 +19,7 @@ const { seedProfiles } = require('./profiles');const { createApp } = require('./app');const { createValkey } = require('openvibe-sdk/valkey');const { gracefulStop } = require('openvibe-sdk/service');--const { startSubscriptions } = require('openvibe-sdk/account-data');++const { startSubscriptions, TOPICS } = require('openvibe-sdk/account-data');const { createRegistry } = require('openvibe-shared/metrics');async function main() {@@@ -68,7 +68,9 @@ async function main() {// kit's 5000 ms default is the right value.// The two account subscriptions at OpenVibe.Events (ADR-033), created when missing; off without EVENTS_URL,// BOT_EVENTS_SECRET or the client secret.++ // Plus network.node.revoked: a machine revoked on Network is revoked here at once (app.js onEvent).const subscriptions = startSubscriptions({++ topics: [...TOPICS, 'network.node.revoked'],eventsUrl: config.events.url, endpoint: `http://127.0.0.1:${config.port}/internal/events`, secret: config.events.secrets[0],networkInternalUrl: config.network.internalUrl, clientId: config.oauth.clientId, clientSecret: config.oauth.clientSecret,});@@@ -4,12 +4,14 @@// as for a credential, `status` persists what the machine declares, `reauth` keeps the socket, and revoke// and rotate reach Network and the machine. Credential devices keep today's behaviour.const assert = require('assert');++const { signDeliveryHeaders } = require('openvibe-sdk/events');const { boot, check, done } = require('./helpers/app');const WHIP_BASE = 'https://whip.test/ingest';++const EVENTS_SECRET = `whsec_${'noderevoked'.repeat(4)}`;(async () => {-- const t = await boot({ env: { BOT_WHIP_BASE: WHIP_BASE } });++ const t = await boot({ env: { BOT_WHIP_BASE: WHIP_BASE, BOT_EVENTS_SECRET: EVENTS_SECRET } });const alex = t.network.newUser('alex');const sam = t.network.newUser('sam');@@@ -202,6 +204,27 @@ const WHIP_BASE = 'https://whip.test/ingest';c.close();});++ await check('network.node.revoked revokes the machine\'s devices and closes its live socket at once', async () => {++ const { principal } = await paired();++ const c = await connect(t.network.signNode(principal));++ const hello = await c.waitFor((m) => m.type === 'hello');++ assert.ok(t.hub.isOnline(hello.device_id));++ const event = {++ event_id: 'evt_01JZ0000000000000000000N01', event_type: 'network.node.revoked', version: 1, source: 'network',++ actor: { type: 'user', id: alex.subject }, timestamp: new Date().toISOString(), visibility: 'internal',++ subject: { type: 'node', id: principal },++ payload: { node_id: `n-${'a'.repeat(26)}`, principal_id: principal, owner: { kind: 'user', subject: alex.subject }, reason: 'owner', at: new Date().toISOString() },++ };++ const body = JSON.stringify({ event }); // Events delivers { cursor?, event }++ const res = await fetch(`${t.base}/internal/events`, { method: 'POST', body, headers: { 'Content-Type': 'application/json', ...signDeliveryHeaders(body, EVENTS_SECRET) } });++ assert.strictEqual(res.status < 300, true, `${res.status} ${await res.text()}`);++ assert.strictEqual(await c.waitForClose(), 4003, 'the socket closes at once, not at the reauth deadline');++ assert.ok((await deviceRow(hello.device_id)).revoked_at, 'the device is revoked');++ assert.ok(!t.hub.isOnline(hello.device_id));++ const again = await fetch(`${t.base}/internal/events`, { method: 'POST', body, headers: { 'Content-Type': 'application/json', ...signDeliveryHeaders(body, EVENTS_SECRET) } });++ assert.strictEqual(again.status < 300, true, 'a redelivery is harmless');++ });++await t.close();done();})();
@@ -131,7 +131,19 @@ function createApp(opts = {}) {
@@ -131,7 +131,19 @@ function createApp(opts = {}) {
const accountSend = opts.accountSend || (config.oauth.clientSecret
const accountSend = opts.accountSend || (config.oauth.clientSecret
? createNetworkSender({ networkInternalUrl: config.network.internalUrl, clientId: config.oauth.clientId, clientSecret: config.oauth.clientSecret, fetch: fetchImpl })
? createNetworkSender({ networkInternalUrl: config.network.internalUrl, clientId: config.oauth.clientId, clientSecret: config.oauth.clientSecret, fetch: fetchImpl })
: async () => { throw new Error('OV_OAUTH_CLIENT_SECRET is not set: Bot cannot answer account events'); });
: async () => { throw new Error('OV_OAUTH_CLIENT_SECRET is not set: Bot cannot answer account events'); });
- app.post('/internal/events', accountData.consumer({ secrets: config.events.secrets, send: accountSend, log }));
+ // network.node.revoked (a person's paired machine revoked on Network): its devices are revoked and a live session is
+ // closed at once, instead of at the machine's next reauth deadline.
+ const onEvent = async (ev) => {
+ if (ev && ev.event_type === 'network.node.revoked') {
+ const principal = String((ev.payload && ev.payload.principal_id) || '');
+ if (!/^nod_[0-9A-HJKMNP-TV-Z]{26}$/.test(principal)) return 'applied';
+ const revoked = await domain.devices.revokeByNode(principal);
+ for (const d of revoked) hub.closeDevice(d.id, 'machine revoked on OpenVibe.Network');
+ if (revoked.length) log.log(`[Bot] node ${principal} revoked on Network: ${revoked.length} device(s) revoked`);
+ }
+ return 'applied';
+ };
+ app.post('/internal/events', accountData.consumer({ secrets: config.events.secrets, send: accountSend, onEvent, log }));
// The OpenVibe Frame's scripts and the showcase stylesheet the front page links (content-hashed URLs).
// The OpenVibe Frame's scripts and the showcase stylesheet the front page links (content-hashed URLs).
app.use('/shared', require('openvibe-shared/serve').handler());
app.use('/shared', require('openvibe-shared/serve').handler());
@@ -601,6 +601,17 @@ function createDomain({ db, config, outbox, link = null, nodes = null, openre =
@@ -601,6 +601,17 @@ function createDomain({ db, config, outbox, link = null, nodes = null, openre =
return updated;
return updated;
}
}
+ /**
+ * Network revoked a node principal (network.node.revoked: its owner, the pairing service or an account deletion):
+ * every live device bound to it is revoked here too, without asking Network again. → the revoked rows
+ */
+ async function revokeByNode(principalId) {
+ const at = iso(now());
+ const rows = await db.many('UPDATE devices SET revoked_at = $2, credential_prev_hash = NULL, prev_valid_until = NULL, updated_at = $2 WHERE node_principal = $1 AND revoked_at IS NULL RETURNING *', [String(principalId), at]);
+ if (rows.length) changed();
+ return rows;
+ }
+
// ── Network-paired devices (node principals, T2 §9.2 B1) ──────────────────────────────────────
// ── Network-paired devices (node principals, T2 §9.2 B1) ──────────────────────────────────────
/**
/**
* The live device bound to Network node principal `principalId`, creating it on first use. Idempotent:
* The live device bound to Network node principal `principalId`, creating it on first use. Idempotent:
@@ -1087,7 +1098,7 @@ function createDomain({ db, config, outbox, link = null, nodes = null, openre =
@@ -1087,7 +1098,7 @@ function createDomain({ db, config, outbox, link = null, nodes = null, openre =
localProfile, saveLocalProfile, useCatalogueProfile },
localProfile, saveLocalProfile, useCatalogueProfile },
members: { roleOf, add: addOperator, remove: removeOperator, list: listOperators },
members: { roleOf, add: addOperator, remove: removeOperator, list: listOperators },
pairing: { create: createPairingCode, redeem, prune: prunePairingCodes, installerCommand, driverForProfile, whipUrl },
pairing: { create: createPairingCode, redeem, prune: prunePairingCodes, installerCommand, driverForProfile, whipUrl },
- devices: { byCredential, bindNode, issuePublishKey, revokeVideo, updateDeclared, revokeNode, ensureServerDevice, get: getDevice, listForRobot: listDevicesForRobot, rotate: rotateDevice, revoke: revokeDevice, touchSeen, setOnline, ownerOf, onChange },
+ devices: { byCredential, bindNode, issuePublishKey, revokeVideo, updateDeclared, revokeNode, ensureServerDevice, get: getDevice, listForRobot: listDevicesForRobot, rotate: rotateDevice, revoke: revokeDevice, revokeByNode, touchSeen, setOnline, ownerOf, onChange },
machines: { mint: mintMachine, list: listMachines, workerRef, setAgentComputer, setWatchProbe },
machines: { mint: mintMachine, list: listMachines, workerRef, setAgentComputer, setWatchProbe },
streaming: { get: streaming, set: setStreaming },
streaming: { get: streaming, set: setStreaming },
video: { live: liveVideo },
video: { live: liveVideo },
@@ -19,7 +19,7 @@ const { seedProfiles } = require('./profiles');
@@ -19,7 +19,7 @@ const { seedProfiles } = require('./profiles');
const { createApp } = require('./app');
const { createApp } = require('./app');
const { createValkey } = require('openvibe-sdk/valkey');
const { createValkey } = require('openvibe-sdk/valkey');
const { gracefulStop } = require('openvibe-sdk/service');
const { gracefulStop } = require('openvibe-sdk/service');
-const { startSubscriptions } = require('openvibe-sdk/account-data');
+const { startSubscriptions, TOPICS } = require('openvibe-sdk/account-data');
const { createRegistry } = require('openvibe-shared/metrics');
const { createRegistry } = require('openvibe-shared/metrics');
async function main() {
async function main() {
@@ -68,7 +68,9 @@ async function main() {
@@ -68,7 +68,9 @@ async function main() {
// kit's 5000 ms default is the right value.
// kit's 5000 ms default is the right value.
// The two account subscriptions at OpenVibe.Events (ADR-033), created when missing; off without EVENTS_URL,
// The two account subscriptions at OpenVibe.Events (ADR-033), created when missing; off without EVENTS_URL,
// BOT_EVENTS_SECRET or the client secret.
// BOT_EVENTS_SECRET or the client secret.
+ // Plus network.node.revoked: a machine revoked on Network is revoked here at once (app.js onEvent).
const subscriptions = startSubscriptions({
const subscriptions = startSubscriptions({
+ topics: [...TOPICS, 'network.node.revoked'],
eventsUrl: config.events.url, endpoint: `http://127.0.0.1:${config.port}/internal/events`, secret: config.events.secrets[0],
eventsUrl: config.events.url, endpoint: `http://127.0.0.1:${config.port}/internal/events`, secret: config.events.secrets[0],
networkInternalUrl: config.network.internalUrl, clientId: config.oauth.clientId, clientSecret: config.oauth.clientSecret,
networkInternalUrl: config.network.internalUrl, clientId: config.oauth.clientId, clientSecret: config.oauth.clientSecret,
});
});
@@ -4,12 +4,14 @@
@@ -4,12 +4,14 @@
// as for a credential, `status` persists what the machine declares, `reauth` keeps the socket, and revoke
// as for a credential, `status` persists what the machine declares, `reauth` keeps the socket, and revoke
// and rotate reach Network and the machine. Credential devices keep today's behaviour.
// and rotate reach Network and the machine. Credential devices keep today's behaviour.
const assert = require('assert');
const assert = require('assert');
+const { signDeliveryHeaders } = require('openvibe-sdk/events');
const { boot, check, done } = require('./helpers/app');
const { boot, check, done } = require('./helpers/app');
const WHIP_BASE = 'https://whip.test/ingest';
const WHIP_BASE = 'https://whip.test/ingest';
+const EVENTS_SECRET = `whsec_${'noderevoked'.repeat(4)}`;
(async () => {
(async () => {
- const t = await boot({ env: { BOT_WHIP_BASE: WHIP_BASE } });
+ const t = await boot({ env: { BOT_WHIP_BASE: WHIP_BASE, BOT_EVENTS_SECRET: EVENTS_SECRET } });
const alex = t.network.newUser('alex');
const alex = t.network.newUser('alex');
const sam = t.network.newUser('sam');
const sam = t.network.newUser('sam');
@@ -202,6 +204,27 @@ const WHIP_BASE = 'https://whip.test/ingest';
@@ -202,6 +204,27 @@ const WHIP_BASE = 'https://whip.test/ingest';
c.close();
c.close();
});
});
+ await check('network.node.revoked revokes the machine\'s devices and closes its live socket at once', async () => {
+ const { principal } = await paired();
+ const c = await connect(t.network.signNode(principal));
+ const hello = await c.waitFor((m) => m.type === 'hello');
+ assert.ok(t.hub.isOnline(hello.device_id));
+ const event = {
+ event_id: 'evt_01JZ0000000000000000000N01', event_type: 'network.node.revoked', version: 1, source: 'network',
+ actor: { type: 'user', id: alex.subject }, timestamp: new Date().toISOString(), visibility: 'internal',
+ subject: { type: 'node', id: principal },
+ payload: { node_id: `n-${'a'.repeat(26)}`, principal_id: principal, owner: { kind: 'user', subject: alex.subject }, reason: 'owner', at: new Date().toISOString() },
+ };
+ const body = JSON.stringify({ event }); // Events delivers { cursor?, event }
+ const res = await fetch(`${t.base}/internal/events`, { method: 'POST', body, headers: { 'Content-Type': 'application/json', ...signDeliveryHeaders(body, EVENTS_SECRET) } });
+ assert.strictEqual(res.status < 300, true, `${res.status} ${await res.text()}`);
+ assert.strictEqual(await c.waitForClose(), 4003, 'the socket closes at once, not at the reauth deadline');
+ assert.ok((await deviceRow(hello.device_id)).revoked_at, 'the device is revoked');
+ assert.ok(!t.hub.isOnline(hello.device_id));
+ const again = await fetch(`${t.base}/internal/events`, { method: 'POST', body, headers: { 'Content-Type': 'application/json', ...signDeliveryHeaders(body, EVENTS_SECRET) } });
+ assert.strictEqual(again.status < 300, true, 'a redelivery is harmless');
+ });
+
await t.close();
await t.close();
done();
done();
})();
})();