feat(openapi): spec-accurate generator with configurable, auth-agnostic security - #153
feat(openapi): spec-accurate generator with configurable, auth-agnostic security#153isaacwasserman wants to merge 7 commits into
Conversation
Rewrites the OpenAPI generator to fix correctness gaps and make auth
documentation the host's choice rather than a hardcoded scheme.
- Merge every method onto its path instead of overwriting, so a POST and a
GET on the same path both survive (fixes a POST being dropped when a GET is
registered after it on the same path).
- Emit all documented verbs (GET/POST/PUT/PATCH/DELETE) and method arrays;
HEAD/OPTIONS/"*" are intentionally skipped (see README).
- Derive path parameters from the route and template ":id" -> "{id}".
- Extract request bodies and query params via the library-agnostic
StandardJSONSchemaV1 interface (~standard.jsonSchema) instead of Zod-only
reflection; fall back to an empty object schema.
- Drop the hardcoded bearerAuth/apiKeyCookie. Add document-level `security`
and `securitySchemes` config plus a per-endpoint `metadata.openapi.security`
override; assert no scheme by default.
- Make the generator's `paths` local (no cross-call state leak) and fix
getHTML's unquoted Scalar configuration.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
…ndardSchemaV1 (#3) Adding `jsonSchema?` to the vendored `StandardSchemaV1.Props` diverged it from `@standard-schema/spec` and perturbed generic type inference in consumers — an endpoint's body type could collapse to `undefined` when the definer is used behind a generic wrapper. Revert the spec type to its original shape and read `~standard.jsonSchema` through a local cast in the generator instead. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A PUT/PATCH/DELETE (or POST) endpoint with no `body` option previously still
advertised an empty `application/json` request body, so client generators would
send `{}` to routes that consume nothing. Only emit `requestBody` when the
endpoint actually declares a body.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Top-level body optionality isn't representable in JSON Schema (Zod emits identical JSON for a schema and its `.optional()`), so probe the validator directly — a body is required unless `~standard.validate(undefined)` passes, matching the runtime. Adds `isBodyRequired`, makes the request-body path async, and adds regression tests for required and optional bodies. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When `options.body` is declared but the schema library doesn't expose `~standard.jsonSchema`, `getRequestBody` returned undefined and dropped the request body, documenting a body-consuming route as bodyless. Fall back to an empty schema (accepts any JSON) so the requestBody is always emitted; only the schema shape degrades. Adds a regression test with a converter-less schema. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| if (options.metadata?.openapi?.requestBody) { | ||
| return options.metadata.openapi.requestBody; | ||
| } | ||
| if (!options.body) return undefined; |
There was a problem hiding this comment.
When an endpoint declares a body schema but sets disableBody: true, the router passes undefined as the body at runtime, but this generator still documents a JSON request body. Generated clients can send body data that the handler will not consume, and required schemas can be shown as required even though body parsing is disabled.
| if (!options.body) return undefined; | |
| if (!options.body || options.disableBody) return undefined; |
|
Want your agent to iterate on Greptile's feedback? Start a greploop in Cursor and it will work through the open comments and keep going until this PR reviews clean. |
* chore: publish as @futonic/better-call and fix body parsing from request Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: bump version to 2.0.6 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| if (options.metadata?.openapi?.requestBody) { | ||
| return options.metadata.openapi.requestBody; | ||
| } | ||
| if (!options.body) return undefined; |
There was a problem hiding this comment.
When an endpoint sets both body and disableBody: true, the router skips body parsing and passes undefined, but this generator still emits a JSON requestBody because it only checks options.body. A PUT, PATCH, POST, or DELETE endpoint using disableBody can still be documented as accepting or requiring JSON that the runtime does not consume. Return no request body when disableBody is set before using schema-derived or metadata-provided bodies.
Fixes #154
Summary
The OpenAPI generator produces an inaccurate document for any service with more than one method per path or with non-GET/POST routes. This rewrites it to be spec-accurate and to make authentication host-driven rather than a hardcoded Better Auth scheme.
Bugs fixed
paths[path]was reassigned per endpoint, so a path with multiple methods kept only the last one — e.g. aPOST /ticketsregistered beforeGET /ticketsdisappeared from the document. Operations are now merged onto the path item.*are intentionally skipped.:idnever templated and no path parameters were generated. Routes now template:id→{id}and emit the corresponding path parameters.ZodObjectproperties).pathsleaked state acrossgenerator()calls.getHTMLemitted invalid JS (unquotedtheme/title/descriptioninterpolation); the Scalar config is nowJSON.stringify-d.Behaviour changes
bearerAuthon every operation and a top-levelapiKeyCookie, with neither scheme defined. Both are removed. The router/generator now accept document-levelsecurity+securitySchemesand honour a per-endpointmetadata.openapi.securityoverride, asserting nothing by default.~standard.jsonSchema), implemented by Zod ≥ 4.2, ArkType ≥ 2.1.28, and others — replacing Zod-only reflection. Falls back to an empty object schema when unavailable.Better AuthtoAPI Reference.Tests
New
src/openapi.test.tsregression suite; fullvitestrun (198 tests) andtsdownbuild pass.I'm running this fork in production, so I'm happy to iterate on the API surface (naming of the config fields, whether to keep the empty-body fallback, etc.) if you'd prefer a different shape.
🤖 Generated with Claude Code