openvibers/openvibe.sdk Commits

SDK 0.25.1: verifyServiceToken({ jwks }) also takes a JWKS client the service already holds (the one it built for openvibe-sdk/sso, or a test's), so service tokens are checked with the key their kid names instead of a service taking the first key itself. Test: kid choice, bad signature and an unreachable client (token.unavailable, fixed reason) through a held client.

OP OpenVibers committed 29 Sept 2026 · 2c28409df0 · 8 files, +27 −10


Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex d0c0a93..57c13b9 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -8,6 +8,12 @@ release may change an API and says so here.
++## 0.25.1 (2026-09-29)
++
++- `verifyServiceToken({ jwks })` also takes a JWKS client the service already holds (`jwksClient(url)`, or one a test
++ injects), so a service that built one for `openvibe-sdk/sso` verifies service tokens with the same keys, chosen by
++ `kid`, instead of taking the first key itself.
++
## 0.25.0 (2026-09-29)
- `openvibe-sdk/sso` (new, server): `createSsoClient({ site, baseUrl, clientId, clientSecret, networkUrl, networkInternalUrl,
ddiff --git a/browser/openvibe-sdk.mjs b/browser/openvibe-sdk.mjs
iindex 3b1921a..c6b3aca 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.25.0, MIT). Do not edit.
++// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.25.1, 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, geo). No server-only code: no service tokens, JWT verification, events, identity.
/* eslint-disable */
@@@ -41,7 +41,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.25.0';
++const SDK_VERSION = '0.25.1';
module.exports = {
createClient,
ddiff --git a/package-lock.json b/package-lock.json
iindex 2d6d9ad..67352d3 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-sdk",
-- "version": "0.25.0",
++ "version": "0.25.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-sdk",
-- "version": "0.25.0",
++ "version": "0.25.1",
"license": "MIT",
"devDependencies": {
"@electric-sql/pglite": "^0.5.8",
ddiff --git a/package.json b/package.json
iindex 31df80f..4f686c5 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-sdk",
-- "version": "0.25.0",
++ "version": "0.25.1",
"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, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.",
"main": "index.js",
"types": "types/index.d.ts",
ddiff --git a/src/auth/service.js b/src/auth/service.js
iindex bc4c1c3..57df27d 100644
---- a/src/auth/service.js
++++ b/src/auth/service.js
@@@ -1,6 +1,6 @@
'use strict';
/**
-- * verifyServiceToken(token, { jwks (a URL or a JWKS document) | publicKey, issuer, audience, contracts, acceptSandbox, log })
++ * verifyServiceToken(token, { jwks (a URL, a jwksClient or a JWKS document) | publicKey, issuer, audience, contracts, acceptSandbox, log })
* → { ok: true, claims } | { ok: false, code, reason }
*
* A Network service or app token (identity.service-token-claims@1), for a service that receives them. The SDK
@@@ -31,7 +31,9 @@ async function verifyServiceToken(token, { jwks = null, publicKey = null, issuer
const kid = headerKid(token);
let keys;
try {
-- keys = typeof jwks === 'string' ? await jwksClient(jwks, { log, ...(fetchImpl ? { fetch: fetchImpl } : {}) }).keysForKid(kid) : keysFromJwks(jwks);
++ keys = typeof jwks === 'string' ? await jwksClient(jwks, { log, ...(fetchImpl ? { fetch: fetchImpl } : {}) }).keysForKid(kid)
++ : typeof jwks.keysForKid === 'function' ? await jwks.keysForKid(kid) // a client the service already holds
++ : keysFromJwks(jwks);
} catch (err) {
try { (log && (log.warn || log.error) || (() => {})).call(log, `[openvibe-sdk] service token not verified: ${(err && err.message) || err}`); } catch { /* */ }
return { ok: false, code: 'token.unavailable', reason: 'signing key not loaded yet' };
ddiff --git a/src/core/index.js b/src/core/index.js
iindex a8d4bc7..5dd2b2b 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.25.0';
++const SDK_VERSION = '0.25.1';
module.exports = {
createClient,
ddiff --git a/test/service-token.test.js b/test/service-token.test.js
iindex ce966fd..13d538a 100644
---- a/test/service-token.test.js
++++ b/test/service-token.test.js
@@@ -52,6 +52,15 @@ run([
assert.equal(hits, 1);
} finally { srv.close(); }
}],
++ ['a JWKS client the service already holds: the key its kid names', async () => {
++ const asked = [];
++ const client = { keysForKid: async (kid) => { asked.push(kid); return [{ kid: 'old', key: a.publicKey }, { kid: 'new', key: b.publicKey }]; } };
++ assert.equal((await verifyServiceToken(sign(svc(), b.privateKey, 'new'), opts({ jwks: client }))).ok, true);
++ assert.equal((await verifyServiceToken(sign(svc(), stranger.privateKey, 'new'), opts({ jwks: client }))).code, 'token.bad_signature');
++ assert.deepEqual(asked, ['new', 'new']);
++ const down = { keysForKid: async () => { throw new Error('http://10.0.0.1/jwks unreachable'); } };
++ assert.deepEqual(Object.values(await verifyServiceToken(sign(svc(), b.privateKey, 'new'), opts({ jwks: down }))), [false, 'token.unavailable', 'signing key not loaded yet']);
++ }],
['misuse is a TypeError: no contracts module, no audience, no key source', async () => {
await assert.rejects(verifyServiceToken('x.y.z', { jwks: doc, audience: 'openvibe.media' }), TypeError);
await assert.rejects(verifyServiceToken('x.y.z', { jwks: doc, contracts }), TypeError);
ddiff --git a/types/auth.d.ts b/types/auth.d.ts
iindex 882aec7..c554572 100644
---- a/types/auth.d.ts
++++ b/types/auth.d.ts
@@@ -143,8 +143,8 @@ export interface ContractsForServiceTokens {
}
export type ServiceTokenResult = { ok: true; claims: Record<string, unknown> } | { ok: false; code: string; reason: string };
export interface VerifyServiceTokenOptions {
-- /** A JWKS URL (the process-wide client: last good keys, rotation, backoff) or a JWKS document. */
-- jwks?: string | { keys?: object[]; public_key?: string } | null;
++ /** A JWKS URL (the process-wide client: last good keys, rotation, backoff), a JWKS client the service already holds, or a JWKS document. */
++ jwks?: string | JwksClient | { keys?: object[]; public_key?: string } | null;
/** A pinned PEM instead of a JWKS. */
publicKey?: string | object | null;
issuer?: string;