Implement Channel Points Twungeon MVP

This commit is contained in:
2026-08-17 07:16:13 -07:00
parent 21b845d16e
commit 222cf903f6
33 changed files with 4399 additions and 75 deletions
+64
View File
@@ -0,0 +1,64 @@
# Testing Twungeon
## Automated gates
From a clean checkout:
```bash
npm install
npm run lint
npm run typecheck
npm test
npm run test:integration
npm run build
```
`npm test` covers deterministic domain rules and 500 generated floors.
`npm run test:integration` covers the HTTP/session boundary and independent
viewer authority. Tests use injected clocks, IDs, random rolls, and synthetic
Twitch identities; they do not require credentials.
## Local gameplay verification
1. Run `npm run dev` and open `http://localhost:3000`.
2. Confirm the exact dormant banner appears and no timer runs.
3. Use **Local viewer login** to spawn and bind a viewer.
4. Confirm 3 HP, 2 AP, no Guard, a ready heal, and a 25-second phase.
5. Exercise movement, invalid walls, Attack, Heal, and Pass. Invalid actions
must not consume AP.
6. Open a second browser/private window with a different ID. Confirm both
characters share the dungeon but each controller spends only its own AP.
7. Refresh during a phase. Confirm a snapshot restores the map/log and no
command replays.
8. Run `npm run build && npm start` and repeat the smoke test against the built
server.
The local-only endpoints `/api/dev/spawn` and `/api/dev/redemption` exist only while
`TWITCH_ENABLED` is false.
## Live-channel campaign
After completing `docs/twitch-setup.md`, use two follower accounts and one
non-follower account. Capture redacted evidence for:
- eligible, ineligible, and duplicate `!spawn` attempts;
- matching and mismatched Extension identities;
- independent commands from two viewers;
- phase expiry and AutoGuard while one viewer is disconnected;
- death and a configured Channel Points reward redemption from the same user;
- escape with the Goblin alive, floor revival, and a total-party wipe;
- refresh/reconnect during a phase and transition; and
- repeated floors and a short soak session.
Do not record tokens, JWTs, secrets, or complete authorization headers. Use the
acceptance matrix in `Twungeon_MVP_Acceptance_Test_and_Build_Checklist.md` for
AT-001 through AT-033 evidence. Automated passing tests do not substitute for
the clean-operator, live Twitch, first-time-viewer, or concept-validation gates.
## Fault injection
Test each failure separately: expired Extension JWT, wrong channel claim,
unlinked identity, invalid broadcaster token, missing follower scope, Twitch
chat disconnect, wrong/duplicate Channel Points redemption, WebSocket interruption, and
stale run/floor/phase command. Expected behavior is a stable rejection or a
fresh snapshot without partial game-state mutation.
+109
View File
@@ -0,0 +1,109 @@
# Twitch setup
This guide connects the Twungeon proof of concept to a Twitch development
channel. Never commit real values: copy `.env.example` to an ignored `.env` or
provide the variables through your process manager.
## Prerequisites
- Node.js 22 or newer and npm.
- A Twitch developer application and a development channel.
- A Twitch Extension with identity sharing enabled. Twungeon requires the
numeric `user_id`; anonymous or opaque-only viewers fail closed.
- A broadcaster user access token that can read chat and check followers.
- The Extension shared secret, copied exactly as the base64 value supplied by
the Extension Manager.
The installed Twurple packages are locked in `package-lock.json`. Re-check
their release documentation and Twitch's current scope requirements before
rotating tokens or upgrading packages.
## Required environment
Set `TWITCH_ENABLED=true`, then provide:
| Variable | Purpose |
| --- | --- |
| `TWITCH_CLIENT_ID` | Developer application/Extension client ID |
| `TWITCH_CLIENT_SECRET` | Server-only application secret |
| `TWITCH_BROADCASTER_ID` | Numeric channel owner ID |
| `TWITCH_CHANNEL_LOGIN` | Channel login joined by Twurple chat |
| `TWITCH_BOT_ACCESS_TOKEN` | Broadcaster user access token |
| `TWITCH_EXTENSION_SECRET` | Base64 Extension shared secret |
| `CHANNEL_POINTS_RESURRECTION_REWARD_ID` | Stable ID of the resurrection custom reward |
| `PUBLIC_BASE_URL` | Public HTTPS backend origin |
The user token currently needs `chat:read`, `moderator:read:followers`, and
`channel:read:redemptions`. The follower and Channel Points subscriptions use
the broadcaster token, so its subject must match `TWITCH_BROADCASTER_ID`.
Twungeon subscribes only to the configured custom reward and uses Twitch's
stable redemption ID as the deduplication key. The broadcaster owns the reward
cost in Twitch; the backend does not duplicate it.
## Extension configuration
1. Host the built static files and backend at an HTTPS origin allowed by the
Extension configuration. Twitch embeds the UI in an iframe and supplies the
Extension Helper JWT through `onAuthorized`.
2. Point the viewer/mobile video component to the application root.
3. Enable identity sharing. A JWT without `user_id`, with the wrong
`channel_id`, an expired signature, or an `external` role is rejected.
4. Keep the Extension secret only in the backend environment. It must never be
included in the UI bundle or URL.
5. Start with `npm run build && npm start`. Confirm `/health` returns `ready:
true` and `twitchMode: "configured"` after chat connects.
The Extension JWT is exchanged for a random 15-minute Twungeon session. A
refresh obtains a new Twitch JWT, re-verifies it, and restores the existing
character; it does not create a player.
## Channel Points reward
1. In the broadcaster dashboard, create one custom reward for resurrection.
2. Choose its title and cost in Twitch. Disable viewer text input unless the
channel has a separate moderation reason to keep it.
3. Enable **Skip Reward Requests Queue** because Twungeon consumes the
redemption immediately and uses read-only EventSub access rather than
managing fulfillment status.
4. Retrieve the reward's stable ID through the Twitch API or Twitch CLI using
the broadcaster token, and set it as
`CHANNEL_POINTS_RESURRECTION_REWARD_ID`.
5. Do not reuse that reward for another action. Redemptions of every other
reward are ignored by the domain.
Bits, Cheers, and Bits-in-Extensions products are outside the MVP and have no
gameplay handler.
## Event flow
1. A follower types `!spawn` in chat.
2. Twurple supplies the stable chatter ID; the API checks that ID against the
broadcaster's followers.
3. The Extension sends its current Twitch JWT to
`POST /api/extension/session`.
4. The backend verifies signature, expiry, channel, role, and numeric user ID.
5. Controls enable only when the verified ID equals the chat-spawn owner ID.
6. A redemption of the configured Channel Points reward by the dead player
invokes resurrection exactly once.
## Troubleshooting and rotation
- **Chat disconnected:** check token validity, `chat:read`, channel login, and
outbound WebSocket access. Existing game state continues; new Twitch inputs
fail closed until reconnect.
- **Follower rejected:** ensure the token belongs to the broadcaster and has
`moderator:read:followers`; confirm the numeric broadcaster ID.
- **Extension returns `UNAUTHENTICATED`:** check the base64 secret, JWT expiry,
allowed channel, and identity-sharing capability. Never log the JWT.
- **`IDENTITY_NOT_BOUND`:** compare the chat and verified Extension numeric IDs;
display names are deliberately ignored for ownership.
- **Reward does not resurrect:** confirm the configured reward ID, the
`channel:read:redemptions` scope, the dead character owner ID, and that the
redemption was not already processed.
- **Public iframe fails:** confirm TLS, allowed origins, Extension asset paths,
and that WebSocket traffic reaches `/ws`.
To rotate credentials, stop new Twitch intake, replace the server environment,
restart, verify `/health`, test one follower check and Extension login, then
revoke the old token/secret. A full process restart creates a new in-memory run,
as required by the MVP persistence boundary.