openvibers/openvibe.live Commits
OpenRe mirror requires Events signature v2 (replay window); openvibe-sdk v0.4.0
Changes
@@@ -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"@@@ -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",@@@ -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();@@@ -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');
@@ -26,7 +26,7 @@
@@ -26,7 +26,7 @@
"node-media-server": "^2.7.1",
"node-media-server": "^2.7.1",
"onnxruntime-node": "^1.20.0",
"onnxruntime-node": "^1.20.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.29.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",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0",
"sanitize-html": "^2.17.7",
"sanitize-html": "^2.17.7",
"sdp-transform": "^3.0.0",
"sdp-transform": "^3.0.0",
@@ -2830,9 +2830,9 @@
@@ -2830,9 +2830,9 @@
}
}
},
},
"node_modules/openvibe-sdk": {
"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",
"license": "MIT",
"engines": {
"engines": {
"node": ">=20"
"node": ">=20"
@@ -44,7 +44,7 @@
@@ -44,7 +44,7 @@
"node-media-server": "^2.7.1",
"node-media-server": "^2.7.1",
"onnxruntime-node": "^1.20.0",
"onnxruntime-node": "^1.20.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.29.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",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0",
"sanitize-html": "^2.17.7",
"sanitize-html": "^2.17.7",
"sdp-transform": "^3.0.0",
"sdp-transform": "^3.0.0",
@@ -138,7 +138,8 @@ function webhookHandler(req, res) {
@@ -138,7 +138,8 @@ function webhookHandler(req, res) {
const secret = process.env.OPENRE_EVENTS_SECRET || '';
const secret = process.env.OPENRE_EVENTS_SECRET || '';
if (!secret) return res.status(503).json({ error: 'OPENRE_EVENTS_SECRET is not set' });
if (!secret) return res.status(503).json({ error: 'OPENRE_EVENTS_SECRET is not set' });
const { parseDelivery } = require('openvibe-sdk/events');
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' });
if (!delivery) return res.status(401).json({ error: 'bad signature' });
// Signed but unusable (no event_id): acknowledge so it is not redelivered forever.
// 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();
if (!delivery.event || !delivery.event.event_id) return res.status(204).end();
@@ -50,15 +50,16 @@ const stub = http.createServer((req, res) => {
@@ -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 }));
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;
return res.status;
}
}
@@ -162,7 +163,9 @@ function sessionEvent(type, sessionId, revision, payload = {}) {
@@ -162,7 +163,9 @@ function sessionEvent(type, sessionId, revision, payload = {}) {
assert.strictEqual(openreCalls.filter(c => c.url.endsWith('/keys/rotate')).pop().subject, SUBJECT);
assert.strictEqual(openreCalls.filter(c => c.url.endsWith('/keys/rotate')).pop().subject, SUBJECT);
// ── Mirror ────────────────────────────────────────────────
// ── 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(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');
assert.strictEqual(d.prepare('SELECT COUNT(*) AS n FROM streams WHERE is_live = 1').get().n, 0, 'no consent, no mirror');