feat: initial release of The Pondering Orb

A retro CRT-themed web TV that channel-surfs live HLS streams through a
security-hardened, same-origin Rust relay.

Front end (vanilla JS + HLS.js, no framework/build step):
- SourceProvider abstraction + canonical Channel/Program model so public
  streams and future self-hosted media share one UI (MockProvider built first
  to keep the UI provably source-agnostic; IptvOrgProvider for live data)
- Red/black phosphor CRT theme, virtualized 1994-style guide (The Almanac),
  cable-box zapping with 7-segment OSD, scrying-static transitions
- Theater/Dim/Ambient immersion modes, dead-stream "orb clouds over" handling
- The Lexicon in-world naming with mundane-mode; full a11y + reduced-motion
- HLS.js pinned + Subresource-Integrity verified

Rust relay (hyper 1.x + tokio-rustls/ring + hickory-resolver):
- Serves the SPA and proxies HLS from one origin (no CORS, untainted canvas)
- SSRF defense: resolve -> reject private/loopback/link-local/metadata IPs ->
  connect to the validated IP (no re-resolve, defeats DNS-rebind), fail-closed
- HTTPS-only egress, re-validated redirects, manifest URL rewrite, size/
  concurrency caps, strict CSP + security headers, path-traversal guard
- /metrics observability + structured logs (host only, never raw URLs)

Verified: all SSRF probes rejected, CSP clean, real Apple HLS manifest
rewritten, ~39k iptv-org channels fetched end-to-end, tests green, zero warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 05:34:09 -04:00
commit c58fa9591b
22 changed files with 3851 additions and 0 deletions
+184
View File
@@ -0,0 +1,184 @@
// app.js — bootstrap + global state + keyboard map + immersion modes.
import { ChannelAggregator } from './providers/source-provider.js';
import { MockProvider } from './providers/mock.js';
import { IptvOrgProvider } from './providers/iptv-org.js';
import { Player } from './player.js';
import { Zapper } from './zapper.js';
import { Guide } from './guide.js';
import { Ambient } from './ambient.js';
import { applyLexicon, setMundane, isMundane } from './lexicon.js';
const $ = (id) => document.getElementById(id);
const els = {
root: $('orb-root'),
screen: $('screen'),
video: $('v'),
ambientCanvas: $('ambient'),
staticCanvas: $('static'),
nosignal: $('nosignal'),
// channel OSD
osdChannel: $('osd-channel'), osdLogo: $('osd-logo'), osdNum: $('osd-num'),
osdName: $('osd-name'), osdNow: $('osd-now'), osdProgFill: $('osd-prog-fill'), osdLive: $('osd-live'),
// number OSD
osdNumber: $('osd-number'), osdNumberVal: $('osd-number-val'),
// controls
btnPlay: $('btn-play'), btnChup: $('btn-chup'), btnChdn: $('btn-chdn'),
btnMute: $('btn-mute'), vol: $('vol'), btnCc: $('btn-cc'), btnQual: $('btn-qual'),
btnPip: $('btn-pip'), btnGuide: $('btn-guide'), btnFull: $('btn-full'),
ctlLiveWrap: $('ctl-live-wrap'), ctlClock: $('ctl-clock'), qualMenu: $('qual-menu'),
// guide
guide: $('guide'), guideScroll: $('guide-scroll'), guideRows: $('guide-rows'),
guideTimehdr: $('guide-timehdr'), guidePlayhead: $('guide-playhead'),
guideClock: $('guide-clock'), guideClose: $('guide-close'),
// help
help: $('help'), helpList: $('help-list'), helpClose: $('help-close'), mundane: $('mundane'),
};
// ---- immersion modes ----
const modes = { theater: false, dim: false, ambient: false };
let ambient;
function setMode(name, on) {
modes[name] = on;
els.root.setAttribute(`data-mode-${name}`, String(on));
if (name === 'ambient') on ? ambient.start() : ambient.stop();
}
function toggleMode(name) { setMode(name, !modes[name]); }
function collapseModes() {
if (modes.dim) return setMode('dim', false);
if (modes.theater) return setMode('theater', false);
if (modes.ambient) return setMode('ambient', false);
}
// dim mode: brighten scrim briefly on activity
let dimActT;
function dimActivity() {
if (!modes.dim) return;
els.root.classList.add('activity');
clearTimeout(dimActT);
dimActT = setTimeout(() => els.root.classList.remove('activity'), 2500);
}
// ---- keyboard help ----
const KEYS = [
['09', 'Tune channel (incantation)'],
['↑ / ↓', 'Volume (player) · navigate (guide)'],
['PgUp/PgDn', 'Channel up / down'],
['Backspace', 'Last channel'],
['G', 'Toggle the Almanac (guide)'],
['T', 'Theater mode'],
['D', 'Dim the hall'],
['A', 'Orb glow (ambient)'],
['F', 'Fullscreen'],
['M', 'Mute'],
['P', 'Picture-in-picture'],
['C', 'Captions'],
['Esc', 'Close / collapse'],
['?', 'This help'],
];
function buildHelp() {
els.helpList.innerHTML = '';
for (const [k, d] of KEYS) {
const dt = document.createElement('dt');
const kbd = document.createElement('kbd'); kbd.textContent = k; dt.appendChild(kbd);
const dd = document.createElement('dd'); dd.textContent = d;
els.helpList.append(dt, dd);
}
}
function toggleHelp(force) {
const show = force ?? els.help.hidden;
els.help.hidden = !show;
}
// ---- global keymap ----
let player, zapper, guide;
function onKeydown(e) {
dimActivity();
const tag = (e.target && e.target.tagName) || '';
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
if (guide.open && ['ArrowUp','ArrowDown','Enter'].includes(e.key)) return; // guide handles
if (e.key >= '0' && e.key <= '9') { zapper.pushDigit(e.key); return; }
switch (e.key) {
case 'g': case 'G': e.preventDefault(); guide.toggle(); break;
case 't': case 'T': toggleMode('theater'); break;
case 'd': case 'D': toggleMode('dim'); break;
case 'a': case 'A': toggleMode('ambient'); break;
case 'f': case 'F': player.toggleFullscreen(); break;
case 'm': case 'M': player.toggleMute(); break;
case 'p': case 'P': player.togglePip(); break;
case 'c': case 'C': player.toggleCaptions(); break;
case 'ArrowUp': if (!guide.open){ e.preventDefault(); player.setVolume(+0.1);} break;
case 'ArrowDown': if (!guide.open){ e.preventDefault(); player.setVolume(-0.1);} break;
case 'PageUp': e.preventDefault(); zapper.tuneToIndexOffset(-1); break;
case 'PageDown': e.preventDefault(); zapper.tuneToIndexOffset(+1); break;
case 'Backspace': e.preventDefault(); zapper.tuneLast(); break;
case '?': toggleHelp(); break;
case 'Escape':
if (!els.help.hidden) { toggleHelp(false); break; }
if (guide.open) { guide.hide(); break; }
collapseModes();
break;
}
}
// ---- provider selection ----
// ?provider=mock | iptv (default: mock unless the relay injected the meta tag)
function pickProviders() {
const params = new URLSearchParams(location.search);
const which = params.get('provider') || 'auto';
const agg = new ChannelAggregator();
if (which === 'mock') { agg.register(new MockProvider()); return { agg, label: 'mock' }; }
if (which === 'iptv') { agg.register(new IptvOrgProvider()); return { agg, label: 'iptv' }; }
const servedByRelay = !!document.querySelector('meta[name="orb-relay"]');
if (servedByRelay) { agg.register(new IptvOrgProvider()); return { agg, label: 'iptv' }; }
agg.register(new MockProvider());
return { agg, label: 'mock' };
}
// ---- boot ----
async function boot() {
applyLexicon();
buildHelp();
els.mundane.checked = isMundane();
els.mundane.addEventListener('change', () => setMundane(els.mundane.checked));
els.helpClose.addEventListener('click', () => toggleHelp(false));
ambient = new Ambient(els.video, els.ambientCanvas);
player = new Player(els, (msg) => console.warn('[orb] stream fatal:', msg));
const { agg, label } = pickProviders();
zapper = new Zapper(els, player, agg);
guide = new Guide(els, agg, (ch) => zapper.tune(ch));
els.btnChup.addEventListener('click', () => zapper.tuneToIndexOffset(-1));
els.btnChdn.addEventListener('click', () => zapper.tuneToIndexOffset(+1));
els.btnGuide.addEventListener('click', () => guide.toggle());
document.addEventListener('keydown', onKeydown);
['mousemove','click','touchstart'].forEach((ev) =>
document.addEventListener(ev, dimActivity, { passive: true }));
try {
const channels = await agg.listAllChannels();
console.info(`[orb] ${label} provider: ${channels.length} channels`);
if (channels.length) await zapper.tune(channels[0]);
runHealthPass(agg, guide);
} catch (e) {
console.error('[orb] boot failed:', e);
player._showNoSignal?.();
}
}
// background health pass → guide skull glyphs (Oracle's Memory seed)
async function runHealthPass(agg, guide) {
for (const ch of agg.channels) {
const owner = agg.providers.get(ch.source);
if (!owner?.healthCheck) continue;
try { const r = await owner.healthCheck(ch.id); guide.setHealth(ch.id, r.ok); }
catch { guide.setHealth(ch.id, false); }
}
}
boot();