openvibers/openvibe.sdk Commits

openvibe-sdk 0.40.1: verifyUserToken refuses node principals (#37)

OP OpenVibers committed 10 Oct 2026 · f2dcec3279 · 9 files, +25 −10

`verifyUserToken` refused service, app and module principal tokens but not **node** principals. A node token (`actor_type: node`, `sub node:nod_…`) is what Network issues to a machine paired with a service, such as a robot's computer for OpenVibe.Bot, and it is signed with the same key. Several services check people's tokens without an audience, so one would have taken a node token as a session: Live, Chat, Media's site, Community, Space, Tips and VIP. Bot already refused node tokens itself.

- `src/auth/jwt.js`: `node` joins the principal actor types and sub prefixes. Either one alone is `token.not_user`, unless the caller passes `allowServiceTokens`.
- `test/jwt.test.js`: a node token is refused by its actor_type plus sub, by its sub alone, and by its actor_type alone.
- 0.40.1: version, `SDK_VERSION`, changelog, STATUS, README and the browser bundle.

`ov test` is green. The services that consume this get the pin with their next bump.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex d4344f2..dc9da9c 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -3,6 +3,14 @@
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.40.1 (2026-10-10)
++
++- **`verifyUserToken` refuses node principals.** A node token (`actor_type: node`, `sub node:nod_…`: a machine paired
++ for a service, such as a robot's computer for OpenVibe.Bot) is signed with Network's key like every principal token.
++ It was refused for service, app and module principals only, so a service that checks user tokens without an
++ audience would have taken a node token for a session. It is `token.not_user` now, by its `actor_type` or its `sub`
++ alone, unless the caller passes `allowServiceTokens`.
++
## 0.40.0 (2026-10-10)
- **New: `createNetworkKeys()`** (`openvibe-sdk/auth`, server). The keys a service verifies Network tokens with, as one
ddiff --git a/README.md b/README.md
iindex 61c80cc..51ac521 100644
---- a/README.md
++++ b/README.md
@@@ -239,7 +239,7 @@ Server-only subpaths are declared `"browser": null` in the exports map, so a bun
- It accepts RS256 only. `alg: none`, HS256 and anything else fail as `token.malformed` before any key is used.
- It checks the signature against the JWKS document, URL or client (`createNetworkKeys().verifyOptions`). A URL is cached for 6 h and fetched again when a token carries an unknown `kid`. It also accepts the Network's legacy `public_key` field or a PEM string.
- It checks `exp`, `nbf` and `iat` (30 s clock skew), then `iss` and `aud`.
-- - It rejects service-principal tokens and typed tokens (a realtime ticket, a FedCM assertion: `typ` or `purpose` set) as `token.not_user`.
++ - It rejects principal tokens (a service, a developer app, a module or a node: a machine paired for a service) and typed tokens (a realtime ticket, a FedCM assertion: `typ` or `purpose` set) as `token.not_user`.
It returns the claims, including `subject_id`.
- `verifyServiceToken(token, { jwks, issuer, audience, contracts })` checks a service or app token for a service that receives them: the SDK chooses the key (the `kid`'s, else each; the shared JWKS client or a pinned PEM), and every rule is your own pinned openvibe-contracts `serviceAuth.verifyServiceToken` (pass the module as `contracts`). No key answers `{ ok: false, code: 'token.unavailable' }` with a fixed reason, never the internal JWKS URL.
ddiff --git a/STATUS.json b/STATUS.json
iindex 36ca511..8d7f743 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -3,7 +3,7 @@
"stage": "alpha",
"code": true,
"wave": "W2",
-- "version": "0.40.0",
++ "version": "0.40.1",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30",
"deployed": false,
"deployment": "none: a package released as git tags; services and apps pin a tag",
ddiff --git a/browser/openvibe-sdk.mjs b/browser/openvibe-sdk.mjs
iindex 51cb42f..666ff4c 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.40.0, MIT). Do not edit.
++// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.40.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, bot, search, chat, ai, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity.
/* eslint-disable */
@@@ -42,7 +42,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.40.0';
++const SDK_VERSION = '0.40.1';
module.exports = {
createClient,
ddiff --git a/package-lock.json b/package-lock.json
iindex a200bb8..5375fac 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-sdk",
-- "version": "0.40.0",
++ "version": "0.40.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-sdk",
-- "version": "0.40.0",
++ "version": "0.40.1",
"license": "MIT",
"devDependencies": {
"@electric-sql/pglite": "^0.5.8",
ddiff --git a/package.json b/package.json
iindex de917d0..abbe76e 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-sdk",
-- "version": "0.40.0",
++ "version": "0.40.1",
"description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, Bot robots, 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/jwt.js b/src/auth/jwt.js
iindex 12401c8..1657012 100644
---- a/src/auth/jwt.js
++++ b/src/auth/jwt.js
@@@ -75,8 +75,10 @@ async function verifyJwt(token, { jwks, publicKey, issuer, audience, clockSkewSe
async function verifyUserToken(token, opts = {}) {
const claims = await verifyJwt(token, opts, 'user');
-- const principal = ['service', 'app', 'mod'].includes(claims.actor_type) || /^(svc|app|mod):/.test(String(claims.sub || ''));
-- if (principal && !opts.allowServiceTokens) throw fail('token.not_user', 'a service principal token is not a user token');
++ // Every Network principal: a service, a developer app, a module, or a node (a machine paired for a service, e.g.
++ // a robot's computer for OpenVibe.Bot). None of them is a person's session, whatever audience it names.
++ const principal = ['service', 'app', 'mod', 'node'].includes(claims.actor_type) || /^(svc|app|mod|node):/.test(String(claims.sub || ''));
++ if (principal && !opts.allowServiceTokens) throw fail('token.not_user', 'a principal token (service, app, module or node) is not a user token');
// A typed token (Network's realtime ticket, an export token: `typ` or `purpose` set) is never a session, whatever its
// issuer and audience say (OpenVibe.Events' rule, now every service's).
if (claims.typ !== undefined || claims.purpose !== undefined) throw fail('token.not_user', 'a typed token is not a user session');
ddiff --git a/src/core/index.js b/src/core/index.js
iindex 3064960..5e9a1c9 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.40.0';
++const SDK_VERSION = '0.40.1';
module.exports = {
createClient,
ddiff --git a/test/jwt.test.js b/test/jwt.test.js
iindex 19fd67f..7237fb5 100644
---- a/test/jwt.test.js
++++ b/test/jwt.test.js
@@@ -61,6 +61,11 @@ run([
const svc = sign({ iss: 'https://openvibe.network', sub: 'svc:live', actor_type: 'service', aud: ['openvibe.network'], cap: [], iat: now(), exp: now() + 300, jti: 'tok_1' }, good.privateKey);
await assert.rejects(verifyUserToken(svc, { jwks }), { code: 'token.not_user' });
assert.equal((await verifyUserToken(svc, { jwks, allowServiceTokens: true })).sub, 'svc:live');
++ // A node token (a machine paired for Bot) is a principal too, by its actor_type or its sub alone.
++ const node = { iss: 'https://openvibe.network', sub: 'node:nod_01JAB2C3D4E5F6G7H8J9K0MNPR', actor_type: 'node', aud: ['openvibe.bot'], cap: [], iat: now(), exp: now() + 300, jti: 'tok_2' };
++ await assert.rejects(verifyUserToken(sign(node, good.privateKey), { jwks }), { code: 'token.not_user' });
++ await assert.rejects(verifyUserToken(sign({ ...node, actor_type: undefined }, good.privateKey), { jwks }), { code: 'token.not_user' });
++ await assert.rejects(verifyUserToken(sign({ ...claims(), actor_type: 'node' }, good.privateKey), { jwks }), { code: 'token.not_user' });
await assert.rejects(verifyUserToken(sign(claims(), good.privateKey), { jwks: { keys: [] } }), { code: 'token.no_key' });
}],