Public: anyone can read this; the crew can change it.

The OpenAPI contract describes OAuth: an oauth2 security scheme with the authorization-code flow and its refresh URL #8440

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

The OpenAPI document now describes OAuth, so a client generated from `/contract/openapi.json` can sign a person in and refresh. `bun test test/openapi` passes (391 tests), `tsc --noEmit` is clean, and the branch is pushed. I did not run `scripts/gate.ts` or the full suite. The forge's check runs the full suite on push.

shipped production is serving it deploy d5b2fe2

opened this

The OpenAPI document now describes OAuth, so a client generated from /contract/openapi.json can sign a person in and refresh. bun test test/openapi passes (391 tests), tsc --noEmit is clean, and the branch is pushed. I did not run scripts/gate.ts or the full suite. The forge's check runs the full suite on push.

  • oauth2 scheme: components.securitySchemes.oauth2 has flows.authorizationCode. authorizationUrl is /oauth/authorize, and tokenUrl and refreshUrl are both /oauth/token. The scopes are ro and rw, the values in OAUTH_SCOPES in src/core/oauth.ts. I wrote the scope list by hand rather than deriving it from OAUTH_SCOPES, so a new scope would need adding in both places.
  • URLs: they are built from the request origin, like servers.
  • openid scheme: OpenAPI doesn't allow openIdConnectUrl inside an oauth2 scheme. It is a separate openid scheme pointing at /.well-known/openid-configuration, and no operation requires it.
  • Operations: every operation that took a bearer token now lists [{token}, {oauth2: [ro, rw]}] as alternatives.
  • Tests: a new test in test/openapi-flow.test.ts checks that refreshUrl ends in /oauth/token, along with the other URLs, the scopes, the openid URL and the two-scheme alternatives. I also edited test/openapi-oa-pull-checks.test.ts: it pinned the old single-scheme security shape, so I updated it to the new one.
  • Not done: I didn't add /oauth/* paths (token, revoke, register) to paths, because the issue only asked for the scheme.

Closes #8433

commented

The OpenAPI document now describes OAuth, so a client generated from /contract/openapi.json can sign a person in and refresh. bun test test/openapi passes (391 tests), tsc --noEmit is clean, and the branch is pushed. I did not run scripts/gate.ts or the full suite. The forge's check runs the full suite on push.

  • oauth2 scheme: components.securitySchemes.oauth2 has flows.authorizationCode. authorizationUrl is /oauth/authorize, and tokenUrl and refreshUrl are both /oauth/token. The scopes are ro and rw, the values in OAUTH_SCOPES in src/core/oauth.ts. I wrote the scope list by hand rather than deriving it from OAUTH_SCOPES, so a new scope would need adding in both places.
  • URLs: they are built from the request origin, like servers.
  • openid scheme: OpenAPI doesn't allow openIdConnectUrl inside an oauth2 scheme. It is a separate openid scheme pointing at /.well-known/openid-configuration, and no operation requires it.
  • Operations: every operation that took a bearer token now lists [{token}, {oauth2: [ro, rw]}] as alternatives.
  • Tests: a new test in test/openapi-flow.test.ts checks that refreshUrl ends in /oauth/token, along with the other URLs, the scopes, the openid URL and the two-scheme alternatives. I also edited test/openapi-oa-pull-checks.test.ts: it pinned the old single-scheme security shape, so I updated it to the new one.
  • Not done: I didn't add /oauth/* paths (token, revoke, register) to paths, because the issue only asked for the scheme.
the crew's run was interrupted: restart, resumes on the next pass — cancelled
commented

I changed no code for this failure. I think it was a load problem in shard 4, not my change, so I pushed an empty commit to get the check to run again.

  • What failed: every failure was an unnamed beforeAll error, undefined is not an object (evaluating 'servers.stop') or app.close. The test servers did not start in shard 4. None of the failing assertions touch OpenAPI.
  • What I ran locally:

- Four of the failing files (mcp-origin-apps, read-api, refresh-meta, render-media) pass: 54 tests, 0 failures. - The OAuth and OpenAPI tests pass: 465 tests, 0 failures. - tsc --noEmit is clean. - I did not run the other failing files, and scripts/fast.ts would have named 1016 files, so I ran no broader set.

  • What the pull changes: the OpenAPI contract now describes OAuth. It lists an oauth2 authorization-code flow with refresh and an openIdConnect scheme. Operations that take a bearer token list both schemes. Tests are in test/openapi-flow.test.ts and test/openapi-oa-pull-checks.test.ts.
  • To try it: fetch /contract/openapi.json on the preview and read components.securitySchemes.

If shard 4 fails the same way again, the cause is in the test harness boot and not in this pull.

Changes requested — before the latest push

The preview serves the new oauth2 and openid schemes and the URLs point at real routes. One thing to fix, plus one smaller point:

  1. The scope list is written by hand in the scheme while the operations use OAUTH_SCOPES. The builder says as much: a new scope would need adding in two places. Build the map from OAUTH_SCOPES with a description table typed Record<OAuthScope,string>, so tsc fails when a scope is added without a description. The new test then compares the keys to OAUTH_SCOPES instead of the literal ["ro","rw"].
  2. Add a short how-to at docs/features/ (for example openapi-oauth.md) saying how a client generated from the contract signs in and refreshes. Do not put it in README.

The openid scheme is declared but no operation requires it. That is acceptable; say in the how-to that it is for discovery only.

commented

Both points from the review are fixed and pushed. tsc is clean and the 28 test/openapi*.test.ts files pass (391 tests). I did not run the full suite.

  • Scopes: the oauth2 scheme now builds its scope map from OAUTH_SCOPES. Descriptions sit in a Record<OAuthScope, string> table in src/contract/openapi.ts, so tsc fails if a scope is added without a description. The new test in test/openapi-flow.test.ts compares the scope keys to OAUTH_SCOPES instead of the literal ["ro","rw"].
  • How-to: docs/features/openapi-oauth.md covers how a generated client signs in and refreshes. It says openid is for discovery only and that no operation requires it. I left the README alone, as asked.

I agreed with all of it and have nothing to push back on.

Changes requested — before the latest push

One high-ish correctness miss. Everything else (scheme shape, URLs, OAUTH_SCOPES-derived map, docs/features how-to, README untouched) is fine.

Fix first: every authenticated operation now declares {oauth2: ["ro","rw"]}. In OpenAPI a scopes array in one requirement means ALL are required. A generated client will therefore request scope=ro rw, and /oauth/authorize grants one scope only (src/core/oauth.ts:200 takes the first valid one, so ro). The client would get a read-only token and every write would fail, which is the opposite of what the contract promises. Reads are also over-asked.

Change: list scopes per operation. Read operations (GET/HEAD) get {oauth2:["ro"]}; mutating operations get {oauth2:["rw"]} (rw includes read). Update the tests: openapi-flow asserts a GET gets ["ro"] and a POST gets ["rw"]; fix the pinned shape in openapi-oa-pull-checks.test.ts accordingly. Keep the doc line saying which scope a client asks for (one scope, ro or rw).

commented

GET /contract/openapi.json now describes OAuth under components.securitySchemes:

  • oauth2: the authorization-code flow with PKCE. authorizationUrl is /oauth/authorize, and tokenUrl and refreshUrl are both /oauth/token.
  • openid: OpenID Connect discovery at /.well-known/openid-configuration. No operation requires it; it is there so clients can discover the endpoints.

Scopes. The scope map is built from OAUTH_SCOPES, with descriptions in a Record<OAuthScope, string>. A scope added without a description fails the type check.

Security on each operation. Every operation that needs a login lists {token: []} or {oauth2: [scope]}, and either is enough. The oauth2 entry names exactly one scope: ro on a GET, rw on anything else. OpenAPI treats scopes in one requirement as all required, and /oauth/authorize grants only one scope, so listing both would have given a generated client a read-only token. Since rw also allows reads, the scope is chosen in one place, oauthScopeFor(method) in src/contract/openapi.ts.

Docs. docs/features/openapi-oauth.md explains how a generated client signs in and refreshes, and that it asks for a single scope, ro or rw.

What I chose not to do. Operations that need the admin scope still say rw. OAuth cannot grant admin, and a narrower contract for those routes belongs in its own pull.

Tests. test/openapi-flow.test.ts checks the scheme URLs and that the scope keys match OAUTH_SCOPES. It also checks that a GET asks for ["ro"] and a POST for ["rw"]. The pinned shape in test/openapi-oa-pull-checks.test.ts now expects ["rw"]. The 28 test/openapi*.test.ts files and tsc --noEmit pass. I did not run the full suite.

Check rerun. An earlier check failed in shard 4 with servers.stop undefined in beforeAll, meaning the test servers did not start. I pushed an empty commit to rerun it. The failing files I ran locally pass.

Try it: curl https://lab--8440--forge-trevato-apps.open-platform.sh/contract/openapi.json | jq '.components.securitySchemes, .paths["/api/v1/admin/invites"].post.security'

🤖 Generated with Claude Code

Approved

Both earlier asks are met: the scope map is built from OAUTH_SCOPES with a Record<OAuthScope,string> table, and docs/features/openapi-oauth.md covers sign-in, refresh and openid being discovery-only (README untouched). The preview serves the oauth2 and openid schemes with the right URLs. Each operation asks for a single scope (ro on GET, rw otherwise), which fits a single-scope grant. The tests cover it and the pinned-shape test was updated. Non-blocking: admin-scope routes still say rw, which the builder disclosed; a narrower contract for them can be its own pull.

commented

Left #8433 open: 3 unticked items remain (src/contract/openapi.ts adds an oauth2 scheme: flows.authorizationCode with authorizationUrl /oauth/authorize, tokenUrl and refreshUrl /oauth/token, and the scopes the forge grants (the OAuth scopes in src/core/oauth.ts), plus an openIdConnectUrl pointing at /.well-known/openid-configuration if that fits the schema.; Operations that accept a bearer token list both schemes as alternatives.; Test: the served document has components.securitySchemes.oauth2.flows.authorizationCode.refreshUrl ending in /oauth/token, and the document still validates in the existing OpenAPI tests (run bun test test/openapi* before pushing).).

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs