Phases 3 & 4: ops hygiene + first test suite - #7
Merged
Conversation
Add lib/rate-limit.ts — per-IP sliding-window limiting backed by the same Upstash Redis the cache uses (env resolution mirrors kv-cache.ts, incl. the KV_REST_API_* names the Vercel Marketplace injects). No-ops when Redis is unconfigured (local dev) and fails open on any Redis error, so it can never take the site down. checkRateLimit() returns a 429 NextResponse (with Retry-After) or null. Wired into all five routes after body/param parsing, tuned per cost via RATE_LIMITS in constants.ts: analyze/raid-overview 30/60s, cla 10/60s (biggest fan-out), report + report-players 60/60s. Hits logged via logEvent for Vercel visibility. The existing SWR hooks already surface `data.error`, so the 429 message displays without frontend changes. Also drops the hardening plan in the repo root for tracking. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZFK9cogYksgHxYeAF9ReJ
Add shared isValidReportCode/badRequest helpers to api-utils. On the three POST routes, validate before building cache keys or querying WCL: - analyze/raid-overview: reportCode matches the code regex and fightId/sourceId are integers (Number.isInteger keeps 0 valid). - cla: guard that fightIds is a non-empty array of integers (a non-array body previously threw on .length → 500), dedupe, and reject > MAX_CLA_FIGHTS (15) so one request can't fan out unbounded WCL calls. The handler now queries the deduped/capped set. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZFK9cogYksgHxYeAF9ReJ
Add cacheLock/cacheUnlock (SET NX EX 20 / DEL) to kv-cache, no-op without Redis. In cachedApiHandler, on a cache miss with Redis configured: the lock holder computes + caches while everyone else polls the cache (~500ms, up to 15s) and returns it as cache:"wait_hit". Waiters that time out fall through and compute themselves so no one dead-ends; the lock releases in finally with the EX TTL as a crash backstop. Without Redis, behavior is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZFK9cogYksgHxYeAF9ReJ
- Passive link replies: per-channel 30s cooldown, and only reply when the link has a fight (bare report links no longer trigger the bot). Reply wrapped in try/catch so a permissions error logs instead of crashing the process (an unhandled rejection terminates Node 20). - Slash commands: per-user 10s cooldown with an ephemeral throttle notice; the whole dispatch is wrapped so a handler rejection can't crash the bot. - api.ts: stop echoing raw upstream response bodies into Discord — throw a typed ApiError(status, cleanMessage) that surfaces only the API's `error` field, and add describeApiError() with a friendly 429 message. Both commands use it. - Fix stale activity string getlootlist.com -> parseforge.gg. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZFK9cogYksgHxYeAF9ReJ
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZFK9cogYksgHxYeAF9ReJ
Add a headers() block: X-Content-Type-Options, Referrer-Policy, and Permissions-Policy on every route; X-Frame-Options: SAMEORIGIN and a Content-Security-Policy-Report-Only on everything EXCEPT /og (link unfurlers fetch the OG image). CSP ships report-only so violations log to the console without blocking, to be promoted to enforcing manually after checking real traffic. Verified: / carries all headers incl. CSP; /og carries only the benign ones (frame-blocking correctly excluded via negative-lookahead source). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZFK9cogYksgHxYeAF9ReJ
Validate report against the code regex (invalid/absent → branded fallback, never fetch); only take the player-scorecard path when fight+source parse to non-negative integers; pin the self-fetch origin to https://parseforge.gg in production (request origin only in dev) so a spoofed Host can't steer it. Keeps the never-fail-an-unfurl catch-all. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZFK9cogYksgHxYeAF9ReJ
buildCLABuffUptimeQuery interpolates sourceIds into the query string. They come from WCL's own actor list (safe today), but coerce each via Number() and drop non-integers/negatives as insurance against a future caller passing unvalidated data into the only string-built query. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZFK9cogYksgHxYeAF9ReJ
maskAllInputs: true (drops the custom un-masking of the report-URL input) and enable_recording_console_log: false so replays can't hoover up client-side console output or typed input. TODO left re: EU consent banner (product call). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZFK9cogYksgHxYeAF9ReJ
getAccessToken now checks module scope, then Redis (wcl:token), then mints from WCL and writes back to both — so a cold serverless instance reuses a token another instance already minted instead of spending a fresh token request (every mint counts against the same client). TTL is set just short of expiry. On a 401, clearAccessToken() drops both the module and Redis copies before retrying. Adds cacheDelete() to kv-cache. No Redis → module-only, as before. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZFK9cogYksgHxYeAF9ReJ
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZFK9cogYksgHxYeAF9ReJ
The bot's parse-url.ts was a thinner, drifting copy — unbounded code length and no query-param / embedded-text handling. Port the web app's more robust logic into it verbatim so behavior matches, and add cross-reference comments in both files noting the intentional duplication (separate packages). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZFK9cogYksgHxYeAF9ReJ
Set NODE_ENV=production and drop to the image's unprivileged `node` user before CMD. Comment notes the runtime must set a restart policy since the bot exits on fatal startup errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZFK9cogYksgHxYeAF9ReJ
Two jobs on push-to-main and PR: web (npm ci, tsc --noEmit, test, build; lint non-blocking given pre-existing lint debt) and bot (npm ci + build). No deploy steps — Vercel handles deploys. Build needs no secrets (prerender degrades gracefully without WCL/Redis env). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZFK9cogYksgHxYeAF9ReJ
Add vitest + a `test` script + config that resolves the @/* alias. 24 tests: - url-parser: every documented format (bare code, hash/query params, fight=last, embedded-in-text, scheme-less, junk) + buildWCLUrl. - async-pool: order preservation, index passing, concurrency cap, empty input. - api-utils: isValidReportCode, and parseBody incl. the "0 is a valid fight/source id" case and invalid-JSON/missing-field 400s. - analysis-engine (started): analyzeDps — empty rankings, mid-field placement, and the 99 percentile cap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZFK9cogYksgHxYeAF9ReJ
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZFK9cogYksgHxYeAF9ReJ
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements Phase 3 (ops hygiene) and Phase 4 (tests) of
PRODUCTION_HARDENING.md. Done together because CI (3.1) runs the test script that Phase 4 adds.Phase 3 — Ops hygiene
.github/workflows/ci.yml): web job (npm ci,tsc --noEmit,npm test,build; lint non-blocking because of the repo's pre-existing lint errors) + bot job (npm ci, build). No deploy steps. Build needs no secrets — prerender degrades gracefully without WCL/Redis env.NODE_ENV=production, drop to the unprivilegednodeuser, comment on the required restart policy.parseWCLUrl(bounded code length, query-param + embedded-text handling) into the bot's drifting copy so behavior matches; cross-reference comments in both.Phase 4 — First test suite (vitest) — 24 tests, all green
fight=last, embedded-in-text, scheme-less, junk) +buildWCLUrl.isValidReportCode+parseBodyincl. the "0 is a valid fight/source id" case, missing-field, and invalid-JSON 400s.analyzeDps— empty rankings, mid-field placement, 99-percentile cap.Verification
npm test→ 24 passed ✅ ·npx tsc --noEmit✅ ·npm run build✅ · bot build ✅npm run lintunchanged at 16 pre-existing findings (0 new)Follow-ups (noted, not in scope)
🤖 Generated with Claude Code