openvibers/openvibe.tools Commits
The gateway keeps retrying Network's public key every 30 s until it loads, instead of only at boot and on a signed-in request (#91)
Changes
@@@ -132,10 +132,11 @@ function createAuthClient(config) {});let lastFetch = 0;++ const gapMs = config.keyRetryMs || 30_000;async function ensureKey() {if (client.publicKey) return client.publicKey;// Don't hammer the Network if it's down — retry at most every 30s-- if (Date.now() - lastFetch < 30_000) return null;++ if (Date.now() - lastFetch < gapMs) return null;lastFetch = Date.now();for (const base of [config.networkInternalUrl, config.networkUrl]) {if (!base) continue;@@@ -162,10 +163,18 @@ function createAuthClient(config) {return client.verifyToken(token);}-- // Warm the key cache at boot (non-fatal if Network is down)-- ensureKey().catch(() => {});++ // Warm the key cache at boot, and keep trying every 30 s until it loads (non-fatal if Network is down). After the++ // 2026-10-23 reboot the gateway started before Network, its one boot fetch failed, and /status showed it without a++ // key for six hours: only a signed-in request would have fetched it again.++ let retry = null;++ const warm = () => ensureKey().catch(() => null).then((key) => {++ if (key) return;++ retry = setTimeout(warm, gapMs);++ if (retry.unref) retry.unref();++ });++ warm();-- return { client, ensureKey, verify };++ return { client, ensureKey, verify, stop: () => { if (retry) clearTimeout(retry); retry = null; } };}/** Token from Authorization header or the shared ov_token cookie. */nnew file mode 100644@@@ -0,0 +1,34 @@++'use strict';++// The gateway's Network key (createAuthClient): loaded at boot, and when Network is not answering yet, retried by++// itself every keyRetryMs (30 s in production) until it loads. After the 2026-10-23 reboot the gateway started before++// Network and showed no key on /status for six hours, because only a signed-in request fetched it again.++const assert = require('assert');++const http = require('http');++const { createAuthClient } = require('../server/auth/routes');++++(async () => {++ console.log = () => {}; console.warn = () => {};++ let up = false; let fetches = 0;++ const server = http.createServer((req, res) => {++ fetches++;++ if (!up || req.url !== '/api/.well-known/jwks') { res.writeHead(503); return res.end(); }++ res.writeHead(200, { 'Content-Type': 'application/json' });++ res.end(JSON.stringify({ public_key: '-----BEGIN PUBLIC KEY-----\nTEST\n-----END PUBLIC KEY-----\n', algorithm: 'RS256' }));++ });++ await new Promise((r) => server.listen(0, '127.0.0.1', r));++ const base = `http://127.0.0.1:${server.address().port}`;++ const auth = createAuthClient({ oauth: { clientId: 'tools', clientSecret: '', redirectUri: `${base}/cb` }, networkUrl: '', networkInternalUrl: base, keyRetryMs: 100 });++ await new Promise((r) => setTimeout(r, 50));++ assert.strictEqual(auth.client.publicKey, null, 'Network down at boot: no key yet');++ assert.strictEqual(fetches, 1);++ up = true;++ await new Promise((r) => setTimeout(r, 250));++ assert.ok(auth.client.publicKey && auth.client.publicKey.includes('TEST'), 'loaded by the retry, with no request asking for it');++ const n = fetches;++ await new Promise((r) => setTimeout(r, 250));++ assert.strictEqual(fetches, n, 'no more fetches once it is loaded');++ auth.stop();++ server.close();++ process.stdout.write('auth-key-retry: ok\n');++ process.exit(0);++})().catch((e) => { process.stderr.write(`${e.stack}\n`); process.exit(1); });
@@ -132,10 +132,11 @@ function createAuthClient(config) {
@@ -132,10 +132,11 @@ function createAuthClient(config) {
});
});
let lastFetch = 0;
let lastFetch = 0;
+ const gapMs = config.keyRetryMs || 30_000;
async function ensureKey() {
async function ensureKey() {
if (client.publicKey) return client.publicKey;
if (client.publicKey) return client.publicKey;
// Don't hammer the Network if it's down — retry at most every 30s
// Don't hammer the Network if it's down — retry at most every 30s
- if (Date.now() - lastFetch < 30_000) return null;
+ if (Date.now() - lastFetch < gapMs) return null;
lastFetch = Date.now();
lastFetch = Date.now();
for (const base of [config.networkInternalUrl, config.networkUrl]) {
for (const base of [config.networkInternalUrl, config.networkUrl]) {
if (!base) continue;
if (!base) continue;
@@ -162,10 +163,18 @@ function createAuthClient(config) {
@@ -162,10 +163,18 @@ function createAuthClient(config) {
return client.verifyToken(token);
return client.verifyToken(token);
}
}
- // Warm the key cache at boot (non-fatal if Network is down)
- ensureKey().catch(() => {});
+ // Warm the key cache at boot, and keep trying every 30 s until it loads (non-fatal if Network is down). After the
+ // 2026-10-23 reboot the gateway started before Network, its one boot fetch failed, and /status showed it without a
+ // key for six hours: only a signed-in request would have fetched it again.
+ let retry = null;
+ const warm = () => ensureKey().catch(() => null).then((key) => {
+ if (key) return;
+ retry = setTimeout(warm, gapMs);
+ if (retry.unref) retry.unref();
+ });
+ warm();
- return { client, ensureKey, verify };
+ return { client, ensureKey, verify, stop: () => { if (retry) clearTimeout(retry); retry = null; } };
}
}
/** Token from Authorization header or the shared ov_token cookie. */
/** Token from Authorization header or the shared ov_token cookie. */
new file mode 100644
new file mode 100644
@@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
+'use strict';
+// The gateway's Network key (createAuthClient): loaded at boot, and when Network is not answering yet, retried by
+// itself every keyRetryMs (30 s in production) until it loads. After the 2026-10-23 reboot the gateway started before
+// Network and showed no key on /status for six hours, because only a signed-in request fetched it again.
+const assert = require('assert');
+const http = require('http');
+const { createAuthClient } = require('../server/auth/routes');
+
+(async () => {
+ console.log = () => {}; console.warn = () => {};
+ let up = false; let fetches = 0;
+ const server = http.createServer((req, res) => {
+ fetches++;
+ if (!up || req.url !== '/api/.well-known/jwks') { res.writeHead(503); return res.end(); }
+ res.writeHead(200, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify({ public_key: '-----BEGIN PUBLIC KEY-----\nTEST\n-----END PUBLIC KEY-----\n', algorithm: 'RS256' }));
+ });
+ await new Promise((r) => server.listen(0, '127.0.0.1', r));
+ const base = `http://127.0.0.1:${server.address().port}`;
+ const auth = createAuthClient({ oauth: { clientId: 'tools', clientSecret: '', redirectUri: `${base}/cb` }, networkUrl: '', networkInternalUrl: base, keyRetryMs: 100 });
+ await new Promise((r) => setTimeout(r, 50));
+ assert.strictEqual(auth.client.publicKey, null, 'Network down at boot: no key yet');
+ assert.strictEqual(fetches, 1);
+ up = true;
+ await new Promise((r) => setTimeout(r, 250));
+ assert.ok(auth.client.publicKey && auth.client.publicKey.includes('TEST'), 'loaded by the retry, with no request asking for it');
+ const n = fetches;
+ await new Promise((r) => setTimeout(r, 250));
+ assert.strictEqual(fetches, n, 'no more fetches once it is loaded');
+ auth.stop();
+ server.close();
+ process.stdout.write('auth-key-retry: ok\n');
+ process.exit(0);
+})().catch((e) => { process.stderr.write(`${e.stack}\n`); process.exit(1); });