openvibers/openvibe.sdk Commits
openvibe-sdk 0.40.1: verifyUserToken refuses node principals (#37)
Changes
@@@ -3,6 +3,14 @@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.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@@@ -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.@@@ -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",@@@ -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,@@@ -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",@@@ -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",@@@ -75,8 +75,10 @@ async function verifyJwt(token, { jwks, publicKey, issuer, audience, clockSkewSeasync 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');@@@ -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,@@@ -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' });}],
@@ -3,6 +3,14 @@
@@ -3,6 +3,14 @@
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.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)
## 0.40.0 (2026-10-10)
- **New: `createNetworkKeys()`** (`openvibe-sdk/auth`, server). The keys a service verifies Network tokens with, as one
- **New: `createNetworkKeys()`** (`openvibe-sdk/auth`, server). The keys a service verifies Network tokens with, as one
@@ -239,7 +239,7 @@ Server-only subpaths are declared `"browser": null` in the exports map, so a bun
@@ -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 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 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 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`.
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.
- `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.
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
"stage": "alpha",
"stage": "alpha",
"code": true,
"code": true,
"wave": "W2",
"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",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30",
"deployed": false,
"deployed": false,
"deployment": "none: a package released as git tags; services and apps pin a tag",
"deployment": "none: a package released as git tags; services and apps pin a tag",
@@ -1,4 +1,4 @@
@@ -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
// 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.
// (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 */
/* eslint-disable */
@@ -42,7 +42,7 @@ const { paginate, offsetPager } = __load(7);
@@ -42,7 +42,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.40.0';
+const SDK_VERSION = '0.40.1';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.40.0",
+ "version": "0.40.1",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.40.0",
+ "version": "0.40.1",
"license": "MIT",
"license": "MIT",
"devDependencies": {
"devDependencies": {
"@electric-sql/pglite": "^0.5.8",
"@electric-sql/pglite": "^0.5.8",
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-sdk",
"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.",
"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",
"main": "index.js",
"types": "types/index.d.ts",
"types": "types/index.d.ts",
@@ -75,8 +75,10 @@ async function verifyJwt(token, { jwks, publicKey, issuer, audience, clockSkewSe
@@ -75,8 +75,10 @@ async function verifyJwt(token, { jwks, publicKey, issuer, audience, clockSkewSe
async function verifyUserToken(token, opts = {}) {
async function verifyUserToken(token, opts = {}) {
const claims = await verifyJwt(token, opts, 'user');
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
// 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).
// 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');
if (claims.typ !== undefined || claims.purpose !== undefined) throw fail('token.not_user', 'a typed token is not a user session');
@@ -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.40.0';
+const SDK_VERSION = '0.40.1';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -61,6 +61,11 @@ run([
@@ -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);
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' });
await assert.rejects(verifyUserToken(svc, { jwks }), { code: 'token.not_user' });
assert.equal((await verifyUserToken(svc, { jwks, allowServiceTokens: true })).sub, 'svc:live');
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' });
await assert.rejects(verifyUserToken(sign(claims(), good.privateKey), { jwks: { keys: [] } }), { code: 'token.no_key' });
}],
}],