openvibers/openvibe.media Commits

Accept legacy public_key JWKS shape; legacy /api/thumbnails/<basename> route

OP OpenVibers committed 17 Aug 2026 · 77acec9070 · 2 files, +29 −3

- auth: fall back to Network's inherited { public_key: PEM } JWKS response so
  user-JWT verification comes online regardless of which shape is served.
- public: old-stack thumbnail URLs (/api/thumbnails/vod-N-ts.jpg) now resolve
  the vod/clip id and 302 to the current canonical /t/ URL, healing basename
  drift from regeneration.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Xw6Sf5zzSALw8m93XNu5JE

Changes

ddiff --git a/server/auth.js b/server/auth.js
iindex 8328adf..c4a67bd 100644
---- a/server/auth.js
++++ b/server/auth.js
@@@ -31,9 +31,15 @@ async function fetchNetworkPublicKey() {
if (!res.ok) throw new Error(`JWKS fetch failed: ${res.status}`);
const body = await res.json();
const jwk = (body.keys || []).find(k => k.kty === 'RSA') || (body.keys || [])[0];
-- if (!jwk) throw new Error('JWKS contained no keys');
-- const keyObj = crypto.createPublicKey({ key: jwk, format: 'jwk' });
-- _networkPublicKeyPem = keyObj.export({ type: 'spki', format: 'pem' });
++ if (jwk) {
++ const keyObj = crypto.createPublicKey({ key: jwk, format: 'jwk' });
++ _networkPublicKeyPem = keyObj.export({ type: 'spki', format: 'pem' });
++ } else if (typeof body.public_key === 'string' && body.public_key.includes('BEGIN')) {
++ // Network's inherited endpoint shape: { public_key: <PEM>, algorithm }
++ _networkPublicKeyPem = crypto.createPublicKey(body.public_key).export({ type: 'spki', format: 'pem' });
++ } else {
++ throw new Error('JWKS contained no keys');
++ }
console.log('[Auth] Network JWKS public key loaded');
return _networkPublicKeyPem;
} catch (err) {
ddiff --git a/server/public/routes.js b/server/public/routes.js
iindex e574bb3..f20dbcc 100644
---- a/server/public/routes.js
++++ b/server/public/routes.js
@@@ -193,6 +193,26 @@ router.get('/t/:id', (req, res) => {
require('../thumbnails/thumbnail-service').serveThumbnail(req, res);
});
++// ── Legacy thumbnail URLs ────────────────────────────────────
++// Old-stack thumbnails lived at /api/thumbnails/<basename>, and migrated Live
++// rows still carry those absolute URLs. Basenames change when a thumbnail is
++// regenerated, so resolve the vod/clip id from the name and redirect to the
++// current canonical URL; serve the exact file when it still exists.
++router.get('/api/thumbnails/:name', (req, res) => {
++ const name = path.basename(String(req.params.name || ''));
++ const m = /^(vod|clip)-(\d+)-\d+\.(?:jpg|jpeg|png)$/i.exec(name);
++ if (m) {
++ const row = m[1].toLowerCase() === 'vod'
++ ? db.getVodById(parseInt(m[2], 10), 'live')
++ : db.getClipById(parseInt(m[2], 10), 'live');
++ if (row && row.thumbnail_url && !row.thumbnail_url.endsWith(`/${name}`)) {
++ return res.redirect(302, row.thumbnail_url);
++ }
++ }
++ req.params.id = name;
++ require('../thumbnails/thumbnail-service').serveThumbnail(req, res);
++});
++
// ── Paste screenshots by filename ────────────────────────────
// Legacy /data/pastes/screenshots/<name> URLs (old avatars, hero moments,
// pre-cutover pastes) map here. Migrated screenshot files have arbitrary