openvibers/openvibe.tips Commits

Tips: the key provider is openvibe-sdk/auth createNetworkKeys (SDK 0.40.0) (#36)

OP OpenVibers committed 10 Oct 2026 · 57a101aadf · 9 files, +20 −31

* openvibe-sdk v0.40.0 (was v0.38.1)

Opened by scripts/pin-bump.js in OpenVibers/OpenVibe.Shared after the v0.40.0 release.

* Tips: the key provider is openvibe-sdk/auth createNetworkKeys (SDK 0.40.0)

This service had a small key provider wrapped around openvibe-sdk/auth's JWKS client. SDK v0.40.0 ships that provider as `createNetworkKeys`, so `createKeyProvider` now returns it. This PR carries the SDK v0.40.0 pin bump, whose own PR is closed as folded in.

- `keys.verifyOptions`, `keys.loaded()`, `keys.refresh()`, `keys.start()` and `keys.stop()` replace `source`, `ready()`, `load()`, `start` and `stop`. Callers in `server/network.js`, `server/api/auth.js`, `server/observability.js` and `server/index.js` are updated.
- New: `start()` retries every 30 s until the first load. Before, the client waited for its 15-minute tick or the first token. After that it refreshes every 15 minutes.
- A rotation is still followed on an unknown kid, and a pinned `OV_NETWORK_PUBLIC_KEY` is used as is.

`ov test` is green, including the API, security, entitlements and actor-limits suites.

Changes

ddiff --git a/README.md b/README.md
iindex f444be9..426b4ef 100644
---- a/README.md
++++ b/README.md
@@@ -452,6 +452,6 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
<!-- versions:start -->
- openvibe-contracts: v0.129.0
--- openvibe-sdk: v0.38.1
++- openvibe-sdk: v0.40.0
- openvibe-shared: v3.0.0
<!-- versions:end -->
ddiff --git a/STATUS.json b/STATUS.json
iindex 36d170b..ce2c8cd 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -10,7 +10,7 @@
"deployment": "Deployed internally, not launched: deployed with `sudo ovhost deploy tips`, /opt/openvibe.tips, unit openvibe-tips, 127.0.0.1:4610 loopback only; https://openvibe.tips still serves the OpenVibe.Sites placeholder. Principal tips; Events subscriptions billing.transaction.* and billing.receipt.* -> /internal/events (secret rotated 2026-09-24); TIPS_CHAT_ADAPTER is empty (no chat side effects); TIPS_CHAT_ADAPTER=chat delivers through OpenVibe.Chat's ingress once tips is granted chat.message.send, chat.event.publish and identity.subject.resolve.",
"database": "PostgreSQL 18 through PgBouncer (transaction mode) as the system of record, Valkey 9 for shared state (per-actor limits, overlay fan-out, overlay stream slots), ADR-035: database ov_tips from OpenVibe.Host's data role, schema in migrations/ (0001_initial expand, 0002_drop_legacy_import contract), async on openvibe-sdk/db v0.28.0, money paths serializable with row locks, PostgreSQL outbox and inbox.",
"contracts": "openvibe-contracts v0.129.0",
-- "sdk": "openvibe-sdk v0.38.1",
++ "sdk": "openvibe-sdk v0.40.0",
"shared": "openvibe-shared v3.0.0",
"notes": "Runtime, API, SSR pages, overlays, the Billing settlement consumer and the events outbox are built, tested and deployed, together with /metrics (loopback only), truthful /api/ready (Billing and Events checks), supporter privacy (anonymous, hidden amount, private message), goal and supporter-page settings, export and erasure (tips.interaction.erased), moderation (word filter, hide/show, invited moderators, tips.interaction.moderated) and per-actor limits. Capabilities (18 tips.*) and payload schemas are in openvibe-contracts. No billing.* event had arrived: Billing runs in shadow. A written internal threat review (docs/threat-review.md) with its fixes and test/security.test.js. Not done: an independent security review; the openvibe.tips launch. Live stays the place tips happen until the Billing cutover (blocked on the owner re-pointing the PowerChat webhook) and the launch rule in README.md holds.",
"features": [
ddiff --git a/package-lock.json b/package-lock.json
iindex bc80bc5..e4f4fa7 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -14,7 +14,7 @@
"express": "^4.21.2",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.129.0",
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.38.1",
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.40.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v3.0.0",
"pg": "^8.23.0"
},
@@@ -859,9 +859,9 @@
}
},
"node_modules/openvibe-sdk": {
-- "version": "0.38.1",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.38.1",
-- "integrity": "sha512-Pn3nji9Nc+4rMRjhwO9AhhR80OW7cwLo/NLObiVR/4TlcOxyk/V8DLzGHQmFjNxqg0xLBj6MOiXYQR2qAxEycg==",
++ "version": "0.40.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.40.0",
++ "integrity": "sha512-fYX0zijJZDdwjXJGwroLDlb/wniY6NXVtgdPVJJHNgcdRmaAydpr6SKuxcLffmkTquyZQptwChsCvo+D6Wx+IA==",
"license": "MIT",
"engines": {
"node": ">=20"
ddiff --git a/package.json b/package.json
iindex 15f4c0c..4f37c13 100644
---- a/package.json
++++ b/package.json
@@@ -26,7 +26,7 @@
"express": "^4.21.2",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.129.0",
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.38.1",
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.40.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v3.0.0",
"pg": "^8.23.0"
},
ddiff --git a/server/api/auth.js b/server/api/auth.js
iindex 1bba431..d8cffdd 100644
---- a/server/api/auth.js
++++ b/server/api/auth.js
@@@ -46,7 +46,7 @@ function createApiAuth({ config, keys, userAuth }) {
const header = String(req.headers.authorization || '');
if (!header.startsWith('Bearer ')) return { principal: ANON };
const token = header.slice(7).trim();
-- if (!keys.ready()) return { error: [503, 'identity.unavailable', 'the Network signing key is not loaded yet'] };
++ if (!keys.loaded()) return { error: [503, 'identity.unavailable', 'the Network signing key is not loaded yet'] };
const payload = decodePayload(token);
if (payload && typeof payload.sub === 'string' && PRINCIPAL_SUB.test(payload.sub)) {
let r;
ddiff --git a/server/index.js b/server/index.js
iindex a97ee58..bb0b8bb 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -55,7 +55,7 @@ async function start() {
const app = createApp({ config, db, valkey, registry });
const { domain, keys, outbox } = app.locals;
-- keys.start();
++ keys.start().catch(() => { /* logged by the JWKS client */ });
const timers = [];
if (config.jobs.enabled) {
ddiff --git a/server/network.js b/server/network.js
iindex 34047d9..63c9425 100644
---- a/server/network.js
++++ b/server/network.js
@@@ -3,35 +3,24 @@
/**
* OpenVibe.Network client.
*
-- * keys the Network's RS256 signing keys through openvibe-sdk/auth: one process-wide JWKS client per URL keeps
-- * them fresh, serves the last good keys through a Network outage, backs off, and refetches at once when a
-- * token names a key it does not have (a rotation). OV_NETWORK_PUBLIC_KEY pins one key instead. Service
++ * keys the Network's RS256 signing keys: openvibe-sdk/auth createNetworkKeys. The JWKS is retried every 30 s
++ * until it loads, then kept fresh; the last good keys are served through a Network outage, and a token that
++ * names a key the client does not have refetches at once (a rotation). OV_NETWORK_PUBLIC_KEY pins one key
++ * instead. Service
* tokens (audience openvibe.tips) and the browser's Network user JWT are both verified offline.
*/
--const crypto = require('crypto');
const contracts = require('openvibe-contracts');
--const { jwksClient, verifyUserToken, verifyServiceToken } = require('openvibe-sdk/auth');
++const { createNetworkKeys, verifyUserToken, verifyServiceToken } = require('openvibe-sdk/auth');
const { OpenVibeAuthClient } = require('openvibe-shared/auth-client');
++/** keys.verifyOptions (spread into the verifiers), keys.loaded(), keys.start(), keys.stop(), keys.refresh(). */
function createKeyProvider(config, { fetchImpl = globalThis.fetch, log = console } = {}) {
-- const url = `${config.network.internalUrl}/api/.well-known/jwks`;
-- const pinned = config.network.publicKey ? crypto.createPublicKey(config.network.publicKey) : null;
-- const client = pinned ? null : jwksClient(url, { fetch: fetchImpl, log });
-- /** Where a verification takes its keys from (openvibe-sdk/auth options). */
-- const source = pinned ? { publicKey: pinned } : { jwks: url, fetch: fetchImpl };
-- return {
-- source,
-- ready: () => (pinned ? true : client.status().ready),
-- /** Fetch the keys now (boot, tests); true when there is a key to verify with. */
-- async load() { if (client) await client.refresh().catch(() => {}); return pinned ? true : client.status().ready; },
-- start() { if (client) client.start(); },
-- stop() { if (client) client.stop(); },
-- };
++ return createNetworkKeys({ network: config.network.internalUrl, publicKey: config.network.publicKey || null, fetch: fetchImpl, log });
}
/** A service principal's token (audience openvibe.tips): contracts' result { ok, code, reason, claims }. */
async function verifyService(config, keys, token) {
-- return await verifyServiceToken(token, { ...keys.source, issuer: config.network.issuer, audience: config.audience, contracts });
++ return await verifyServiceToken(token, { ...keys.verifyOptions, issuer: config.network.issuer, audience: config.audience, contracts });
}
/**
@@@ -53,7 +42,7 @@ function createUserAuth(config, keys) {
*/
async function verify(token) {
if (!token) return null;
-- try { return await verifyUserToken(token, { ...keys.source, issuer: config.network.issuer }); } catch { return null; }
++ try { return await verifyUserToken(token, { ...keys.verifyOptions, issuer: config.network.issuer }); } catch { return null; }
}
return { client, verify };
}
ddiff --git a/server/observability.js b/server/observability.js
iindex 6d9bfa1..f219eed 100644
---- a/server/observability.js
++++ b/server/observability.js
@@@ -48,7 +48,7 @@ function createTipsReadiness({ db, valkey = null, keys, config, outbox, release
checks: [
{ name: 'db', required: true, check: () => db.ready() },
{ name: 'valkey', required: false, check: () => (valkey ? valkey.ready() : skip('VALKEY_URL unset: limits, overlay fan-out and stream slots are per process (one process only)')) },
-- { name: 'network_jwks', required: false, check: () => (keys.ready() ? true : 'Network signing key not loaded yet: tokens cannot be verified') },
++ { name: 'network_jwks', required: false, check: () => (keys.loaded() ? true : 'Network signing key not loaded yet: tokens cannot be verified') },
{ name: 'billing', required: false, cacheMs: PING_TTL_MS, timeoutMs: 2500, check: probe(`${config.billing.url}/api/health`, fetchImpl) },
{ name: 'events', required: false, cacheMs: outbox.enabled ? PING_TTL_MS : 0, timeoutMs: 2500, check: events },
],
ddiff --git a/test/helpers/app.js b/test/helpers/app.js
iindex 234b219..bbe6b81 100644
---- a/test/helpers/app.js
++++ b/test/helpers/app.js
@@@ -65,7 +65,7 @@ async function boot(opts = {}) {
const ownValkey = opts.valkey === undefined && process.env.TIPS_TEST_STORE === 'pg' ? testValkey() : null;
const valkey = opts.valkey || ownValkey;
const app = createApp({ config, db: store.db, valkey, now: () => Date.now() + clock.offset, log, ...(opts.appOpts || {}) });
-- await app.locals.keys.load();
++ await app.locals.keys.refresh();
const server = await new Promise((resolve) => { const s = http.createServer(app); s.listen(0, '127.0.0.1', () => resolve(s)); });
const base = `http://127.0.0.1:${server.address().port}`;
const domain = app.locals.domain;