Skip to content

feat(web): embeddable player, oEmbed, and per-channel RSS - #77

Merged
ralyodio merged 2 commits into
masterfrom
worktree-transistor-rips
Aug 17, 2026
Merged

feat(web): embeddable player, oEmbed, and per-channel RSS#77
ralyodio merged 2 commits into
masterfrom
worktree-transistor-rips

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Three distribution surfaces so a PairUX live can leave pairux.com, taken from the feature set of podcast hosts like Transistor. All three read data through RPCs that are already anon-executable, so there is no migration.

What's new

/embed/<joinCode> — embeddable player
A bare, framable page: plays a finished recording, or shows a poster that links out when the room is live. Titled bar underneath links back to the live and the channel.

Middleware relaxes frame-ancestors to * for this path only. That's safe because the page is read-only — a <video> plus outbound links, no authenticated action — so there is nothing for a framing site to clickjack. Every other path keeps the restrictive 'self' chrome-extension: list. The same flag tells the root layout to drop analytics, the feedback widget and the PWA install prompt, which have no business rendering inside someone else's page.

/api/oembed — oEmbed provider
Discovered via <link rel="alternate" type="application/json+oembed"> on /l/<joinCode>. Slack, Notion, WordPress, Ghost and Discord turn a pasted permalink into the embedded player. joinCodeFromUrl allowlists our own hosts, so a crafted url param can't aim the provider at a third party — including lookalikes like pairux.com.evil.com. Honours maxwidth/maxheight, returns 501 for format=xml per spec.

/c/<handle>/rss.xml — per-channel feed
A channel's finished recordings as RSS 2.0 with itunes:* tags and <enclosure>s, making any channel subscribable in Apple Podcasts, Overcast, Pocket Casts or an ordinary feed reader. Autodiscovered from the channel page, plus a visible RSS button by the Recordings heading.

/l/<joinCode> enhancements
VideoObject JSON-LD, canonical + oEmbed discovery links, a player Twitter card, and a copyable embed snippet.

A note on scope

The original plan called for a new per-recording permalink at /c/<handle>/r/<id>. That turned out to be redundant — /l/<joinCode> already is the per-session permalink, with the video, description, likes and comments. A second URL for the same content would have split ranking signals, so this PR enriches the existing page instead of adding a rival to it.

Known limitation

<enclosure length="0"> — the RSS spec wants a byte count, but list_channel_recordings doesn't expose size_bytes. Zero is the conventional "unknown" and clients tolerate it. Adding it properly means a migration, which this PR deliberately avoids.

Verification

  • pnpm lint — 0 errors (2 warnings, both pre-existing on untouched files)
  • pnpm typecheck — clean
  • pnpm test711 passed / 73 files, including 21 new tests for the helpers
  • pnpm build — succeeds, all three routes present
  • Smoke-tested against the built standalone server: oEmbed returns 400 / 404 / 404 / 501 / 404 across missing-url, foreign-host, lookalike-host, xml-format and unknown-code; /embed serves frame-ancestors * while /live still serves 'self' chrome-extension:; RSS and /l/ both 404 cleanly on unknown slugs.

🤖 Generated with Claude Code

Three distribution surfaces so a PairUX live can leave pairux.com. All
three read existing data through RPCs that are already anon-executable,
so there is no migration.

- /embed/<joinCode>: a bare, framable player. Plays a finished recording,
  or shows a poster linking out when the room is live. Middleware relaxes
  frame-ancestors to * for this path only (the page is read-only, so
  there is nothing to clickjack) and flags the request so the root layout
  drops analytics, the feedback widget and the install prompt.

- /api/oembed: oEmbed provider, discovered from /l/<joinCode>. Turns a
  pasted permalink into the player in Slack, Notion, WordPress and Ghost.
  joinCodeFromUrl allowlists our hosts so a crafted url cannot point the
  provider elsewhere.

- /c/<handle>/rss.xml: the channel's finished recordings as an RSS feed
  with itunes:* tags and enclosures, making a channel subscribable in
  podcast apps and feed readers. enclosure length is 0 because the public
  RPC does not expose size_bytes.

/l/<joinCode> gains a VideoObject JSON-LD block, canonical + oEmbed
discovery links, a player Twitter card and a copyable embed snippet.
It stays the single canonical URL for a live: no rival permalink was
added, so ranking signals are not split.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ralyodio
ralyodio marked this pull request as ready for review August 17, 2026 01:54
<script
type="application/ld+json"
nonce={nonce}
dangerouslySetInnerHTML={{ __html: JSON.stringify(videoSchema(session)) }}
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

66 finding(s)

HIGH/CRITICAL: 17 | MEDIUM: 49

Severity Rule Location
HIGH sh-eval-expansion .githooks/pre-commit:33
HIGH js-electron-node-integration apps/desktop/src/main/window.ts:49
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:691
HIGH sh-unquoted-expansion-destructive apps/installer/scripts/install.sh:715
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:813
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:815
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:894
HIGH sh-unquoted-expansion-destructive apps/installer/scripts/install.sh:910
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:1064
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:1066
HIGH sh-remote-script-execution apps/installer/scripts/install.sh:1128
HIGH secret-generic-credential apps/livekit/fly.toml:12
HIGH sh-remote-script-execution apps/livekit/setup-livekit-server.sh:93
HIGH sh-remote-script-execution apps/turn/deploy-droplet.sh:62
HIGH secret-generic-credential apps/turn/fly.toml:11
HIGH secret-generic-credential docs/API.md:747
HIGH secret-generic-credential docs/API.md:753
MEDIUM insecure-temp-file .githooks/commit-msg:19
MEDIUM insecure-temp-file .githooks/post-commit:22
MEDIUM insecure-temp-file apps/desktop/src/main/streaming/index.test.ts:64
MEDIUM insecure-temp-file apps/installer/scripts/install.sh:370
MEDIUM insecure-temp-file apps/installer/scripts/install.sh:393
MEDIUM insecure-temp-file apps/installer/scripts/install.sh:402
MEDIUM insecure-temp-file apps/installer/scripts/install.sh:438
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:48
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:73
MEDIUM js-unescaped-html-sink apps/web/src/app/c/[handle]/page.tsx:192
MEDIUM js-unescaped-html-sink apps/web/src/app/l/[joinCode]/page.tsx:125
MEDIUM js-unescaped-html-sink apps/web/src/app/l/[joinCode]/page.tsx:219
MEDIUM js-unescaped-html-sink apps/web/src/app/layout.tsx:142
MEDIUM js-unescaped-html-sink apps/web/src/app/live/page.tsx:145
MEDIUM js-unescaped-html-sink apps/web/src/app/page.tsx:122
MEDIUM js-unescaped-html-sink apps/web/src/app/pricing/page.tsx:284
MEDIUM js-open-redirect apps/web/src/app/pricing/UpgradeButton.tsx:50
MEDIUM js-unescaped-html-sink apps/web/src/app/u/[username]/page.tsx:282
MEDIUM redos-nested-quantifier apps/web/src/lib/deliverable.ts:11
MEDIUM manifest-install-lifecycle-script package.json:55
MEDIUM sql-template-interpolation packages/ai-core/src/prompts.ts:36
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:81
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:96
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:112
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:133
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:160
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:189
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:214
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:237
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:273
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:298
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:324
MEDIUM insecure-temp-file packages/remote-input/src/backends/waylandYdotool.test.ts:346

…and 16 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

The CI Lint job runs `pnpm format:check` alongside eslint; the three new
files were eslint-clean but not Prettier-formatted, which failed the job.
Formatting only — no behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ralyodio
ralyodio merged commit 2301196 into master Aug 17, 2026
13 checks passed
@ralyodio
ralyodio deleted the worktree-transistor-rips branch August 17, 2026 02:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants