Skip to content

feat(openapi): spec-accurate generator with configurable, auth-agnostic security - #153

Open
isaacwasserman wants to merge 7 commits into
better-auth:v2.0.xfrom
isaacwasserman:main
Open

feat(openapi): spec-accurate generator with configurable, auth-agnostic security#153
isaacwasserman wants to merge 7 commits into
better-auth:v2.0.xfrom
isaacwasserman:main

Conversation

@isaacwasserman

@isaacwasserman isaacwasserman commented Jul 13, 2026

Copy link
Copy Markdown

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

  • Methods clobbered per path. paths[path] was reassigned per endpoint, so a path with multiple methods kept only the last one — e.g. a POST /tickets registered before GET /tickets disappeared from the document. Operations are now merged onto the path item.
  • Only GET/POST emitted. PATCH/DELETE/PUT endpoints were silently absent. All documented verbs (and method arrays) are now emitted; HEAD/OPTIONS/* are intentionally skipped.
  • :id never templated and no path parameters were generated. Routes now template :id{id} and emit the corresponding path parameters.
  • Scalar bodies rendered empty for scalar fields (the old Zod reflection only recursed into ZodObject properties).
  • Module-level paths leaked state across generator() calls.
  • getHTML emitted invalid JS (unquoted theme/title/description interpolation); the Scalar config is now JSON.stringify-d.

Behaviour changes

  • Auth is no longer hardcoded. The generator asserted bearerAuth on every operation and a top-level apiKeyCookie, with neither scheme defined. Both are removed. The router/generator now accept document-level security + securitySchemes and honour a per-endpoint metadata.openapi.security override, asserting nothing by default.
  • Library-agnostic schema extraction via the StandardJSONSchemaV1 interface (~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.
  • Default document title changed from Better Auth to API Reference.

Tests

New src/openapi.test.ts regression suite; full vitest run (198 tests) and tsdown build 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

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>
@greptile-apps

greptile-apps Bot commented Jul 13, 2026

Copy link
Copy Markdown

Greptile Summary

This PR rewrites the OpenAPI generator for more accurate route documentation. The main changes are:

  • Merged operations for shared paths and added more HTTP verbs.
  • Added path parameter templating and query/body schema generation through Standard JSON Schema.
  • Made security configuration opt-in through router and endpoint metadata.
  • Added regression tests for OpenAPI output and Scalar HTML config.

Confidence Score: 4/5

This is close, but one request-body case should be fixed before merging.

  • Endpoints with disableBody: true can still be documented with a JSON request body.
  • Generated clients can send or require JSON that the runtime intentionally leaves undefined.
  • The other reviewed body-generation fixes look consistent with the changed tests.

Files Needing Attention: packages/better-call/src/openapi.ts

Fix All in Cursor Fix All in Codex Fix All in Claude Code

Reviews (7): Last reviewed commit: "chore: publish as @futonic/better-call a..." | Re-trigger Greptile

Comment thread packages/better-call/src/openapi.ts
…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>
Comment thread packages/better-call/src/openapi.ts
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>
Comment thread packages/better-call/src/openapi.ts
isaacwasserman and others added 2 commits July 13, 2026 13:40
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Disabled Bodies Still Emit

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.

Suggested change
if (!options.body) return undefined;
if (!options.body || options.disableBody) return undefined;

Fix in Cursor Fix in Codex Fix in Claude Code

@greptile-apps

greptile-apps Bot commented Jul 30, 2026

Copy link
Copy Markdown

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>
Comment on lines 226 to 229
if (options.metadata?.openapi?.requestBody) {
return options.metadata.openapi.requestBody;
}
if (!options.body) return undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Disabled Bodies Emit

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.

Fix in Cursor Fix in Codex Fix in Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenAPI generator drops operations: methods clobbered per path, PATCH/DELETE ignored, :id not templated

1 participant