openvibers/openvibe.tips Commits
Events webhook requires signature v2 (replay window); openvibe-sdk v0.4.0
Changes
@@@ -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"@@@ -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": {@@@ -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)) {@@@ -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) };}@@@ -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');
@@ -15,7 +15,7 @@
@@ -15,7 +15,7 @@
"express": "^4.21.2",
"express": "^4.21.2",
"jsonwebtoken": "^9.0.2",
"jsonwebtoken": "^9.0.2",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.15.0",
"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"
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.2.1"
},
},
"engines": {
"engines": {
@@ -999,9 +999,9 @@
@@ -999,9 +999,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 @@
"express": "^4.21.2",
"express": "^4.21.2",
"jsonwebtoken": "^9.0.2",
"jsonwebtoken": "^9.0.2",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.15.0",
"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"
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.2.1"
},
},
"engines": {
"engines": {
@@ -48,7 +48,8 @@ function consumerRouter({ domain, config, log = console }) {
@@ -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 });
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);
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, 'tips.bad_signature', { detail: 'X-OpenVibe-Signature does not verify', ctx: req.ov });
if (!delivery) return http.sendProblem(res, 401, 'tips.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)) {
@@ -13,7 +13,7 @@ const os = require('os');
@@ -13,7 +13,7 @@ const os = require('os');
const path = require('path');
const path = require('path');
const http = require('http');
const http = require('http');
const crypto = require('crypto');
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 { startNetwork, startBilling, startEvents, startLive } = require('./stubs');
const EVENTS_SECRET = 'e'.repeat(48);
const EVENTS_SECRET = 'e'.repeat(48);
@@ -70,12 +70,16 @@ async function boot(opts = {}) {
@@ -70,12 +70,16 @@ async function boot(opts = {}) {
return { status: res.status, headers: res.headers, json, text };
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 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`, {
const res = await fetch(`${base}/internal/events`, {
method: 'POST', body: raw,
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) };
return { status: res.status, json: await res.json().catch(() => null) };
}
}
@@ -234,13 +234,15 @@ const { boot, check, done } = require('./helpers/app');
@@ -234,13 +234,15 @@ const { boot, check, done } = require('./helpers/app');
assert.strictEqual(totals.external, 500);
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 ev = billing.foreignDonation({ from: null, to: alex.subject, amount: 5 });
const bad = await t.deliver(ev, { secret: 'x'.repeat(48) });
const bad = await t.deliver(ev, { secret: 'x'.repeat(48) });
assert.strictEqual(bad.status, 401);
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 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);
assert.strictEqual(proxied.status, 404);
const forged = await t.deliver({ ...ev, source: 'live' });
const forged = await t.deliver({ ...ev, source: 'live' });
assert.strictEqual(forged.json.outcome, 'ignored:source');
assert.strictEqual(forged.json.outcome, 'ignored:source');