openvibers/openvibe.live Commits

OpenRe mirror requires Events signature v2 (replay window); openvibe-sdk v0.4.0

OP OpenVibers committed 23 Sept 2026 · ae07553168 · 4 files, +17 −13

POST /internal/openre-events calls parseDelivery(..., { requireV2: true }): a delivery is
accepted only when X-OpenVibe-Signature-V2 (HMAC of "<t>.<raw body>") verifies and t is within
300 s of this clock. A v1-only delivery (v2 stripped from a capture) or a stale v2 is 401.
test/openre-switch: deliveries carry all three signature headers (signDeliveryHeaders); the bad
signature case breaks v2 too; new v1-only and stale-v2 (301 s) refusals.

Changes

ddiff --git a/package-lock.json b/package-lock.json
iindex 39a52970..35e4b691 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -26,7 +26,7 @@
"node-media-server": "^2.7.1",
"onnxruntime-node": "^1.20.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.29.0",
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.2.2",
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0",
"sanitize-html": "^2.17.7",
"sdp-transform": "^3.0.0",
@@@ -2830,9 +2830,9 @@
}
},
"node_modules/openvibe-sdk": {
-- "version": "0.2.2",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.2.2",
-- "integrity": "sha512-kDifUp3EcmnTWzElAmaCZnX659WErcnm4+qVEoAiYQWX/w/1p4TWvoHbUPsrX65TSROVR3tiyVkl801n/0yyCw==",
++ "version": "0.4.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
++ "integrity": "sha512-Hyz55YOMIs0xwCQ1NXsN0sUYhlbKXAX3uWY2fPCdEEL18FXUYVhK8wlG/5JMMypFHnjDiSd9VHzBNG12hWsmkQ==",
"license": "MIT",
"engines": {
"node": ">=20"
ddiff --git a/package.json b/package.json
iindex e9c700c5..027046b4 100644
---- a/package.json
++++ b/package.json
@@@ -44,7 +44,7 @@
"node-media-server": "^2.7.1",
"onnxruntime-node": "^1.20.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.29.0",
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.2.2",
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0",
"sanitize-html": "^2.17.7",
"sdp-transform": "^3.0.0",
ddiff --git a/server/openre/mirror.js b/server/openre/mirror.js
iindex 5a32b1c1..bfee9876 100644
---- a/server/openre/mirror.js
++++ b/server/openre/mirror.js
@@@ -138,7 +138,8 @@ function webhookHandler(req, res) {
const secret = process.env.OPENRE_EVENTS_SECRET || '';
if (!secret) return res.status(503).json({ error: 'OPENRE_EVENTS_SECRET is not set' });
const { parseDelivery } = require('openvibe-sdk/events');
-- const delivery = parseDelivery(req.rawBody, req.headers, secret);
++ // Signature v2 only: HMAC over "<t>.<raw body>" with t within ±300 s; a v1-only (v2 stripped) or stale delivery is refused.
++ const delivery = parseDelivery(req.rawBody, req.headers, secret, { requireV2: true });
if (!delivery) return res.status(401).json({ error: 'bad signature' });
// Signed but unusable (no event_id): acknowledge so it is not redelivered forever.
if (!delivery.event || !delivery.event.event_id) return res.status(204).end();
ddiff --git a/test/openre-switch.test.js b/test/openre-switch.test.js
iindex 6ffdc336..b4a4152a 100644
---- a/test/openre-switch.test.js
++++ b/test/openre-switch.test.js
@@@ -50,15 +50,16 @@ const stub = http.createServer((req, res) => {
});
});
--function signed(event, secret) {
-- const { signDelivery } = require('openvibe-sdk/events');
++function signed(event, secret, { now } = {}) {
++ const { signDelivery, signDeliveryHeaders } = require('openvibe-sdk/events');
const raw = Buffer.from(JSON.stringify({ event, seq: 1 }));
-- return { raw, sig: signDelivery(raw, secret) };
++ return { raw, headers: signDeliveryHeaders(raw, secret, { now }), v1: { 'X-OpenVibe-Signature': signDelivery(raw, secret) } };
}
--async function deliver(base, event, { secret = 'whsec_test', sig } = {}) {
-- const s = signed(event, secret);
-- const res = await fetch(`${base}/internal/openre-events`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-OpenVibe-Signature': sig || s.sig }, body: s.raw });
++/** POST a delivery: all three signature headers by default; `v1Only` sends only X-OpenVibe-Signature; `now` backdates the v2 timestamp. */
++async function deliver(base, event, { secret = 'whsec_test', headers, v1Only = false, now } = {}) {
++ const s = signed(event, secret, { now });
++ const res = await fetch(`${base}/internal/openre-events`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(headers || (v1Only ? s.v1 : s.headers)) }, body: s.raw });
return res.status;
}
@@@ -162,7 +163,9 @@ function sessionEvent(type, sessionId, revision, payload = {}) {
assert.strictEqual(openreCalls.filter(c => c.url.endsWith('/keys/rotate')).pop().subject, SUBJECT);
// ── Mirror ────────────────────────────────────────────────
-- assert.strictEqual(await deliver(base, sessionEvent('started', SES(3), 2), { sig: 'sha256=00' }), 401, 'bad signature');
++ assert.strictEqual(await deliver(base, sessionEvent('started', SES(3), 2), { headers: { 'X-OpenVibe-Signature': 'sha256=00', 'X-OpenVibe-Timestamp': '1', 'X-OpenVibe-Signature-V2': 't=1,v2=00' } }), 401, 'bad signature');
++ assert.strictEqual(await deliver(base, sessionEvent('started', SES(3), 2), { v1Only: true }), 401, 'v1 only (no v2 header): refused');
++ assert.strictEqual(await deliver(base, sessionEvent('started', SES(3), 2), { now: Date.now() - 301000 }), 401, 'stale v2 (outside the 300 s window): refused');
assert.strictEqual(await deliver(base, sessionEvent('started', SES(3), 2, { mirror_to_live: false })), 204);
assert.strictEqual(d.prepare('SELECT COUNT(*) AS n FROM streams WHERE is_live = 1').get().n, 0, 'no consent, no mirror');