fix(playtest): require the secrets, and stop reflecting every CORS origin #244
No reviewers
Labels
No labels
bug
dependencies
documentation
duplicate
enhancement
github-actions
good first issue
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Evobug/api!244
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/playtest-env-hardening"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Follow-up to #242 and the provider-credentials change, from deploying the portal to production.
CORS
CORSPlugin's default isorigin: (origin) => origin— it reflects whatever origin asked. The portal is a SPA on a different origin, so the oRPC handler is reachable from browsers, andplaytest.*reveals Steam keys and NDA name/address. Now an allowlist:CORS_ALLOWED_ORIGINS, defaulting toPLAYTEST_PORTAL_URL. Server-side callers (bot, allcom.zone SSR) send noOriginand are unaffected.Secrets have no defaults any more
JWT_SECRETfell back to"allcom-zone-secret-key-change-in-production"in six places across four contracts. That literal is in this repository, so a deployment that forgot the variable would verify tokens anyone could mint — and since playtesters reuse the Evobug user JWT, that now reaches another user's NDA record.ADMIN_SYNC_KEYhad the same shape (|| "change-me-in-production").Both are required and resolved through the new
src/config.ts.validateEnvironment()runs before anything connects or listens and reports every problem at once, including a wrong-lengthPLAYTEST_ENCRYPTION_KEY— which was read lazily, so a container missing it started, reported healthy, served pages, and only failed later at admin login.portalEnvand empty strings??only falls back onundefined. docker-compose defines every variable it lists, so- PLAYTEST_MICKLE_CLIENT_ID=${PLAYTEST_MICKLE_CLIENT_ID}with nothing in.envarrives as""and would win over a working shared credential — the provider 503s and the fallback never fires. Switched to||, with a test.Key rotation
PLAYTEST_ENCRYPTION_KEY_PREVIOUSaccepts retired keys for decryption only, so rotation becomes deploy-then-backfill instead of an outage. It deliberately does not cover the keyedfingerprintcolumns (Steam key dedupe, recovery codes), which are equality indexes and must be recomputed by the re-encryption job — documented in the code and.env.example.Verification
706 pass / 0 fail (28 files),
tsgoclean, oxlint 0 errors and no new warnings on the touched files.Deployment note
The API will refuse to start without
DATABASE_URL,JWT_SECRET,PLAYTEST_ENCRYPTION_KEYandADMIN_SYNC_KEY. All four are already set on the production host, andADMIN_SYNC_KEYis shared with the bot (both containers restarted with a matching value), so this is safe to deploy as-is.🤖 Generated with Claude Code
Four hardening changes the portal work surfaced. CORS: CORSPlugin's default is `origin: (origin) => origin`, which reflects whatever asked. The portal is a SPA on another origin, so the handler is browser-reachable, and playtest.* reveals Steam keys and NDA identity data - any page on the web could drive it in a logged-in visitor's browser. Allowlist via CORS_ALLOWED_ORIGINS, defaulting to PLAYTEST_PORTAL_URL. Server-side callers send no Origin and are unaffected. Secrets: JWT_SECRET defaulted to a literal that ships in this repository, in six places across four contracts, so a deployment that forgot it verified tokens anyone could mint - now that portal identity rides the same token, that reaches another user's NDA record. ADMIN_SYNC_KEY had the same shape. Both are required, resolved through src/config.ts, and validated at boot alongside PLAYTEST_ENCRYPTION_KEY - which was read lazily, so a container missing it started, reported healthy, and failed later at admin login. portalEnv used `??`, which only falls back on undefined. docker-compose defines every variable it lists, so `- PLAYTEST_MICKLE_CLIENT_ID=${...}` with nothing in .env arrives as "" and would have shadowed a working shared credential, 503-ing the provider with the fallback never firing. PLAYTEST_ENCRYPTION_KEY_PREVIOUS accepts retired keys for decryption, turning rotation into deploy-then-backfill instead of an outage. It does not cover the keyed fingerprint columns, which the re-encryption job has to recompute. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>