Files
pondering-orb/web/providers/mock.js
T
Diablo_Rain c58fa9591b 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>
2026-06-15 05:34:09 -04:00

118 lines
4.8 KiB
JavaScript

// mock.js — MockProvider. Built FIRST, on purpose.
//
// Proves the UI is fully source-agnostic with ZERO network: fake channels, a
// rolling "scheduled-live" EPG timeline (so now/next + the guide populate), and
// a couple of real CORS-clean public TEST streams so playback can be exercised
// standalone (no relay needed). One channel points at a dead URL to exercise the
// "the orb clouds over" no-signal path.
import { SourceProvider } from './source-provider.js';
/** Tiny inline-SVG logo so channels render with no network. */
function logo(initials, accent = '#ff2b2b') {
const svg =
`<svg xmlns="http://www.w3.org/2000/svg" width="96" height="54">` +
`<rect width="96" height="54" fill="#0a0000"/>` +
`<rect x="1" y="1" width="94" height="52" fill="none" stroke="${accent}" stroke-width="1.5"/>` +
`<text x="48" y="34" font-family="monospace" font-size="22" font-weight="bold" ` +
`text-anchor="middle" fill="${accent}">${initials}</text></svg>`;
return 'data:image/svg+xml,' + encodeURIComponent(svg);
}
// CORS-clean public test streams (HLS). Stable, broadcaster-hosted demos.
const TEST_STREAMS = {
bipbop: 'https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8',
mux: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8',
bbb: 'https://test-streams.mux.dev/test_001/stream.m3u8',
dead: 'https://stream.invalid.example/offline.m3u8',
};
const TITLE_POOL = {
'Arcane Cinema': ['The Reanimator', 'Hex of the Hollow', 'Salt & Sigil', 'Midnight Grimoire',
'The Pondering Hour', 'Witchlight', 'The Veil Lifts', 'Candle & Bone'],
'Hack the Planet': ['Packets at Dawn', 'Root of All Evil', 'The Blue Box', 'Zero Cool',
'Buffer Overflow', 'The Last Daemon', 'Kernel Panic', 'Shell Games'],
'Old Signals': ['Test Pattern Theatre', 'Static & Snow', 'The Late Late Broadcast',
'Color Bars', 'Sign-Off Sermon', 'Dead Air'],
'The Almanac': ['What Fate Foretells', 'Stars & Portents', 'The Long Now', 'Tomorrow, Maybe'],
};
function buildSchedule(channelId, group, now) {
/** @type {import('./source-provider.js').Program[]} */
const programs = [];
const titles = TITLE_POOL[group] || TITLE_POOL['Old Signals'];
// Deterministic-ish per channel so reloads look stable.
let seed = [...channelId].reduce((a, c) => a + c.charCodeAt(0), 0);
const rand = () => { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed / 0x7fffffff; };
// Start two hours back, fill ~10h.
let t = now - 2 * 3600 * 1000;
t = t - (t % (30 * 60 * 1000)); // align to :00/:30
const end = now + 8 * 3600 * 1000;
let i = Math.floor(rand() * titles.length);
while (t < end) {
const slots = 1 + Math.floor(rand() * 3); // 30/60/90 min
const dur = slots * 30 * 60 * 1000;
programs.push({
channelId,
title: titles[i % titles.length],
desc: 'A scheduled broadcast. Tuning in drops you mid-program, live — just like real TV.',
start: t,
stop: t + dur,
});
t += dur;
i++;
}
return programs;
}
const CHANNELS = [
{ n: 2, name: 'Arcane Cinema', group: 'Arcane Cinema', stream: 'mux', init: 'AC' },
{ n: 3, name: 'Hack the Planet',group: 'Hack the Planet', stream: 'bbb', init: 'HP' },
{ n: 4, name: 'Bip-Bop Test', group: 'Old Signals', stream: 'bipbop', init: 'BB' },
{ n: 5, name: 'The Late Show', group: 'Old Signals', stream: 'mux', init: 'LS' },
{ n: 6, name: 'Dead Air', group: 'Old Signals', stream: 'dead', init: 'XX' },
{ n: 7, name: 'The Almanac', group: 'The Almanac', stream: 'bbb', init: 'AL' },
];
export class MockProvider extends SourceProvider {
get id() { return 'mock'; }
get name() { return 'The Mock Realm'; }
get supportsGuides() { return true; }
async listChannels() {
return CHANNELS.map((c) => ({
id: `mock:${c.n}`,
source: 'mock',
name: c.name,
number: c.n,
logo: logo(c.init),
group: c.group,
country: 'XX',
language: 'eng',
kind: 'live',
streamUrls: [TEST_STREAMS[c.stream]],
guideId: `mock:${c.n}`,
}));
}
async getStreamHandle(channelId) {
const c = CHANNELS.find((x) => `mock:${x.n}` === channelId);
if (!c) return null;
// Mock returns direct URLs (test streams are CORS-clean) so the UI runs standalone.
return { url: TEST_STREAMS[c.stream], protocol: 'hls' };
}
async getPrograms(channelId, window) {
const c = CHANNELS.find((x) => `mock:${x.n}` === channelId);
if (!c) return [];
const all = buildSchedule(channelId, c.group, Date.now());
return all.filter((p) => p.stop > window.from && p.start < window.to);
}
async healthCheck(channelId) {
const c = CHANNELS.find((x) => `mock:${x.n}` === channelId);
if (c && c.stream === 'dead') return { ok: false, error: 'mock offline channel' };
return { ok: true };
}
}