openvibers/openvibe.sdk Commits
v0.23.1: the release version is whole (SDK_VERSION, lockfile and browser bundle said 0.22.0 in 0.23.0); a malformed JWKS entry is skipped instead of failing the whole key set; a kid-less legacy JWKS never refetches for a kid; log declared on the verify options. Found by an independent DeepSeek review of v0.23.0. npm test 44/44 on Node 22 with the test services, run after the version bump.
Changes
@@@ -8,6 +8,14 @@ release may change an API and says so here.++## 0.23.1 (2026-09-29)++++- The release version is whole again: `SDK_VERSION` and the browser bundle say 0.23.1 (0.23.0 shipped them at 0.22.0,++ so version, bundle and pack tests failed on its tree).++- JWKS: a malformed entry in `keys` (null, a non-object) is skipped instead of marking the whole key set failed; a++ legacy document with only `public_key` no longer refetches every 30 s for a token that names a kid.++- Types: `log` is declared on the verify options (it is passed to the JWKS client).++## 0.23.0 (2026-09-29)- **JWKS client** (`openvibe-sdk/auth`: `jwksClient(url, opts)`, `createJwksClient`, `jwksStatus()`), now behind `verifyUserToken`/`verifyAppToken` for a JWKS URL (plan T0/T1: one refresher instead of hand-written copies that swallowed errors). Keys stay fresh for 6 h; after that the last good keys are served while one refresh runs in the background, and a failed fetch keeps them and backs off (1 s doubling to 5 min) instead of rejecting sign-ins or hammering Network. A token with an unknown `kid` refetches at once (a rotation is honoured immediately); unknown-kid refetches are spaced 30 s apart, so a flood of made-up kids costs one fetch per 30 s. Only state changes are logged (`log` option: the first failure, the recovery). `status()` / `jwksStatus()` report readiness, staleness, failures and the next try for `/api/ready`; `start({ intervalMs })` refreshes in the background on an unref'd timer. Before: an expired cache plus a failed fetch rejected every token with 503, and each unknown kid forced a refetch.@@@ -3,7 +3,7 @@"stage": "alpha","code": true,"wave": "W2",-- "version": "0.23.0",++ "version": "0.23.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.22.0, MIT). Do not edit.++// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.23.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.22.0';++const SDK_VERSION = '0.23.1';module.exports = {createClient,@@@ -1,12 +1,12 @@{"name": "openvibe-sdk",-- "version": "0.22.0",++ "version": "0.23.1","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-sdk",-- "version": "0.22.0",++ "version": "0.23.1","license": "MIT","devDependencies": {"@electric-sql/pglite": "^0.5.8",@@@ -1,6 +1,6 @@{"name": "openvibe-sdk",-- "version": "0.23.0",++ "version": "0.23.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",@@@ -29,7 +29,7 @@ const BACKOFF_MS = [1000, 2000, 4000, 8000, 16000, 32000, 64000, 128000, 300000]function keysFromJwks(doc) {const out = [];for (const jwk of (doc && Array.isArray(doc.keys) ? doc.keys : [])) {-- if (jwk.kty !== 'RSA' || (jwk.use && jwk.use !== 'sig') || (jwk.alg && jwk.alg !== 'RS256')) continue;++ if (!jwk || typeof jwk !== 'object' || jwk.kty !== 'RSA' || (jwk.use && jwk.use !== 'sig') || (jwk.alg && jwk.alg !== 'RS256')) continue;try { out.push({ kid: jwk.kid || null, key: crypto.createPublicKey({ key: jwk, format: 'jwk' }) }); } catch { /* skip unusable key */ }}if (!out.length && doc && typeof doc.public_key === 'string') {@@@ -83,6 +83,8 @@ function createJwksClient(url, { fetch: fetchImpl = globalThis.fetch, log = nullasync function keysForKid(kid) {const have = await keys();if (!kid || have.some((k) => k.kid === kid)) return have;++ // A legacy document without kids (Network's public_key) can never name one: refetching would change nothing.++ if (have.every((k) => k.kid == null)) return have;// An unknown kid: a rotation, or garbage. Unknown-kid refetches are spaced minRefetchMs apart, never inside a// backoff, and one already running is joined.if (st.inflight) return await st.inflight;@@@ -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.22.0';++const SDK_VERSION = '0.23.1';module.exports = {createClient,@@@ -88,6 +88,17 @@ run([assert.equal(w.fetches, 3);}],++ ['a malformed entry is skipped; a kid-less legacy document never refetches for a kid', async () => {++ const w = world(); w.doc = { keys: [null, 'x', K1] };++ const c = w.client();++ assert.deepEqual(kids(await c.keys()), ['k1'], 'null and non-objects are skipped, the good key kept');++ const { publicKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });++ const w2 = world(); w2.doc = { public_key: publicKey.export({ type: 'spki', format: 'pem' }) };++ const c2 = w2.client();++ await c2.keysForKid('k1'); w2.t += 31_000; await c2.keysForKid('k1');++ assert.equal(w2.fetches, 1, 'no refetch: a document without kids can never name one');++ }],++['start() refreshes in the background without holding the process open', async () => {const w = world(), c = w.client();c.start({ intervalMs: 60_000 });@@@ -50,6 +50,8 @@ export interface UserTokenClaims {[claim: string]: unknown;}export interface VerifyUserTokenOptions {++ /** Receives the JWKS client's state changes (the first failure, the recovery). */++ log?: JwksClientOptions['log'];/** JWKS document, or its URL (fetched and cached 6 h, refetched on an unknown kid). */jwks?: { keys?: object[]; public_key?: string } | string;/** PEM string or a crypto KeyObject instead of a JWKS. */@@@ -120,6 +122,8 @@ export type AppTokenClaims = ServiceTokenClaims & {on_behalf_of?: string;};export interface VerifyAppTokenOptions {++ /** Receives the JWKS client's state changes (the first failure, the recovery). */++ log?: JwksClientOptions['log'];jwks?: { keys?: object[]; public_key?: string } | string;publicKey?: string | object;issuer?: string;
@@ -8,6 +8,14 @@ release may change an API and says so here.
@@ -8,6 +8,14 @@ release may change an API and says so here.
+## 0.23.1 (2026-09-29)
+
+- The release version is whole again: `SDK_VERSION` and the browser bundle say 0.23.1 (0.23.0 shipped them at 0.22.0,
+ so version, bundle and pack tests failed on its tree).
+- JWKS: a malformed entry in `keys` (null, a non-object) is skipped instead of marking the whole key set failed; a
+ legacy document with only `public_key` no longer refetches every 30 s for a token that names a kid.
+- Types: `log` is declared on the verify options (it is passed to the JWKS client).
+
## 0.23.0 (2026-09-29)
## 0.23.0 (2026-09-29)
- **JWKS client** (`openvibe-sdk/auth`: `jwksClient(url, opts)`, `createJwksClient`, `jwksStatus()`), now behind `verifyUserToken`/`verifyAppToken` for a JWKS URL (plan T0/T1: one refresher instead of hand-written copies that swallowed errors). Keys stay fresh for 6 h; after that the last good keys are served while one refresh runs in the background, and a failed fetch keeps them and backs off (1 s doubling to 5 min) instead of rejecting sign-ins or hammering Network. A token with an unknown `kid` refetches at once (a rotation is honoured immediately); unknown-kid refetches are spaced 30 s apart, so a flood of made-up kids costs one fetch per 30 s. Only state changes are logged (`log` option: the first failure, the recovery). `status()` / `jwksStatus()` report readiness, staleness, failures and the next try for `/api/ready`; `start({ intervalMs })` refreshes in the background on an unref'd timer. Before: an expired cache plus a failed fetch rejected every token with 503, and each unknown kid forced a refetch.
- **JWKS client** (`openvibe-sdk/auth`: `jwksClient(url, opts)`, `createJwksClient`, `jwksStatus()`), now behind `verifyUserToken`/`verifyAppToken` for a JWKS URL (plan T0/T1: one refresher instead of hand-written copies that swallowed errors). Keys stay fresh for 6 h; after that the last good keys are served while one refresh runs in the background, and a failed fetch keeps them and backs off (1 s doubling to 5 min) instead of rejecting sign-ins or hammering Network. A token with an unknown `kid` refetches at once (a rotation is honoured immediately); unknown-kid refetches are spaced 30 s apart, so a flood of made-up kids costs one fetch per 30 s. Only state changes are logged (`log` option: the first failure, the recovery). `status()` / `jwksStatus()` report readiness, staleness, failures and the next try for `/api/ready`; `start({ intervalMs })` refreshes in the background on an unref'd timer. Before: an expired cache plus a failed fetch rejected every token with 503, and each unknown kid forced a refetch.
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
"stage": "alpha",
"stage": "alpha",
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
- "version": "0.23.0",
+ "version": "0.23.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.22.0, MIT). Do not edit.
+// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.23.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, search, chat, ai, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity.
// (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 */
/* eslint-disable */
@@ -41,7 +41,7 @@ const { paginate, offsetPager } = __load(7);
@@ -41,7 +41,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.22.0';
+const SDK_VERSION = '0.23.1';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.22.0",
+ "version": "0.23.1",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.22.0",
+ "version": "0.23.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.23.0",
+ "version": "0.23.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.",
"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",
"main": "index.js",
"types": "types/index.d.ts",
"types": "types/index.d.ts",
@@ -29,7 +29,7 @@ const BACKOFF_MS = [1000, 2000, 4000, 8000, 16000, 32000, 64000, 128000, 300000]
@@ -29,7 +29,7 @@ const BACKOFF_MS = [1000, 2000, 4000, 8000, 16000, 32000, 64000, 128000, 300000]
function keysFromJwks(doc) {
function keysFromJwks(doc) {
const out = [];
const out = [];
for (const jwk of (doc && Array.isArray(doc.keys) ? doc.keys : [])) {
for (const jwk of (doc && Array.isArray(doc.keys) ? doc.keys : [])) {
- if (jwk.kty !== 'RSA' || (jwk.use && jwk.use !== 'sig') || (jwk.alg && jwk.alg !== 'RS256')) continue;
+ if (!jwk || typeof jwk !== 'object' || jwk.kty !== 'RSA' || (jwk.use && jwk.use !== 'sig') || (jwk.alg && jwk.alg !== 'RS256')) continue;
try { out.push({ kid: jwk.kid || null, key: crypto.createPublicKey({ key: jwk, format: 'jwk' }) }); } catch { /* skip unusable key */ }
try { out.push({ kid: jwk.kid || null, key: crypto.createPublicKey({ key: jwk, format: 'jwk' }) }); } catch { /* skip unusable key */ }
}
}
if (!out.length && doc && typeof doc.public_key === 'string') {
if (!out.length && doc && typeof doc.public_key === 'string') {
@@ -83,6 +83,8 @@ function createJwksClient(url, { fetch: fetchImpl = globalThis.fetch, log = null
@@ -83,6 +83,8 @@ function createJwksClient(url, { fetch: fetchImpl = globalThis.fetch, log = null
async function keysForKid(kid) {
async function keysForKid(kid) {
const have = await keys();
const have = await keys();
if (!kid || have.some((k) => k.kid === kid)) return have;
if (!kid || have.some((k) => k.kid === kid)) return have;
+ // A legacy document without kids (Network's public_key) can never name one: refetching would change nothing.
+ if (have.every((k) => k.kid == null)) return have;
// An unknown kid: a rotation, or garbage. Unknown-kid refetches are spaced minRefetchMs apart, never inside a
// An unknown kid: a rotation, or garbage. Unknown-kid refetches are spaced minRefetchMs apart, never inside a
// backoff, and one already running is joined.
// backoff, and one already running is joined.
if (st.inflight) return await st.inflight;
if (st.inflight) return await st.inflight;
@@ -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.22.0';
+const SDK_VERSION = '0.23.1';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -88,6 +88,17 @@ run([
@@ -88,6 +88,17 @@ run([
assert.equal(w.fetches, 3);
assert.equal(w.fetches, 3);
}],
}],
+ ['a malformed entry is skipped; a kid-less legacy document never refetches for a kid', async () => {
+ const w = world(); w.doc = { keys: [null, 'x', K1] };
+ const c = w.client();
+ assert.deepEqual(kids(await c.keys()), ['k1'], 'null and non-objects are skipped, the good key kept');
+ const { publicKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });
+ const w2 = world(); w2.doc = { public_key: publicKey.export({ type: 'spki', format: 'pem' }) };
+ const c2 = w2.client();
+ await c2.keysForKid('k1'); w2.t += 31_000; await c2.keysForKid('k1');
+ assert.equal(w2.fetches, 1, 'no refetch: a document without kids can never name one');
+ }],
+
['start() refreshes in the background without holding the process open', async () => {
['start() refreshes in the background without holding the process open', async () => {
const w = world(), c = w.client();
const w = world(), c = w.client();
c.start({ intervalMs: 60_000 });
c.start({ intervalMs: 60_000 });
@@ -50,6 +50,8 @@ export interface UserTokenClaims {
@@ -50,6 +50,8 @@ export interface UserTokenClaims {
[claim: string]: unknown;
[claim: string]: unknown;
}
}
export interface VerifyUserTokenOptions {
export interface VerifyUserTokenOptions {
+ /** Receives the JWKS client's state changes (the first failure, the recovery). */
+ log?: JwksClientOptions['log'];
/** JWKS document, or its URL (fetched and cached 6 h, refetched on an unknown kid). */
/** JWKS document, or its URL (fetched and cached 6 h, refetched on an unknown kid). */
jwks?: { keys?: object[]; public_key?: string } | string;
jwks?: { keys?: object[]; public_key?: string } | string;
/** PEM string or a crypto KeyObject instead of a JWKS. */
/** PEM string or a crypto KeyObject instead of a JWKS. */
@@ -120,6 +122,8 @@ export type AppTokenClaims = ServiceTokenClaims & {
@@ -120,6 +122,8 @@ export type AppTokenClaims = ServiceTokenClaims & {
on_behalf_of?: string;
on_behalf_of?: string;
};
};
export interface VerifyAppTokenOptions {
export interface VerifyAppTokenOptions {
+ /** Receives the JWKS client's state changes (the first failure, the recovery). */
+ log?: JwksClientOptions['log'];
jwks?: { keys?: object[]; public_key?: string } | string;
jwks?: { keys?: object[]; public_key?: string } | string;
publicKey?: string | object;
publicKey?: string | object;
issuer?: string;
issuer?: string;