# Connection OAuth2 helper

`@inseam/connection-oauth2` — a factory that turns OAuth2 provider config into a ready-made `ConnectionDefinition`. PKCE, authorization-code via inbound callback, refresh-on-expiry inside `verify`, opaque JSON credential serialization. Convenience over a base class; plugins that need behavior outside the standard flow write their own `ConnectionDefinition` from scratch.

**See also:** [design](../design/connection.md) · [spec](../spec/connection.md#oauth2-helper--inseamconnection-oauth2)

## What it does

`oauth2(config)` returns a `ConnectionDefinition<OAuth2Credential>` implementing:

- **PKCE auth-code flow.** Generates an S256 challenge from random bytes, allocates a one-shot callback URL via `ctx.inboundHttp.allocateCallback()`, yields a single `redirect` step, validates `state` on return, exchanges `code` at `tokenUrl`.
- **Refresh-on-expiry inside `verify`.** When `expiresAt` is past and a `refreshToken` is present, the helper refreshes and returns the rotated bytes via `VerifyResult.credential` so core persists it atomically.
- **`InboundHttpService` requirement.** If `ctx.inboundHttp` is missing or unavailable, setup yields `failed` cleanly — it does not throw.
- **No revoke on teardown.** Provider revocation behavior varies; the helper is a no-op there.

Three injectable adapters keep the helper runtime-agnostic and testable: `http` (`HttpAdapter`, default wraps WHATWG `fetch`), `clock`, and `random` (defaults to `crypto.getRandomValues`). Provider/protocol failures surface as `OAuth2Error`; `OAuth2Error` does not extend `ConnectionError` because the helper is a separate package.

## How to use it

A Gmail plugin:

```ts
import { oauth2 } from "@inseam/connection-oauth2";

export const gmailConnection = oauth2({
  kind: "gmail",
  displayName: "Gmail",
  authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
  tokenUrl: "https://oauth2.googleapis.com/token",
  scopes: ["https://www.googleapis.com/auth/gmail.readonly"],
  clientId: () => process.env.GMAIL_CLIENT_ID!,
  clientSecret: () => process.env.GMAIL_CLIENT_SECRET!,
});
```

Register it like any other [Connection kind](./connection.md#how-to-use-it). The host's `InboundHttpService` must be wired into `openConnectionRegistry` or setup will yield `failed`.

Full config and credential shapes: [spec — OAuth2 helper](../spec/connection.md#oauth2-helper--inseamconnection-oauth2).

## How to extend it

The helper is convenience, not a base class — it has no extension seams. If you need behavior it doesn't cover (different challenge method, audience parameter, non-standard token endpoint shape, custom revoke), write a `ConnectionDefinition` directly against `@inseam/connection-contract`. Use the helper's `pkgs/connection-oauth2/src/index.ts` as a reference implementation — it's a single file, end-to-end.

For per-provider polish without leaving the helper, `deriveDisplayName(cred)` can fetch the account's email/username from the provider on first connect.

## Where the code lives

- Helper package: `pkgs/connection-oauth2/src/index.ts`
- Public surface: `oauth2`, `OAuth2Credential`, `OAuth2Config`, `OAuth2Error`

## Related

- [Connection](./connection.md) — the port this helper plugs into
- [Spec — connection](../spec/connection.md) — `ConnectionDefinition`, `InboundHttpService`, the contract the helper satisfies
- [Monorepo package model](./monorepo-package-model.md) — why helper packages depend only on the contract, never on core
