fix: fail closed on invalid self-hosted API responses - #83
Conversation
…sed-json-contract # Conflicts: # src/server/self-hosted/openapi.ts # src/test-support/v1-stub.ts
|
Triaged against The fail-closed core is good and complements what landedValidating every declared 1.
|
| verify duration | |
|---|---|
| this PR (isolated processes) | 3m35s |
main |
~2m00s |
| #84 | 2m08s |
It also costs signal. bun test produced one aggregate summary; 225 invocations under bash -euo pipefail abort at the first failing file, so a red run now shows one file instead of the full failure set. And the only aggregate assertion left is test "${#test_files[@]}" -gt 0 — the file list is non-empty, but nothing asserts the expected test count, so a file that silently runs zero tests passes.
If there is a cross-file pollution case I did not reproduce, it is worth naming and fixing directly — isolation hides that class permanently.
2. The env fix overlaps #84 — and we found it independently
5513c03 ci: clear self-hosted env in local tests adds -u EMAILS_CLIENT_ENV_SECRET (plus EMAILS_SESSION_TOKEN, DATABASE_URL, …). I hit the same thing from the other direction in #84: the mere presence of EMAILS_CLIENT_ENV_SECRET routes the store to self-hosted mode (src/db/self-hosted-store.ts:83), and ~126 local-mode tests fail with HTTP 401. Two sessions finding this independently is a good argument it belongs on main on its own.
The two PRs do auto-merge cleanly (I test-merged them), though the result carries -u EMAILS_CLIENT_ENV_SECRET twice — harmless, worth tidying in whichever lands second. Your list is the more complete one.
3. The version bump does not belong here
package.json goes 1.3.1 → 1.3.2. A release decision shouldn't ride along inside a fail-closed correctness fix — please split it out.
4. This deepens the structures that are about to be deleted
The EMAILS_MODE axis is slated for removal and the .local/.remote split for collapse behind a store seam. Against that:
| destination | added | removed |
|---|---|---|
src/self-hosted-wire-regression.test.ts |
+1124 | 0 |
src/lib/self-hosted-wire.ts |
+567 | 0 |
src/lib/self-hosted-mail-data-source{,.test}.ts |
+249 | -116 |
src/db/self-hosted-store{,.test}.ts |
+300 | -49 |
src/cli/tui/data.remote.ts, src/db/provisioning.remote.ts |
+42 | -14 |
| total in doomed files | +2309 | -179 |
Plus 11 new EMAILS_MODE references, and 36,774 of the 44,232 insertions are one generated artifact (self-hosted-response-contracts.generated.ts).
Landing this as-is means writing ~2,300 lines of it twice. If the validation can be expressed once at the store seam, the refactor inherits it instead of having to port it.
Suggested split
Land now, independently — small, valuable, no interaction with the refactor:
5513c03env scrubbing (or take fix(db): batch cold database initialisation behind an immediate transaction #84's and add your extra names)- extending the generated-SDK drift check to
src/lib/self-hosted-response-contracts.generated.ts— a real gap - the
test:selfhost-sdk-typesstep
Drop: 4330ff2 process isolation (evidence above), and the 1.3.2 bump.
Hold / restructure against the store seam: the wire-validation body and its generated contracts.
Not merging, and why
Green CI is necessary but not sufficient here: the PR bundles a correctness fix with a release bump and a CI restructuring that measurably costs time and signal without being needed. I am not merging it — the split above is a judgement call for whoever owns the refactor sequencing, and it is cheap to do before the seam lands rather than after. Happy to be wrong about the isolation commit if there is a pollution case I missed.
For reference: #84 fixes the timeout flakes at their cause (cold SQLite init was doing ~200 individually-fsynced DDL statements; 940 ms → 50 ms) with no test changes and no raised timeouts, which removes the pressure that a --max-concurrency 1 workaround would otherwise be relieving.
Correction and addendum to my previous commentI need to correct something I said above, and the corrected version is more useful. What I got wrong. I said the process isolation "is not needed" because this PR's tree is green under a single-process Note the totals move: 2627 → 2248 tests actually run, 138 → 17 skips. So the suite genuinely is order-fragile, and What that something is — and it originates in this PR. The 27 CI failures were caused by None of Two files delete
So the shape is: this PR introduces a Two smaller notes on that commit, still worth acting on:
One defect neither of us flagged: the CHANGELOG attributionThe new Credit where it is dueTwo changes in here actively reduce secret exposure and I would not want them lost in a split: // SelfHostedHttpError no longer captures or interpolates the response body
- readonly bodyText: string,
- super(`Self-hosted ${method} ${path} failed: HTTP ${status}${bodyText ? ` — ${bodyText.slice(0,300)}` : ""}`);
+ super(`Self-hosted ${method} ${path} failed: HTTP ${status}`);// requireCredential:false is now genuinely credentialless
- const credential = process.env[EMAILS_SESSION_TOKEN_ENV]?.trim() || process.env["EMAILS_SELF_HOSTED_API_KEY"]?.trim() || "";
+ config = { baseUrl, credential: "" };Plus a response-size ceiling ( One thing for a reviewer's eye: the generated per-resource item schemas in Net: my split recommendation is unchanged, with the env-restore fix added to the "land now" list and promoted above the CI change. |
`<=` leaves slack a regression can walk back through: a ceiling above the live count is a licence to re-add exactly as many branches as the gap is wide with the guard still green. The delivery-doctor collapse lowers six counts, and two earlier collapses left slack behind — #108 left two structural metrics one wide each and the process-wide-read metric two wide, and #83's note already recorded the tree-wide variable metric three under its ceiling. All eleven ceilings are now exactly the count measured over this commit's git ls-files corpus. Verified two ways, because a ceiling that does not bite is worse than none: the added prose moves no count (the identifiers are named by role and file:line, as the plan requires of anything inside this corpus), and staging a single tracked occurrence of the deployment variable turns the ratchet red at 240 against 239.
Summary
Verification
This PR does not publish the package or deploy a service.