openvibers/openvibe.live Commits
Live: a channel's own OpenVibe avatar in its social cards loads again (the image proxy refuses SVG) (#121)
Changes
@@@ -39,13 +39,17 @@ const RARITY = { common: 'Common', uncommon: 'Uncommon', rare: 'Rare', epic: 'Epconst httpsOnly = (u) => (typeof u === 'string' && /^https:\/\//i.test(u) ? u : null);/**-- * A preview as the channel page shows it: every picture (avatar, live thumbnail, post image) through Live's image proxy-- * (/api/img-proxy), so a viewer's browser never asks the platform itself. That keeps viewers private, and the page's++ * A preview as the channel page shows it: every picture (avatar, live thumbnail, post image) from another platform goes++ * through Live's image proxy (/api/img-proxy), so a viewer's browser never asks the platform itself. That keeps viewers private, and the page's* img-src needs no list of every platform's CDN (a Twitch avatar on static-cdn.jtvnw.net was blocked).*/++// OpenVibe's own image hosts are in the page's img-src and serve generated avatars as SVG, which the image proxy refuses++// (415): their pictures stay direct.++const FIRST_PARTY_IMAGES = new Set(['openvibe.network', 'openvibe.media', 'openvibe.community']);function forPage(p) {if (!p || typeof p !== 'object') return p;-- const via = (u) => (httpsOnly(u) ? `/api/img-proxy?url=${encodeURIComponent(u)}` : null);++ const firstParty = (u) => { try { return FIRST_PARTY_IMAGES.has(new URL(u).hostname); } catch { return false; } };++ const via = (u) => (!httpsOnly(u) ? null : firstParty(u) ? u : `/api/img-proxy?url=${encodeURIComponent(u)}`);const out = { ...p, image: via(p.image) };if (p.live) out.live = { ...p.live, thumbnail: via(p.live.thumbnail) };if (Array.isArray(p.items)) out.items = p.items.map((it) => ({ ...it, image: via(it.image) }));@@@ -127,6 +127,9 @@ async function check(name, fn) { await fn(); passed++; console.log(` ✓ ${nameassert.ok(p.live.thumbnail.startsWith('/api/img-proxy?url=https%3A%2F%2Fstatic-cdn.jtvnw.net'));assert.deepStrictEqual(p.items.map((i) => i.image && i.image.slice(0, 15)), ['/api/img-proxy?', null]);assert.strictEqual(forPage({ title: 'x', image: 'http://plain.example/a.png' }).image, null, 'never a plain-http picture');++ // OpenVibe's own avatar (a generated SVG, which the proxy refuses with 415) stays direct: its host is in img-src.++ assert.strictEqual(forPage({ title: 'iZoR_One', image: 'https://openvibe.network/avatar/iZoR_One?s=160' }).image, 'https://openvibe.network/avatar/iZoR_One?s=160');++ assert.strictEqual(forPage({ title: 'x', image: 'https://openvibe.network.evil.example/a.png' }).image.slice(0, 15), '/api/img-proxy?', 'only the exact hosts');assert.match(require('fs').readFileSync(require('path').join(__dirname, '..', 'server', 'social', 'routes.js'), 'utf8'), /res\.json\(forPage\(p\)\)/);});console.log(`social-links: ${passed} checks passed`);
@@ -39,13 +39,17 @@ const RARITY = { common: 'Common', uncommon: 'Uncommon', rare: 'Rare', epic: 'Ep
@@ -39,13 +39,17 @@ const RARITY = { common: 'Common', uncommon: 'Uncommon', rare: 'Rare', epic: 'Ep
const httpsOnly = (u) => (typeof u === 'string' && /^https:\/\//i.test(u) ? u : null);
const httpsOnly = (u) => (typeof u === 'string' && /^https:\/\//i.test(u) ? u : null);
/**
/**
- * A preview as the channel page shows it: every picture (avatar, live thumbnail, post image) through Live's image proxy
- * (/api/img-proxy), so a viewer's browser never asks the platform itself. That keeps viewers private, and the page's
+ * A preview as the channel page shows it: every picture (avatar, live thumbnail, post image) from another platform goes
+ * through Live's image proxy (/api/img-proxy), so a viewer's browser never asks the platform itself. That keeps viewers private, and the page's
* img-src needs no list of every platform's CDN (a Twitch avatar on static-cdn.jtvnw.net was blocked).
* img-src needs no list of every platform's CDN (a Twitch avatar on static-cdn.jtvnw.net was blocked).
*/
*/
+// OpenVibe's own image hosts are in the page's img-src and serve generated avatars as SVG, which the image proxy refuses
+// (415): their pictures stay direct.
+const FIRST_PARTY_IMAGES = new Set(['openvibe.network', 'openvibe.media', 'openvibe.community']);
function forPage(p) {
function forPage(p) {
if (!p || typeof p !== 'object') return p;
if (!p || typeof p !== 'object') return p;
- const via = (u) => (httpsOnly(u) ? `/api/img-proxy?url=${encodeURIComponent(u)}` : null);
+ const firstParty = (u) => { try { return FIRST_PARTY_IMAGES.has(new URL(u).hostname); } catch { return false; } };
+ const via = (u) => (!httpsOnly(u) ? null : firstParty(u) ? u : `/api/img-proxy?url=${encodeURIComponent(u)}`);
const out = { ...p, image: via(p.image) };
const out = { ...p, image: via(p.image) };
if (p.live) out.live = { ...p.live, thumbnail: via(p.live.thumbnail) };
if (p.live) out.live = { ...p.live, thumbnail: via(p.live.thumbnail) };
if (Array.isArray(p.items)) out.items = p.items.map((it) => ({ ...it, image: via(it.image) }));
if (Array.isArray(p.items)) out.items = p.items.map((it) => ({ ...it, image: via(it.image) }));
@@ -127,6 +127,9 @@ async function check(name, fn) { await fn(); passed++; console.log(` ✓ ${name
@@ -127,6 +127,9 @@ async function check(name, fn) { await fn(); passed++; console.log(` ✓ ${name
assert.ok(p.live.thumbnail.startsWith('/api/img-proxy?url=https%3A%2F%2Fstatic-cdn.jtvnw.net'));
assert.ok(p.live.thumbnail.startsWith('/api/img-proxy?url=https%3A%2F%2Fstatic-cdn.jtvnw.net'));
assert.deepStrictEqual(p.items.map((i) => i.image && i.image.slice(0, 15)), ['/api/img-proxy?', null]);
assert.deepStrictEqual(p.items.map((i) => i.image && i.image.slice(0, 15)), ['/api/img-proxy?', null]);
assert.strictEqual(forPage({ title: 'x', image: 'http://plain.example/a.png' }).image, null, 'never a plain-http picture');
assert.strictEqual(forPage({ title: 'x', image: 'http://plain.example/a.png' }).image, null, 'never a plain-http picture');
+ // OpenVibe's own avatar (a generated SVG, which the proxy refuses with 415) stays direct: its host is in img-src.
+ assert.strictEqual(forPage({ title: 'iZoR_One', image: 'https://openvibe.network/avatar/iZoR_One?s=160' }).image, 'https://openvibe.network/avatar/iZoR_One?s=160');
+ assert.strictEqual(forPage({ title: 'x', image: 'https://openvibe.network.evil.example/a.png' }).image.slice(0, 15), '/api/img-proxy?', 'only the exact hosts');
assert.match(require('fs').readFileSync(require('path').join(__dirname, '..', 'server', 'social', 'routes.js'), 'utf8'), /res\.json\(forPage\(p\)\)/);
assert.match(require('fs').readFileSync(require('path').join(__dirname, '..', 'server', 'social', 'routes.js'), 'utf8'), /res\.json\(forPage\(p\)\)/);
});
});
console.log(`social-links: ${passed} checks passed`);
console.log(`social-links: ${passed} checks passed`);