// Per-video OpenGraph rendering for crawlers — see api/_lib/ogRenderer.mjs. // // Two things here are security-relevant and get the most attention: // 1. Escaping. Titles/descriptions come from arbitrary nostr events, and this // HTML is served from bitvid's own origin. // 2. Gating. A crawler never authenticates, so anything emitted is public // forever in third-party caches. Deleted/private/NSFW/non-whitelisted // videos must fall back to the generic site card. import test from "node:test"; import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import { CRAWLER_USER_AGENT_PATTERN, escapeHtmlAttribute, isCrawlerUserAgent, isPubliclyShareable, normalizeText, parseVideoEvent, renderOgHtml, sanitizeAbsoluteUrl, } from "../api/_lib/ogRenderer.mjs"; const PUBKEY = "b7c6f6915cfa9a62fff6a1f02604de88c23c6c6c6d1b8f62c7cc10749f307e81"; const publicVideo = () => ({ pubkey: PUBKEY, title: "Why I'm (sort of) not worried about AI", description: "A talk about creativity and machines.", thumbnail: "https://cdn.example/thumb.jpg", deleted: false, isPrivate: false, isNsfw: false, invalid: false, }); const FALLBACK = { siteName: "bitvid", title: "bitvid - Decentralized Video Sharing", description: "Decentralized video sharing on Nostr.", image: "https://bitvid.network/assets/jpg/bitvid.jpg", url: "https://bitvid.network", }; const render = (video, extra = {}) => renderOgHtml({ video, shareUrl: "https://bitvid.network/?v=nevent1abc", embedUrl: "https://bitvid.network/embed.html?pointer=nevent1abc", fallback: FALLBACK, gate: isPubliclyShareable(video, extra.gateOptions || {}), ...extra.render, }); // --- escaping ------------------------------------------------------------- test("escapeHtmlAttribute neutralizes attribute-breaking characters", () => { assert.equal( escapeHtmlAttribute(`">`), ""><script>alert(1)</script>" ); assert.equal(escapeHtmlAttribute("a & b"), "a & b"); assert.equal(escapeHtmlAttribute("it's"), "it's"); assert.equal(escapeHtmlAttribute(null), ""); assert.equal(escapeHtmlAttribute(42), ""); }); test("a hostile title cannot break out of the meta tag", () => { const video = publicVideo(); video.title = `">"), false, "no raw script tag"); assert.equal(html.includes(`"> { const video = publicVideo(); video.description = `" onload="alert(1)`; const html = render(video); assert.equal(html.includes(`onload="alert(1)`), false); assert.match(html, /" onload="alert\(1\)/); }); // --- URL sanitization ----------------------------------------------------- test("sanitizeAbsoluteUrl accepts only http(s)", () => { assert.equal( sanitizeAbsoluteUrl("https://cdn.example/a.jpg"), "https://cdn.example/a.jpg" ); assert.equal(sanitizeAbsoluteUrl("http://cdn.example/a.jpg"), "http://cdn.example/a.jpg"); assert.equal(sanitizeAbsoluteUrl("javascript:alert(1)"), ""); assert.equal(sanitizeAbsoluteUrl("data:image/png;base64,AAAA"), ""); assert.equal(sanitizeAbsoluteUrl("assets/jpg/local.jpg"), ""); assert.equal(sanitizeAbsoluteUrl(""), ""); assert.equal(sanitizeAbsoluteUrl(null), ""); }); test("a javascript: thumbnail never reaches og:image", () => { const video = publicVideo(); video.thumbnail = "javascript:alert(1)"; const html = render(video); assert.equal(html.includes("javascript:"), false); // Falls back to the site image rather than emitting nothing. assert.match(html, /og:image" content="https:\/\/bitvid\.network\/assets/); }); // --- text normalization --------------------------------------------------- test("normalizeText collapses whitespace and truncates with an ellipsis", () => { assert.equal(normalizeText(" a \n b \t c ", 50), "a b c"); const long = "x".repeat(300); const out = normalizeText(long, 120); assert.equal(out.length, 120); assert.equal(out.endsWith("…"), true); assert.equal(normalizeText(null, 10), ""); }); // --- gating --------------------------------------------------------------- test("a clean public video is shareable", () => { assert.deepEqual(isPubliclyShareable(publicVideo()), { allowed: true, reason: "ok", }); }); test("deleted, private, NSFW and invalid videos are never described", () => { for (const [field, reason] of [ ["deleted", "deleted"], ["isPrivate", "private"], ["isNsfw", "nsfw"], ["invalid", "invalid-video"], ]) { const video = publicVideo(); video[field] = true; const result = isPubliclyShareable(video); assert.equal(result.allowed, false, `${field} must gate the preview`); assert.equal(result.reason, reason); } }); test("a missing video or author is gated", () => { assert.equal(isPubliclyShareable(null).allowed, false); assert.equal(isPubliclyShareable(undefined).reason, "missing-video"); const noAuthor = publicVideo(); noAuthor.pubkey = ""; assert.equal(isPubliclyShareable(noAuthor).reason, "missing-author"); }); test("a blacklisted author is gated regardless of whitelist mode", () => { const result = isPubliclyShareable(publicVideo(), { blacklist: new Set([PUBKEY]), }); assert.equal(result.allowed, false); assert.equal(result.reason, "blacklisted"); }); test("whitelist mode fails CLOSED when the list is missing or empty", () => { // The dangerous case: relays timed out, so we cannot prove the author is // allowed. Emitting a rich card here would leak gated metadata. assert.equal( isPubliclyShareable(publicVideo(), { whitelistEnabled: true }).reason, "whitelist-unavailable" ); assert.equal( isPubliclyShareable(publicVideo(), { whitelistEnabled: true, whitelist: new Set(), }).reason, "whitelist-unavailable" ); }); test("whitelist mode admits a listed author and rejects an unlisted one", () => { assert.equal( isPubliclyShareable(publicVideo(), { whitelistEnabled: true, whitelist: new Set([PUBKEY]), }).allowed, true ); assert.equal( isPubliclyShareable(publicVideo(), { whitelistEnabled: true, whitelist: new Set(["a".repeat(64)]), }).reason, "not-whitelisted" ); }); test("gated videos render the generic site card, leaking no metadata", () => { const video = publicVideo(); video.isPrivate = true; video.title = "Secret internal recording"; video.thumbnail = "https://cdn.example/secret.jpg"; const html = render(video); assert.equal(html.includes("Secret internal recording"), false); assert.equal(html.includes("secret.jpg"), false); assert.match(html, /og:title" content="bitvid - Decentralized Video Sharing"/); assert.match(html, /og:type" content="website"/); // No player card for a gated video. assert.equal(html.includes("twitter:player"), false); }); // --- rendering ------------------------------------------------------------ test("a public video renders a player card pointing at the embed", () => { const html = render(publicVideo()); assert.match(html, /og:type" content="video\.other"/); assert.match(html, /og:title" content="Why I'm \(sort of\) not worried about AI"/); assert.match(html, /og:image" content="https:\/\/cdn\.example\/thumb\.jpg"/); assert.match( html, /og:video:url" content="https:\/\/bitvid\.network\/embed\.html\?pointer=nevent1abc"/ ); assert.match(html, /twitter:card" content="player"/); assert.match( html, /twitter:player" content="https:\/\/bitvid\.network\/embed\.html\?pointer=nevent1abc"/ ); assert.match(html, /twitter:player:width" content="1280"/); assert.match(html, /twitter:player:height" content="720"/); assert.match(html, / { const html = renderOgHtml({ video: publicVideo(), shareUrl: "https://bitvid.network/?v=nevent1abc", fallback: FALLBACK, gate: { allowed: true, reason: "ok" }, }); assert.match(html, /twitter:card" content="summary_large_image"/); assert.equal(html.includes("twitter:player"), false); }); test("the rendered document is well-formed and self-contained", () => { const html = render(publicVideo()); assert.match(html, /^/); assert.match(html, /<\/html>\n$/); // Crawler stub only — it must not pull in the app or any script. assert.equal(/