Discord: Connect a Discord server — OAuth UI (#225 S7) — merge after S6 #259
No Reviewers
Labels
Clear labels
area/ai-pipeline
area/auth
area/billing
area/correlation
area/discord
area/frontend
area/governance
area/growth
area/infra-deploy
area/legal
area/map
area/mqtt
area/node-ota
area/osint
area/recording-audio
area/sdr-radio
area/transcription
board
board:agenda
minutes:draft
minutes:final
role:ceo
role:cfo
role:cio
role:ciso
role:cmo
role:coo
role:cto
Scene/entity extraction, summarizer, AI providers, embeddings
AuthN/Z, roles, tenancy/org scoping, API keys, credentials
Plans, entitlements, AI spend accounting and caps
Incident correlation, linking, lifecycle, severity, corr_debug
Discord bot, radio bots, voice streams, bot tokens, webhooks
Web UI: Next.js app and the edge-node local dashboard
Board process, minutes, unattended runner, agent tooling
Go-to-market, outreach, cold email, discovery conversations
CI/CD, VM, registry, Firestore rules/indexes deploy, Terraform/Caddy
ToS, retention, licences/DAAs, CAN-SPAM, E&O, name suppression
Live map: pins, paths, tiles, legend, geocoding
Node-C2 MQTT, checkins, commands, node sweeper, dynsec
Node install, enrollment, OTA updater, image skew
External feeds: cameras, 511, ADS-B, AIS, outages, and their licences
Call capture, boundaries, PulseAudio, ring buffer, trimming, attribution
OP25 decode, SDR hardware/tuning, conventional, second SDR, mesh radio
STT, transcript correction, place verification, name accuracy
Board process: minutes and board-filed items
Needs board scoping at the next sitting; CEO removes the label once scoped
One exec pre-read, closed by CEO
CEO ruling; open until enacted
Filed by the CEO
Filed by the CFO
Filed by the CIO
Filed by the CISO
Filed by the CMO
Filed by the COO
Filed by the CTO
kind
behavior-bug
Wrong product behavior; the instrument reads it correctly
kind
measurement-bug
Corrupts the recording/instrument (debug data, evals, health checks), not behavior
priority
p0
Drop everything: prod broken, live exposure, or owner-ruled P0
priority
p1
Next up: blocks a gate, a customer, or core incident quality
priority
p2
Normal: real defect or planned work, schedule in turn
priority
p3
Low: minor, cosmetic, or someday
status
blocked
Waiting on an external party, spend, or another issue
status
in-progress
Branch or PR open against it
status
needs-decision
Waiting on an owner or board ruling before work
status
needs-info
Root cause or repro unknown; needs a reading first
type
bug
Something built is wrong: code, data, or a measurement
type
chore
Maintenance, refactor, tests, pinning, cleanup: no user-visible change
type
decision
A ruling to make or record: board minutes, owner calls, legal reviews
type
docs
Docs, runbooks, CLAUDE.md, skills, SOPs
type
feature
New capability or an extension of an existing one
type
security
Security, privacy, credential or tenant-isolation exposure
type
tracking
Tracker, checklist, initiative or punch-list spanning several changes
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: logan/server-26#259
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Merge after S6 (the OAuth backend). Without S6 the Connect button gets a 404 and the card falls back to the link code, so nothing breaks, but nothing new works either.
Part of #225, slice S7 of the plan (comment 3108). Mostly frontend; one c2-core change (system_id on OAuth start/options) plus the #257 voice-check state.
Fixes #256 #257 #258
What changed
/discord/connected(new): the redirect URI to register on each bot app (https://drb.cusano.net/discord/connected).code/statein sessionStorage (10 min TTL), strips them from the URL withreplaceState,no-referrervia metadata and a response header./login?next=/discord/connected, then resumes.error=access_denied: "Nothing changed". Othererrorvalues: only a short code is echoed, never free text.POST /discord/oauth/complete {code, state}once (ref guard; the code is single-use). Result card: "Linked X and added Bot. N voice channels ready, M need a permission fix" + a button toreturn_to.middleware.ts: callback path added toNO_SESSION_COOKIE_GATE(plan finding 2). New finding: it is needed for signed-in users too.drb_sessionisSameSite=Strict, so the browser does not send it on the cross-site redirect back from discord.com, and the middleware would have bounced everyone to/loginwith the code dropped./login: honours?next=for an allowlist of exactly one path, so there is no open redirect.DiscordServersCard: "Connect a Discord server" is the primary button, with a radio-bot picker: Automatic / the bot for a system / the org's own bots (fromGET /tokens/org/preferred)./drb linkis a collapsed secondary option and opens itself when start answers 404.RadioBotsPanel(admin): write-only client-secret field per bot, "Connect on/off", "Turn off", and "N of 100 servers" with the 90-server pool cap warning.lib/discordOAuth.ts(new, kept out ofc2api.ts): the only navigation target it accepts is anhttps://discord.com/.../oauth2/authorizeURL.Contract assumed (S6 was still uncommitted when this was built; I read its in-progress
internal/discord_oauth.py)POST /discord/oauth/start{token_id?, system_id?, node_id?, return_to}→{url | authorize_url, bot_name?}; 404 = no fitting bot has a secretPOST /discord/oauth/complete{code, state}→{guild_id, guild_name, channels_ready, channels_blocked, return_to, bot_name?, stopped?}(counts or lists both accepted)PUT /tokens/{id}/oauth{client_secret}; never echoedDELETE /tokens/{id}/oauthclear_secret)GET /tokensoauth_enabled: boolin the allowlistlimits.pool_bot_guild_capAlignment points for S6 (resolved in
32d223e: start/options accept system_id): S6's state storesnode_id, notsystem_id. If start ignoressystem_id, the "bot for a system" choice quietly falls back to Automatic. Either acceptsystem_idin S6, or I'll drop that option. IfGET /tokensdoesn't exposeoauth_enabled, the admin column always reads "Connect off".Verification
tsc --noEmit: exit 0next build(dummy Firebase env): success,/discord/connectedprerendered staticdrb-qa-review,security-reviewskill (plan asks for both on S6+S7; for the main session after S6 lands). No live OAuth test: needs S6 + a registered redirect URI.Update
32d223e(after merging main2c254c1)POST /discord/oauth/startandGET /discord/oauth/optionstake an optionalsystem_id(org's own or shared system; another org's = 404). The system's preferred bot is strict: if it can't connect, 404, never a quiet swap to Automatic.invalidchannel state + "Radio bot needs replacing" badge, no invite links.🤖 Generated with Claude Code
- /discord/connected: Discord's OAuth redirect target. Stashes code/state in sessionStorage, strips them from the URL (replaceState), no-referrer (metadata + response header), sends a signed-out user to /login?next=/discord/connected and resumes, access_denied = "Nothing changed", result card "Linked X: N ready, M need a fix". - middleware.ts: the callback path joins NO_SESSION_COOKIE_GATE (plan finding 2). Also needed for signed-in users: drb_session is SameSite=Strict, so it is not sent on the cross-site redirect back from discord.com. - /login honours ?next= for an allowlist of one path (no open redirect). - DiscordServersCard: Connect is the primary action with a radio-bot picker (automatic / the bot for a system / the org's own bots); the /drb link code moves to a collapsed secondary option and opens itself when /discord/oauth/start answers 404. - RadioBotsPanel: write-only client-secret field per bot (PUT/DELETE /tokens/{id}/oauth), "Connect on/off", "N of 100 servers" with the pool cap (90) warning. - lib/discordOAuth.ts: the S6 client, kept out of c2api.ts; only ever navigates to a discord.com /oauth2/authorize URL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Review (S7, checked against S6's local commit
db4e326, not yet pushed). Pushedeaa0be0fixing:streams_stoppedvsstoppedon /complete (stopped-streams note never rendered),bot.namenesting on /start, a per-bot 404 hiding the Connect button, and 'bot for a system' now sendstoken_idwhen the preferred bot is the org's own. S6db4e326already hasoauth_enabledon GET /tokens andDELETE /tokens/{id}/oauth. Still open before merge: S6'sStartBodyhas nosystem_id, so choosing a system whose preferred bot is a platform-pinned pool bot silently falls back to Automatic (pydantic ignores the extra field). Either S6 addssystem_id(same branch as itsnode_idpath) or S7 drops those options. tsc exit=0; c2-core pytest 810 passed (no Python in this PR). No live OAuth run.🤖 Generated with Claude Code
Pushed
32d223e(merged main2c254c1in13a3525, no conflicts;eaa0be0kept, no force-push).StartBodyandGET /discord/oauth/optionsnow takesystem_id. Same visibility as the node path: the org's own system or a shared one; another org's or unknown system = 404 ("System not found."). The system's preferred bot is strict: if it can't connect (no secret, at the pool cap, another org's bot), 404 instead of silently inviting Automatic. No preference = the usual order. The picker's "bot for a system" now always sendssystem_id. 3 new tests._channel_statereturnsinvalidwhen a member bot's token was rejected; the picker shows "Radio bot needs replacing" (DRB support) with no invite links. 1 new test.Tests: c2-core pytest
913 passed, 4 warnings; frontendtsc --noEmitexit 0. Not run: live OAuth, drb-qa-review, security-review.🤖 Generated with Claude Code