openvibers/openvibe.deals Commits

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

OP OpenVibers committed 23 Sept 2026 · e72aeeccb6 · 4 files, +16 −14

POST /internal/events verifies with verifyDeliveryV2(raw, req.headers, secret) for each
configured secret: X-OpenVibe-Signature-V2 must match "<t>.<raw body>" and t must be within
300 s of this clock. A v1-only delivery (v2 stripped from a capture) or a stale v2 is
401 deals.bad_signature; v1 is no longer consulted.

Tests: deliveries use signDeliveryHeaders; new v1-only and stale-v2 (301 s) refusals.

Changes

ddiff --git a/package-lock.json b/package-lock.json
iindex c412fb2..7c99bcf 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -17,7 +17,7 @@
"helmet": "^8.1.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.23.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v0.2.1",
-- "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"
},
"devDependencies": {
@@@ -1060,9 +1060,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 52f2f2a..0651c88 100644
---- a/package.json
++++ b/package.json
@@@ -22,7 +22,7 @@
"helmet": "^8.1.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.23.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v0.2.1",
-- "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"
},
"devDependencies": {
ddiff --git a/server/http/internal.js b/server/http/internal.js
iindex bcd48ba..4d1517b 100644
---- a/server/http/internal.js
++++ b/server/http/internal.js
@@@ -11,7 +11,7 @@
*/
const express = require('express');
const { http } = require('openvibe-contracts');
--const { verifyDelivery, createInbox } = require('openvibe-sdk/events');
++const { verifyDeliveryV2, createInbox } = require('openvibe-sdk/events');
const CONSUMER = 'deals-sources';
const TYPES = new Set(['sources.item.created', 'sources.item.updated', 'sources.item.removed']);
@@@ -25,8 +25,8 @@ function createInternalRoutes({ config, store, importer }) {
const secrets = config.events.webhookSecrets;
if (!secrets.length) return http.sendProblem(res, 503, 'deals.webhook_disabled', { detail: 'DEALS_EVENTS_SECRET is not set', ctx: req.ov });
const raw = Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0);
-- const sig = req.get('x-openvibe-signature');
-- if (!secrets.some((s) => verifyDelivery(raw, sig, s))) return http.sendProblem(res, 401, 'deals.bad_signature', { detail: 'X-OpenVibe-Signature does not verify', ctx: req.ov });
++ // v2 only: signature over "<t>.<raw body>" and t within ±300 s (a replayed or v1-only delivery fails).
++ if (!secrets.some((s) => verifyDeliveryV2(raw, req.headers, s))) return http.sendProblem(res, 401, 'deals.bad_signature', { detail: 'X-OpenVibe-Signature-V2 does not verify or is outside the replay window', ctx: req.ov });
let body;
try { body = JSON.parse(raw.toString('utf8')); } catch { body = null; }
const event = body && body.event;
ddiff --git a/test/import.test.js b/test/import.test.js
iindex 1d67df5..7583430 100644
---- a/test/import.test.js
++++ b/test/import.test.js
@@@ -6,7 +6,7 @@
const assert = require('assert');
const { boot, check, done, robotsOf, HOUR } = require('./helpers/boot');
const { sourceItem } = require('./helpers/mocks');
--const { signDelivery } = require('openvibe-sdk/events');
++const { signDelivery, signDeliveryHeaders } = require('openvibe-sdk/events');
(async () => {
const t = await boot();
@@@ -125,17 +125,19 @@ const { signDelivery } = require('openvibe-sdk/events');
assert.ok(!(await t.get('/feed.xml')).text.includes(garden.slug));
});
-- await check('the signed event wake-up: bad signature 401, deals items scheduled once, others ignored', async () => {
++ await check('the signed event wake-up: bad signature, v1-only and stale v2 401, deals items scheduled once, others ignored', async () => {
const env = (id, category) => ({ event: { event_id: id, event_type: 'sources.item.updated', source: 'sources', version: 1, timestamp: new Date().toISOString(), actor: { type: 'service', id: 'sources' }, subject: { type: 'item', id: 'itm_x' }, visibility: 'internal', payload: { category } }, seq: 1 });
-- const post = (body, sig) => t.get('/internal/events', { body, headers: { 'content-type': 'application/json', 'x-openvibe-signature': sig } });
++ const post = (body, sig) => t.get('/internal/events', { body, headers: { 'content-type': 'application/json', ...(typeof sig === 'string' ? { 'x-openvibe-signature': sig } : sig) } });
const good = JSON.stringify(env('evt_01K5AAAAAAAAAAAAAAAAAAAAA1', 'deals'));
assert.strictEqual((await post(good, 'sha256=00')).status, 401);
-- const r1 = await post(good, signDelivery(good, 'whsec-test'));
++ assert.strictEqual((await post(good, signDelivery(good, 'whsec-test'))).status, 401, 'v1 only: refused');
++ assert.strictEqual((await post(good, signDeliveryHeaders(good, 'whsec-test', { now: Date.now() - 301000 }))).status, 401, 'stale v2: refused');
++ const r1 = await post(good, signDeliveryHeaders(good, 'whsec-test'));
assert.deepStrictEqual([r1.status, r1.json().outcome, r1.json().duplicate], [200, 'import_scheduled', false]);
-- const r2 = await post(good, signDelivery(good, 'whsec-test'));
++ const r2 = await post(good, signDeliveryHeaders(good, 'whsec-test'));
assert.strictEqual(r2.json().duplicate, true);
const news = JSON.stringify(env('evt_01K5AAAAAAAAAAAAAAAAAAAAA2', 'news'));
-- assert.strictEqual((await post(news, signDelivery(news, 'whsec-test'))).json().outcome, 'ignored');
++ assert.strictEqual((await post(news, signDeliveryHeaders(news, 'whsec-test'))).json().outcome, 'ignored');
});
await check('Sources down: the pull fails honestly (readiness says so), keeps its cursor, invents nothing', async () => {