openvibers/openvibe.trade 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.22.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.22.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": {@@@ -22,7 +22,8 @@ function createWebhook({ store, config, sync, log = console }) {router.post('/internal/events', express.raw({ type: '*/*', limit: '1mb' }), function receiveEventsDelivery(req, res) {if (!config.events.webhookSecret) return http.sendProblem(res, 404, 'route.not_found', { detail: 'Not found', ctx: req.ov });const raw = Buffer.isBuffer(req.body) ? req.body : Buffer.from('');-- const delivery = parseDelivery(raw, req.headers, config.events.webhookSecret);++ // 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(raw, req.headers, config.events.webhookSecret, { requireV2: true });if (!delivery) return http.sendProblem(res, 401, 'webhook.signature_invalid', { detail: 'The delivery signature does not verify', ctx: req.ov });const e = delivery.event;if (typeof e.event_id !== 'string' || typeof e.event_type !== 'string') return http.sendProblem(res, 400, 'webhook.malformed', { detail: 'Not an event envelope', ctx: req.ov });@@@ -6,7 +6,7 @@* wakes the sync and is idempotent per event id. Deterministic resolution.*/const assert = require('assert');--const { signDelivery } = require('openvibe-sdk/events');++const { signDelivery, signDeliveryHeaders } = require('openvibe-sdk/events');const { boot, check, done } = require('./helpers/boot');const { mapItem } = require('../server/domain/mapping');@@@ -114,17 +114,21 @@ const { mapItem } = require('../server/domain/mapping');assert.throws(() => t.ctx.instruments.addAlias(acme, 'cik', '320193'), (e) => e.status === 409);});-- await check('the signed webhook wakes the sync once per event id; a bad signature is 401', async () => {++ await check('the signed webhook wakes the sync once per event id; a bad signature, v1-only or stale v2 is 401', async () => {t.sources.putItem({ canonical_url: 'https://www.sec.gov/Archives/edgar/data/320193/000032019326000106/0000320193-26-000106-index.htm', title: 'APPLE INC', summary: '8-K', published_at: '2026-09-22T12:10:00.000Z', retrieved_at: t.iso(t.T0) });const body = JSON.stringify({ event: { event_id: 'evt_01JABCDEFGHJKMNPQRSTVWXYZ0', event_type: 'sources.item.created', payload: {} }, seq: 7 });const bad = await t.get('/internal/events', { method: 'POST', body, headers: { 'content-type': 'application/json', 'x-openvibe-signature': 'sha256=00' } });assert.strictEqual(bad.status, 401);-- const ok = await t.get('/internal/events', { method: 'POST', body, headers: { 'content-type': 'application/json', 'x-openvibe-signature': signDelivery(body, 'hook-secret') } });++ const v1only = await t.get('/internal/events', { method: 'POST', body, headers: { 'content-type': 'application/json', 'x-openvibe-signature': signDelivery(body, 'hook-secret') } });++ assert.strictEqual(v1only.status, 401, 'v1 only: refused (requireV2)');++ const stale = await t.get('/internal/events', { method: 'POST', body, headers: { 'content-type': 'application/json', ...signDeliveryHeaders(body, 'hook-secret', { now: Date.now() - 301000 }) } });++ assert.strictEqual(stale.status, 401, 'stale v2 (outside the 300 s window): refused');++ const ok = await t.get('/internal/events', { method: 'POST', body, headers: { 'content-type': 'application/json', ...signDeliveryHeaders(body, 'hook-secret') } });assert.strictEqual(ok.status, 200);assert.strictEqual(ok.json().sync, true);await t.ctx.sync.run();assert.strictEqual((await t.get('/i/AAPL.json')).json().documents.length, 1);-- const again = await t.get('/internal/events', { method: 'POST', body, headers: { 'content-type': 'application/json', 'x-openvibe-signature': signDelivery(body, 'hook-secret') } });++ const again = await t.get('/internal/events', { method: 'POST', body, headers: { 'content-type': 'application/json', ...signDeliveryHeaders(body, 'hook-secret') } });assert.strictEqual(again.json().duplicate, true);assert.strictEqual(again.json().sync, false);});
@@ -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.22.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.22.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.22.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.22.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": {
@@ -22,7 +22,8 @@ function createWebhook({ store, config, sync, log = console }) {
@@ -22,7 +22,8 @@ function createWebhook({ store, config, sync, log = console }) {
router.post('/internal/events', express.raw({ type: '*/*', limit: '1mb' }), function receiveEventsDelivery(req, res) {
router.post('/internal/events', express.raw({ type: '*/*', limit: '1mb' }), function receiveEventsDelivery(req, res) {
if (!config.events.webhookSecret) return http.sendProblem(res, 404, 'route.not_found', { detail: 'Not found', ctx: req.ov });
if (!config.events.webhookSecret) return http.sendProblem(res, 404, 'route.not_found', { detail: 'Not found', ctx: req.ov });
const raw = Buffer.isBuffer(req.body) ? req.body : Buffer.from('');
const raw = Buffer.isBuffer(req.body) ? req.body : Buffer.from('');
- const delivery = parseDelivery(raw, req.headers, config.events.webhookSecret);
+ // 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(raw, req.headers, config.events.webhookSecret, { requireV2: true });
if (!delivery) return http.sendProblem(res, 401, 'webhook.signature_invalid', { detail: 'The delivery signature does not verify', ctx: req.ov });
if (!delivery) return http.sendProblem(res, 401, 'webhook.signature_invalid', { detail: 'The delivery signature does not verify', ctx: req.ov });
const e = delivery.event;
const e = delivery.event;
if (typeof e.event_id !== 'string' || typeof e.event_type !== 'string') return http.sendProblem(res, 400, 'webhook.malformed', { detail: 'Not an event envelope', ctx: req.ov });
if (typeof e.event_id !== 'string' || typeof e.event_type !== 'string') return http.sendProblem(res, 400, 'webhook.malformed', { detail: 'Not an event envelope', ctx: req.ov });
@@ -6,7 +6,7 @@
@@ -6,7 +6,7 @@
* wakes the sync and is idempotent per event id. Deterministic resolution.
* wakes the sync and is idempotent per event id. Deterministic resolution.
*/
*/
const assert = require('assert');
const assert = require('assert');
-const { signDelivery } = require('openvibe-sdk/events');
+const { signDelivery, signDeliveryHeaders } = require('openvibe-sdk/events');
const { boot, check, done } = require('./helpers/boot');
const { boot, check, done } = require('./helpers/boot');
const { mapItem } = require('../server/domain/mapping');
const { mapItem } = require('../server/domain/mapping');
@@ -114,17 +114,21 @@ const { mapItem } = require('../server/domain/mapping');
@@ -114,17 +114,21 @@ const { mapItem } = require('../server/domain/mapping');
assert.throws(() => t.ctx.instruments.addAlias(acme, 'cik', '320193'), (e) => e.status === 409);
assert.throws(() => t.ctx.instruments.addAlias(acme, 'cik', '320193'), (e) => e.status === 409);
});
});
- await check('the signed webhook wakes the sync once per event id; a bad signature is 401', async () => {
+ await check('the signed webhook wakes the sync once per event id; a bad signature, v1-only or stale v2 is 401', async () => {
t.sources.putItem({ canonical_url: 'https://www.sec.gov/Archives/edgar/data/320193/000032019326000106/0000320193-26-000106-index.htm', title: 'APPLE INC', summary: '8-K', published_at: '2026-09-22T12:10:00.000Z', retrieved_at: t.iso(t.T0) });
t.sources.putItem({ canonical_url: 'https://www.sec.gov/Archives/edgar/data/320193/000032019326000106/0000320193-26-000106-index.htm', title: 'APPLE INC', summary: '8-K', published_at: '2026-09-22T12:10:00.000Z', retrieved_at: t.iso(t.T0) });
const body = JSON.stringify({ event: { event_id: 'evt_01JABCDEFGHJKMNPQRSTVWXYZ0', event_type: 'sources.item.created', payload: {} }, seq: 7 });
const body = JSON.stringify({ event: { event_id: 'evt_01JABCDEFGHJKMNPQRSTVWXYZ0', event_type: 'sources.item.created', payload: {} }, seq: 7 });
const bad = await t.get('/internal/events', { method: 'POST', body, headers: { 'content-type': 'application/json', 'x-openvibe-signature': 'sha256=00' } });
const bad = await t.get('/internal/events', { method: 'POST', body, headers: { 'content-type': 'application/json', 'x-openvibe-signature': 'sha256=00' } });
assert.strictEqual(bad.status, 401);
assert.strictEqual(bad.status, 401);
- const ok = await t.get('/internal/events', { method: 'POST', body, headers: { 'content-type': 'application/json', 'x-openvibe-signature': signDelivery(body, 'hook-secret') } });
+ const v1only = await t.get('/internal/events', { method: 'POST', body, headers: { 'content-type': 'application/json', 'x-openvibe-signature': signDelivery(body, 'hook-secret') } });
+ assert.strictEqual(v1only.status, 401, 'v1 only: refused (requireV2)');
+ const stale = await t.get('/internal/events', { method: 'POST', body, headers: { 'content-type': 'application/json', ...signDeliveryHeaders(body, 'hook-secret', { now: Date.now() - 301000 }) } });
+ assert.strictEqual(stale.status, 401, 'stale v2 (outside the 300 s window): refused');
+ const ok = await t.get('/internal/events', { method: 'POST', body, headers: { 'content-type': 'application/json', ...signDeliveryHeaders(body, 'hook-secret') } });
assert.strictEqual(ok.status, 200);
assert.strictEqual(ok.status, 200);
assert.strictEqual(ok.json().sync, true);
assert.strictEqual(ok.json().sync, true);
await t.ctx.sync.run();
await t.ctx.sync.run();
assert.strictEqual((await t.get('/i/AAPL.json')).json().documents.length, 1);
assert.strictEqual((await t.get('/i/AAPL.json')).json().documents.length, 1);
- const again = await t.get('/internal/events', { method: 'POST', body, headers: { 'content-type': 'application/json', 'x-openvibe-signature': signDelivery(body, 'hook-secret') } });
+ const again = await t.get('/internal/events', { method: 'POST', body, headers: { 'content-type': 'application/json', ...signDeliveryHeaders(body, 'hook-secret') } });
assert.strictEqual(again.json().duplicate, true);
assert.strictEqual(again.json().duplicate, true);
assert.strictEqual(again.json().sync, false);
assert.strictEqual(again.json().sync, false);
});
});