mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
Picks up BitLogin's login-surface expansion (NIP-07 extension, NIP-46
remote signer with nostrconnect QR, passkey sign-in), three rounds of
adversarial-audit fixes, and the mandatory recovery-phrase ceremony for
passkey-created accounts. Vendored via scripts/build-bitlogin-widget.mjs,
which pins the source commit in vendor/bitlogin.integrity.json; the
shared-chunk hashes changed, so three old chunks are removed and three
new ones added.
CSP fix, required for sign-in to work at all: BitLogin's crypto worker
compiles an Argon2id WASM module, and index.html's script-src had no
'wasm-unsafe-eval', so WebAssembly.compile would be blocked and every
BitLogin password flow would fail -- silently, because the page still
loads. This exact omission broke sign-in on bitlogin.network for four
days. The keyword permits WASM compilation only; JS eval stays blocked,
and the CSP comment's stale "no bundle uses wasm" claim is corrected.
tests/csp-policy.test.mjs now pins the keyword for index.html so it
cannot be dropped again. Baseline verified unchanged: npm run test:unit
green, npm run lint clean, and the raw tests/*.test.mjs glob fails the
same 10 pre-existing files before and after this change.
test_integrity_note:
change_type: ["new_tests"]
scenarios:
- id: SCN-csp-allows-wasm-for-bitlogin
given: "index.html mounts <bitlogin-auth>, whose worker compiles an Argon2id WASM module"
when: "the page's Content-Security-Policy script-src is evaluated"
then: "'wasm-unsafe-eval' is present, so WebAssembly.compile is permitted while 'unsafe-eval' remains absent"
observable_outcomes:
- "script-src directive parsed from the index.html CSP meta tag contains 'wasm-unsafe-eval'"
- "existing assertions still forbid 'unsafe-inline' and 'unsafe-eval'"
determinism_controls:
- "pure static parse of a committed HTML file; no clock, network, or browser"
anti_cheat_rationale:
prevents:
- "silently relaxing script-src to make sign-in work"
- "dropping the keyword and shipping a login page that cannot run Argon2id"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
459 lines
19 KiB
HTML
459 lines
19 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<!--
|
|
Content-Security-Policy: defense-in-depth against XSS. The critical
|
|
directive is script-src, which is strict — only same-origin scripts, the
|
|
hash of the inline theme-bootstrap below, and the nostr-tools CDN
|
|
fallbacks (js/nostrToolsBootstrap.js). No 'unsafe-inline'/'unsafe-eval'.
|
|
'wasm-unsafe-eval' IS required: BitLogin's crypto worker compiles an
|
|
Argon2id WASM module, and a script-src without it blocks
|
|
WebAssembly.compile and breaks every BitLogin password flow. That
|
|
keyword permits WASM compilation only — JS eval stays blocked. This
|
|
blocks injected
|
|
script and object elements, inline handlers (none exist), and base-tag
|
|
hijack, so an injected payload cannot execute and read the in-memory
|
|
storage master key / signer / DM plaintext. connect/img/media stay broad
|
|
because relays, R2/S3 endpoints, and video/thumbnail URLs are
|
|
user/content defined. If you change the inline theme script below, update
|
|
the sha256 hash here (tests/csp-policy.test.mjs enforces this).
|
|
NOTE: any future view loaded by js/viewManager.js must use external-src
|
|
script tags (script-src 'self'), not inline scripts.
|
|
-->
|
|
<meta
|
|
http-equiv="Content-Security-Policy"
|
|
content="default-src 'self'; script-src 'self' 'wasm-unsafe-eval' '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 | Decentralized Video Sharing</title>
|
|
|
|
<meta
|
|
name="description"
|
|
content="bitvid is a decentralized video hub that prioritizes URL-first playback with WebTorrent fallback so your audience can stream reliably."
|
|
/>
|
|
<meta name="robots" content="index,follow" />
|
|
|
|
<!-- Open Graph Meta Tags -->
|
|
<meta property="og:title" content="bitvid - Decentralized Video Sharing" />
|
|
<meta
|
|
property="og:description"
|
|
content="Share videos and follow creators freely, in a truly decentralized way."
|
|
/>
|
|
<meta property="og:image" content="assets/jpg/bitvid.jpg" />
|
|
<meta property="og:url" content="https://bitvid.network" />
|
|
<meta property="og:type" content="website" />
|
|
<meta property="og:locale" content="en_US" />
|
|
|
|
<!-- App Icons -->
|
|
<link rel="icon" href="assets/favicon.ico" sizes="any" />
|
|
<link
|
|
rel="apple-touch-icon"
|
|
sizes="180x180"
|
|
href="assets/png/apple-touch-icon.png"
|
|
/>
|
|
<link
|
|
rel="icon"
|
|
type="image/png"
|
|
sizes="32x32"
|
|
href="assets/png/favicon-32x32.png"
|
|
/>
|
|
<link
|
|
rel="icon"
|
|
type="image/png"
|
|
sizes="16x16"
|
|
href="assets/png/favicon-16x16.png"
|
|
/>
|
|
<link rel="manifest" href="/site.webmanifest" />
|
|
<!--
|
|
Use theme tokens so browsers pick up palette updates without hard-coded
|
|
hex values scattered across templates.
|
|
-->
|
|
<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>
|
|
|
|
<!-- Styles -->
|
|
<!--
|
|
Build-time asset hashing rewrites these source paths to content-hashed
|
|
files in dist/, keeping source templates readable while preserving cache
|
|
busting semantics.
|
|
-->
|
|
<link href="css/tailwind.generated.css" rel="stylesheet" />
|
|
<link href="css/docs.css" rel="stylesheet" />
|
|
</head>
|
|
<body class="sidebar-collapsed" data-ds="new">
|
|
<!--
|
|
SIDEBAR CONTAINER:
|
|
The <aside id="sidebar"> inside handles visibility.
|
|
By default, no special position or z-index on #sidebarContainer.
|
|
-->
|
|
<div id="sidebarContainer">
|
|
<!-- components/sidebar.html gets injected here -->
|
|
</div>
|
|
|
|
<!--
|
|
MAIN CONTENT:
|
|
Layout spacing follows the --sidebar-width custom property on every viewport so the
|
|
collapsible rail always shifts the application shell instead of floating above it.
|
|
-->
|
|
<!--
|
|
`fade-in` is scrubbed by js/index.js after the first animation so that
|
|
re-rendering views does not cause the shell to flicker. Only apply it to
|
|
elements that should animate exactly once on initial paint.
|
|
-->
|
|
|
|
<div
|
|
id="app"
|
|
class="fade-in flex min-h-screen flex-col px-4 pb-8"
|
|
data-sidebar-state="collapsed"
|
|
>
|
|
<!-- Header -->
|
|
<header class="app-shell__header ds-header mb-8 w-full">
|
|
<!-- Logo -->
|
|
<!-- Logo fades in once on initial load. -->
|
|
<a
|
|
href="index.html"
|
|
aria-label="Return to the bitvid home page"
|
|
class="fade-in fade-in-delay-100"
|
|
>
|
|
<span
|
|
class="ds-header__logo inline-flex items-center text-text-strong bv-logo"
|
|
role="img"
|
|
aria-label="bitvid logo"
|
|
>
|
|
<svg
|
|
class="h-full w-auto"
|
|
viewBox="0 0 318.32 108.98"
|
|
aria-hidden="true"
|
|
focusable="false"
|
|
>
|
|
<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>
|
|
</span>
|
|
</a>
|
|
|
|
<div class="header-search__region">
|
|
<form
|
|
id="headerSearchForm"
|
|
role="search"
|
|
class="header-search"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="header-search__filter"
|
|
aria-label="Open search filters"
|
|
>
|
|
<span
|
|
aria-hidden="true"
|
|
class="header-search__icon header-search__icon--filter"
|
|
></span>
|
|
</button>
|
|
<label for="headerSearchInput" class="sr-only">Search videos</label>
|
|
<input
|
|
id="headerSearchInput"
|
|
class="header-search__input"
|
|
type="search"
|
|
name="query"
|
|
placeholder="Search videos"
|
|
autocomplete="off"
|
|
data-testid="search-input"
|
|
/>
|
|
<button type="submit" class="header-search__submit">
|
|
<span class="sr-only">Submit search</span>
|
|
<span
|
|
aria-hidden="true"
|
|
class="header-search__icon header-search__icon--submit"
|
|
></span>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Buttons on the far right -->
|
|
|
|
<!-- Header action buttons share the fade-in treatment for consistency. -->
|
|
|
|
<div class="ml-auto flex items-center">
|
|
<div class="fade-in fade-in-delay-200 flex items-center space-x-4">
|
|
<button
|
|
id="themeToggle"
|
|
class="header-icon-toggle"
|
|
type="button"
|
|
aria-pressed="false"
|
|
>
|
|
<span class="sr-only">Toggle theme</span>
|
|
<span
|
|
aria-hidden="true"
|
|
class="header-icon-toggle__icon-wrapper"
|
|
data-theme-toggle-icon
|
|
data-theme-icon-state="dark"
|
|
>
|
|
<svg
|
|
class="header-icon-toggle__icon header-icon-toggle__icon--dark"
|
|
viewBox="0 0 24 24"
|
|
role="presentation"
|
|
>
|
|
<path
|
|
d="M10.719 2.082c-2.572 2.028-4.719 5.212-4.719 9.918 0 4.569 1.938 7.798 4.548 9.895-4.829-.705-8.548-4.874-8.548-9.895 0-5.08 3.808-9.288 8.719-9.918zm1.281-2.082c-6.617 0-12 5.383-12 12s5.383 12 12 12c1.894 0 3.87-.333 5.37-1.179-3.453-.613-9.37-3.367-9.37-10.821 0-7.555 6.422-10.317 9.37-10.821-1.74-.682-3.476-1.179-5.37-1.179zm0 10.999c1.437.438 2.562 1.564 2.999 3.001.44-1.437 1.565-2.562 3.001-3-1.436-.439-2.561-1.563-3.001-3-.437 1.436-1.562 2.561-2.999 2.999zm8.001.001c.958.293 1.707 1.042 2 2.001.291-.959 1.042-1.709 1.999-2.001-.957-.292-1.707-1.042-2-2-.293.958-1.042 1.708-1.999 2zm-1-9c-.437 1.437-1.563 2.562-2.998 3.001 1.438.44 2.561 1.564 3.001 3.002.437-1.438 1.563-2.563 2.996-3.002-1.433-.437-2.559-1.564-2.999-3.001z"
|
|
fill="currentColor"
|
|
/>
|
|
</svg>
|
|
<svg
|
|
class="header-icon-toggle__icon header-icon-toggle__icon--light"
|
|
viewBox="0 0 24 24"
|
|
role="presentation"
|
|
>
|
|
<path
|
|
d="M4.069 13h-4.069v-2h4.069c-.041.328-.069.661-.069 1s.028.672.069 1zm3.034-7.312l-2.881-2.881-1.414 1.414 2.881 2.881c.411-.529.885-1.003 1.414-1.414zm11.209 1.414l2.881-2.881-1.414-1.414-2.881 2.881c.528.411 1.002.886 1.414 1.414zm-6.312-3.102c.339 0 .672.028 1 .069v-4.069h-2v4.069c.328-.041.661-.069 1-.069zm0 16c-.339 0-.672-.028-1-.069v4.069h2v-4.069c-.328.041-.661.069-1 .069zm7.931-9c.041.328.069.661.069 1s-.028.672-.069 1h4.069v-2h-4.069zm-3.033 7.312l2.88 2.88 1.415-1.414-2.88-2.88c-.412.528-.886 1.002-1.415 1.414zm-11.21-1.415l-2.88 2.88 1.414 1.414 2.88-2.88c-.528-.411-1.003-.885-1.414-1.414zm2.312-4.897c0 2.206 1.794 4 4 4s4-1.794 4-4-1.794-4-4-4-4 1.794-4 4zm10 0c0 3.314-2.686 6-6 6s-6-2.686-6-6 2.686-6 6-6 6 2.686 6 6z"
|
|
fill="currentColor"
|
|
/>
|
|
</svg>
|
|
</span>
|
|
</button>
|
|
<!-- Login Button -->
|
|
<button
|
|
id="loginButton"
|
|
class="btn focus-ring h-12 w-12 rounded-full p-0"
|
|
aria-label="Open login modal"
|
|
data-testid="login-button"
|
|
>
|
|
<svg
|
|
class="h-9 w-9"
|
|
viewBox="0 0 24 24"
|
|
role="presentation"
|
|
focusable="false"
|
|
aria-hidden="true"
|
|
>
|
|
<path
|
|
d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0Zm0 22c-3.123 0-5.914-1.441-7.749-3.69.259-.588.783-.995 1.867-1.246 2.244-.518 4.459-.981 3.393-2.945C6.356 8.299 8.612 5 12 5s4.366 3.177 1.489 9.119c-1.035 1.952 1.1 2.416 3.393 2.945 1.082.25 1.61.655 1.871 1.241C17.917 20.558 15.125 22 12 22Z"
|
|
fill="var(--color-bg)"
|
|
fill-rule="nonzero"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
|
|
<!-- Upload Button (hidden by default) -->
|
|
<button
|
|
id="uploadButton"
|
|
class="btn focus-ring hidden h-12 w-12 rounded-full p-0 text-xl font-bold leading-none"
|
|
data-testid="upload-button"
|
|
>
|
|
+
|
|
</button>
|
|
|
|
<!-- Profile Button (hidden by default) -->
|
|
<button
|
|
id="profileButton"
|
|
class="btn-ghost focus-ring relative hidden h-12 w-12 rounded-full p-0"
|
|
data-testid="profile-button"
|
|
>
|
|
<div class="h-10 w-10 overflow-hidden rounded-full">
|
|
<img
|
|
id="profileAvatar"
|
|
src="assets/svg/default-profile.svg"
|
|
alt="Profile"
|
|
class="h-full w-full object-cover"
|
|
/>
|
|
</div>
|
|
<span id="profileUnreadDot" aria-hidden="true"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Notification Portal -->
|
|
<div
|
|
id="notificationPortal"
|
|
class="notification-portal"
|
|
role="region"
|
|
aria-live="polite"
|
|
aria-label="System notifications"
|
|
>
|
|
<!-- Error Container -->
|
|
<div
|
|
id="errorContainer"
|
|
class="notification-banner notification-banner--critical hidden"
|
|
data-state="critical"
|
|
></div>
|
|
|
|
<!-- Status Container -->
|
|
<div
|
|
id="statusContainer"
|
|
class="notification-banner notification-banner--info status-banner hidden"
|
|
role="status"
|
|
aria-live="polite"
|
|
>
|
|
<span class="status-spinner" aria-hidden="true"></span>
|
|
<span class="status-message" data-status-message></span>
|
|
</div>
|
|
|
|
<!-- Success Container -->
|
|
<div
|
|
id="successContainer"
|
|
class="notification-banner notification-banner--success hidden"
|
|
></div>
|
|
</div>
|
|
|
|
<!-- Dynamic views (like most-recent-videos.html, etc.) -->
|
|
<main id="viewContainer" class="mb-8 flex-grow"></main>
|
|
|
|
<!-- Modal Container (external modals) -->
|
|
<div id="modalContainer"></div>
|
|
|
|
<!-- Tagline / Slogan -->
|
|
|
|
<!-- Tagline fade matches the cards. -->
|
|
|
|
<div class="site-tagline fade-in fade-in-delay-300 mb-8 text-center">
|
|
<h2 class="site-tagline__text text-2xl font-bold tracking-wide text-muted-strong">
|
|
seed. zap. subscribe.
|
|
</h2>
|
|
</div>
|
|
<div class="text-center mb-8">
|
|
<!-- VERSION_INFO -->
|
|
</div>
|
|
|
|
<!-- Mobile Search FAB -->
|
|
<button
|
|
id="mobileSearchFab"
|
|
class="mobile-safe-bottom focus-ring fixed bottom-6 right-6 z-50 flex h-14 w-14 items-center justify-center rounded-full bg-accent-strong shadow-lg transition-transform duration-200 hover:scale-105 active:scale-95 md:hidden"
|
|
aria-label="Open search"
|
|
>
|
|
<svg
|
|
class="h-6 w-6 text-white"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2.5"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<circle cx="11" cy="11" r="8"></circle>
|
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
|
</svg>
|
|
</button>
|
|
|
|
<!-- Mobile Search Container -->
|
|
<div
|
|
id="mobileSearchContainer"
|
|
class="fixed top-4 left-4 right-4 z-50 hidden max-w-sm mx-auto origin-top transition-all duration-200 md:hidden"
|
|
>
|
|
<form
|
|
id="mobileSearchForm"
|
|
role="search"
|
|
class="flex items-center gap-3 rounded-full border border-border bg-overlay-panel bg-opacity-95 p-2 shadow-xl backdrop-blur-md"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="header-search__filter shrink-0"
|
|
aria-label="Open search filters"
|
|
>
|
|
<span
|
|
aria-hidden="true"
|
|
class="header-search__icon header-search__icon--filter"
|
|
></span>
|
|
</button>
|
|
<label for="mobileSearchInput" class="sr-only">Search videos</label>
|
|
<input
|
|
id="mobileSearchInput"
|
|
class="header-search__input min-w-0 flex-1 bg-transparent px-2 text-base text-text outline-none placeholder:text-muted"
|
|
type="search"
|
|
name="query"
|
|
placeholder="Search videos"
|
|
autocomplete="off"
|
|
/>
|
|
<button type="submit" class="header-search__submit shrink-0">
|
|
<span class="sr-only">Submit search</span>
|
|
<span
|
|
aria-hidden="true"
|
|
class="header-search__icon header-search__icon--submit"
|
|
></span>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Additional Scripts -->
|
|
<script type="module" src="js/index.js"></script>
|
|
<script src="vendor/marked.min.js"></script>
|
|
<script src="vendor/highlight.min.js"></script>
|
|
<div id="uiOverlay" data-overlay-root aria-hidden="true"></div>
|
|
</body>
|
|
</html>
|