openvibers/openvibe.tips Commits

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

OP OpenVibers committed 23 Sept 2026 · 435f32b404 · 5 files, +20 −13

POST /internal/events calls parseDelivery(..., { requireV2: true }) for each configured secret:
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 tips.bad_signature, so a captured Billing delivery cannot be replayed later.

Tests: the delivery helper sends signDeliveryHeaders; new v1-only and stale-v2 (301 s) refusals.

Changes

ddiff --git a/package-lock.json b/package-lock.json
iindex 9b4fd65..3b9fed6 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -15,7 +15,7 @@
"express": "^4.21.2",
"jsonwebtoken": "^9.0.2",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.15.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.2.1"
},
"engines": {
@@@ -999,9 +999,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 12799b6..54e9d5c 100644
---- a/package.json
++++ b/package.json
@@@ -22,7 +22,7 @@
"express": "^4.21.2",
"jsonwebtoken": "^9.0.2",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.15.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.2.1"
},
"engines": {
ddiff --git a/server/events/consumer.js b/server/events/consumer.js
iindex dc721b1..b81bfd6 100644
---- a/server/events/consumer.js
++++ b/server/events/consumer.js
@@@ -48,7 +48,8 @@ function consumerRouter({ domain, config, log = console }) {
if (!secrets.length) return http.sendProblem(res, 503, 'tips.webhook_disabled', { detail: 'TIPS_EVENTS_SECRET is not set', ctx: req.ov });
const raw = Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0);
let delivery = null;
-- for (const s of secrets) { delivery = parseDelivery(raw, req.headers, s); if (delivery) break; }
++ // Signature v2 only: HMAC over "<t>.<raw body>" with t within ±300 s; a v1-only (v2 stripped) or stale delivery is refused.
++ for (const s of secrets) { delivery = parseDelivery(raw, req.headers, s, { requireV2: true }); if (delivery) break; }
if (!delivery) return http.sendProblem(res, 401, 'tips.bad_signature', { detail: 'X-OpenVibe-Signature does not verify', ctx: req.ov });
const event = delivery.event;
if (!event || typeof event.event_id !== 'string' || !/^evt_[0-9A-HJKMNP-TV-Z]{26}$/.test(event.event_id)) {
ddiff --git a/test/helpers/app.js b/test/helpers/app.js
iindex cf52638..3c79ac7 100644
---- a/test/helpers/app.js
++++ b/test/helpers/app.js
@@@ -13,7 +13,7 @@ const os = require('os');
const path = require('path');
const http = require('http');
const crypto = require('crypto');
--const { signDelivery } = require('openvibe-sdk/events');
++const { signDelivery, signDeliveryHeaders } = require('openvibe-sdk/events');
const { startNetwork, startBilling, startEvents, startLive } = require('./stubs');
const EVENTS_SECRET = 'e'.repeat(48);
@@@ -70,12 +70,16 @@ async function boot(opts = {}) {
return { status: res.status, headers: res.headers, json, text };
}
-- /** POST a signed Events delivery of `event` (as OpenVibe.Events would). */
-- async function deliver(event, { secret = EVENTS_SECRET, seq = 1 } = {}) {
++ /**
++ * POST a signed Events delivery of `event` (as OpenVibe.Events would: v1 and v2 headers).
++ * `v1Only` sends only X-OpenVibe-Signature; `now` (ms) backdates the v2 timestamp.
++ */
++ async function deliver(event, { secret = EVENTS_SECRET, seq = 1, v1Only = false, now } = {}) {
const raw = JSON.stringify({ event, seq });
++ const sig = v1Only ? { 'X-OpenVibe-Signature': signDelivery(raw, secret) } : signDeliveryHeaders(raw, secret, { now });
const res = await fetch(`${base}/internal/events`, {
method: 'POST', body: raw,
-- headers: { 'Content-Type': 'application/json', 'X-OpenVibe-Signature': signDelivery(raw, secret), 'X-OpenVibe-Seq': String(seq) },
++ headers: { 'Content-Type': 'application/json', ...sig, 'X-OpenVibe-Seq': String(seq) },
});
return { status: res.status, json: await res.json().catch(() => null) };
}
ddiff --git a/test/settlement.test.js b/test/settlement.test.js
iindex 2a9250f..274ff3d 100644
---- a/test/settlement.test.js
++++ b/test/settlement.test.js
@@@ -234,13 +234,15 @@ const { boot, check, done } = require('./helpers/app');
assert.strictEqual(totals.external, 500);
});
-- await check('the Billing webhook: bad signatures refused, proxied requests hidden, other sources ignored', async () => {
++ await check('the Billing webhook: bad, v1-only and stale signatures refused, proxied requests hidden, other sources ignored', async () => {
const ev = billing.foreignDonation({ from: null, to: alex.subject, amount: 5 });
const bad = await t.deliver(ev, { secret: 'x'.repeat(48) });
assert.strictEqual(bad.status, 401);
++ assert.strictEqual((await t.deliver(ev, { v1Only: true })).status, 401, 'v1 only (no v2 header): refused');
++ assert.strictEqual((await t.deliver(ev, { now: Date.now() - 301000 })).status, 401, 'stale v2 (outside the 300 s window): refused');
const raw = JSON.stringify({ event: ev, seq: 1 });
-- const { signDelivery } = require('openvibe-sdk/events');
-- const proxied = await fetch(`${t.base}/internal/events`, { method: 'POST', body: raw, headers: { 'Content-Type': 'application/json', 'X-OpenVibe-Signature': signDelivery(raw, 'e'.repeat(48)), 'X-Forwarded-For': '203.0.113.9' } });
++ const { signDeliveryHeaders } = require('openvibe-sdk/events');
++ const proxied = await fetch(`${t.base}/internal/events`, { method: 'POST', body: raw, headers: { 'Content-Type': 'application/json', ...signDeliveryHeaders(raw, 'e'.repeat(48)), 'X-Forwarded-For': '203.0.113.9' } });
assert.strictEqual(proxied.status, 404);
const forged = await t.deliver({ ...ev, source: 'live' });
assert.strictEqual(forged.json.outcome, 'ignored:source');