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.
Changes
@@@ -3,6 +3,10 @@All notable changes to `openvibe-sdk`. The package follows semver; while it is `0.x`, a minorrelease 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.@@@ -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,@@@ -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",@@@ -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",@@@ -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,@@@ -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),};@@@ -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');
@@ -3,6 +3,10 @@
@@ -3,6 +3,10 @@
All notable changes to `openvibe-sdk`. The package follows semver; while it is `0.x`, a minor
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.
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)
## 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.
**`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.
@@ -1,4 +1,4 @@
@@ -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
// 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.
// (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 */
/* eslint-disable */
@@ -40,7 +40,7 @@ const { paginate, offsetPager } = __load(7);
@@ -40,7 +40,7 @@ const { paginate, offsetPager } = __load(7);
const semver = __load(6);
const semver = __load(6);
/** The package version (test/version.test.js keeps it equal to package.json). */
/** 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 = {
module.exports = {
createClient,
createClient,
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.12.0",
+ "version": "0.13.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.12.0",
+ "version": "0.13.0",
"license": "MIT",
"license": "MIT",
"devDependencies": {
"devDependencies": {
"better-sqlite3": "^11.10.0",
"better-sqlite3": "^11.10.0",
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-sdk",
"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.",
"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",
"main": "index.js",
"types": "types/index.d.ts",
"types": "types/index.d.ts",
@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');
@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');
const semver = require('./semver');
const semver = require('./semver');
/** The package version (test/version.test.js keeps it equal to package.json). */
/** 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 = {
module.exports = {
createClient,
createClient,
@@ -52,13 +52,13 @@ function signDeliveryV2(rawBody, secret, timestamp = Math.floor(Date.now() / 100
@@ -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() } = {}) {
function signDeliveryHeaders(rawBody, secret, { now = Date.now() } = {}) {
const timestamp = Math.floor(Number(now) / 1000);
const timestamp = Math.floor(Number(now) / 1000);
return {
return {
- 'X-OpenVibe-Signature': signDelivery(rawBody, secret),
'X-OpenVibe-Timestamp': String(timestamp),
'X-OpenVibe-Timestamp': String(timestamp),
'X-OpenVibe-Signature-V2': signDeliveryV2(rawBody, secret, timestamp),
'X-OpenVibe-Signature-V2': signDeliveryV2(rawBody, secret, timestamp),
};
};
@@ -164,7 +164,7 @@ run([
@@ -164,7 +164,7 @@ run([
assert.throws(() => signDeliveryV2(raw, secret, 1.5), TypeError);
assert.throws(() => signDeliveryV2(raw, secret, 1.5), TypeError);
assert.match(signDeliveryV2(raw, secret), /^t=\d{10},v2=[0-9a-f]{64}$/, 'defaults to now');
assert.match(signDeliveryV2(raw, secret), /^t=\d{10},v2=[0-9a-f]{64}$/, 'defaults to now');
const all = signDeliveryHeaders(raw, secret, { now: now + 999 });
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 () => {
['parseDelivery: a present v2 must verify and be fresh (no v1 fallback); requireV2 refuses v1-only', async () => {
@@ -173,13 +173,13 @@ run([
@@ -173,13 +173,13 @@ run([
const now = 1790000000000;
const now = 1790000000000;
const signed = signDeliveryHeaders(raw, secret, { now });
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 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 }), want);
assert.deepEqual(parseDelivery(raw, signed, secret, { now, requireV2: true }), want);
assert.deepEqual(parseDelivery(raw, signed, secret, { now, requireV2: true }), want);
assert.deepEqual(parseDelivery(raw, new Headers(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, { ...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');
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.
// No v2 header: v1 only while requireV2 is off.
@@ -188,6 +188,7 @@ run([
@@ -188,6 +188,7 @@ run([
assert.equal(parseDelivery(raw, { 'x-openvibe-signature': 'sha256=bad' }, secret), null);
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.
// 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.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 () => {
['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([
@@ -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');
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);
const req = srv.requests.at(-1);
assert.equal(req.headers['x-openvibe-subscription-id'], sub.id);
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.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(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.match(req.headers.traceparent, /^00-[0-9a-f]{32}-[0-9a-f]{16}-01$/);
assert.equal((await platform.deliverEvents()).attempts.length, 0, 'nothing new');
assert.equal((await platform.deliverEvents()).attempts.length, 0, 'nothing new');