Files
bitvid/embed.html
thePR0M3TH3ANandClaude Opus 4.8 b844b71f55 fix(csp,smoke): allow ws://localhost relays; fix smoke publish await
Two pre-existing issues surfaced by running the smoke test:

1. CSP connect-src allowed `wss:` but not `ws:`, so ALL insecure-websocket
   relays were blocked — including ws://localhost (dev + local relays + the
   smoke mock relay). Add `ws://localhost:* ws://127.0.0.1:*` to connect-src in
   index.html and embed.html. Remote cleartext ws:// stays blocked (deliberate
   security posture — flagged for product decision).

2. smoke-test.mjs published via Promise.any(relays.map(url => pool.publish([url],
   evt))). SimplePool.publish returns an ARRAY of per-relay promises, so this
   Promise.any'd arrays-of-arrays — resolving on the array object immediately
   without awaiting the publish and swallowing errors. Fixed to await
   pool.publish(relays, evt) via Promise.allSettled and surface per-relay
   results.

Smoke test now passes (publish -> relay verify). CSP policy test green; build green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-19 22:24:58 -04:00

234 lines
9.4 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!--
Content-Security-Policy for the embeddable player. Same strict script-src
as index.html (same inline theme-bootstrap hash). frame-ancestors is left
unset on purpose: this page is meant to be embedded anywhere. If you
change the inline theme script below, update the sha256 hash here
(tests/csp-policy.test.mjs enforces this).
-->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' 'sha256-dnv/l3q8gLqdw0etekr5/atoIRbM/wSVSgQjZEsC7G0=' https://cdn.jsdelivr.net https://esm.sh; style-src 'self' 'unsafe-inline'; img-src 'self' https: data: blob:; media-src 'self' https: blob: mediastream:; connect-src 'self' https: wss: ws://localhost:* ws://127.0.0.1:* blob: data:; worker-src 'self' blob:; font-src 'self' data:; frame-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; manifest-src 'self'"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>bitvid | Embed</title>
<meta name="description" content="Watch embedded bitvid videos." />
<meta name="robots" content="noindex,follow" />
<meta name="theme-color" content="var(--color-page)" />
<meta name="bitvid-sw-version" content="bitvid-sw-2026-02-10" />
<script>
(() => {
try {
const STORAGE_KEY = "bitvid:theme";
const root = document.documentElement;
const themeMeta = document.querySelector('meta[name="theme-color"]');
const fallbackColors = {
dark: "rgb(15, 23, 42)",
light: "rgb(248, 250, 252)",
};
const prefersDark =
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches;
const storedTheme = window.localStorage.getItem(STORAGE_KEY);
let resolvedTheme = storedTheme || (prefersDark ? "dark" : "light");
if (resolvedTheme === "default") {
resolvedTheme = "dark";
}
if (resolvedTheme) {
root.dataset.theme = resolvedTheme;
}
const updateThemeColor = () => {
if (!themeMeta) {
return;
}
const computedThemeColor =
(window.getComputedStyle &&
getComputedStyle(root).getPropertyValue("--color-page").trim()) ||
"";
const metaColor =
computedThemeColor ||
fallbackColors[resolvedTheme] ||
fallbackColors[prefersDark ? "dark" : "light"];
if (metaColor) {
themeMeta.content = metaColor;
}
};
updateThemeColor();
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", updateThemeColor, {
once: true,
});
} else if ("requestAnimationFrame" in window) {
window.requestAnimationFrame(updateThemeColor);
}
} catch (error) {
/* no-op */
}
})();
</script>
<link href="css/tailwind.generated.css" rel="stylesheet" />
</head>
<body class="h-screen w-screen overflow-hidden bg-transparent text-text" data-ds="new">
<main
id="embedRoot"
class="flex h-full w-full flex-col items-center justify-center"
>
<section
class="relative group flex h-full w-full flex-col items-center justify-center overflow-hidden"
>
<video
id="embedVideo"
class="h-full w-full object-cover bg-transparent"
controls
playsinline
preload="metadata"
></video>
<!-- Overlay -->
<div
id="embedOverlay"
class="pointer-events-none absolute inset-0 flex flex-col justify-start bg-gradient-to-b from-black/80 via-transparent to-transparent p-4 opacity-0 transition-opacity duration-200 group-hover:opacity-100"
>
<div class="pointer-events-auto flex w-full items-center justify-between">
<a
id="embedWatchButton"
href="#"
target="_blank"
rel="noopener noreferrer"
class="flex items-center gap-2 rounded-full border border-white/20 bg-white/10 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-white/20 backdrop-blur-md"
>
<span>Watch on</span>
<svg
class="bv-logo h-4 w-auto"
data-variant="inverse"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 318.32 108.98"
>
<g class="bv-logo__accent">
<rect x="7.1" y="7.8" width="5.84" height="5.48" />
<rect x="7.1" y="18.78" width="24.02" height="5.48" />
<rect x="7.1" y="29.77" width="42.85" height="5.48" />
<rect x="7.1" y="40.76" width="61.68" height="5.48" />
<rect x="7.1" y="51.75" width="80.88" height="5.48" />
<rect x="7.1" y="62.74" width="61.68" height="5.48" />
<rect x="7.1" y="73.73" width="42.85" height="5.48" />
<rect x="7.1" y="84.72" width="24.02" height="5.48" />
<rect x="7.1" y="95.71" width="5.84" height="5.48" />
</g>
<g class="bv-logo__wordmark">
<path
d="M121.28 25.08v44.54c0 5.67 2.79 8.54 8.22 8.54h11.57c5.51 0 8.3-2.87 8.3-8.54v-13.09c0-5.67-2.79-8.46-8.3-8.46h-16.6v-5.83h17.72c8.7 0 13.01 4.47 13.01 13.33v15.01c0 8.86-4.31 13.33-13.01 13.33h-13.73c-8.7 0-13.01-4.47-13.01-13.33V25.08h5.83Z"
/>
<path
d="M168.53 25.08v6.26h-5.83v-6.26h5.83Zm0 17.56v41.27h-5.83v-41.27h5.83Z"
/>
<path
d="M191.36 25.08v17.16h9.02v5.19h-9.02v36.48h-5.83v-36.48h-9.82v-5.19h9.82V25.08h5.83Z"
/>
<path
d="M212.88 42.24l16.84 35.68 16.6-35.68h6.07l-19.56 41.67h-6.31l-20.04-41.67h6.39Z"
/>
<path
d="M264.05 25.08v6.26h-5.83v-6.26h5.83Zm0 17.16v41.67h-5.83v-41.67h5.83Z"
/>
<path
d="M311.21 25.08v45.5c0 8.86-4.31 13.33-13.01 13.33h-13.73c-8.7 0-13.01-4.47-13.01-13.33v-15.01c0-8.86 4.31-13.33 13.01-13.33h17.72v5.83h-16.6c-5.51 0-8.3 2.79-8.3 8.46v13.09c0 5.67 2.79 8.54 8.3 8.54h11.49c5.51 0 8.3-2.87 8.3-8.54V25.08h5.83Z"
/>
</g>
</svg>
</a>
<div
class="flex items-center gap-3 text-xs font-medium text-white drop-shadow-md"
>
<span
id="embedViewCount"
class="flex items-center gap-1.5"
title="Views"
>
<svg
class="h-4 w-4 text-white/80"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
></path>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
></path>
</svg>
<span data-view-count-text>--</span>
</span>
<span
id="embedLikeCount"
class="flex items-center gap-1.5"
title="Likes"
>
<svg
class="h-3.5 w-3.5 text-white/80"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
></path>
</svg>
<span data-count>--</span>
</span>
<span
id="embedDislikeCount"
class="flex items-center gap-1.5"
title="Dislikes"
>
<svg
class="h-3.5 w-3.5 text-white/80"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h3a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-3"
></path>
</svg>
<span data-count>--</span>
</span>
</div>
</div>
</div>
<p
id="embedStatus"
class="pointer-events-none absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-md bg-black/60 px-3 py-1 text-sm text-white/90 backdrop-blur-sm"
role="status"
aria-live="polite"
>
Loading video…
</p>
</section>
</main>
<script type="module" src="js/nostrToolsBootstrap.js"></script>
<script type="module" src="js/embed.js"></script>
</body>
</html>