SIGN IN SIGN UP

refactor: replace embedded enjoi fork with ajv-backed schema validator (#5388)

* refactor: replace embedded enjoi fork with ajv-backed schema validator

Replace the 566-line embedded enjoi-resolver.ts (a fork of the unmaintained
enjoi npm package) with an ajv-backed schema-validator.ts. Joi-compatible API
surface preserved so call sites and external SDK consumers keep working.

Drop joi, @hapi/bourne, @hapi/hoek; add ajv. Net -1 direct dep,
-4 packages in node_modules.

Closes #5384 — ajv treats empty `{}` schemas as "accept any" per spec, no
log warning emitted.

Coercion parity with joi+enjoi verified end-to-end: nested string -> number
/boolean, launch=anyOf<object,string> JSON parsing, additionalProperties: {}
handling, anyOf declared-order semantics. New schema-coercion-parity.spec.ts
locks in 9 documented examples from docs.browserless.io against the real
generated route schemas.

scripts/build-schemas.js is untouched; every generated build/routes/**/*.json
and static/docs/swagger.json are byte-identical to main. The old path
src/shared/utils/enjoi-resolver.ts survives as an 11-line passive re-export
shim for external SDK consumers that deep-imported it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(schema-validator): correctness, type cleanup, more tests

Address review feedback on the new ajv-backed validator:

- Empty string no longer coerces to true for boolean fields (joi rejected
  this; the prior coercion was a quirk, not parity).
- After parsing a top-level JSON string into an object/array, recurse so
  nested stringified primitives also coerce.
- anyOf/oneOf object and array branches iterate alternatives in declared
  order and return the first that produces a successful coercion, matching
  joi's `.alternatives().try()` semantics.
- Drop dead `ValidateOptions.abortEarly` parameter (silently ignored) and
  the unused `ValidationResult<T>` generic. Update call sites accordingly.
- Document the `allErrors: true` mitigation (HTTP body size cap in
  `Config.getMaxPayloadSize()`).
- Trim duplicated JSDoc on `compileSchema` and clean up filler comments.

Expand `schema-coercion-parity.spec.ts` with five high-value cases:
empty-string-not-boolean rejection, nested coercion after JSON-string
parse, `additionalProperties: {}` accept-any (locks in the issue #5384
regression), `additionalProperties: false` rejection, and proto-pollution
key stripping. Add a `before` hook that skips the suite on single-browser
images where chromium route schemas are not shipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(schema-validator): propagate coerced values + anyOf order

Addresses two follow-up review findings:

- `valid.value` (the coerced payload) was dropped on the floor in server.ts.
  Handlers now see the coerced object/number/boolean for body and query
  params, not the original strings. (3 call sites: HTTP query, HTTP body,
  WS query.)
- anyOf/oneOf object and array branches no longer scan past an alternative
  that accepted the input unchanged. First declared matching-type
  alternative wins, consistent with the documented "declared order"
  semantics. ajv still enforces the actual contract at validate time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(schema-validator): skip anyOf alts with missing required keys

CodeRabbit pointed out that the previous "first matching-type alt wins"
rule fails when alts have disjoint required fields. For
  `anyOf: [{required:['a']}, {required:['b']}]`
with input `{b: '1'}`, joi would have tried alt A, failed (missing `a`),
then coerced via alt B. The previous fix bailed at A unchanged and never
tried B.

Now: when picking the object alternative, skip alts whose `required` keys
aren't all present in the input. First survivor wins. This handles both
the disjoint-required case and the `[{id:string},{id:number}]` ordering
case from the prior review round.

Adds a regression test locking the new behavior in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(schema-validator): resolve required keys through $ref/allOf chains

CodeRabbit pointed out that `inputCouldMatchAlt` only handled a single
`$ref` hop and missed required keys composed via `allOf`. For schemas
that use `anyOf: [{$ref: '#/definitions/AltA'}, ...]` where the alts
themselves use `allOf` (common from TJS), the helper returned true for
every alt and the wrong branch was selected.

Replace the single-hop check with `collectRequiredKeys`, which walks
`$ref` chains and `allOf` recursively to gather the full required set.
Also extend `inferExpectedType` to descend through `allOf` so alts whose
type comes from a subschema are correctly identified as object/array.

Adds a regression test using a definition-backed schema with allOf.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A
artiom committed
b004d8a02311e42bfce3050d4906fac91d4f5ec6
Parent: 4fef6ea
Committed by GitHub <noreply@github.com> on 5/26/2026, 3:41:26 PM