Repository navigation
Fix environment separation - #208
Conversation
karthikeya1976
left a comment
There was a problem hiding this comment.
Reviewed the full diff and ran every affected suite for real (backend 264/264, chessServer 33/33, stockfishServer 12/12, frontend 156/156, TypeScript clean) against a local worktree with the real gitignored config alongside the new files — not just re-reading the PR's own claimed numbers.
Real, well-reasoned fixes confirmed:
- The
environments/environment→environmentsimport-path fix (~30 files) is a genuine, previously-silent bug: importingenvironment.jsdirectly bypassesenvironment.prod.jsentirely, so every one of these components was using hardcoded dev URLs in any real production build. Confirmedsrc/core/environments/(the old CI step's write target) doesn't exist anywhere in the tree — that secrets-injection step really was a no-op. db.js: refusing to fall back to in-memory MongoDB in production, and skipping test-user seeding in production, are both real safety fixes — the old behavior meant a prod DB outage could've silently switched the whole app to an ephemeral, fake-seeded database with no visible signal beyond a log line.- CORS wildcard+
credentials:truefix in chessServer/stockfishServer is correct, and the new tests actually exercise the previously-broken case (disallowed origin now returns 200 instead of an unhandled 500 with a leaked stack trace). - Checked whether dropping
credentialsfrom middlewareNode'scors()call breaks anything: it doesn't. The frontend never sendscredentials: "include"anywhere (seebadgesApi.ts's own comment on why) — auth is carried via aBearerheader end to end, so no cross-origin cookie flow depends on it. - Verified the
config/default.js+config/default.jsoncoexistence empirically (own test harness) rather than assuming:node-configloads both anddefault.json(the real, gitignored secrets file) wins on any overlapping key, so no risk ofdefault.js's placeholder empty strings clobbering real production secrets.
One thing worth flagging before merge, not necessarily blocking:
deploy/prod/docker-compose.yml's env var names changed substantially (mongoURI→MONGO_URI, indexKey→INDEX_KEY, jwtSecret dropped, auth/channel dropped, several renamed). Combined with the new validateEnvironment.js calling process.exit(1) on any missing required var, this means the actual production server's env/secrets file needs a coordinated manual update at deploy time, or the middleware container will refuse to boot after this ships. I didn't see that called out explicitly in the PR description or a TODO — worth a one-line deployment note (or confirmation it's already been coordinated) so whoever deploys this isn't surprised by a crash-looping container.
Everything else — CI workflow secrets fix, .env.example files, validateEnvironment additions — is accurate and matches what's actually wired up in the code (spot-checked CHAT_RATE_LIMIT against routes/chat.js as one example).
Nice find on the dead CI step and the prod-fallback DB issue — both are the kind of bug that's invisible until it actually happens in production.
karthikeya1976
left a comment
There was a problem hiding this comment.
Approving the backend/infra portions I own as codeowner (middlewareNode, chessServer, stockfishServer, deploy) — already reviewed in detail in my prior comment, all 5 test suites verified passing in an isolated worktree, CORS/config-merge/prod-fallback claims verified empirically rather than assumed. One non-blocking note left in that comment: confirm the production env/secrets file is updated in lockstep with the docker-compose.yml var renames before deploying, since validateEnvironment.js will now exit(1) on any missing required var.
This PR also touches react-ystemandchess (the environments import-path fix across ~30 components, and the Dockerfile), which is Sarita's codeowner scope — that portion still needs her review/approval before this can merge.
|
Two concrete blockers from my review: The indexKey: "" default breaks login in any dev/test environment without INDEX_KEY set (verified: jwt.sign throws on the empty secret, auth.js swallows it into a generic 500). That's a real, reproducible bug, not a style nit. |
Saritahimthani
left a comment
There was a problem hiding this comment.
Given INDEX_KEY is empty by default in middlewareNode/config/default.js, logging in locally throws (jwt.sign rejects an empty secret) whenever INDEX_KEY isn't set in the environment — verified with tests/routeSecurity.test.js, which currently fails with a 500 instead of 200 on POST /auth/login.
Can you apply this fix?
diff --git a/middlewareNode/config/default.js b/middlewareNode/config/default.js
index 5864fdf8..7e3826e9 100644
--- a/middlewareNode/config/default.js
+++ b/middlewareNode/config/default.js
@@ -1,7 +1,11 @@
module.exports = {
mongoURI: "",
jwtSecret: "",
- indexKey: "",
-
// Non-empty so jwt.sign() (used for login) doesn't throw "secretOrPrivateKey
-
// must have a value" in dev/test when INDEX_KEY isn't set. Every real
-
// deployment overrides this via INDEX_KEY — validateEnvironment.js
-
// requires it in production.
-
indexKey: "dev-index-key-replace-in-prod",
corsOptions: {
origin: "http://localhost:3000",
-> Same placeholder value deploy/dev/docker-compose.yml already uses, so it's consistent with the rest of the repo. I verified this locally — with the change applied, the full middlewareNode suite goes from 1 failing to 264/264 passing. Once this is in, I'm good to approve.
ef7c765 to
59cadc8
Compare
indexKey: "" caused jwt.sign()/jwt.verify() to throw whenever INDEX_KEY wasn't set, breaking login, passport JWT auth, token verification, and password-reset token generation in dev/test. Uses a random key generated once per process boot instead of a fixed fallback literal, so no signing key is ever checked into git history. custom-environment-variables.json already overrides this from INDEX_KEY when set; validateEnvironment.js still requires INDEX_KEY in production.
|
Reworked the indexKey fix based on internal review — went with a random per-boot key instead of a hardcoded fallback literal. Why the change: the originally proposed indexKey: "dev-index-key-replace-in-prod" is a fixed string that would live in git history permanently — the same category of issue as #215's "development-jwt-secret-key" fallback (checked into both default.js and auth.js there). Any deployment that forgets to set INDEX_KEY would silently sign JWTs with a secret visible to anyone with repo access. What it does instead: config/default.js now generates a random 32-byte key via crypto.randomBytes(32) once per process boot, used only as the last-resort fallback when INDEX_KEY isn't set anywhere else. custom-environment-variables.json already maps INDEX_KEY → indexKey, so real deployments (or a local config/default.json) override this automatically — no behavior change there. validateEnvironment.js still hard-requires INDEX_KEY in production, so this fallback is dev/test-only by construction. When it's in use, a console.warn logs that fact (not the key value) so it's visible in dev/test output. Scope note: config.get("indexKey") is read in five places, not just login — auth.js (sign), passport.js (JWT strategy verify), middleware.js, users.js (verify), and changePasswordTemplate.js (password-reset token sign). All five were equally broken by the empty-string default, so this fixes password-reset and passport-protected routes locally too, not just POST /auth/login. Tradeoffs worth knowing:
Verified: npm test → 264/264 passing, including routeSecurity.test.js's login case going from failing to passing. |
Mirrors chessServer/src/tests/cors.test.js's three cases (allowed origin, disallowed origin rejected without a 500, wildcard disables credentials) against stockfishServer's identical corsOptions logic, which had no dedicated coverage. Adds supertest as a devDependency since it wasn't installed anywhere in this service.
The component's import moved to "../../../../environments" (the fixed path every other file in this PR moved to), but this test's jest.mock still targeted the old "../../../../environments/environment" path. The mock was silently not intercepting anything — the test passed by coincidence against the real dev environment module, not the mocked one. Fetch calls in LeaderboardModal.tsx build URLs from environment.urls.middlewareURL, so this now genuinely exercises the mock.
… rewrite chessClientURL existed on main but was silently dropped in an earlier commit (228ee3f) that was actually about renaming chessServer -> chessServerURL. Traced it back to a prior analysis that claimed the key was unused; that claim was already false when it was written -- PlayStudent.tsx has read environment.urls.chessClientURL since three weeks before that analysis ran, via an `as any` cast that likely hid it from type-aware reference tooling. Restored as optional (falls back to '' if REACT_APP_CHESS_CLIENT_URL is unset), not required like the other three URLs -- PlayStudent.tsx already degrades gracefully on a missing value, and making this one required would turn a single broken button into an app-wide crash for every visitor. Wired REACT_APP_CHESS_CLIENT_URL through the Dockerfile, tag_build_containers.sh, and ci.yml (using the existing CHESS_CLIENT_URL secret) to match the other three URLs' plumbing. Verified empirically: a real production build succeeds with the var unset (no crash), and the value is correctly inlined into the bundle when it is set. Frontend suite still 156/156, TypeScript clean.
|
Independently verified Sweksha's random-per-boot `indexKey` fix (commit on `config/default.js`, currently HEAD `a2fcbec4`) against Sarita's originally-reported bug:
So the random-key approach does fully resolve the reported bug, not just coincidentally pass the suite. Also confirmed #200 is now closed — that resolves the duplicate-PR concern from your first comment; #208 is the sole surviving PR for this work. The one open item from Sarita's Aug 28 comment is whether the random-per-boot approach (vs. the hardcoded placeholder originally proposed) is acceptable given the tradeoffs Sweksha wrote up (session invalidation on every `nodemon` restart, password-reset links breaking across a restart, and the staging/qa environments only getting a `console.warn` instead of a hard failure). Those are real dev-workflow costs worth a explicit yes/no rather than assuming silence means agreement — flagging so this doesn't sit waiting on an unstated question. CI is green on the current HEAD. |
|
Thanks for the independent verification — good to have the duplicate-PR question closed out too. Also just pushed a merge commit (8280fc5) resolving a conflict with main (#207's stockfish crash-permission fix landed and touched the same area of stockfishServer/src/index.js) — kept both changes, no logic lost, 16/16 stockfishServer tests passing. Heads up that this dismissed your approval again, so it'll need a re-look. On the indexKey open item: since Sarita's out for a week and the tradeoff question is really about whether the random-per-boot approach is acceptable engineering-wise (not something that needs her specifically — that file's in your codeowner scope, not hers), can I get your explicit yes/no on it directly? You've already reviewed both versions in depth, so you're in a good spot to make that call without waiting on her. To recap the tradeoffs for a quick decision:
If it's a no, let me know what you'd want instead. |
karthikeya1976
left a comment
There was a problem hiding this comment.
Review summary
Solid overall direction — separating dev/prod CORS config, adding env validation with fail-fast, and adding CORS regression tests is the right call. A few things I'd like addressed before merge, plus some lower-priority follow-ups.
Please fix before merge
-
CI never runs
middlewareNode's new tests (.github/workflows/ci.yml)
middlewareNode/tests/cors.test.jsis added andnpm testis wired up correctly in itspackage.json, but the CI workflow only has Install/Build steps formiddlewareNode— no Test step (unlikechessServer/stockfishServer, which both have one). The new CORS test never actually runs, so it can't catch regressions in the logic it exists to protect. -
middlewareNode's CORS rewrite silently dropscredentials(middlewareNode/src/server.js)
The old code forwarded the wholecorsOptionsconfig object (includingcredentials: true) intocors(). The new inline callback only reads.origin, socredentialsnow defaults tofalseandAccess-Control-Allow-Credentialsis never sent — unlikechessServer/stockfishServer's equivalent rewrite, which explicitly keeps acredentials: !hasWildcardinterlock. Currently latent (no frontend call usescredentials: "include"against middleware today), but it's a silent capability regression with nothing testing for it. -
Production Mongo failure now hard-exits using a timeout tuned for the old fallback path (middlewareNode/src/config/db.js)
process.exit(1)on connection failure is right for prod, but it still usesserverSelectionTimeoutMS: 1000(1s), which was tuned for "fail fast to a harmless in-memory fallback," not "fail fast and kill the process." A brief network blip or Atlas cold-start during a deploy could now crash-loop the container. Worth bumping the timeout (or adding a couple of retries) for the production path specifically. -
deploy/prod/tag_build_containers.shnever version-tags the middleware image
chessserver/stockfishserver/ystemandchessall build as:${TAG};middlewarenodealways builds as bare:latest. A versioned release/rollback (TAG=v1.2.0) silently leaves middleware on whateverlatestcurrently is, breaking rollback consistency across the four services.
Worth a follow-up (not blocking)
- CORS rejection changed from a thrown
Error(→ 500 + logged error) tocallback(null, false)(→ silent 200, no CORS header, no log line) across all three servers. Looks intentional per the new tests, but it's a real observability regression for diagnosing a misconfiguredCORS_ORIGINin production — worth at least a log line on rejection. - CORS + env-validation logic is now implemented three separate times (
chessServer,stockfishServer,middlewareNode) with no shared module, and it's already drifted (see #2 above, andmiddlewareNode'svalidateEnvironmentrequiresCORS_ORIGINunconditionally while the other two acceptALLOWED_ORIGINSas a fallback). Might be worth extracting into a shared helper at some point so this doesn't keep drifting. chessServer/stockfishServerrequire("cors")without declaring it in their ownpackage.json(pre-existing, but the new test files deepen the reliance on it resolving via hoisting).- No validation that
CORS_ORIGINentries are well-formed (e.g. a trailing slash would silently never match a browser'sOriginheader). tag_build_containers.shusesset -ebut notset -u; an unsetREACT_APP_*var passes through as an empty string todocker buildand fails confusingly deep inside the CRA build instead of failing fast at the shell level.
Nice addition of test coverage for CORS behavior — just want CI actually running it and the credentials gap closed before this goes out.
45bc67c to
8280fc5
Compare
The workflow had Install/Build steps for middlewareNode but no Test step, unlike chessServer/stockfishServer, so its test suite (including the new cors.test.js) never actually ran in CI. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013xHcrGS7T6Kp4vDif3mYc8
chessserver/stockfishserver/ystemandchess all build as :${TAG}, but
middlewarenode always built as bare :latest, so a versioned release
(TAG=v1.2.0) silently left it on whatever :latest happened to be,
breaking rollback consistency across the four services.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xHcrGS7T6Kp4vDif3mYc8
Production now exits the process on a failed connection instead of falling back to in-memory MongoDB, but it still used the 1s timeout that was tuned for "fail fast to a harmless fallback" rather than "fail fast and kill the process." A brief network blip or an Atlas cold-start during deploy could crash-loop the container. Dev/test keep the fast 1s timeout since they still fall back to in-memory MongoDB. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013xHcrGS7T6Kp4vDif3mYc8
The CORS rewrite's inline origin callback only read .origin from config, silently dropping the credentials: true that config/default.json sets for real deployments (the old code forwarded the whole corsOptions object into cors(), so credentials worked there). Mirrors the credentials: !hasWildcard interlock already used in chessServer and stockfishServer: allow credentials only when a specific origin (not "*") is configured, since wildcard + credentials is an unsafe CORS configuration. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013xHcrGS7T6Kp4vDif3mYc8
The CORS rewrite changed rejections from a thrown Error (500 + logged error) to callback(null, false) (silent 200, no CORS header, no log line). That's the correct HTTP behavior, but it's an observability regression: a misconfigured CORS_ORIGIN in production now fails silently with nothing in server logs to point at the cause. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013xHcrGS7T6Kp4vDif3mYc8
Both services only had cors available because it's a transitive
dependency of socket.io, hoisted to the top of node_modules by
chance. If socket.io's own dependency tree ever changes, require("cors")
could break with no change to either service's own package.json.
Pin the same version middlewareNode already declares (^2.8.5).
set -e alone didn't catch an unset REACT_APP_* var — it would pass through as an empty string to docker build and fail confusingly deep inside the CRA build instead of failing fast at the shell level.
A configured origin like https://ystemandchess.com/ (with a trailing slash) would never match a browser's Origin header, which never includes one — silently breaking CORS for that origin with no indication why. Strip the trailing slash and warn when it happens.
Generate the fallback key once and persist it to a gitignored local file (config/.dev-index-key) instead of regenerating it every process boot. Keeps production unchanged (validateEnvironment.js still hard-requires INDEX_KEY there, and runs before this file is ever loaded) and avoids committing a key, while no longer invalidating dev JWTs and password-reset links on every nodemon restart.
|
Pushed fixes for everything from the 9/4 review, plus the indexKey follow-up. Summary of everything since 8280fc5: Blocking items addressed:
Non-blocking follow-ups, also addressed:
On indexKey — went with Sahana's call (persist-to-gitignored-file): 81992ea now generates the dev/test fallback key once and persists it to config/.dev-index-key instead of regenerating every boot. Production's unaffected — validateEnvironment.js still hard-requires INDEX_KEY there and runs before this file is ever loaded. I'm leaving the shared CORS/env-validation module extraction as a separate ticket rather than another commit here — it's a real refactor that also needs a Docker build-context decision, not a drop-in fix. All three test suites passing after every change (middlewareNode 264/264, chessServer 33/33, stockfishServer 16/16). Ready for re-review whenever you get a chance. |
karthikeya1976
left a comment
There was a problem hiding this comment.
Re-reviewed after the Sept 4 changes-requested pass — all four blocking items are genuinely fixed, verified against the actual branch content (not just the commit messages):
- CI runs middlewareNode's tests — confirmed a
Test middlewareNodestep now exists in.github/workflows/ci.yml, and the latest CI run on this branch passes. credentialsrestored in middlewareNode's CORS —server.jsnow setscredentials: !hasWildcard, matching the interlock already used in chessServer/stockfishServer.- Production Mongo timeout bumped —
db.jsnow usesserverSelectionTimeoutMS: IS_PRODUCTION ? 10000 : 1000, so a brief network blip or Atlas cold-start won't crash-loop the container; dev/test keep the fast 1s timeout to their in-memory fallback. - middlewareNode image is version-tagged —
tag_build_containers.shnow buildsmiddlewarenode:${TAG}instead of bare:latest.
Two of the non-blocking follow-ups were also addressed: CORS rejections are now logged (console.warn on reject), and a trailing slash on a configured origin is normalized with a warning instead of silently never matching.
Re-ran all three backend suites myself in an isolated worktree against the current branch head (d871e3ff), not just re-reading claimed numbers: middlewareNode 264/264, chessServer 33/33, stockfishServer 15/15 (1 pre-existing skip) — matches the PR's own reported counts exactly.
Remaining open items are genuinely non-blocking (shared CORS/env-validation module across the three servers, set -u in the build script) and are already tracked as follow-up work in the PR description. Approving the backend/infra portions I own as codeowner.
🤖 Generated with Claude Code
Brings in #208 (environment separation), #247 (Tailwind standardization) and the other changes merged since the branch was cut on 2026-08-23. Conflicts in Lessons.tsx, ActivitiesModal.tsx and LeaderboardModal.tsx were import-only: main's side is kept (SCSS modules dropped by #247, environment imported from ../environments), with SVG components switched from CRA's `{ ReactComponent as X }` to Vite's `X from './x.svg?react'`.
environment.prod.js (from #208) read vars via process.env[name]. Vite's define is a literal text replacement and cannot resolve a dynamic key. Vite 8 rewrites the leftover bare `process.env` to `{}`, so the lookup compiled to `{}[name]`, which is always undefined. The required-URL guard then threw on load and blanked the whole site. Jest runs in Node, so the test suite could not catch it. - environment.prod.js: reference every variable by its full literal name - vite.config.mts: define all six REACT_APP_* vars the app reads, loaded via loadEnv so .env files work as they did under CRA; NODE_ENV derives from the Vite mode - scripts/verify-build-env.mjs + CI: fail if process.env survives in shipped JS/HTML, or if a required URL was not inlined into the bundle (the second check is what actually catches the {}[name] case; verified it fails against main's old environment.prod.js and passes on this one) - Dockerfile and volta pin: Node 20.19.0 (EOL) -> 24.21.0 LTS
The plan covers what changed after #208 merged (the env var blank-page bug, merge conflicts, Node 24, password-reset URL), the prerequisite branches that gate the merge, and coordination with the CD pipeline. The original migration write-up stays as the reference for the migration itself.
Summary
Separates dev/prod environment configuration across the frontend and all backend services, and adds startup validation so a misconfigured deployment fails loudly instead of silently. While working through CORS-related config in each service, also found and fixed an unsafe wildcard+credentials combination and an unhandled-500 bug on rejected CORS origins.
Type of Change
Key Changes
config/default.jsand.env.example, replaces the oldcustom-environment-variables.jsonapproach, addsvalidateEnvironment.jsto check required vars at startup, updatesdb.jsandserver.jsaccordingly.env.exampleandvalidateEnvironment.jsfor the same startup validationenvironment.prod.jsnow supportsREACT_APP_*build-time overrides (REACT_APP_MIDDLEWARE_URL,REACT_APP_STOCKFISH_SERVER_URL,REACT_APP_CHESS_SERVER_URL,REACT_APP_AGORA_APP_ID), falling back to existing placeholders when unset; fixes several components importing from the oldenvironments/environmentpath instead ofenvironments.github/workflows/ci.yml): build step now feeds theseREACT_APP_*vars into the production frontend build (previously written to a path nothing in the app imports, so they never reached the built app); adds aworkflow_dispatchtrigger for manual runsTesting
Test counts (updated after review commits): React 156/156, chessServer 33/33, stockfishServer 15/15 (was 12/12; +3 CORS tests added during review), middlewareNode 264/264 — no regressions.
Bugs Fixed (if applicable)
credentials: truewas unconditional, even whenallowedOriginsincludes"*"— an unsafe wildcard+credentials combination. Now computeshasWildcardand setscredentials: !hasWildcard.callback(new Error(...))in the CORS origin function produced an unhandled 500 (leaking a stack trace outside production) on any disallowed-origin request, since no error-handling middleware is registered. Changed tocallback(null, false)to match the pattern already used elsewhere. AddedmiddlewareNode/tests/cors.test.jsandchessServer/src/tests/cors.test.jsto cover this going forward.TODO (Follow-up Work)
chessServerandstockfishServerinto a shared module — currently blocked on each service's Docker build context being scoped to its own directory, so this would mean restructuring the build tooConfirm whether dropping— confirmed accidental (traced to a stale "unused code" analysis that predatedchessClientURLfromenvironment.prod.jsis intentionalPlayStudent.tsx's actual usage of it). Restored as an optional build-time var (REACT_APP_CHESS_CLIENT_URL) ina2fcbec4, wired through the Dockerfile,tag_build_containers.sh, and CI. TheTODO(merge-chessclient-refactor)comment inPlayStudent.tsxitself is unrelated follow-up work and still stands.Additional Notes
Replaces #200 — same underlying changes, rebased onto the latest main and squashed into one SSH-signed commit to satisfy the branch's signed-commit requirement. fix/environment-separation is left untouched for reference to the original commit history and review discussion.