openvibers/openvibe.services Commits
Services: the webhook tester imitates Events' delivery without a sequence number (#40)
Changes
@@@ -3,8 +3,8 @@/*** Webhook tester and signed-event inspector.*-- * OpenVibe.Events delivers `{ "event": <events.event-envelope@1>, "seq": <n> }` signed under the-- * subscription secret:++ * OpenVibe.Events delivers `{ "event": <events.event-envelope@1> }` (no sequence number: a receiver dedupes on++ * event.event_id; ADR-042) signed under the subscription secret:* X-OpenVibe-Timestamp: <unix seconds this attempt was sent>* X-OpenVibe-Signature-V2: t=<that timestamp>,v2=<hex HMAC-SHA256 of "<t>.<raw body>">* Every consumer requires v2 and refuses it more than 300 s from its own clock (replay window).@@@ -94,9 +94,9 @@ function inspect({ rawBody, signatureV2, timestamp, secret, now = Date.now() })const obj = JSON.parse(body);if (obj && typeof obj === 'object' && obj.event) {const v = contracts.validate('events.event-envelope@1', obj.event);-- parsed = { event_type: obj.event.event_type || null, event_id: obj.event.event_id || null, seq: obj.seq ?? null, envelopeValid: v.valid, envelopeErrors: v.errors };++ parsed = { event_type: obj.event.event_type || null, event_id: obj.event.event_id || null, envelopeValid: v.valid, envelopeErrors: v.errors };} else {-- parsed = { note: 'JSON, but not an Events delivery ({ event, seq })' };++ parsed = { note: 'JSON, but not an Events delivery ({ event })' };}} catch { parsed = { note: 'not JSON' }; }return { accepted: v2.valid, v2, bodyBytes: Buffer.byteLength(body), parsed };@@@ -123,15 +123,13 @@ function sample({ eventType, secret, producer, now = Date.now() }) {trace_id: crypto.randomBytes(16).toString('hex'),};const check = contracts.validate('events.event-envelope@1', envelope);-- const seq = 1;-- const body = JSON.stringify({ event: envelope, seq });++ const body = JSON.stringify({ event: envelope });const subscriptionId = `sub_${contracts.ids.ulid(now)}`;const headers = {'Content-Type': 'application/json','User-Agent': 'OpenVibe.Events/0.1','X-OpenVibe-Event-Id': envelope.event_id,'X-OpenVibe-Event-Type': eventType,-- 'X-OpenVibe-Seq': String(seq),'X-OpenVibe-Subscription-Id': subscriptionId,'X-OpenVibe-Delivery-Attempt': '1','X-OpenVibe-Hops': '0',@@@ -95,7 +95,7 @@ ${got.code ? html`<h2>Exchange it (on your server)</h2>viewer: req.viewer, config, path: '/tools/webhooks', title: 'Webhook tester', index: true, scripts: ['js/webhook.js'],crumbs: [{ label: 'Webhooks' }],body: html`<h1>Webhook tester and signed-event inspector</h1>--<p>OpenVibe.Events delivers each event as <code>POST</code> with the body <code>{"event": <envelope>, "seq": <n>}</code> and these headers, signed with your subscription secret:</p>++<p>OpenVibe.Events delivers each event as <code>POST</code> with the body <code>{"event": <envelope>}</code> (no sequence number: dedupe on the event id) and these headers, signed with your subscription secret:</p><ul><li><code>X-OpenVibe-Timestamp: <unix seconds></code>, the time this attempt was sent. Every retry gets a new one.</li><li><code>X-OpenVibe-Signature-V2: t=<that timestamp>,v2=<hex HMAC-SHA256 of "<t>.<raw body>"></code> (v2).</li>@@@ -134,7 +134,7 @@ ${v2.timestampsAgree != null ? html`<dt>X-OpenVibe-Timestamp</dt><dd>${code(v2.s<dt>Expected</dt><dd>${code(v2.expected || '(needs a t= value)')}</dd><dt>Given</dt><dd>${code(v2.given || '(none)')}</dd></dl><h3>Body</h3><dl class="facts"><dt>Size</dt><dd>${v.bodyBytes} bytes</dd>--${v.parsed && v.parsed.event_type ? html`<dt>Event</dt><dd>${code(v.parsed.event_type)} ${code(v.parsed.event_id || '')} seq ${v.parsed.seq}; envelope ${v.parsed.envelopeValid ? 'validates' : html`does not validate: ${v.parsed.envelopeErrors.map((e) => `${e.path} ${e.message}`).join('; ')}`}</dd>` : ''}++${v.parsed && v.parsed.event_type ? html`<dt>Event</dt><dd>${code(v.parsed.event_type)} ${code(v.parsed.event_id || '')}; envelope ${v.parsed.envelopeValid ? 'validates' : html`does not validate: ${v.parsed.envelopeErrors.map((e) => `${e.path} ${e.message}`).join('; ')}`}</dd>` : ''}${v.parsed && v.parsed.note ? html`<dt>Shape</dt><dd>${v.parsed.note}</dd>` : ''}</dl></div>`;}@@@ -19,7 +19,7 @@ const hmacV1 = (body, secret) => `sha256=${crypto.createHmac('sha256', secret).u(async () => {const secret = 'whsec_' + crypto.randomBytes(16).toString('hex');-- const body = JSON.stringify({ event: { event_id: contracts.ids.newId('event'), event_type: 'network.app.created', version: 1, source: 'network', actor: { type: 'system', id: 'network' }, timestamp: new Date().toISOString(), subject: { type: 'app', id: 'app_1' }, payload: {} }, seq: 7 });++ const body = JSON.stringify({ event: { event_id: contracts.ids.newId('event'), event_type: 'network.app.created', version: 1, source: 'network', actor: { type: 'system', id: 'network' }, timestamp: new Date().toISOString(), subject: { type: 'app', id: 'app_1' }, payload: {} } });// A clock 0.7 s into a second: ages are whole seconds, the window is checked on the exact time.const nowSec = Math.floor(Date.now() / 1000);@@@ -37,7 +37,7 @@ const hmacV1 = (body, secret) => `sha256=${crypto.createHmac('sha256', secret).uassert.strictEqual(v.v2.expected, v2(body, secret, nowSec));assert.strictEqual(v.v2.windowSec, 300);assert.strictEqual(v.parsed.event_type, 'network.app.created');-- assert.strictEqual(v.parsed.seq, 7);++ assert.strictEqual('seq' in v.parsed, false, 'Events sends no sequence number (ADR-042)');assert.strictEqual(v.bodyBytes, Buffer.byteLength(body));});@@@ -81,7 +81,7 @@ const hmacV1 = (body, secret) => `sha256=${crypto.createHmac('sha256', secret).uassert.strictEqual(wrongSecret.accepted, false);assert.strictEqual(wrongSecret.v2.signatureValid, false);assert.match(wrongSecret.v2.reason, /does not match/);-- assert.strictEqual(run({ rawBody: body.replace('"seq":7', '"seq":8') }).accepted, false);++ assert.strictEqual(run({ rawBody: body.replace('"source":"network"', '"source":"netw0rk"') }).accepted, false);// The HMAC for one time moved onto another: the time is covered by the signature.const moved = run({ signatureV2: good.replace(`t=${nowSec}`, `t=${nowSec - 1}`) });assert.strictEqual(moved.accepted, false);@@@ -137,7 +137,8 @@ const hmacV1 = (body, secret) => `sha256=${crypto.createHmac('sha256', secret).uassert.strictEqual(verifyDeliveryV2(s.body, headers, secret, { now: nowMs }), true);const parsed = parseDelivery(s.body, headers, secret, { requireV2: true, now: nowMs });assert.strictEqual(parsed.event.event_type, 'media.object.ready');-- assert.strictEqual(parsed.seq, 1);++ assert.strictEqual('seq' in JSON.parse(s.body), false, 'the sample is { event }, like Events sends it');++ assert.strictEqual(headers['x-openvibe-seq'], undefined, 'and no X-OpenVibe-Seq');assert.strictEqual(contracts.validate('events.event-envelope@1', parsed.event).valid, true);assert.strictEqual(parseDelivery(s.body, headers, secret, { requireV2: true, now: nowMs + 301_000 }), null, 'the sample goes stale after 300 s');assert.strictEqual(parseDelivery(s.body, headers, 'wrong', { requireV2: true, now: nowMs }), null);
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
/**
/**
* Webhook tester and signed-event inspector.
* Webhook tester and signed-event inspector.
*
*
- * OpenVibe.Events delivers `{ "event": <events.event-envelope@1>, "seq": <n> }` signed under the
- * subscription secret:
+ * OpenVibe.Events delivers `{ "event": <events.event-envelope@1> }` (no sequence number: a receiver dedupes on
+ * event.event_id; ADR-042) signed under the subscription secret:
* X-OpenVibe-Timestamp: <unix seconds this attempt was sent>
* X-OpenVibe-Timestamp: <unix seconds this attempt was sent>
* X-OpenVibe-Signature-V2: t=<that timestamp>,v2=<hex HMAC-SHA256 of "<t>.<raw body>">
* X-OpenVibe-Signature-V2: t=<that timestamp>,v2=<hex HMAC-SHA256 of "<t>.<raw body>">
* Every consumer requires v2 and refuses it more than 300 s from its own clock (replay window).
* Every consumer requires v2 and refuses it more than 300 s from its own clock (replay window).
@@ -94,9 +94,9 @@ function inspect({ rawBody, signatureV2, timestamp, secret, now = Date.now() })
@@ -94,9 +94,9 @@ function inspect({ rawBody, signatureV2, timestamp, secret, now = Date.now() })
const obj = JSON.parse(body);
const obj = JSON.parse(body);
if (obj && typeof obj === 'object' && obj.event) {
if (obj && typeof obj === 'object' && obj.event) {
const v = contracts.validate('events.event-envelope@1', obj.event);
const v = contracts.validate('events.event-envelope@1', obj.event);
- parsed = { event_type: obj.event.event_type || null, event_id: obj.event.event_id || null, seq: obj.seq ?? null, envelopeValid: v.valid, envelopeErrors: v.errors };
+ parsed = { event_type: obj.event.event_type || null, event_id: obj.event.event_id || null, envelopeValid: v.valid, envelopeErrors: v.errors };
} else {
} else {
- parsed = { note: 'JSON, but not an Events delivery ({ event, seq })' };
+ parsed = { note: 'JSON, but not an Events delivery ({ event })' };
}
}
} catch { parsed = { note: 'not JSON' }; }
} catch { parsed = { note: 'not JSON' }; }
return { accepted: v2.valid, v2, bodyBytes: Buffer.byteLength(body), parsed };
return { accepted: v2.valid, v2, bodyBytes: Buffer.byteLength(body), parsed };
@@ -123,15 +123,13 @@ function sample({ eventType, secret, producer, now = Date.now() }) {
@@ -123,15 +123,13 @@ function sample({ eventType, secret, producer, now = Date.now() }) {
trace_id: crypto.randomBytes(16).toString('hex'),
trace_id: crypto.randomBytes(16).toString('hex'),
};
};
const check = contracts.validate('events.event-envelope@1', envelope);
const check = contracts.validate('events.event-envelope@1', envelope);
- const seq = 1;
- const body = JSON.stringify({ event: envelope, seq });
+ const body = JSON.stringify({ event: envelope });
const subscriptionId = `sub_${contracts.ids.ulid(now)}`;
const subscriptionId = `sub_${contracts.ids.ulid(now)}`;
const headers = {
const headers = {
'Content-Type': 'application/json',
'Content-Type': 'application/json',
'User-Agent': 'OpenVibe.Events/0.1',
'User-Agent': 'OpenVibe.Events/0.1',
'X-OpenVibe-Event-Id': envelope.event_id,
'X-OpenVibe-Event-Id': envelope.event_id,
'X-OpenVibe-Event-Type': eventType,
'X-OpenVibe-Event-Type': eventType,
- 'X-OpenVibe-Seq': String(seq),
'X-OpenVibe-Subscription-Id': subscriptionId,
'X-OpenVibe-Subscription-Id': subscriptionId,
'X-OpenVibe-Delivery-Attempt': '1',
'X-OpenVibe-Delivery-Attempt': '1',
'X-OpenVibe-Hops': '0',
'X-OpenVibe-Hops': '0',
@@ -95,7 +95,7 @@ ${got.code ? html`<h2>Exchange it (on your server)</h2>
@@ -95,7 +95,7 @@ ${got.code ? html`<h2>Exchange it (on your server)</h2>
viewer: req.viewer, config, path: '/tools/webhooks', title: 'Webhook tester', index: true, scripts: ['js/webhook.js'],
viewer: req.viewer, config, path: '/tools/webhooks', title: 'Webhook tester', index: true, scripts: ['js/webhook.js'],
crumbs: [{ label: 'Webhooks' }],
crumbs: [{ label: 'Webhooks' }],
body: html`<h1>Webhook tester and signed-event inspector</h1>
body: html`<h1>Webhook tester and signed-event inspector</h1>
-<p>OpenVibe.Events delivers each event as <code>POST</code> with the body <code>{"event": <envelope>, "seq": <n>}</code> and these headers, signed with your subscription secret:</p>
+<p>OpenVibe.Events delivers each event as <code>POST</code> with the body <code>{"event": <envelope>}</code> (no sequence number: dedupe on the event id) and these headers, signed with your subscription secret:</p>
<ul>
<ul>
<li><code>X-OpenVibe-Timestamp: <unix seconds></code>, the time this attempt was sent. Every retry gets a new one.</li>
<li><code>X-OpenVibe-Timestamp: <unix seconds></code>, the time this attempt was sent. Every retry gets a new one.</li>
<li><code>X-OpenVibe-Signature-V2: t=<that timestamp>,v2=<hex HMAC-SHA256 of "<t>.<raw body>"></code> (v2).</li>
<li><code>X-OpenVibe-Signature-V2: t=<that timestamp>,v2=<hex HMAC-SHA256 of "<t>.<raw body>"></code> (v2).</li>
@@ -134,7 +134,7 @@ ${v2.timestampsAgree != null ? html`<dt>X-OpenVibe-Timestamp</dt><dd>${code(v2.s
@@ -134,7 +134,7 @@ ${v2.timestampsAgree != null ? html`<dt>X-OpenVibe-Timestamp</dt><dd>${code(v2.s
<dt>Expected</dt><dd>${code(v2.expected || '(needs a t= value)')}</dd><dt>Given</dt><dd>${code(v2.given || '(none)')}</dd></dl>
<dt>Expected</dt><dd>${code(v2.expected || '(needs a t= value)')}</dd><dt>Given</dt><dd>${code(v2.given || '(none)')}</dd></dl>
<h3>Body</h3>
<h3>Body</h3>
<dl class="facts"><dt>Size</dt><dd>${v.bodyBytes} bytes</dd>
<dl class="facts"><dt>Size</dt><dd>${v.bodyBytes} bytes</dd>
-${v.parsed && v.parsed.event_type ? html`<dt>Event</dt><dd>${code(v.parsed.event_type)} ${code(v.parsed.event_id || '')} seq ${v.parsed.seq}; envelope ${v.parsed.envelopeValid ? 'validates' : html`does not validate: ${v.parsed.envelopeErrors.map((e) => `${e.path} ${e.message}`).join('; ')}`}</dd>` : ''}
+${v.parsed && v.parsed.event_type ? html`<dt>Event</dt><dd>${code(v.parsed.event_type)} ${code(v.parsed.event_id || '')}; envelope ${v.parsed.envelopeValid ? 'validates' : html`does not validate: ${v.parsed.envelopeErrors.map((e) => `${e.path} ${e.message}`).join('; ')}`}</dd>` : ''}
${v.parsed && v.parsed.note ? html`<dt>Shape</dt><dd>${v.parsed.note}</dd>` : ''}</dl></div>`;
${v.parsed && v.parsed.note ? html`<dt>Shape</dt><dd>${v.parsed.note}</dd>` : ''}</dl></div>`;
}
}
@@ -19,7 +19,7 @@ const hmacV1 = (body, secret) => `sha256=${crypto.createHmac('sha256', secret).u
@@ -19,7 +19,7 @@ const hmacV1 = (body, secret) => `sha256=${crypto.createHmac('sha256', secret).u
(async () => {
(async () => {
const secret = 'whsec_' + crypto.randomBytes(16).toString('hex');
const secret = 'whsec_' + crypto.randomBytes(16).toString('hex');
- const body = JSON.stringify({ event: { event_id: contracts.ids.newId('event'), event_type: 'network.app.created', version: 1, source: 'network', actor: { type: 'system', id: 'network' }, timestamp: new Date().toISOString(), subject: { type: 'app', id: 'app_1' }, payload: {} }, seq: 7 });
+ const body = JSON.stringify({ event: { event_id: contracts.ids.newId('event'), event_type: 'network.app.created', version: 1, source: 'network', actor: { type: 'system', id: 'network' }, timestamp: new Date().toISOString(), subject: { type: 'app', id: 'app_1' }, payload: {} } });
// A clock 0.7 s into a second: ages are whole seconds, the window is checked on the exact time.
// A clock 0.7 s into a second: ages are whole seconds, the window is checked on the exact time.
const nowSec = Math.floor(Date.now() / 1000);
const nowSec = Math.floor(Date.now() / 1000);
@@ -37,7 +37,7 @@ const hmacV1 = (body, secret) => `sha256=${crypto.createHmac('sha256', secret).u
@@ -37,7 +37,7 @@ const hmacV1 = (body, secret) => `sha256=${crypto.createHmac('sha256', secret).u
assert.strictEqual(v.v2.expected, v2(body, secret, nowSec));
assert.strictEqual(v.v2.expected, v2(body, secret, nowSec));
assert.strictEqual(v.v2.windowSec, 300);
assert.strictEqual(v.v2.windowSec, 300);
assert.strictEqual(v.parsed.event_type, 'network.app.created');
assert.strictEqual(v.parsed.event_type, 'network.app.created');
- assert.strictEqual(v.parsed.seq, 7);
+ assert.strictEqual('seq' in v.parsed, false, 'Events sends no sequence number (ADR-042)');
assert.strictEqual(v.bodyBytes, Buffer.byteLength(body));
assert.strictEqual(v.bodyBytes, Buffer.byteLength(body));
});
});
@@ -81,7 +81,7 @@ const hmacV1 = (body, secret) => `sha256=${crypto.createHmac('sha256', secret).u
@@ -81,7 +81,7 @@ const hmacV1 = (body, secret) => `sha256=${crypto.createHmac('sha256', secret).u
assert.strictEqual(wrongSecret.accepted, false);
assert.strictEqual(wrongSecret.accepted, false);
assert.strictEqual(wrongSecret.v2.signatureValid, false);
assert.strictEqual(wrongSecret.v2.signatureValid, false);
assert.match(wrongSecret.v2.reason, /does not match/);
assert.match(wrongSecret.v2.reason, /does not match/);
- assert.strictEqual(run({ rawBody: body.replace('"seq":7', '"seq":8') }).accepted, false);
+ assert.strictEqual(run({ rawBody: body.replace('"source":"network"', '"source":"netw0rk"') }).accepted, false);
// The HMAC for one time moved onto another: the time is covered by the signature.
// The HMAC for one time moved onto another: the time is covered by the signature.
const moved = run({ signatureV2: good.replace(`t=${nowSec}`, `t=${nowSec - 1}`) });
const moved = run({ signatureV2: good.replace(`t=${nowSec}`, `t=${nowSec - 1}`) });
assert.strictEqual(moved.accepted, false);
assert.strictEqual(moved.accepted, false);
@@ -137,7 +137,8 @@ const hmacV1 = (body, secret) => `sha256=${crypto.createHmac('sha256', secret).u
@@ -137,7 +137,8 @@ const hmacV1 = (body, secret) => `sha256=${crypto.createHmac('sha256', secret).u
assert.strictEqual(verifyDeliveryV2(s.body, headers, secret, { now: nowMs }), true);
assert.strictEqual(verifyDeliveryV2(s.body, headers, secret, { now: nowMs }), true);
const parsed = parseDelivery(s.body, headers, secret, { requireV2: true, now: nowMs });
const parsed = parseDelivery(s.body, headers, secret, { requireV2: true, now: nowMs });
assert.strictEqual(parsed.event.event_type, 'media.object.ready');
assert.strictEqual(parsed.event.event_type, 'media.object.ready');
- assert.strictEqual(parsed.seq, 1);
+ assert.strictEqual('seq' in JSON.parse(s.body), false, 'the sample is { event }, like Events sends it');
+ assert.strictEqual(headers['x-openvibe-seq'], undefined, 'and no X-OpenVibe-Seq');
assert.strictEqual(contracts.validate('events.event-envelope@1', parsed.event).valid, true);
assert.strictEqual(contracts.validate('events.event-envelope@1', parsed.event).valid, true);
assert.strictEqual(parseDelivery(s.body, headers, secret, { requireV2: true, now: nowMs + 301_000 }), null, 'the sample goes stale after 300 s');
assert.strictEqual(parseDelivery(s.body, headers, secret, { requireV2: true, now: nowMs + 301_000 }), null, 'the sample goes stale after 300 s');
assert.strictEqual(parseDelivery(s.body, headers, 'wrong', { requireV2: true, now: nowMs }), null);
assert.strictEqual(parseDelivery(s.body, headers, 'wrong', { requireV2: true, now: nowMs }), null);