openvibers/openvibe.reviews Commits
Events webhook requires signature v2 (replay window); openvibe-sdk v0.4.0
Changes
@@@ -16,7 +16,7 @@"express-rate-limit": "^7.5.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.20.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"},"engines": {@@@ -1044,9 +1044,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"@@@ -21,7 +21,7 @@"express-rate-limit": "^7.5.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.20.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"},"engines": {@@@ -45,7 +45,8 @@ function consumerRouter({ db, svc, sync, config, log = console }) {if (!secrets.length) return http.sendProblem(res, 503, 'reviews.webhook_disabled', { detail: 'REVIEWS_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, 'reviews.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)) {@@@ -10,7 +10,7 @@ const fs = require('fs');const os = require('os');const path = require('path');const { serviceAuth, ids } = require('openvibe-contracts');--const { signDelivery } = require('openvibe-sdk/events');++const { signDelivery, signDeliveryHeaders } = require('openvibe-sdk/events');const { load } = require('../server/config');const { start } = require('../server/index');@@@ -209,10 +209,14 @@ async function createEntity(h, body) {return r.json.entity;}--/** A signed delivery from OpenVibe.Events to POST /internal/events. */--async function deliver(h, envelope, { secret = WEBHOOK_SECRET } = {}) {++/**++ * A signed delivery from OpenVibe.Events to POST /internal/events (v1 and v2 headers, as Events++ * sends it). `v1Only` sends only X-OpenVibe-Signature; `now` (ms) backdates the v2 timestamp.++ */++async function deliver(h, envelope, { secret = WEBHOOK_SECRET, v1Only = false, now } = {}) {const raw = JSON.stringify({ event: envelope, seq: 1 });-- return req(h, 'POST', '/internal/events', { body: raw, headers: { 'X-OpenVibe-Signature': signDelivery(raw, secret) } });++ const headers = v1Only ? { 'X-OpenVibe-Signature': signDelivery(raw, secret) } : signDeliveryHeaders(raw, secret, { now });++ return req(h, 'POST', '/internal/events', { body: raw, headers });}function sourcesEvent(type, item, extra = {}) {@@@ -194,7 +194,7 @@ t('every produced event is a valid events.event-envelope@1', async () => {} finally { await h.stop(); }});--t('import needs the capability; a bad webhook signature is refused', async () => {++t('import needs the capability; a bad, v1-only or stale webhook signature is refused', async () => {const h = await boot();try {const it = h.sources.put(steamItem());@@@ -203,6 +203,10 @@ t('import needs the capability; a bad webhook signature is refused', async () =>assert.strictEqual(denied.json.code, 'capability.denied');const bad = await deliver(h, sourcesEvent('sources.item.created', it), { secret: 'wrong' });assert.strictEqual(bad.status, 401);++ const v1only = await deliver(h, sourcesEvent('sources.item.created', it), { v1Only: true });++ assert.strictEqual(v1only.status, 401, 'v1 only (no v2 header): refused');++ const stale = await deliver(h, sourcesEvent('sources.item.created', it), { now: Date.now() - 301000 });++ assert.strictEqual(stale.status, 401, 'stale v2 (outside the 300 s window): refused');const other = await deliver(h, { ...sourcesEvent('sources.item.created', it), source: 'news' });assert.strictEqual(other.json.outcome, 'ignored:source');} finally { await h.stop(); }
@@ -16,7 +16,7 @@
@@ -16,7 +16,7 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.20.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.20.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"
},
},
"engines": {
"engines": {
@@ -1044,9 +1044,9 @@
@@ -1044,9 +1044,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"
@@ -21,7 +21,7 @@
@@ -21,7 +21,7 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.20.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.20.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"
},
},
"engines": {
"engines": {
@@ -45,7 +45,8 @@ function consumerRouter({ db, svc, sync, config, log = console }) {
@@ -45,7 +45,8 @@ function consumerRouter({ db, svc, sync, config, log = console }) {
if (!secrets.length) return http.sendProblem(res, 503, 'reviews.webhook_disabled', { detail: 'REVIEWS_EVENTS_SECRET is not set', ctx: req.ov });
if (!secrets.length) return http.sendProblem(res, 503, 'reviews.webhook_disabled', { detail: 'REVIEWS_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);
let delivery = null;
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, 'reviews.bad_signature', { detail: 'X-OpenVibe-Signature does not verify', ctx: req.ov });
if (!delivery) return http.sendProblem(res, 401, 'reviews.bad_signature', { detail: 'X-OpenVibe-Signature does not verify', ctx: req.ov });
const event = delivery.event;
const event = delivery.event;
if (!event || typeof event.event_id !== 'string' || !/^evt_[0-9A-HJKMNP-TV-Z]{26}$/.test(event.event_id)) {
if (!event || typeof event.event_id !== 'string' || !/^evt_[0-9A-HJKMNP-TV-Z]{26}$/.test(event.event_id)) {
@@ -10,7 +10,7 @@ const fs = require('fs');
@@ -10,7 +10,7 @@ const fs = require('fs');
const os = require('os');
const os = require('os');
const path = require('path');
const path = require('path');
const { serviceAuth, ids } = require('openvibe-contracts');
const { serviceAuth, ids } = require('openvibe-contracts');
-const { signDelivery } = require('openvibe-sdk/events');
+const { signDelivery, signDeliveryHeaders } = require('openvibe-sdk/events');
const { load } = require('../server/config');
const { load } = require('../server/config');
const { start } = require('../server/index');
const { start } = require('../server/index');
@@ -209,10 +209,14 @@ async function createEntity(h, body) {
@@ -209,10 +209,14 @@ async function createEntity(h, body) {
return r.json.entity;
return r.json.entity;
}
}
-/** A signed delivery from OpenVibe.Events to POST /internal/events. */
-async function deliver(h, envelope, { secret = WEBHOOK_SECRET } = {}) {
+/**
+ * A signed delivery from OpenVibe.Events to POST /internal/events (v1 and v2 headers, as Events
+ * sends it). `v1Only` sends only X-OpenVibe-Signature; `now` (ms) backdates the v2 timestamp.
+ */
+async function deliver(h, envelope, { secret = WEBHOOK_SECRET, v1Only = false, now } = {}) {
const raw = JSON.stringify({ event: envelope, seq: 1 });
const raw = JSON.stringify({ event: envelope, seq: 1 });
- return req(h, 'POST', '/internal/events', { body: raw, headers: { 'X-OpenVibe-Signature': signDelivery(raw, secret) } });
+ const headers = v1Only ? { 'X-OpenVibe-Signature': signDelivery(raw, secret) } : signDeliveryHeaders(raw, secret, { now });
+ return req(h, 'POST', '/internal/events', { body: raw, headers });
}
}
function sourcesEvent(type, item, extra = {}) {
function sourcesEvent(type, item, extra = {}) {
@@ -194,7 +194,7 @@ t('every produced event is a valid events.event-envelope@1', async () => {
@@ -194,7 +194,7 @@ t('every produced event is a valid events.event-envelope@1', async () => {
} finally { await h.stop(); }
} finally { await h.stop(); }
});
});
-t('import needs the capability; a bad webhook signature is refused', async () => {
+t('import needs the capability; a bad, v1-only or stale webhook signature is refused', async () => {
const h = await boot();
const h = await boot();
try {
try {
const it = h.sources.put(steamItem());
const it = h.sources.put(steamItem());
@@ -203,6 +203,10 @@ t('import needs the capability; a bad webhook signature is refused', async () =>
@@ -203,6 +203,10 @@ t('import needs the capability; a bad webhook signature is refused', async () =>
assert.strictEqual(denied.json.code, 'capability.denied');
assert.strictEqual(denied.json.code, 'capability.denied');
const bad = await deliver(h, sourcesEvent('sources.item.created', it), { secret: 'wrong' });
const bad = await deliver(h, sourcesEvent('sources.item.created', it), { secret: 'wrong' });
assert.strictEqual(bad.status, 401);
assert.strictEqual(bad.status, 401);
+ const v1only = await deliver(h, sourcesEvent('sources.item.created', it), { v1Only: true });
+ assert.strictEqual(v1only.status, 401, 'v1 only (no v2 header): refused');
+ const stale = await deliver(h, sourcesEvent('sources.item.created', it), { now: Date.now() - 301000 });
+ assert.strictEqual(stale.status, 401, 'stale v2 (outside the 300 s window): refused');
const other = await deliver(h, { ...sourcesEvent('sources.item.created', it), source: 'news' });
const other = await deliver(h, { ...sourcesEvent('sources.item.created', it), source: 'news' });
assert.strictEqual(other.json.outcome, 'ignored:source');
assert.strictEqual(other.json.outcome, 'ignored:source');
} finally { await h.stop(); }
} finally { await h.stop(); }