Skip to content

A watch party that works everywhere, with nixamp as the room and the identity - #223

Merged
ralyodio merged 1 commit into
masterfrom
feat/watch-party-nixamp-everywhere
Sep 17, 2026
Merged

ralyodio merged 1 commit into
masterfrom
feat/watch-party-nixamp-everywhere

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Why

/watch-party did not do what its name said. The host could not pick a file (the modal's open state was thrown away), Sync All had no handler, the chat was a picture of one, the member list never refreshed, and there was no sync between browsers at all. The link nixamp hands its members, /watch-party?code=ABC123, landed on an empty form. So the OAuth 2.1 bridge from PR #217 was bolted onto a party that did not work, and a nixamp user arriving from the app, a terminal or a TV hit the members-only login with no way in.

What

The party itself

  • The host's player is the clock. Play, pause and seek go to a new PATCH /api/watch-party/playback; every other member polls the party every 3 s and follows within 2 s, with a "tap to join playback" overlay when the browser refuses to autoplay.
  • The host is proven by the session; the guest id is accepted only for a guest-hosted party, so a member cannot present the host id they saw in the member list (the media PATCH had that hole).
  • Media selection is wired; the member list refreshes; the party's URL goes in the address bar for sharing.
  • ?code= fills the form and joins on its own once there is a name. The name defaults to the nixamp handle, or the account, so a member sent from nixamp is in with no typing.

nixamp, on every surface

  • Chat is the nixamp room's chat (live_event_chat), read through the bridge by anybody with the code and posted as the member's own nixamp self. The same lines appear on nixamp.com's room page (A party's room is a page: nixamp.com/live/<slug>, open to read, with the chat nixamp#218), in nixamp party, the desktop app, MCP and on a TV.
  • A host with nixamp connected has the party bridged as soon as they open it; play/pause/seek sync nixamp immediately, not at the next 15 s.
  • Sign in with nixamp (OAuth 2.1, PKCE, now with the email scope) on /login and /signup. The start route runs signed out; the callback finds or creates the account with that email and mints a real Supabase session server-side (admin magic link generated and spent in the same request, token never leaves the process), then lands on ?redirect=, which is the party for somebody who came from nixamp. /api/v1/nixamp/oauth is public in proxy.ts for that reason; redirect is restricted to a path on this site.
  • Buttons are Focusable for the Fire TV WebView.

Tests

  • playback/route.test.ts: host moves playback, pause is a paused party, a member with the host's id is refused, a guest host is accepted, bad positions refused, gone parties 404/410.
  • nixamp/route.test.ts: chat read is open, any member posts as themselves, no nixamp → 409 with a connect link back to this party.
  • signin.test.ts: account made confirmed with the handle as its name, existing account signed in, no email refused, other failures surfaced, safeRedirect refuses // and /\, the cookie shape matches a password login.
  • Full suite: 215 files, 2898 tests pass. pnpm typecheck clean; lint has no errors.

After merge

  • Existing nixamp connections were granted without email; they keep working for parties. Only Sign in with nixamp needs it, and that is a fresh consent.
  • Parties are still in-memory per process (_store.ts), as before; only the bridge row is durable.

🤖 Generated with Claude Code

…identity

The page did not do what its name said: the host could not pick a file (the
modal's open state was thrown away), Sync All had no handler, the chat was a
picture of one, the member list never refreshed, and the link nixamp hands
its members (/watch-party?code=ABC123) landed on an empty form. The nixamp
bridge was bolted onto a party that did not sync.

Now:

- The host's player is the clock. Play, pause and seek go to a new
  PATCH /api/watch-party/playback; every other member polls the party every
  3 s and follows within 2 s, with a "tap to join playback" overlay when the
  browser will not autoplay. The host is proven by the session, and the
  guest id is accepted only for a guest-hosted party, so a member cannot
  present the host id they saw in the member list.
- ?code= joins on its own once there is a name, and the name defaults to the
  nixamp handle (or the account) so a member sent from nixamp is in with no
  typing. The party's URL is put in the address bar for sharing.
- Chat is the nixamp room's chat: read through the bridge by anybody with the
  code, posted as the member's own nixamp self, so the same lines appear in
  the nixamp app, a terminal, the desktop app and on a TV. Without nixamp
  connected the box says so and links to connecting, landing back here.
- A host with nixamp connected has the party bridged as soon as they open it;
  play/pause/seek sync nixamp immediately instead of at the next 15 s.
- Sign in with nixamp (OAuth 2.1, PKCE, now with the email scope) on /login
  and /signup: the start route runs signed out, the callback finds or makes
  the account with that email and mints a real Supabase session server-side
  (admin magic link generated and spent in the same request), then lands on
  ?redirect=, which is the party for somebody who arrived from nixamp. The
  OAuth routes are public in proxy.ts for that reason.
- Buttons are Focusable for the Fire TV WebView.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

99 finding(s)

HIGH/CRITICAL: 11 | MEDIUM: 29 | LOW: 59

Severity Rule Location
HIGH secret-private-key src/app/settings/seedbox-section.tsx:412
HIGH secret-generic-api-key docs/incidents/2026-05-okshanaby-supply-chain.md:18
HIGH tls-verification-disabled src/app/api/iptv-proxy/route.ts:38
HIGH tls-verification-disabled src/app/api/iptv/channels/route.ts:35
HIGH tls-verification-disabled src/app/api/iptv/playlists/[id]/route.ts:73
HIGH tls-verification-disabled src/app/api/iptv/playlists/route.ts:64
HIGH js-cors-origin-reflected src/app/api/public/shares/[slug]/checkout/route.ts:40
HIGH js-cors-origin-reflected src/app/api/public/vod/[slug]/checkout/route.ts:34
HIGH tls-verification-disabled src/lib/iptv/shares/upstream.ts:37
HIGH tls-verification-disabled workers/iptv-cache/epg-fetcher.ts:25
HIGH tls-verification-disabled workers/iptv-cache/playlist-fetcher.ts:62
MEDIUM secret-jwt .github/workflows/ci.yml:120
MEDIUM secret-jwt .github/workflows/ci.yml:121
MEDIUM secret-jwt .github/workflows/ci.yml:123
MEDIUM secret-jwt .github/workflows/ci.yml:162
MEDIUM secret-jwt .github/workflows/ci.yml:164
MEDIUM secret-jwt docs/tunein (2).py:9
MEDIUM secret-jwt docs/tunein.py:9
MEDIUM sh-remote-script-execution scripts/setup-server.sh:182
MEDIUM sh-remote-script-execution scripts/setup-server.sh:419
MEDIUM sh-remote-script-execution scripts/setup-server.sh:428
MEDIUM sh-unquoted-expansion-destructive scripts/setup-server.sh:1096
MEDIUM sh-unquoted-expansion-destructive scripts/setup-server.sh:1106
MEDIUM js-unescaped-html-sink src/app/api/player/route.ts:110
MEDIUM js-unescaped-html-sink src/app/api/player/route.ts:249
MEDIUM js-unescaped-html-sink src/app/blog/[slug]/page.tsx:40
MEDIUM js-unescaped-html-sink src/app/blog/[slug]/page.tsx:66
MEDIUM js-unescaped-html-sink src/app/email/email-content.tsx:566
MEDIUM js-open-redirect src/app/login/page.tsx:68
MEDIUM js-open-redirect src/app/pricing/page.tsx:162
MEDIUM js-open-redirect src/app/rent/[slug]/rent-client.tsx:170
MEDIUM js-unescaped-html-sink src/app/rss/rss-content.tsx:615
MEDIUM js-open-redirect src/app/vod/[slug]/vod-client.tsx:134
MEDIUM js-open-redirect src/app/watch/[slug]/watch-client.tsx:129
MEDIUM js-unescaped-html-sink src/app/youtube/youtube-content.tsx:546
MEDIUM js-open-redirect src/components/account/iptv-subscription-section.tsx:135
MEDIUM js-open-redirect src/components/account/iptv-subscription-section.tsx:167
MEDIUM js-unescaped-html-sink src/components/news/news-section.tsx:361
MEDIUM js-unescaped-html-sink src/components/news/news-section.tsx:734
MEDIUM redos-nested-quantifier src/lib/metadata-enrichment/metadata-enrichment.ts:317
LOW tls-verification-disabled docs/tunein (2).py:34
LOW tls-verification-disabled docs/tunein (2).py:37
LOW py-xpath-injection docs/tunein (2).py:37
LOW tls-verification-disabled docs/tunein (2).py:47
LOW py-xpath-injection docs/tunein (2).py:47
LOW tls-verification-disabled docs/tunein.py:34
LOW tls-verification-disabled docs/tunein.py:37
LOW py-xpath-injection docs/tunein.py:37
LOW tls-verification-disabled docs/tunein.py:47
LOW py-xpath-injection docs/tunein.py:47

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

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio marked this pull request as ready for review September 17, 2026 13:57
@ralyodio
ralyodio merged commit 8b05648 into master Sep 17, 2026
9 checks passed
@ralyodio
ralyodio deleted the feat/watch-party-nixamp-everywhere branch September 17, 2026 13:59
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.

1 participant