chore(lint): require a check on JSON parsed in package source

Merged
OK
9d ago
+880-232

What and why

  • JSON.parse and .json() return any. Typing that result (as T, const x: T =, return from a function typed T) skips every check, so a bad body surfaces later as a TypeError or a wrong value.
  • New rule e2e/no-unchecked-json in the local plugin, on for packages/*/src/**. Parsed JSON must be typed unknown (or Promise<unknown>) wherever it lands, until a schema or a check narrows it.
    • It covers assertions, declarations (an untyped one too), assignment to a typed let, and returns, including from an untyped function. An inline callback such as .then((r) => r.json()) is exempt, because the call it feeds is checked where it lands.
    • It looks through await, ?., !, .catch(...) and .then((r) => r.json()).
    • It is AST-only, so passing parsed JSON straight to a typed parameter still gets through.
  • 26 hits fixed, most with a real check:
    • oauth: zod schemas plus one readJsonBody(response, schema, subject) in oauth/errors.ts. A body that is not JSON, or has the wrong shape, becomes FLOW_FAILED naming the vendor.
    • config: typed readers. A malformed inline source map from an earlier loader (tsx) is dropped instead of failing the import.
    • eas, limrun: typed readers (no zod there). isRecord moves to packages/eas/src/json.ts.
    • lightpanda: /json/version read as unknown. A body without a version now reports unknown instead of polling until the startup timeout.
  • 3 reasoned disables:
    • agent/act-validation.ts:117: our own JSON.stringify output.
    • web/src/observation.ts:163: page-reported metadata, as unchecked as the structured branch next to it.
    • web/src/browser.ts:521: the public json<T = unknown>(), where the caller names T.
// packages/e2e/src/oauth/token-endpoint.ts
return readJsonBody(response, TokenResponseSchema, `the ${vendor} token response`, 'is missing access_token');

Verified

Ran it locally: yes

  • Rule tests: 18 unchecked shapes reported, 13 unknown-typed or non-JSON ones pass. src is clean.

  • Each behavior change has a unit test that fails on the base and passes here. For example:

    inputbasethis branch
    token endpoint answers HTMLSyntaxErrorFLOW_FAILED: the Acme token response is not JSON
    device-flow poll answers nullTypeErrorFLOW_FAILED
    EAS createDeviceRunSession without its fieldsTypeErrorEAS: unexpected response to createDeviceRunSession
    ChatGPT device grant without code_verifierposts the string "undefined"FLOW_FAILED before the exchange
  • Real loader (node --enable-source-maps --import packages/e2e/src/config/register.ts), a .ts file carrying an inline map of {"version":3}:

    basethis branch
    TypeError: Cannot read properties of undefined (reading 'map') at new TraceMapError: boom on line 3, mapped to the file
  • Testbed suite on the built packages: 78 passed.

Checklist

  • Changeset added (pnpm changeset) for any change to a published package, or the change is testbed, docs, or CI only.
  • Breaking change: title carries !, the changeset body names what breaks and what replaces it, and the deprecation warning landed a window earlier (stability policy in CONTRIBUTING.md).
  • Docs updated in the same PR: the docs/**/*.mdx page for the behavior, and skills/e2e/ if the skill describes it.
  • Contract change: emitted .d.ts reviewed, tests/types/sdk-types.ts updated; wire change edits the schema and both fixtures.
  • Engine contract change (e2e/engine): changesets for e2e, @e2e-dev/web, and @e2e-dev/mobile.