Skip to content

OpenAPI OAuth2 authorization-code onboarding backend#251

Merged
RhysSullivan merged 7 commits intomainfrom
04-14-openapi_oauth2_authorization-code_onboarding_backend
Apr 15, 2026
Merged

OpenAPI OAuth2 authorization-code onboarding backend#251
RhysSullivan merged 7 commits intomainfrom
04-14-openapi_oauth2_authorization-code_onboarding_backend

Conversation

@RhysSullivan
Copy link
Copy Markdown
Owner

Plumbs the full OAuth2 authorization-code flow through the OpenAPI
plugin end-to-end: session persistence, plugin extension methods,
HTTP API endpoints, stored source config, and pre-request token
refresh in the invoker.

Types:

  • OAuth2Auth class captures the per-source OAuth state (scheme
    name, flow, tokenUrl, client/access/refresh secret IDs, scopes,
    expiresAt). Persisted on the stored source config so refresh can
    run without a round-trip to the spec.
  • OpenApiOAuthSession class for the pending start→complete session
    (spec text, namespace, name, baseUrl, headers, client secret IDs,
    PKCE verifier, scopes, flow URLs).
  • InvocationConfig.oauth2: Option<OAuth2Auth> so the invoker can
    detect OAuth sources and refresh the access token before each
    request.
  • SourceConfig.oauth2 field so the config persists across reloads.

Operation store:

  • putOAuthSession / getOAuthSession / deleteOAuthSession on the
    interface; KV store adds a third scoped KV namespace keyed off
    ${ns}.oauth-sessions. In-memory store gets a third scoped KV.
  • config-file-store inherits the new methods via its spread
    decoration.

Plugin extension:

  • startOAuth(input): generate sessionId + PKCE verifier, persist
    the pending session (spec text + onboarding metadata), resolve the
    clientId secret, and build the authorization URL via
    @executor/plugin-oauth2's buildAuthorizationUrl. Returns the
    URL + sessionId + scopes to the frontend.
  • completeOAuth({ state, code, error }): look up the session,
    delete it, call exchangeAuthorizationCode, store tokens via
    storeOAuthTokens, return an OpenApiOAuthCompleteResult carrying
    all the secret IDs. The frontend then calls addSpec with
    oauth2 set on the config.
  • addSpec honors config.oauth2 by setting
    invocationConfig.oauth2 and writing the auth descriptor onto
    SourceConfig.oauth2.

Invoker:

  • resolveOAuthAccessToken wraps
    @executor/plugin-oauth2's withRefreshedAccessToken with a
    secrets adapter and a persistAuth callback that writes the
    refreshed expiry / tokenType back to the stored source config.
    Runs before every request when the source has oauth2 set, and
    injects Authorization: Bearer <token> (overwriting any
    spec-declared Authorization header).

HTTP API:

  • New endpoints on the openapi group: POST /oauth/start,
    POST /oauth/complete, GET /oauth/callback.
  • oauthCallback uses runOAuthCallback from
    @executor/plugin-oauth2/http to render the popup HTML; channel
    name is executor:openapi-oauth-result.
  • OpenApiOAuthError tagged error carries OAuth-specific failures
    through the API layer with status 400.

Only authorizationCode is wired end-to-end; clientCredentials is
declared in the preview flows but intentionally left for follow-up
work (it has a different shape — no popup, direct server-side
exchange — and can be added without schema changes since
OAuth2Preset already carries both flow kinds).

All 29 packages typecheck; the existing 32 openapi tests pass
unchanged.

Generic provider-agnostic helpers for PKCE, authorization URL building,
code exchange, refresh, and the refresh-skew predicate. Locks every
real-world quirk the prior google-discovery oauth.ts handled (string
expires_in, error_description fallback chain, non-JSON 5xx bodies,
empty access_token, 20s timeout, body vs Basic client auth) into a
33-test fidelity suite so future cleanups fail loudly instead of
silently breaking refresh.

Ports google-discovery to use the shared helpers via a thin wrapper
that preserves Google-only authorization params (access_type=offline,
prompt=consent, include_granted_scopes=true) through the extraParams
escape hatch. All existing google-discovery tests still pass.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Apr 15, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
executor-marketing cc15929 Commit Preview URL Apr 15 2026, 06:09 PM

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 15, 2026

Open in StackBlitz

@executor/sdk

npm i https://pkg.pr.new/RhysSullivan/executor/@executor/sdk@251

@executor/plugin-file-secrets

npm i https://pkg.pr.new/RhysSullivan/executor/@executor/plugin-file-secrets@251

@executor/plugin-google-discovery

npm i https://pkg.pr.new/RhysSullivan/executor/@executor/plugin-google-discovery@251

@executor/plugin-graphql

npm i https://pkg.pr.new/RhysSullivan/executor/@executor/plugin-graphql@251

@executor/plugin-keychain

npm i https://pkg.pr.new/RhysSullivan/executor/@executor/plugin-keychain@251

@executor/plugin-mcp

npm i https://pkg.pr.new/RhysSullivan/executor/@executor/plugin-mcp@251

@executor/plugin-oauth2

npm i https://pkg.pr.new/RhysSullivan/executor/@executor/plugin-oauth2@251

@executor/plugin-onepassword

npm i https://pkg.pr.new/RhysSullivan/executor/@executor/plugin-onepassword@251

@executor/plugin-openapi

npm i https://pkg.pr.new/RhysSullivan/executor/@executor/plugin-openapi@251

@executor/plugin-workos-vault

npm i https://pkg.pr.new/RhysSullivan/executor/@executor/plugin-workos-vault@251

commit: b5a0beb

@RhysSullivan RhysSullivan force-pushed the 04-14-extract_oauth2_flows_from_openapi_security_schemes branch from 967f538 to f9f845a Compare April 15, 2026 00:32
@RhysSullivan RhysSullivan force-pushed the 04-14-openapi_oauth2_authorization-code_onboarding_backend branch 3 times, most recently from 1ee3112 to 52facf2 Compare April 15, 2026 01:25
@RhysSullivan RhysSullivan force-pushed the 04-14-extract_oauth2_flows_from_openapi_security_schemes branch from f9f845a to ddab854 Compare April 15, 2026 01:25
Split the package into three entrypoints (`.`, `/http`, `/react`) so
browser consumers do not pull Node crypto. New surface:

- `OAuthPopupResult<TAuth>` + `isOAuthPopupResult` in a shared types
  module so both the server-side HTML generator and the client-side
  popup opener agree on the message shape.
- `/http` entry: `popupDocument(payload, channelName)` HTML generator
  (with XSS-safe serialization, dark-mode CSS, postMessage +
  BroadcastChannel fallback), and `runOAuthCallback` wrapper that turns
  a `completeOAuth` Effect into the HTML body for the redirect route.
- `/react` entry: `openOAuthPopup` browser util with centered-popup
  math, same-origin message filtering, settle-once semantics, and
  popup-blocked detection.

Ports `google-discovery` to use them: `api/handlers.ts` drops its
inline `popupDocument` / OAuth glue and becomes a ~20-line handler,
and `AddGoogleDiscoverySource.tsx` drops its inline `openOAuthPopup`.

20 new fidelity tests lock every behavior (XSS escaping, channel name
escape, origin filtering, settle-once across both channels, popup
block handling).
Generic helpers that take a secrets-I/O adapter and a `persistAuth`
callback so plugins can transparently refresh access tokens before a
request without re-implementing the ~150-line token-rotation dance.

- `storeOAuthTokens`: persist a fresh OAuth2TokenResponse as new access
  and (optionally) refresh-token secrets via a `createSecret` callback
  and return a `StoredOAuthTokens` descriptor ready to write to a
  source config.
- `withRefreshedAccessToken`: check `shouldRefreshToken`, resolve
  client/refresh credentials from secrets, call `refreshAccessToken`,
  overwrite the access-token secret in place, optionally rotate the
  refresh-token secret, invoke `persistAuth` with the updated snapshot,
  and return the current access token to be injected into the request.

Ports `google-discovery`:
- `completeOAuth` drops its manual `storeSecret` dance (now a ~15 line
  call to `storeOAuthTokens`).
- `invoke.ts`'s `resolveOAuthAccessToken` shrinks from ~150 lines of
  secret-resolution + refresh + persist glue to ~75 lines of adapter.
- The google-discovery-local `refreshAccessToken` wrapper is removed
  (unused after the port); the google token URL is exported so the
  invoker can pass it to the shared helper.

12 new fidelity tests lock in every branch (no refresh, refresh within
skew, rotated refresh token, preserved refresh token, null refresh ID,
public client without client_secret, secret-resolve failure wrapping).
Exposes the `flows` object on `components.securitySchemes` through the
preview pipeline so the onboarding UI can offer an OAuth2 path for any
spec that declares one. Previously we detected `type: oauth2` but
silently dropped the flow metadata, leaving users to paste a bearer
token manually.

Schema additions:
- `OAuth2AuthorizationCodeFlow`, `OAuth2ClientCredentialsFlow`, and
  `OAuth2Flows` classes capturing `authorizationUrl`, `tokenUrl`,
  `refreshUrl`, and `scopes` per flow. `implicit` and `password` are
  intentionally excluded — both are deprecated by OAuth 2.1 and adding
  them would only encourage users to onboard onto them.
- `SecurityScheme` now carries `flows`, `bearerFormat`, and
  `openIdConnectUrl` (previously dropped).
- `OAuth2Preset` class — one entry per (scheme × flow) pair with the
  label, URLs, and scope map the onboarding UI needs to render a
  "Connect via OAuth2" flow with a scope checklist.
- `SpecPreview.oauth2Presets` carries the derived presets.

Extraction:
- `extractSecuritySchemes` now takes a `DocResolver` and resolves
  `$ref` entries instead of silently dropping them. Specs that alias
  their security schemes through `components.securitySchemes` refs
  (rare but real) now surface correctly.
- `extractFlows` tolerates malformed flows (missing `tokenUrl` /
  `authorizationUrl`) by dropping them, so a broken flow does not
  prevent the rest of the spec from previewing.

5 new tests cover authorizationCode + clientCredentials extraction,
the `$ref` resolution path, `bearerFormat` / `openIdConnectUrl`
capture, and the malformed-flow ignore case.
Plumbs the full OAuth2 authorization-code flow through the OpenAPI
plugin end-to-end: session persistence, plugin extension methods,
HTTP API endpoints, stored source config, and pre-request token
refresh in the invoker.

Types:
- `OAuth2Auth` class captures the per-source OAuth state (scheme
  name, flow, tokenUrl, client/access/refresh secret IDs, scopes,
  expiresAt). Persisted on the stored source config so refresh can
  run without a round-trip to the spec.
- `OpenApiOAuthSession` class for the pending start→complete session
  (spec text, namespace, name, baseUrl, headers, client secret IDs,
  PKCE verifier, scopes, flow URLs).
- `InvocationConfig.oauth2: Option<OAuth2Auth>` so the invoker can
  detect OAuth sources and refresh the access token before each
  request.
- `SourceConfig.oauth2` field so the config persists across reloads.

Operation store:
- `putOAuthSession` / `getOAuthSession` / `deleteOAuthSession` on the
  interface; KV store adds a third scoped KV namespace keyed off
  `${ns}.oauth-sessions`. In-memory store gets a third scoped KV.
- `config-file-store` inherits the new methods via its spread
  decoration.

Plugin extension:
- `startOAuth(input)`: generate sessionId + PKCE verifier, persist
  the pending session (spec text + onboarding metadata), resolve the
  clientId secret, and build the authorization URL via
  `@executor/plugin-oauth2`'s `buildAuthorizationUrl`. Returns the
  URL + sessionId + scopes to the frontend.
- `completeOAuth({ state, code, error })`: look up the session,
  delete it, call `exchangeAuthorizationCode`, store tokens via
  `storeOAuthTokens`, return an `OpenApiOAuthCompleteResult` carrying
  all the secret IDs. The frontend then calls `addSpec` with
  `oauth2` set on the config.
- `addSpec` honors `config.oauth2` by setting
  `invocationConfig.oauth2` and writing the auth descriptor onto
  `SourceConfig.oauth2`.

Invoker:
- `resolveOAuthAccessToken` wraps
  `@executor/plugin-oauth2`'s `withRefreshedAccessToken` with a
  secrets adapter and a `persistAuth` callback that writes the
  refreshed expiry / tokenType back to the stored source config.
  Runs before every request when the source has `oauth2` set, and
  injects `Authorization: Bearer <token>` (overwriting any
  spec-declared `Authorization` header).

HTTP API:
- New endpoints on the openapi group: `POST /oauth/start`,
  `POST /oauth/complete`, `GET /oauth/callback`.
- `oauthCallback` uses `runOAuthCallback` from
  `@executor/plugin-oauth2/http` to render the popup HTML; channel
  name is `executor:openapi-oauth-result`.
- `OpenApiOAuthError` tagged error carries OAuth-specific failures
  through the API layer with status 400.

Only `authorizationCode` is wired end-to-end; `clientCredentials` is
declared in the preview flows but intentionally left for follow-up
work (it has a different shape — no popup, direct server-side
exchange — and can be added without schema changes since
`OAuth2Preset` already carries both flow kinds).

All 29 packages typecheck; the existing 32 openapi tests pass
unchanged.
@RhysSullivan RhysSullivan force-pushed the 04-14-openapi_oauth2_authorization-code_onboarding_backend branch from 52facf2 to b5a0beb Compare April 15, 2026 02:12
@RhysSullivan RhysSullivan force-pushed the 04-14-extract_oauth2_flows_from_openapi_security_schemes branch from ddab854 to 12433ef Compare April 15, 2026 02:12
@RhysSullivan RhysSullivan marked this pull request as ready for review April 15, 2026 06:39
Copy link
Copy Markdown
Owner Author

RhysSullivan commented Apr 15, 2026

Merge activity

@RhysSullivan RhysSullivan changed the base branch from 04-14-extract_oauth2_flows_from_openapi_security_schemes to graphite-base/251 April 15, 2026 06:48
@RhysSullivan RhysSullivan changed the base branch from graphite-base/251 to main April 15, 2026 18:05
@RhysSullivan RhysSullivan merged commit d900123 into main Apr 15, 2026
1 of 6 checks passed
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.

1 participant