Skip to content

nixamp.com as an OAuth 2.1 authorization server, and a watch party as a nixamp room - #127

Merged
ralyodio merged 2 commits into
mainfrom
feat/watch-party-oauth21
Sep 12, 2026
Merged

nixamp.com as an OAuth 2.1 authorization server, and a watch party as a nixamp room#127
ralyodio merged 2 commits into
mainfrom
feat/watch-party-oauth21

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

bittorrented.com has watch parties. nixamp has rooms, chat, invitations, a directory and five clients that can already open one. This makes them the same thing, with the identity link done by OAuth 2.1.

Which side is the authorization server

nixamp. It already keeps the accounts, the revocable tokens and the provider identities; bittorrented keeps none of those. src/oauth.ts stays what it was -- nixamp signing in with GitHub -- and the new src/oauth-server.ts is the other direction: somebody signing in with nixamp.

What "2.1" actually means here

The things OAuth 2.0 left optional are not optional:

  • PKCE (S256) on every request, from every client, public or confidential. No plain.
  • Exact redirect URI matching. The only thing allowed to vary is a loopback port (RFC 8252 §7.3), because a CLI cannot know its port before it listens. A prefix, a subdomain and a different path are all refused, and there is a test for each.
  • A code is spent once. A second use is treated as theft: everything that code produced is withdrawn.
  • Refresh tokens rotate. Each refresh answers a new one and retires the old; a retired one presented again withdraws the whole family, since the only way that happens is two parties holding one secret.
  • No implicit grant, no password grant -- refused by name rather than shrugged at.
  • RFC 8414 metadata at /.well-known/oauth-authorization-server.

Access tokens are nixamp's own nxa_ tokens with a client's name on them. That is what makes them revocable, and it is why every existing /api/v1 route understands one with no change at all.

An error that cannot be redirected is a page, not a bounce: sending a refusal to an unregistered redirect URI would make this an open redirector.

The bridge

src/watch-party.ts is deliberately thin. A bridged party is a live_events row like any other, plus one row saying which external party it is, where to watch it, and the second the host says everybody is at. That is what makes discovery, invitations, chat, layouts and /live/:slug work with no special case.

  • Bridging is idempotent, because the client calls it every time somebody opens the party page.
  • A watch link must be on the client's own site, read off the redirect URIs it registered rather than configured twice.
  • Playback is advisory and cheap: the host writes it, a late joiner reads it and adds the drift. A paused film reports no drift, or every client would seek away from the frame everybody is looking at.

Surfaces

where what
web a Watch parties panel, and Connected apps in the Account panel with a Disconnect button
CLI nixamp party list / join / host / sync / end
MCP nixamp mcp -- a new stdio MCP server, JSON-RPC on a pipe, no dependency -- offering those same five as tools
desktop / TV inherited: both run the same web build, so the panel is already there

The film never crosses over. What nixamp carries is the room.

Testing

bun test test web/test: 545 pass, 1 skip, 0 fail. bun run typecheck clean (root + both web apps). 30 new tests across test/oauth-server.test.ts and test/party-cli.test.ts, including the S256 digest cross-checked against the value the bittorrented client's WebCrypto produces.

Notes

🤖 Generated with Claude Code

https://claude.ai/code/session_01QuUkTrofSjQ15j79mRuy4f

ralyodio and others added 2 commits September 12, 2026 11:24
…ixamp room

nixamp.com becomes an OAuth 2.1 authorization server, and a watch party on a
site that connects through it becomes an ordinary nixamp live event with a
room -- so the same party is joinable from the web app, the terminal, the
desktop app, the television and an agent over MCP, without any of them
learning anything about torrents.

Which side is the authorization server was the first question. nixamp already
keeps the accounts, the revocable tokens and the provider identities, and
bittorrented.com keeps none of those, so nixamp is the AS and bittorrented is
the client. src/oauth.ts stays what it was -- nixamp signing in WITH GitHub --
and src/oauth-server.ts is the other direction.

It is 2.1 rather than 2.0, which is to say the things 2.0 left optional are
not optional here: PKCE with S256 on every request from every client, public
or confidential; redirect URIs matched exactly, with only a loopback port
allowed to vary because a CLI cannot know its port before it listens; a code
spent once, where a second use withdraws everything the first produced;
refresh tokens that rotate, where a retired one presented again withdraws the
whole family. There is no implicit grant and no password grant, and both are
refused by name rather than shrugged at. RFC 8414 metadata says so at
/.well-known/oauth-authorization-server.

Access tokens are nixamp's own `nxa_` tokens with a client's name on them,
which is what makes them revocable and what makes every existing /api/v1 route
understand one with no change. An error that cannot be redirected is a page:
bouncing a refusal to an unregistered redirect URI would make this an open
redirector.

The bridge itself (src/watch-party.ts) is deliberately thin. A bridged party
is a live_events row like any other, plus one row saying which external party
it is, where to watch it, and the second the host says everybody is at. That
is what makes discovery, invitations, chat, layouts and /live/:slug work with
no special case. Bridging is idempotent, because the client calls it every
time somebody opens the party page. A watch link must be on the client's own
site, read off the redirect URIs it registered rather than configured twice.

Surfaces: web gets a Watch parties panel and a Connected apps list in the
Account panel; `nixamp party` gets list/join/host/sync/end; `nixamp mcp` is a
new stdio MCP server -- JSON-RPC on a pipe, no dependency -- offering those
same five as tools to an agent.

545 tests pass, typecheck clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuUkTrofSjQ15j79mRuy4f
After rebasing on #125 the INSERT carries kind, doors and the ticket
fields, so the positional fake read status out of the wrong slot and
every bridged party came back 500.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuUkTrofSjQ15j79mRuy4f
@ralyodio
ralyodio force-pushed the feat/watch-party-oauth21 branch from 0a3fb14 to 112dfe7 Compare September 12, 2026 11:29
@ralyodio
ralyodio marked this pull request as ready for review September 12, 2026 11:29
@ralyodio
ralyodio merged commit 7a4f5b5 into main Sep 12, 2026
2 checks passed
@ralyodio
ralyodio deleted the feat/watch-party-oauth21 branch September 12, 2026 11:29
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