openvibers/openvibe.sdk Commits

v0.13.0: deliveries are v2 only, as OpenVibe.Events sends them since 2026-09-28 (shim C-60). signDeliveryHeaders returns X-OpenVibe-Timestamp and X-OpenVibe-Signature-V2 only, and the mock platform's deliverEvents sends no v1 header. signDelivery and verifyDelivery stay for testing a legacy receiver; parseDelivery still accepts v1-only unless requireV2, which every production consumer sets. Tests: the header set, the mock's delivery, and a valid v1 never rescuing a bad v2. npm test 33/33.

OP OpenVibers committed 28 Sept 2026 · 1f70fe4dfe · 7 files, +21 −16


Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex a7fe256..818383d 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -3,6 +3,10 @@
All notable changes to `openvibe-sdk`. The package follows semver; while it is `0.x`, a minor
release may change an API and says so here.
++## 0.13.0 (2026-09-28)
++
++**Deliveries are v2 only**, as OpenVibe.Events sends them since 2026-09-28 (the v1 header, a replayable HMAC of the body alone, is retired: shim C-60). `signDeliveryHeaders` returns `X-OpenVibe-Timestamp` and `X-OpenVibe-Signature-V2` only, and the mock platform's `deliverEvents` sends no `X-OpenVibe-Signature`. A consumer test that asserted the v1 header must drop that assertion. `signDelivery` and `verifyDelivery` stay for testing a legacy receiver, and `parseDelivery` still accepts a v1-only delivery unless `requireV2`, which every production consumer sets.
++
## 0.12.0 (2026-09-28)
**`openvibe-sdk/limits`** (roadmap WS-R task 4): per-actor rate limits at a service's capability boundaries, where nginx can only limit by address. `createActorLimiter({ limits: { minute, hour, day } })` returns `limits(name, ownLimits)`, Express middleware that counts each actor in fixed windows and refuses past a limit with 429 problem+json (`rate_limited`, `Retry-After` from the tightest exceeded window) before the route runs.
ddiff --git a/browser/openvibe-sdk.mjs b/browser/openvibe-sdk.mjs
iindex f827f2d..faed13d 100644
---- a/browser/openvibe-sdk.mjs
++++ b/browser/openvibe-sdk.mjs
@@@ -1,4 +1,4 @@
--// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.12.0, MIT). Do not edit.
++// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.13.0, MIT). Do not edit.
// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces
// (auth, registry, modules, realtime, media, community, search, chat, ai, jobs, tools, projects). No server-only code: no service tokens, JWT verification, events, identity.
/* eslint-disable */
@@@ -40,7 +40,7 @@ const { paginate, offsetPager } = __load(7);
const semver = __load(6);
/** The package version (test/version.test.js keeps it equal to package.json). */
--const SDK_VERSION = '0.12.0';
++const SDK_VERSION = '0.13.0';
module.exports = {
createClient,
ddiff --git a/package-lock.json b/package-lock.json
iindex 70031f5..d5017ac 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-sdk",
-- "version": "0.12.0",
++ "version": "0.13.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-sdk",
-- "version": "0.12.0",
++ "version": "0.13.0",
"license": "MIT",
"devDependencies": {
"better-sqlite3": "^11.10.0",
ddiff --git a/package.json b/package.json
iindex b61e7e2..6308784 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-sdk",
-- "version": "0.12.0",
++ "version": "0.13.0",
"description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, with deadlines, safe retries, problem errors and trace propagation.",
"main": "index.js",
"types": "types/index.d.ts",
ddiff --git a/src/core/index.js b/src/core/index.js
iindex e91818c..b85426f 100644
---- a/src/core/index.js
++++ b/src/core/index.js
@@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');
const semver = require('./semver');
/** The package version (test/version.test.js keeps it equal to package.json). */
--const SDK_VERSION = '0.12.0';
++const SDK_VERSION = '0.13.0';
module.exports = {
createClient,
ddiff --git a/src/events.js b/src/events.js
iindex 102e3ab..aee6a5d 100644
---- a/src/events.js
++++ b/src/events.js
@@@ -52,13 +52,13 @@ function signDeliveryV2(rawBody, secret, timestamp = Math.floor(Date.now() / 100
}
/**
-- * The three signature headers Events puts on a delivery (X-OpenVibe-Signature,
-- * X-OpenVibe-Timestamp, X-OpenVibe-Signature-V2), for tests that post deliveries to a consumer.
++ * The signature headers Events puts on a delivery (X-OpenVibe-Timestamp, X-OpenVibe-Signature-V2), for tests that
++ * post deliveries to a consumer. Events has sent v2 only since 2026-09-28 (the v1 header, a replayable HMAC of the
++ * body alone, is retired: shim C-60); signDelivery still makes a v1 value for anyone testing a legacy receiver.
*/
function signDeliveryHeaders(rawBody, secret, { now = Date.now() } = {}) {
const timestamp = Math.floor(Number(now) / 1000);
return {
-- 'X-OpenVibe-Signature': signDelivery(rawBody, secret),
'X-OpenVibe-Timestamp': String(timestamp),
'X-OpenVibe-Signature-V2': signDeliveryV2(rawBody, secret, timestamp),
};
ddiff --git a/test/events.test.js b/test/events.test.js
iindex c880653..5325290 100644
---- a/test/events.test.js
++++ b/test/events.test.js
@@@ -164,7 +164,7 @@ run([
assert.throws(() => signDeliveryV2(raw, secret, 1.5), TypeError);
assert.match(signDeliveryV2(raw, secret), /^t=\d{10},v2=[0-9a-f]{64}$/, 'defaults to now');
const all = signDeliveryHeaders(raw, secret, { now: now + 999 });
-- assert.deepEqual(all, { 'X-OpenVibe-Signature': signDelivery(raw, secret), 'X-OpenVibe-Timestamp': String(t), 'X-OpenVibe-Signature-V2': v2 });
++ assert.deepEqual(all, { 'X-OpenVibe-Timestamp': String(t), 'X-OpenVibe-Signature-V2': v2 }, 'v2 only, as Events sends since C-60');
}],
['parseDelivery: a present v2 must verify and be fresh (no v1 fallback); requireV2 refuses v1-only', async () => {
@@@ -173,13 +173,13 @@ run([
const now = 1790000000000;
const signed = signDeliveryHeaders(raw, secret, { now });
const want = { event: { event_id: 'evt_2', event_type: 'media.vod.ready' }, seq: 9, subscriptionId: null, attempt: 1 };
-- const v1only = { 'x-openvibe-signature': signed['X-OpenVibe-Signature'] };
-- // Both headers (what Events sends): v2 decides.
++ const v1only = { 'x-openvibe-signature': signDelivery(raw, secret) };
++ // v2 (what Events sends): v2 decides.
assert.deepEqual(parseDelivery(raw, signed, secret, { now }), want);
assert.deepEqual(parseDelivery(raw, signed, secret, { now, requireV2: true }), want);
assert.deepEqual(parseDelivery(raw, new Headers(signed), secret, { now, requireV2: true }), want);
-- assert.equal(parseDelivery(raw, signed, secret, { now: now + 301000 }), null, 'stale v2 with a valid v1: refused, never a v1 fallback');
-- assert.equal(parseDelivery(raw, { ...signed, 'X-OpenVibe-Signature-V2': `t=${now / 1000},v2=${'0'.repeat(64)}` }, secret, { now }), null, 'bad v2 with a valid v1: refused');
++ assert.equal(parseDelivery(raw, signed, secret, { now: now + 301000 }), null, 'stale v2: refused');
++ assert.equal(parseDelivery(raw, { ...signed, 'X-OpenVibe-Signature-V2': `t=${now / 1000},v2=${'0'.repeat(64)}` }, secret, { now }), null, 'bad v2: refused');
assert.equal(parseDelivery(raw, { ...signed, 'X-OpenVibe-Signature-V2': '' }, secret, { now }), null, 'an empty v2 header is present and bad');
assert.equal(parseDelivery(raw, { ...v1only, 'x-openvibe-signature-v2': signed['X-OpenVibe-Signature-V2'] }, secret, { now: now + 200000, toleranceSec: 100 }), null, 'toleranceSec is passed through');
// No v2 header: v1 only while requireV2 is off.
@@@ -188,6 +188,7 @@ run([
assert.equal(parseDelivery(raw, { 'x-openvibe-signature': 'sha256=bad' }, secret), null);
// A v2 delivery with a bad v1 still parses: v1 is not consulted once v2 is there.
assert.deepEqual(parseDelivery(raw, { ...signed, 'X-OpenVibe-Signature': 'sha256=bad' }, secret, { now }), want);
++ assert.equal(parseDelivery(raw, { ...signed, 'X-OpenVibe-Signature-V2': `t=${now / 1000},v2=${'0'.repeat(64)}`, 'x-openvibe-signature': signDelivery(raw, secret) }, secret, { now }), null, 'a valid v1 never rescues a bad v2');
}],
['onPage runs only after every item of its page was handled: a crash mid-page keeps the old cursor', async () => {
@@@ -266,10 +267,10 @@ run([
assert.deepEqual(handled, [[2, 2, 'media.vod.ready'], [4, 1, 'media.vod.ready']], 'in seq order, attempt counted, non-matching skipped');
const req = srv.requests.at(-1);
assert.equal(req.headers['x-openvibe-subscription-id'], sub.id);
-- assert.match(req.headers['x-openvibe-signature'], /^sha256=[0-9a-f]{64}$/);
++ assert.equal(req.headers['x-openvibe-signature'], undefined, 'no v1 header, like Events');
assert.match(req.headers['x-openvibe-timestamp'], /^\d{10}$/);
assert.equal(req.headers['x-openvibe-signature-v2'].split(',')[0], `t=${req.headers['x-openvibe-timestamp']}`);
-- assert.equal(verifyDeliveryV2(req.body, req.headers, secret), true, 'the mock sends v2 too');
++ assert.equal(verifyDeliveryV2(req.body, req.headers, secret), true, 'the mock sends v2');
assert.match(req.headers.traceparent, /^00-[0-9a-f]{32}-[0-9a-f]{16}-01$/);
assert.equal((await platform.deliverEvents()).attempts.length, 0, 'nothing new');