P1: Validate config at startup + fix boolean env parsing (#174, #74) - #193
P1: Validate config at startup + fix boolean env parsing (#174, #74)#193dkijania wants to merge 3 commits into
Conversation
|
Nice fix — the 1. Call out the silent behavior flip in the PR description / an upgrade note. Because of the old bug, anyone who set 2. Lock in the multi-host test('accepts multi-host HA connection strings', () => {
assert.deepStrictEqual(
validateConfig({ PG_CONN: 'postgres://host1:5432,host2:5432/archive' }),
[]
);
});(The HA form is documented in |
PG_CONN is deliberately only checked for non-emptiness. That is what keeps the documented HA form (postgres://host1:5432,host2:5432/archive) working — a stricter URL parser here would reject it and break every HA deployment, including the archives the mina-explorer talks to. Pins it so a future "hardening" of this check fails loudly instead of silently. The behaviour-flip upgrade note is on the PR description, since the repo has no CHANGELOG. Addresses review feedback on #193. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @SanabriaRusso — both done. 1. Upgrade note. Added to the PR description ( One case worth singling out that I added while writing it: 2. Multi-host test('accepts multi-host HA connection strings', () => {
assert.deepStrictEqual(
validateConfig({ PG_CONN: 'postgres://host1:5432,host2:5432/archive' }),
[]
);
});Your point about why is the part worth preserving, so it's in the test as a comment: the non-empty check is deliberate, and a stricter URL parser here would break every HA deployment. The test exists so a future "hardening" fails loudly instead of quietly rejecting the documented form. |
|
Verdict: MERGEABLE ✅ Second pass at What I checked
Every var the validator touches
Defaults for unset vars are identical before and after, which is why this is mergeable. Non-blocking nits1. The upgrade note is wrong about
The three vars that genuinely flip on → off are exactly the three that used bare truthiness: 2. The one real crash-loop path deserves a line in the note: a junk boolean value.
3. // src/config.ts
/** Root query fields in schema.graphql — keep in sync. */
const KNOWN_QUERIES = ['events', 'actions', 'networkState', 'blocks'] as const;
// ...inside validateConfig(), after the POSITIVE_INT_VARS loop:
const enabledQueries = env.ENABLED_QUERIES;
if (enabledQueries !== undefined) {
const names = enabledQueries
.split(',')
.map((q) => q.trim())
.filter((q) => q !== '');
if (names.length === 0) {
errors.push(
'ENABLED_QUERIES is set but lists no queries; unset it to expose all of ' +
`${KNOWN_QUERIES.join(', ')}.`
);
}
const unknown = names.filter(
(n) => !(KNOWN_QUERIES as readonly string[]).includes(n)
);
if (unknown.length > 0) {
errors.push(
`ENABLED_QUERIES contains unknown queries: ${unknown.join(', ')}. ` +
`Known queries: ${KNOWN_QUERIES.join(', ')}.`
);
}
}// tests/unit/config.test.ts
test('accepts a valid ENABLED_QUERIES subset', () => {
assert.deepStrictEqual(
validateConfig({ ...valid, ENABLED_QUERIES: 'blocks, networkState' }),
[]
);
});
test('rejects a typo in ENABLED_QUERIES that would delete a root field', () => {
const errors = validateConfig({ ...valid, ENABLED_QUERIES: 'blocks,event' });
assert.ok(errors.some((e) => /unknown queries: event/.test(e)));
});
test('rejects an empty ENABLED_QUERIES list', () => {
assert.ok(
validateConfig({ ...valid, ENABLED_QUERIES: '' }).some((e) =>
/ENABLED_QUERIES/.test(e)
)
);
});<!-- docs/getting-started.md, config table -->
| `ENABLED_QUERIES` | *(all)* | Comma-separated subset of `events,actions,networkState,blocks` to expose; omitted fields are removed from the schema |4. The config module isn't the single source of truth yet. Still reading 5. Ordering caveat, harmless today. 6. Two trivia: Automated second-pass review — focus: downstream compatibility with mina-explorer / mina-explorer-api. |
SanabriaRusso
left a comment
There was a problem hiding this comment.
Approving on the basis of the second-pass review comment above: no mid-to-high severity security, compatibility, or degradation issue found, and the downstream contract with mina-explorer / mina-explorer-api holds — GraphQL validation error text reaches errors[].message verbatim, the browser SPA's cross-origin access is preserved, and the real consumer query shapes (including the 2000-block analytics query and the 500-row page crawl) still pass.
Two things this approval does not mean:
- It does not close the non-blocking items in the review comment. Several are worth fixing before or shortly after merge; they are written up there with patches.
- It does not by itself mean the branch is ready to merge.
mainrequires branches to be up to date, so this needs an update-branch (or a rebase, if the branch is conflicting) first, and a few PRs in this series have cross-PR ordering constraints called out in their review comments.
Automated second-pass review — focus: downstream compatibility with mina-explorer / mina-explorer-api.
PG_CONN is deliberately only checked for non-emptiness. That is what keeps the documented HA form (postgres://host1:5432,host2:5432/archive) working — a stricter URL parser here would reject it and break every HA deployment, including the archives the mina-explorer talks to. Pins it so a future "hardening" of this check fails loudly instead of silently. The behaviour-flip upgrade note is on the PR description, since the repo has no CHANGELOG. Addresses review feedback on #193. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6daf297 to
14b9313
Compare
SanabriaRusso
left a comment
There was a problem hiding this comment.
Approving. I went after the two things that would have made this dangerous — a partial boolean migration, and a startup validator stricter than what actually works in production — and neither is present.
The boolean migration is complete. I grepped every process.env. read in src/ on main: there are exactly five boolean vars and all five are routed through parseBoolean — ENABLE_LOGGING and ENABLE_INTROSPECTION (plugins.ts:16,30), ENABLE_BLOCK_TRANSACTION_DETAILS and ENABLE_GRAPHIQL (server.ts:12,21), ENABLE_JAEGER (jaeger-tracing.ts:23). The remaining raw reads (PORT, PG_CONN, LOG_LEVEL, BLOCK_RANGE_SIZE, CORS_ORIGIN, JAEGER_ENDPOINT, JAEGER_SERVICE_NAME) are strings, numbers, or presence checks and are correctly left alone. A partial migration was the likely defect here and it isn't one. ENABLE_INTROSPECTION=false finally means off.
The PG_CONN validation is correctly loose, and 2eded9b9 is exactly the right test to have written. src/config.ts:53-56 checks non-emptiness only — there is no URI parsing at all — so no postgres.js-valid form can be rejected, multi-host HA included. The new tests assert that postgres://host1:5432,host2:5432/archive and the credentials+?sslmode=require form both validate clean, with a comment stating the looseness is deliberate so a future "hardening" fails loudly. That's the right instinct, and the opposite of the failure mode I was checking for.
Also worth noting the required-PG_CONN check isn't a new fatal condition at all: ArchiveNodeAdapter's constructor (archive-node-adapter.ts:38-42) already throws on a falsy connection string on main. This just moves the failure earlier with a better message.
For the record, the complete set of newly-fatal conditions (src/config.ts:51-101) is: blank PG_CONN (not new, per above); a boolean var with an unrecognised non-empty spelling; PORT/BLOCK_RANGE_SIZE not a positive integer (today these silently fall back via Number(x) || default, so this only rejects garbage); and an unknown name in ENABLED_QUERIES. Nothing there rejects a config that works today except the boolean-spelling case in nit 1.
14b93138 catches something genuinely bad. KNOWN_QUERIES (src/config.ts:21) is ['events','actions','networkState','blocks'] — I diffed that against schema.graphql's type Query and it is an exact match, nothing more or less. And I reproduced what happens today without the validator: ENABLED_QUERIES="" on main builds a schema whose Query type has zero fields — makeExecutableSchema accepts it without complaint — so the server boots healthy, passes every health check, and returns Cannot query field for every request. A silent total outage. Refusing to boot is strictly better.
ENABLED_QUERIES is a server-side boot-time var (it filters the schema in src/resolvers.ts:61-93), not a client-supplied filter, so nothing mina-explorer-api sends can trip it.
Docs check out. Most of both diffs is prettier table re-alignment. The new prose is accurate, and it corrects a pre-existing error: ENABLE_LOGGING gates useOpenTelemetry, so "Enable OpenTelemetry request tracing" is right and the old "Enable request logging" was wrong. The new ENABLED_QUERIES row correctly documents default (all) and the four valid names.
Non-blocking nits
-
y/n/t/faren't recognised spellings.ENABLE_GRAPHIQL=yworks today (yields off, since main compares=== 'true') and after this PR aborts startup. The failure is loud, names the variable and value, and is documented — which is why this isn't blocking — but a one-line hedge:-const TRUE_VALUES = new Set(['true', '1', 'yes', 'on']); -const FALSE_VALUES = new Set(['false', '0', 'no', 'off']); +const TRUE_VALUES = new Set(['true', 't', '1', 'yes', 'y', 'on']); +const FALSE_VALUES = new Set(['false', 'f', '0', 'no', 'n', 'off']);
-
assertValidConfig()runs aftersrc/resolvers.tshas already evaluated. ESM imports are hoisted, so theENABLED_QUERIESschema filtering atresolvers.ts:61-93happens at module-load time, beforeindex.tsreaches the validator. The outcome is still correct (bad schema built, never served, validator throws, process exits 1) so the validation isn't dead code — but the schema is constructed pointlessly first. Only worth restructuring if you touch this again. -
Release-note item:
ENABLE_GRAPHIQL=1/=yesflips from off to on (previously only the literal'true'counted). All checked-in examples use"true"/"false"so this is unlikely to bite, but it is an exposure change and belongs in the notes alongside theENABLE_INTROSPECTIONfix. -
BLOCK_RANGE_SIZE=0flips from "silently means 10000" to fatal. Theoretical.
Downstream: none for either consumer. The one behavioural flip with downstream reach is ENABLE_INTROSPECTION="false" finally meaning off — on main any non-empty value turns introspection on, so a deployment that set false has been serving introspection this whole time. I grepped both consumer repos: every __schema/introspection hit is developer tooling against the Mina daemon or documentation prose, never a request path against this service. explorer-api's { __typename } probe is unaffected — NoSchemaIntrospectionCustomRule rejects only fields whose type is an introspection type, and __typename returns String. No schema or error-text change.
Merge ordering: src/server/plugins.ts is touched by this PR, #188 and #194 in the same ~20 lines, and docs/getting-started.md's config table by this PR and #188. All semantically compatible — they just overlap textually.
PG_CONN is deliberately only checked for non-emptiness. That is what keeps the documented HA form (postgres://host1:5432,host2:5432/archive) working — a stricter URL parser here would reject it and break every HA deployment, including the archives the mina-explorer talks to. Pins it so a future "hardening" of this check fails loudly instead of silently. The behaviour-flip upgrade note is on the PR description, since the repo has no CHANGELOG. Addresses review feedback on #193. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
14b9313 to
f7bd98c
Compare
PG_CONN is deliberately only checked for non-emptiness. That is what keeps the documented HA form (postgres://host1:5432,host2:5432/archive) working — a stricter URL parser here would reject it and break every HA deployment, including the archives the mina-explorer talks to. Pins it so a future "hardening" of this check fails loudly instead of silently. The behaviour-flip upgrade note is on the PR description, since the repo has no CHANGELOG. Addresses review feedback on #193. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
f7bd98c to
9d8e936
Compare
SanabriaRusso
left a comment
There was a problem hiding this comment.
Re-approving after the rebase (the previous approval was dismissed by the force-push).
Re-verified the delta against the commit I approved (14b9313). The only content change is the rebase absorbing the env vars that landed on main after my review — ENABLE_METRICS (#191) and READINESS_PING_TIMEOUT_MS (#187) — into this PR's reformatted config tables. I diffed the documented env-var sets both ways: nothing was dropped from README.md or docs/getting-started.md; the only addition beyond main is ENABLED_QUERIES, which is this PR's own. All checks green.
Carrying forward the round-2 verification unchanged:
- The validator is not stricter than reality:
PG_CONNis checked for non-emptiness only, with no URI parsing, so no postgres.js-valid form can be rejected. The new tests pin multi-host and credentials+sslmode forms as valid, with a comment saying the looseness is deliberate. Correct call. ENABLED_QUERIESvalidation catches something real: todayENABLED_QUERIES=""builds a zero-fieldQueryschema that boots healthy and fails every request — a silent total outage.
Please take this before merge (two lines, CI will not catch it)
The boolean migration was complete when I reviewed it. It is no longer complete, through no fault of this branch — ENABLE_METRICS arrived on main with #191 after my review, and the rebase carried it in untouched:
src/server/plugins.ts:17—if (process.env.ENABLE_METRICS === 'true'), notparseBoolean(...).src/config.ts—ENABLE_METRICSis absent fromBOOLEAN_VARS, so it is not validated.
That makes two statements this PR's own docs add at docs/getting-started.md:175 false for exactly one variable:
Boolean variables (
ENABLE_*) accepttrue/false,1/0,yes/no, oron/off(case-insensitive) … Any other non-empty spelling now aborts startup.
With this branch as it stands, ENABLE_METRICS=1 (or yes, on, TRUE) silently yields no /metrics endpoint, and a typo silently yields the same instead of aborting. The blast radius is small in practice — the reference manifests in #196 and the runbook all use the literal true — but this is precisely the class of bug (#74) the PR exists to eliminate, and the failure is silent.
Fix: add 'ENABLE_METRICS' to BOOLEAN_VARS and change plugins.ts:17 to parseBoolean(process.env.ENABLE_METRICS). Approving rather than blocking because it regresses nothing relative to main and the shipped reference config is unaffected — but the docs sentence is wrong as written until one of the two is changed.
Release note: two behaviour changes here are breaking and belong in the v1.0.0 notes — ENABLE_INTROSPECTION="false" finally meaning off (deployments that set "false" have been serving introspection this whole time), and ENABLE_GRAPHIQL=1/yes now enabling GraphiQL.
…ing (#74) Booleans were read with ad-hoc truthiness — `if (process.env.ENABLE_LOGGING)`, `if (!process.env.ENABLE_INTROSPECTION)`, `if (!process.env.ENABLE_JAEGER ...)` — so the string "false" was truthy and *enabled* the feature. There was also no startup validation, so typos surfaced as confusing runtime behaviour. - Add `src/config.ts`: a `parseBoolean` that understands true/false, 1/0, yes/no, on/off (case-insensitive), plus `validateConfig`/`assertValidConfig` that aggregate problems (missing PG_CONN, non-numeric PORT/BLOCK_RANGE_SIZE, mistyped booleans) and fail fast with one clear message. - Route every boolean env read through `parseBoolean` (plugins, server, jaeger tracing), fixing the "false enables it" bug (#74) and making all the strict `=== 'true'` checks accept the same spellings. - Call `assertValidConfig()` first thing at startup. Unit tests cover boolean spellings (incl. the "false" case), each validation rule, error aggregation, and the throwing behaviour. Closes #174. Closes #74. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QSuak9smCHbp4N17xjjLF6
PG_CONN is deliberately only checked for non-emptiness. That is what keeps the documented HA form (postgres://host1:5432,host2:5432/archive) working — a stricter URL parser here would reject it and break every HA deployment, including the archives the mina-explorer talks to. Pins it so a future "hardening" of this check fails loudly instead of silently. The behaviour-flip upgrade note is on the PR description, since the repo has no CHANGELOG. Addresses review feedback on #193. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9d8e936 to
9c58970
Compare
This PR fixes a bug, and fixing it changes running behaviour for anyone who worked around it. Two things to check before rolling out:
1.
ENABLE_*=falsenow actually means false.Because of #74, setting
ENABLE_LOGGING=false,ENABLE_JAEGER=false,ENABLE_INTROSPECTION=false, orENABLE_BLOCK_TRANSACTION_DETAILS=falseleft the feature ON — the string"false"is truthy. After this PR they turn OFF on the next deploy.That is the fix, but it means a deployment that has (unknowingly) depended on the old state will see the feature disappear. If you set one of these to
falseand want the feature, switch it totrue.Worth noting for
ENABLE_BLOCK_TRANSACTION_DETAILSin particular: the mina-explorer needs it on for block-detail views, so any archive that set it tofalsehas actually been serving those and will stop.2. Invalid config now aborts boot instead of silently defaulting.
Values that used to fall back quietly —
BLOCK_RANGE_SIZE=abc → 10000, a non-numericPORT, a mistyped boolean likeENABLE_JAEGER=sometimes— now fail fast with a clear message. That is the point, but a deployment carrying a typo it never noticed will refuse to start rather than come up misconfigured. The error names the offending variable.There is no CHANGELOG in the repo yet, so this section is the release note; it should be carried into the 1.0.0 notes (#198).
What & why
Part of the production-readiness epic (#163). Closes #174, closes #74.
Two problems:
if (process.env.ENABLE_LOGGING),if (!process.env.ENABLE_INTROSPECTION),if (!process.env.ENABLE_JAEGER ...). The string"false"is truthy, soENABLE_LOGGING=falseenabled logging,ENABLE_JAEGER=falseenabled tracing, etc.PG_CONN, non-numericPORT) surfaced as confusing runtime behaviour instead of failing fast.Changes
src/config.ts:parseBoolean— understandstrue/false,1/0,yes/no,on/off(case-insensitive); unrecognised/empty → fallback.validateConfig/assertValidConfig— aggregate problems (missingPG_CONN, non-positive-integerPORT/BLOCK_RANGE_SIZE, mistyped booleans) and throw one clear message.parseBoolean(plugins.ts,server.ts,jaeger-tracing.ts) — fixes Fix Boolean env vars parsing #74 and makes the previously-strict=== 'true'checks accept the same spellings.assertValidConfig()runs first thing at startup, so misconfig fails fast.Testing
npm run build— cleannpm run test:unit— all pass; 11 new assertions covering boolean spellings (incl. the"false"case), each validation rule, error aggregation, and the thrownpm run lint/npx prettier --debug-check .— clean🤖 Generated with Claude Code