Skip to content

Commit cd5258c

Browse files
Improve bot auth workflow and local testing guidance (#34)
1 parent 18f97d3 commit cd5258c

29 files changed

+1489
-156
lines changed

.env.deploy.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ TWITCH_EVENTSUB_SECRET=
1212
SESSION_SECRET=
1313
ADMIN_TWITCH_USER_IDS=
1414
TWITCH_BOT_USERNAME=requestbot
15-
TWITCH_SCOPES=openid user:read:moderated_channels channel:bot
15+
TWITCH_SCOPES=openid user:read:moderated_channels moderator:read:chatters channel:bot

.env.example

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ TWITCH_EVENTSUB_SECRET=local-dev-eventsub-secret
1414
SESSION_SECRET=local-dev-session-secret
1515
# Twitch user IDs for initial admins - comma separated, no quotation marks, like this: 1234567,2345678
1616
ADMIN_TWITCH_USER_IDS=
17-
# The username of the shared bot account
17+
# The username of the shared bot account.
18+
# For local development, this should usually be your dedicated test bot account.
19+
# Production should keep its own production bot username in deployed env/secrets.
1820
TWITCH_BOT_USERNAME=requestbot
19-
# Scopes for the shared bot account
20-
TWITCH_SCOPES=openid user:read:moderated_channels channel:bot
21+
# Broadcaster OAuth scopes used by the main app login.
22+
# These should include the channel permissions needed for bot replies and chatter lookups.
23+
TWITCH_SCOPES=openid user:read:moderated_channels moderator:read:chatters channel:bot

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,35 @@ All notable changes to this project will be documented in this file.
55
## [Unreleased]
66

77
### Added
8+
- Shared bot reconnect controls for admins, including the ability to replace the connected bot account safely.
9+
- VIP token management from both chat commands and the dashboard, with Twitch user lookup, chatter-aware search, and an editable token table in the app.
10+
- Public played-history search.
11+
- Charter blacklisting, including exact charter matching and clearer handling when only some song versions are blocked.
12+
- Richer sample catalog metadata for artists, charters, tunings, and future filtering work.
813
- Production-ready Sentry scaffolding for the Cloudflare app and backend workers, with DSN-based opt-in for local development and Cloudflare-managed secrets for deployed environments.
914
- GitHub issue templates, a pull request template, and a repository `CODE_OF_CONDUCT.md`.
1015

1116
### Changed
17+
- Bot replies now use Twitch's bot-badge-compatible reply path, and the app prompts broadcasters to reconnect Twitch if required permissions are missing.
18+
- Broadcaster login now requests the Twitch permissions needed for chatter-aware moderation and bot-badged replies.
19+
- The app header and settings pages now surface Twitch reauthorization more clearly when a reconnect is required.
20+
- Local-development guidance now strongly separates production bot/broadcaster usage from local testing and explains the risks of cross-environment chat handling.
21+
- The moderation dashboard now supports faster Twitch username search with debouncing, in-chat prioritization, and clearer saved-state feedback for VIP tokens.
22+
- Search results now show newer song versions first, and public search includes a dedicated `!edit` copy command.
23+
- Public playlist, dashboard playlist, search, and home-page experiences have been refined for mobile screens and easier browsing.
24+
- Blacklist and setlist management now use exact IDs instead of loose text matching, improving moderation accuracy.
25+
- Public search now behaves more like a browsable catalog and shows clearer demo-database guidance.
1226
- Simplified catalog song source URLs to always derive the Ignition download link from the song source ID instead of storing `source_url` in the database.
1327
- Added a migration to remove the redundant `catalog_songs.source_url` column and updated the sample catalog seed to match the new schema.
1428
- Tightened schema version checks so the app only accepts migrations that are actually present in the repo.
1529
- Expanded deployment and environment documentation for Sentry configuration in local development and production.
1630

31+
### Fixed
32+
- Duplicate EventSub deliveries for `!addvip` no longer grant multiple VIP tokens or queue duplicate bot replies.
33+
- Twitch reply handling now distinguishes between accepted API requests and messages that Twitch actually sent to chat.
34+
- Bot/account status screens now show the real connected bot identity instead of only the configured bot name.
35+
- Production deployment config regeneration now stays in sync after remote migrations.
36+
1737
## [0.1.0] - 2026-03-18
1838

1939
### Added

CONTRIBUTING.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,19 @@
99
```bash
1010
npm install
1111
npm run db:bootstrap:local
12-
npm run lint
1312
npm run typecheck
1413
npm run test
14+
npm run format
15+
npm run lint
1516
npm run build
1617
```
1718

19+
If you changed user-facing flows or browser interactions, also run:
20+
21+
```bash
22+
npm run test:e2e
23+
```
24+
1825
4. Open a pull request.
1926
5. Wait for CI to pass.
2027
6. Review the preview deployment if one is enabled for the repository.
@@ -42,6 +49,7 @@ The app checks the latest applied migration at runtime and fails early if the lo
4249

4350
- Keep changes focused.
4451
- Include tests when you change behavior.
52+
- Run `npm run format` before `npm run lint`. This repo has been seeing avoidable AI-generated formatting drift, and Biome lint is much cleaner after formatting first.
4553
- If a change affects Twitch auth, EventSub, playlist mutations, or migrations, call that out in the PR description.
4654
- If a change affects deployment or Cloudflare bindings, update the docs in the same PR.
4755

README.md

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ TWITCH_CLIENT_SECRET=
8282
TWITCH_EVENTSUB_SECRET=local-dev-eventsub-secret
8383
SESSION_SECRET=local-dev-session-secret
8484
TWITCH_BOT_USERNAME=requestbot
85-
TWITCH_SCOPES=openid user:read:moderated_channels channel:bot
85+
TWITCH_SCOPES=openid user:read:moderated_channels moderator:read:chatters channel:bot
8686
ADMIN_TWITCH_USER_IDS=
8787
VITE_ALLOWED_HOSTS=
8888
```
@@ -95,7 +95,7 @@ For basic local development, set:
9595
- `TWITCH_EVENTSUB_SECRET`
9696
- `SESSION_SECRET`
9797
- `TWITCH_BOT_USERNAME`
98-
- `TWITCH_SCOPES=openid user:read:moderated_channels channel:bot`
98+
- `TWITCH_SCOPES=openid user:read:moderated_channels moderator:read:chatters channel:bot`
9999
- `VITE_ALLOWED_HOSTS=` if you need extra Vite hostnames
100100

101101
To test Twitch sign-in, bot behavior, and EventSub locally, also set:
@@ -106,6 +106,8 @@ To test Twitch sign-in, bot behavior, and EventSub locally, also set:
106106

107107
`ADMIN_TWITCH_USER_IDS` should contain the Twitch user ID for the admin account that is allowed to connect the shared bot account and access admin pages.
108108

109+
If a broadcaster connected before `channel:bot` was added to your configured scopes, they need to reconnect Twitch from the app before bot replies can use Twitch's bot badge path.
110+
109111
Sentry stays off locally unless you explicitly set a DSN:
110112

111113
- `SENTRY_DSN`
@@ -165,6 +167,26 @@ http://localhost:9000
165167

166168
The repo auto-runs local D1 migrations before `dev`, `test`, and `build`.
167169

170+
### Verification before commit
171+
172+
Run checks in this order:
173+
174+
```bash
175+
npm run typecheck
176+
npm run test
177+
npm run format
178+
npm run lint
179+
npm run build
180+
```
181+
182+
If you changed browser-driven flows, also run:
183+
184+
```bash
185+
npm run test:e2e
186+
```
187+
188+
Formatting before lint is intentional. Biome lint is much cleaner after `npm run format`, and this avoids a lot of AI-generated formatting churn before commit.
189+
168190
### 6. Twitch application setup for local auth
169191

170192
If you want Twitch login to work locally, your Twitch developer application must include both redirect URIs:
@@ -475,7 +497,7 @@ gh secret set CLOUDFLARE_D1_DATABASE_ID
475497
gh secret set CLOUDFLARE_SESSION_KV_ID
476498
gh secret set APP_URL --body "https://your-production-url.example"
477499
gh variable set TWITCH_BOT_USERNAME --body "your_bot_username"
478-
gh variable set TWITCH_SCOPES --body "openid user:read:moderated_channels channel:bot"
500+
gh variable set TWITCH_SCOPES --body "openid user:read:moderated_channels moderator:read:chatters channel:bot"
479501
```
480502

481503
For the full deploy and GitHub workflow details, use [docs/deployment-workflow.md](/docs/deployment-workflow.md).

docs/bot-operations.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,13 @@ TWITCH_EVENTSUB_SECRET=...
4040
SESSION_SECRET=...
4141
ADMIN_TWITCH_USER_IDS=your_main_twitch_user_id
4242
TWITCH_BOT_USERNAME=Pants_Bot_
43-
TWITCH_SCOPES=openid user:read:moderated_channels channel:bot
43+
TWITCH_SCOPES=openid user:read:moderated_channels moderator:read:chatters channel:bot
4444
```
4545

46+
For local development, `TWITCH_BOT_USERNAME` should usually be your dedicated test bot account. Production should keep its own bot username in deployed env or secrets. The app enforces that the connected bot login matches `TWITCH_BOT_USERNAME`, so changing bot accounts locally requires changing local `.env` first.
47+
48+
`TWITCH_SCOPES` belongs to the broadcaster login flow, not the bot login flow. It should include `channel:bot` so chat replies can use Twitch's bot badge path. If a broadcaster connected before `channel:bot` was present, they need to reconnect Twitch.
49+
4650
2. Make sure your Twitch developer application has both redirect URIs registered:
4751

4852
- `${APP_URL}/auth/twitch/callback`
@@ -70,6 +74,26 @@ npm run db:migrate
7074

7175
- Replies are sent with the bot account's user token, not the broadcaster token.
7276
- If Twitch returns `401` while sending chat, the backend refreshes the bot token once and retries automatically.
77+
- Testing against the same broadcaster in both production and local/tunnel environments can cause both environments to receive and process the same chat command.
78+
79+
### Local vs production warning
80+
81+
If a streamer or moderator tests bot commands on a channel that is connected in the live app while a local tunnel/dev environment is also connected for that same broadcaster, cross-environment behavior can become confusing.
82+
83+
There are two different cases:
84+
85+
- same broadcaster + same bot account:
86+
local and production compete for the same `channel.chat.message` subscription, so one environment can effectively take over chat handling from the other
87+
- same broadcaster + different bot accounts:
88+
Twitch can allow both subscriptions, so both environments can receive commands from that same channel and both may reply
89+
90+
For safe bot testing:
91+
92+
- use a dedicated test broadcaster/channel whenever possible
93+
- use a dedicated test bot account for local development
94+
- set local `.env` `TWITCH_BOT_USERNAME` to that test bot account before reconnecting the bot
95+
- do not sign a production broadcaster into the local environment
96+
- avoid keeping both production and local EventSub subscriptions active for the same broadcaster
7397

7498
### Current limits
7599

docs/local-development.md

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,50 @@ npm run dev
6666

6767
To exercise Twitch login, EventSub, and bot replies, fill in the Twitch-related values in `.env`.
6868

69+
For local bot testing, set `TWITCH_BOT_USERNAME` in `.env` to your dedicated test bot account, not the production bot account. The bot OAuth callback only accepts the username configured in local env, so if you want to connect `jimmy_test_bot_` locally, your local `.env` must also say `TWITCH_BOT_USERNAME=jimmy_test_bot_`.
70+
71+
Keep the production bot username only in production secrets or deployed env. Do not point local development at the production bot account unless you intentionally want local testing to use the live bot identity.
72+
73+
`TWITCH_SCOPES` applies to the broadcaster's main app login, not the shared bot login. It needs `channel:bot` so bot replies can use Twitch's bot badge path, and it needs `moderator:read:chatters` for the chatter-first VIP lookup flow.
74+
75+
### Important local testing warning
76+
77+
Do not test bot commands against a channel that is also connected in the live app unless you intentionally want both environments to react.
78+
79+
There are two different failure modes:
80+
81+
#### Same broadcaster + same bot account in local and production
82+
83+
This does not usually create duplicate chat handling.
84+
85+
Instead, it creates a subscription ownership conflict. Twitch treats `channel.chat.message` subscriptions as unique by event type plus condition, and the condition includes both the broadcaster ID and bot user ID. If local and production both try to use the same broadcaster with the same bot account, one environment can end up owning the subscription and the other can fail or appear to stop receiving chat events.
86+
87+
That means local testing can still interfere with production, even if both environments do not reply at the same time.
88+
89+
#### Same broadcaster + different bot accounts in local and production
90+
91+
This is the more dangerous case for duplicate behavior.
92+
93+
Because the bot user ID is different, Twitch can allow both subscriptions at once. Then a single chat command in that broadcaster's channel can be seen by both environments:
94+
95+
- once by production
96+
- once by local development
97+
98+
That can cause:
99+
100+
- duplicate bot replies in chat
101+
- duplicated side effects if both environments act on the same command
102+
- confusing logs where both environments appear to handle the same message
103+
104+
Recommended practice:
105+
106+
- use a separate test broadcaster/channel for local bot testing
107+
- use a separate test bot account for local bot testing
108+
- set local `.env` `TWITCH_BOT_USERNAME` to the test bot account username
109+
- do not connect a production broadcaster to local development
110+
- keep only one active EventSub webhook subscription for a given broadcaster when you are debugging command behavior
111+
- do not leave a local tunnel subscription active while also testing the same channel in production
112+
69113
### Public HTTPS for local Twitch testing
70114

71115
`localhost` is enough for basic Twitch OAuth testing, but full local testing for this app works better with a public HTTPS URL because Twitch webhooks need a reachable callback target.
@@ -144,6 +188,8 @@ Then update:
144188
- `https://dev.example.com/auth/twitch/callback`
145189
- `https://dev.example.com/auth/twitch/bot/callback`
146190

191+
Before testing chat commands through the tunnel, make sure the same broadcaster is not still actively subscribed to the production EventSub callback unless that is intentional.
192+
147193
#### ngrok
148194

149195
As an alternative:
@@ -159,32 +205,52 @@ Use the generated HTTPS URL for:
159205

160206
If the ngrok URL changes, update both `.env` and the Twitch app redirect URIs.
161207

162-
### Daily commands
208+
### Verification before commit
209+
210+
Run checks in this order:
163211

164-
Lint:
212+
1. Typecheck:
165213

166214
```bash
167-
npm run lint
215+
npm run typecheck
168216
```
169217

170-
Typecheck:
218+
2. Tests:
171219

172220
```bash
173-
npm run typecheck
221+
npm run test
174222
```
175223

176-
Tests:
224+
3. Format:
177225

178226
```bash
179-
npm run test
227+
npm run format
228+
```
229+
230+
4. Lint:
231+
232+
```bash
233+
npm run lint
180234
```
181235

182-
Production build:
236+
5. Production build:
183237

184238
```bash
185239
npm run build
186240
```
187241

242+
If you changed browser-driven behavior or UI flows, also run:
243+
244+
```bash
245+
npm run test:e2e
246+
```
247+
248+
Why `format` before `lint`:
249+
250+
- Biome lint is much less noisy after formatting first
251+
- AI-generated edits often introduce avoidable formatting drift
252+
- running `npm run format` first catches a large class of pre-commit issues cheaply
253+
188254
### Cloudflare deploy inputs
189255

190256
`npm run deploy` and `npm run db:bootstrap:remote` read deployment values from `.env.deploy`, not `.env`.

src/components/dashboard-page-header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export function DashboardPageHeader(props: DashboardPageHeaderProps) {
3232
</div>
3333

3434
{props.aside ? (
35-
<div className="dashboard-page-header__aside grid shrink-0 gap-3 md:justify-items-end">
35+
<div className="dashboard-page-header__aside grid w-full min-w-0 gap-3 md:w-auto md:max-w-sm md:justify-items-end">
3636
{props.aside}
3737
</div>
3838
) : null}

0 commit comments

Comments
 (0)