Implement Channel Points Twungeon MVP
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user