Discord: per-bot OAuth "Connect a Discord server" backend (server-26#225 S6)

An org owner links a server by inviting one radio bot through that bot's
own OAuth2 invite (owner decision Q1). The state is a single-use, 10-minute
nonce bound to org + user (stored only as its sha256); the server linked is
the one Discord's code exchange names, never the redirect query.

- routers/discord_oauth.py: GET /discord/oauth/options, POST /start,
  POST /complete. 404 while discord_oauth_redirect_uri is empty or no
  usable bot has a secret. Bot order: explicit token_id, the node system's
  preferred bot, the org's own bot, least-full pool bot under the cap.
- internal/discord_oauth.py: state mint/consume transaction (login-CSRF:
  state.uid must be the caller; any presentation burns it), code exchange,
  revoke, client-credentials secret check. Through discord_rest's shared
  client and breaker; secrets/codes/tokens never logged or in errors.
- discord_guild_link: one link transaction body (_apply_link) and one
  settle step that stops a server's streams when the org it acted for
  changes (plan finding 3). /drb link redeem, the admin PUT link and OAuth
  all use it; the previous org is now read inside the link transaction.
- tokens: GET /tokens is an allowlist (+ oauth_enabled, server_count,
  server_cap); PUT/DELETE /tokens/{id}/oauth (admin) checks the secret with
  Discord before storing it in bot_oauth/{id}, never echoes it.
- firestore.rules: explicit deny for bot_oauth and discord_oauth_states.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Logan Cusano
2026-10-05 02:52:57 -04:00
co-authored by Claude Opus 5.5
parent e54191a5d0
commit db4e326332
10 changed files with 1177 additions and 60 deletions
+10
View File
@@ -154,6 +154,16 @@ service cloud.firestore {
allow read, write: if false;
}
// Radio-bot OAuth2 client secrets and in-flight "Connect a Discord
// server" states (server-26#225 S6). Secrets: c2-core only, never any client.
match /bot_oauth/{tokenId} {
allow read, write: if false;
}
match /discord_oauth_states/{stateHash} {
allow read, write: if false;
}
match /waitlist/{entryId} {
allow read, write: if false; // POST /waitlist writes via the admin SDK
}