openvibers/openvibe.events Commits
Deliveries carry only the v2 signature (shim C-60 retired). The v1 header, X-OpenVibe-Signature, was an HMAC of the body with no timestamp and so replayable, and it is no longer sent. Checked on 2026-09-28: every consumer in production verifies v2 (openvibe-sdk parseDelivery with requireV2, or verifyDeliveryV2: Live, Network, Media, Chat, Community, Games, Tools, Tips, VIP, News, Reviews, Deals, Trade and Search). The rotation overlap still signs v2 with both secrets. Tests assert there is no v1 header and verify v2. npm test 18/18.
Changes
@@@ -20,9 +20,12 @@* most maxAppInflight to developer-app endpoints (so apps can never hold every slot).*/const crypto = require('crypto');--const { sign, signV2 } = require('../lib/client');++const { signV2 } = require('../lib/client');/**++ * Deliveries carry only the v2 signature: the v1 header (an HMAC of the body, no timestamp, so replayable) is no++ * longer sent (shim C-60 retired 2026-09-28; every consumer verifies v2).++ ** The v2 header for a delivery. During a secret rotation's overlap it carries a second v2= signature made* with the previous secret, so a consumer still configured with either secret accepts it (openvibe-sdk's* verifyDeliveryV2 checks every v2= value).@@@ -76,7 +79,6 @@ function createWorker({ store, config, clock = { now: () => Date.now() }, fetchI'X-OpenVibe-Subscription-Id': sub.id,'X-OpenVibe-Delivery-Attempt': String(attempt),'X-OpenVibe-Hops': String(row.hops),-- 'X-OpenVibe-Signature': sign(body, sub.secret),'X-OpenVibe-Timestamp': String(timestamp),'X-OpenVibe-Signature-V2': signatureV2(sub, body, timestamp, clock.now()),traceparent: `00-${row.trace_id}-${crypto.randomBytes(8).toString('hex')}-01`,@@@ -4,7 +4,7 @@const assert = require('assert');const nodeHttp = require('http');const { ids } = require('openvibe-contracts');--const { verifyDelivery, verifyDeliveryV2 } = require('../lib/client');++const { verifyDeliveryV2 } = require('../lib/client');const apps = require('../server/apps');const { createGuardedPost, isPublicAddress } = require('../server/egress');const { load } = require('../server/config');@@@ -248,8 +248,8 @@ t('delivery: signed, scoped, sandbox never meets production', async () => {assert.deepStrictEqual(byPath('/b-project'), [], 'never another project');assert.strictEqual(firstParty.calls.length, 0, 'first-party subscriptions never get sandbox events');const a = seen.find(g => g.url === '/a');-- assert.ok(verifyDelivery(a.rawBody, a.headers['x-openvibe-signature'], subA.secret), 'HMAC-signed with the subscription secret');-- assert.ok(verifyDeliveryV2(a.rawBody, a.headers, subA.secret, { now: h.clock.now() }), 'and v2-signed (timestamped) like first-party deliveries');++ assert.ok(verifyDeliveryV2(a.rawBody, a.headers, subA.secret, { now: h.clock.now() }), 'v2-signed (timestamped) with the subscription secret, like first-party deliveries');++ assert.strictEqual(a.headers['x-openvibe-signature'], undefined, 'no v1 signature (C-60 retired)');assert.strictEqual(JSON.parse(a.rawBody).event.source, apps.appSource(appA));assert.ok(pinned.length && pinned.every(ip => ip === '127.0.0.1'), 'the socket connected to the address the guard checked');@@@ -95,7 +95,7 @@ t('SSRF: only http(s) endpoints on 127.0.0.1 or *.openvibe.* are accepted', asynassert.strictEqual(r.body.endpoint, 'http://127.0.0.1:9/x');});--t('success: signed POST, verified by verifyDelivery; trace propagated', async () => {++t('success: signed POST (v2 only), verified by verifyDeliveryV2; trace propagated', async () => {const stub = await subscriber();const sub = (await subscribe(media, { topic_pattern: 'live.stream.*', endpoint: stub.url })).body;const env = envelope('live', { trace_id: '4bf92f3577b34da6a3ce929d0e0e4736' });@@@ -104,9 +104,10 @@ t('success: signed POST, verified by verifyDelivery; trace propagated', async ()await h.worker.drain();assert.strictEqual(stub.calls.length, 1);const call = stub.calls[0];-- assert.ok(verifyDelivery(call.rawBody, call.headers['x-openvibe-signature'], sub.secret));-- assert.ok(!verifyDelivery(call.rawBody, call.headers['x-openvibe-signature'], 'whsec_wrong_secret_wrong_secret_wrong'));-- assert.ok(!verifyDelivery(Buffer.concat([call.rawBody, Buffer.from(' ')]), call.headers['x-openvibe-signature'], sub.secret));++ assert.strictEqual(call.headers['x-openvibe-signature'], undefined, 'no v1 signature any more (C-60 retired)');++ assert.ok(verifyDeliveryV2(call.rawBody, call.headers, sub.secret, { now: h.clock.now() }));++ assert.ok(!verifyDeliveryV2(call.rawBody, call.headers, 'whsec_wrong_secret_wrong_secret_wrong', { now: h.clock.now() }));++ assert.ok(!verifyDeliveryV2(Buffer.concat([call.rawBody, Buffer.from(' ')]), call.headers, sub.secret, { now: h.clock.now() }));const ts = Number(call.headers['x-openvibe-timestamp']);assert.strictEqual(ts, Math.floor(h.clock.now() / 1000), 'X-OpenVibe-Timestamp is the attempt time in unix seconds');assert.match(call.headers['x-openvibe-signature-v2'], new RegExp(`^t=${ts},v2=[0-9a-f]{64}$`));@@@ -132,7 +132,7 @@ t('consumer crash mid-processing + redelivery + replay -> exactly one effect (crlet secret = null;let mode = 'crash-after-commit';const stub = await subscriber((call) => {-- if (!events.verifyDelivery(call.rawBody, call.headers['x-openvibe-signature'], secret)) return 401;++ if (!events.verifyDeliveryV2(call.rawBody, call.headers, secret, { now: h.clock.now() })) return 401;const ev = call.body.event;if (mode === 'crash-before-commit') {mode = 'ok';
@@ -20,9 +20,12 @@
@@ -20,9 +20,12 @@
* most maxAppInflight to developer-app endpoints (so apps can never hold every slot).
* most maxAppInflight to developer-app endpoints (so apps can never hold every slot).
*/
*/
const crypto = require('crypto');
const crypto = require('crypto');
-const { sign, signV2 } = require('../lib/client');
+const { signV2 } = require('../lib/client');
/**
/**
+ * Deliveries carry only the v2 signature: the v1 header (an HMAC of the body, no timestamp, so replayable) is no
+ * longer sent (shim C-60 retired 2026-09-28; every consumer verifies v2).
+ *
* The v2 header for a delivery. During a secret rotation's overlap it carries a second v2= signature made
* The v2 header for a delivery. During a secret rotation's overlap it carries a second v2= signature made
* with the previous secret, so a consumer still configured with either secret accepts it (openvibe-sdk's
* with the previous secret, so a consumer still configured with either secret accepts it (openvibe-sdk's
* verifyDeliveryV2 checks every v2= value).
* verifyDeliveryV2 checks every v2= value).
@@ -76,7 +79,6 @@ function createWorker({ store, config, clock = { now: () => Date.now() }, fetchI
@@ -76,7 +79,6 @@ function createWorker({ store, config, clock = { now: () => Date.now() }, fetchI
'X-OpenVibe-Subscription-Id': sub.id,
'X-OpenVibe-Subscription-Id': sub.id,
'X-OpenVibe-Delivery-Attempt': String(attempt),
'X-OpenVibe-Delivery-Attempt': String(attempt),
'X-OpenVibe-Hops': String(row.hops),
'X-OpenVibe-Hops': String(row.hops),
- 'X-OpenVibe-Signature': sign(body, sub.secret),
'X-OpenVibe-Timestamp': String(timestamp),
'X-OpenVibe-Timestamp': String(timestamp),
'X-OpenVibe-Signature-V2': signatureV2(sub, body, timestamp, clock.now()),
'X-OpenVibe-Signature-V2': signatureV2(sub, body, timestamp, clock.now()),
traceparent: `00-${row.trace_id}-${crypto.randomBytes(8).toString('hex')}-01`,
traceparent: `00-${row.trace_id}-${crypto.randomBytes(8).toString('hex')}-01`,
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
const assert = require('assert');
const assert = require('assert');
const nodeHttp = require('http');
const nodeHttp = require('http');
const { ids } = require('openvibe-contracts');
const { ids } = require('openvibe-contracts');
-const { verifyDelivery, verifyDeliveryV2 } = require('../lib/client');
+const { verifyDeliveryV2 } = require('../lib/client');
const apps = require('../server/apps');
const apps = require('../server/apps');
const { createGuardedPost, isPublicAddress } = require('../server/egress');
const { createGuardedPost, isPublicAddress } = require('../server/egress');
const { load } = require('../server/config');
const { load } = require('../server/config');
@@ -248,8 +248,8 @@ t('delivery: signed, scoped, sandbox never meets production', async () => {
@@ -248,8 +248,8 @@ t('delivery: signed, scoped, sandbox never meets production', async () => {
assert.deepStrictEqual(byPath('/b-project'), [], 'never another project');
assert.deepStrictEqual(byPath('/b-project'), [], 'never another project');
assert.strictEqual(firstParty.calls.length, 0, 'first-party subscriptions never get sandbox events');
assert.strictEqual(firstParty.calls.length, 0, 'first-party subscriptions never get sandbox events');
const a = seen.find(g => g.url === '/a');
const a = seen.find(g => g.url === '/a');
- assert.ok(verifyDelivery(a.rawBody, a.headers['x-openvibe-signature'], subA.secret), 'HMAC-signed with the subscription secret');
- assert.ok(verifyDeliveryV2(a.rawBody, a.headers, subA.secret, { now: h.clock.now() }), 'and v2-signed (timestamped) like first-party deliveries');
+ assert.ok(verifyDeliveryV2(a.rawBody, a.headers, subA.secret, { now: h.clock.now() }), 'v2-signed (timestamped) with the subscription secret, like first-party deliveries');
+ assert.strictEqual(a.headers['x-openvibe-signature'], undefined, 'no v1 signature (C-60 retired)');
assert.strictEqual(JSON.parse(a.rawBody).event.source, apps.appSource(appA));
assert.strictEqual(JSON.parse(a.rawBody).event.source, apps.appSource(appA));
assert.ok(pinned.length && pinned.every(ip => ip === '127.0.0.1'), 'the socket connected to the address the guard checked');
assert.ok(pinned.length && pinned.every(ip => ip === '127.0.0.1'), 'the socket connected to the address the guard checked');
@@ -95,7 +95,7 @@ t('SSRF: only http(s) endpoints on 127.0.0.1 or *.openvibe.* are accepted', asyn
@@ -95,7 +95,7 @@ t('SSRF: only http(s) endpoints on 127.0.0.1 or *.openvibe.* are accepted', asyn
assert.strictEqual(r.body.endpoint, 'http://127.0.0.1:9/x');
assert.strictEqual(r.body.endpoint, 'http://127.0.0.1:9/x');
});
});
-t('success: signed POST, verified by verifyDelivery; trace propagated', async () => {
+t('success: signed POST (v2 only), verified by verifyDeliveryV2; trace propagated', async () => {
const stub = await subscriber();
const stub = await subscriber();
const sub = (await subscribe(media, { topic_pattern: 'live.stream.*', endpoint: stub.url })).body;
const sub = (await subscribe(media, { topic_pattern: 'live.stream.*', endpoint: stub.url })).body;
const env = envelope('live', { trace_id: '4bf92f3577b34da6a3ce929d0e0e4736' });
const env = envelope('live', { trace_id: '4bf92f3577b34da6a3ce929d0e0e4736' });
@@ -104,9 +104,10 @@ t('success: signed POST, verified by verifyDelivery; trace propagated', async ()
@@ -104,9 +104,10 @@ t('success: signed POST, verified by verifyDelivery; trace propagated', async ()
await h.worker.drain();
await h.worker.drain();
assert.strictEqual(stub.calls.length, 1);
assert.strictEqual(stub.calls.length, 1);
const call = stub.calls[0];
const call = stub.calls[0];
- assert.ok(verifyDelivery(call.rawBody, call.headers['x-openvibe-signature'], sub.secret));
- assert.ok(!verifyDelivery(call.rawBody, call.headers['x-openvibe-signature'], 'whsec_wrong_secret_wrong_secret_wrong'));
- assert.ok(!verifyDelivery(Buffer.concat([call.rawBody, Buffer.from(' ')]), call.headers['x-openvibe-signature'], sub.secret));
+ assert.strictEqual(call.headers['x-openvibe-signature'], undefined, 'no v1 signature any more (C-60 retired)');
+ assert.ok(verifyDeliveryV2(call.rawBody, call.headers, sub.secret, { now: h.clock.now() }));
+ assert.ok(!verifyDeliveryV2(call.rawBody, call.headers, 'whsec_wrong_secret_wrong_secret_wrong', { now: h.clock.now() }));
+ assert.ok(!verifyDeliveryV2(Buffer.concat([call.rawBody, Buffer.from(' ')]), call.headers, sub.secret, { now: h.clock.now() }));
const ts = Number(call.headers['x-openvibe-timestamp']);
const ts = Number(call.headers['x-openvibe-timestamp']);
assert.strictEqual(ts, Math.floor(h.clock.now() / 1000), 'X-OpenVibe-Timestamp is the attempt time in unix seconds');
assert.strictEqual(ts, Math.floor(h.clock.now() / 1000), 'X-OpenVibe-Timestamp is the attempt time in unix seconds');
assert.match(call.headers['x-openvibe-signature-v2'], new RegExp(`^t=${ts},v2=[0-9a-f]{64}$`));
assert.match(call.headers['x-openvibe-signature-v2'], new RegExp(`^t=${ts},v2=[0-9a-f]{64}$`));
@@ -132,7 +132,7 @@ t('consumer crash mid-processing + redelivery + replay -> exactly one effect (cr
@@ -132,7 +132,7 @@ t('consumer crash mid-processing + redelivery + replay -> exactly one effect (cr
let secret = null;
let secret = null;
let mode = 'crash-after-commit';
let mode = 'crash-after-commit';
const stub = await subscriber((call) => {
const stub = await subscriber((call) => {
- if (!events.verifyDelivery(call.rawBody, call.headers['x-openvibe-signature'], secret)) return 401;
+ if (!events.verifyDeliveryV2(call.rawBody, call.headers, secret, { now: h.clock.now() })) return 401;
const ev = call.body.event;
const ev = call.body.event;
if (mode === 'crash-before-commit') {
if (mode === 'crash-before-commit') {
mode = 'ok';
mode = 'ok';