openvibers/openvibe.deals Commits
Events webhook requires signature v2 (replay window); openvibe-sdk v0.4.0
Changes
@@@ -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"@@@ -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": {@@@ -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;@@@ -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 () => {
@@ -17,7 +17,7 @@
@@ -17,7 +17,7 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.23.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-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"
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0"
},
},
"devDependencies": {
"devDependencies": {
@@ -1060,9 +1060,9 @@
@@ -1060,9 +1060,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"
@@ -22,7 +22,7 @@
@@ -22,7 +22,7 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.23.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-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"
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0"
},
},
"devDependencies": {
"devDependencies": {
@@ -11,7 +11,7 @@
@@ -11,7 +11,7 @@
*/
*/
const express = require('express');
const express = require('express');
const { http } = require('openvibe-contracts');
const { http } = require('openvibe-contracts');
-const { verifyDelivery, createInbox } = require('openvibe-sdk/events');
+const { verifyDeliveryV2, createInbox } = require('openvibe-sdk/events');
const CONSUMER = 'deals-sources';
const CONSUMER = 'deals-sources';
const TYPES = new Set(['sources.item.created', 'sources.item.updated', 'sources.item.removed']);
const TYPES = new Set(['sources.item.created', 'sources.item.updated', 'sources.item.removed']);
@@ -25,8 +25,8 @@ function createInternalRoutes({ config, store, importer }) {
@@ -25,8 +25,8 @@ function createInternalRoutes({ config, store, importer }) {
const secrets = config.events.webhookSecrets;
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 });
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 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;
let body;
try { body = JSON.parse(raw.toString('utf8')); } catch { body = null; }
try { body = JSON.parse(raw.toString('utf8')); } catch { body = null; }
const event = body && body.event;
const event = body && body.event;
@@ -6,7 +6,7 @@
@@ -6,7 +6,7 @@
const assert = require('assert');
const assert = require('assert');
const { boot, check, done, robotsOf, HOUR } = require('./helpers/boot');
const { boot, check, done, robotsOf, HOUR } = require('./helpers/boot');
const { sourceItem } = require('./helpers/mocks');
const { sourceItem } = require('./helpers/mocks');
-const { signDelivery } = require('openvibe-sdk/events');
+const { signDelivery, signDeliveryHeaders } = require('openvibe-sdk/events');
(async () => {
(async () => {
const t = await boot();
const t = await boot();
@@ -125,17 +125,19 @@ const { signDelivery } = require('openvibe-sdk/events');
@@ -125,17 +125,19 @@ const { signDelivery } = require('openvibe-sdk/events');
assert.ok(!(await t.get('/feed.xml')).text.includes(garden.slug));
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 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'));
const good = JSON.stringify(env('evt_01K5AAAAAAAAAAAAAAAAAAAAA1', 'deals'));
assert.strictEqual((await post(good, 'sha256=00')).status, 401);
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]);
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);
assert.strictEqual(r2.json().duplicate, true);
const news = JSON.stringify(env('evt_01K5AAAAAAAAAAAAAAAAAAAAA2', 'news'));
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 () => {
await check('Sources down: the pull fails honestly (readiness says so), keeps its cursor, invents nothing', async () => {