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.

OP OpenVibers committed 29 Sept 2026 · b673d433cd · 9 files, +33 −8


Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 0222290..63ee0a6 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -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.
ddiff --git a/STATUS.json b/STATUS.json
iindex a1bd360..dc5ef48 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -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",
ddiff --git a/browser/openvibe-sdk.mjs b/browser/openvibe-sdk.mjs
iindex c98301a..3a394e3 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.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,
ddiff --git a/package-lock.json b/package-lock.json
iindex b793bfd..62d77a6 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -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",
ddiff --git a/package.json b/package.json
iindex 57bcdb3..89a03e6 100644
---- a/package.json
++++ b/package.json
@@@ -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",
ddiff --git a/src/auth/jwks.js b/src/auth/jwks.js
iindex e5817c4..8ce3614 100644
---- a/src/auth/jwks.js
++++ b/src/auth/jwks.js
@@@ -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 = null
async 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;
ddiff --git a/src/core/index.js b/src/core/index.js
iindex a78645b..9414005 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.22.0';
++const SDK_VERSION = '0.23.1';
module.exports = {
createClient,
ddiff --git a/test/jwks.test.js b/test/jwks.test.js
iindex 8b26387..a53fb58 100644
---- a/test/jwks.test.js
++++ b/test/jwks.test.js
@@@ -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 });
ddiff --git a/types/auth.d.ts b/types/auth.d.ts
iindex bd82ab8..37d2855 100644
---- a/types/auth.d.ts
++++ b/types/auth.d.ts
@@@ -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;