# 06 — OAuth2

Declare OAuth with `ext.setupOauth2(props)`. Its **presence ⇒ `requireOAuth = true`**. It is declarative
for the common case; any handler below overrides its step. Combined scopes = `scopes` ∪ every op's
`oauth2Scopes` (deduped), passed to handlers as `scopes`.

| Property | Type | Description |
|---|---|---|
| `scopes` | string[] | Base scope list. |
| `authorizeUrl` / `tokenUrl` | string | Declarative endpoints. |
| `pkce` | boolean | Runtime generates verifier/challenge + threads state. |
| `clientAuth` | `'basic' \| 'body'` | Client credential placement. |
| `identityRequest` | fn | `({ apiRequest }) => raw` — fetch the account identity. |
| `identity` | fn | `(raw) => { name, imageUrl?, userData? }`. |
| `getConnectionURL` (esc) | fn | see below. |
| `executeCallback` (esc) | fn | see below. |
| `refreshToken` (esc) | fn | see below. |
| `serializeToken` / `deserializeToken` | fn | Composite-token (de)serialization; runtime deserializes the header token into `oauth.token` before `initContext`. |

## Escape-hatch handlers → returns

| Handler | Arguments | Returns |
|---|---|---|
| `getConnectionURL` | `{ config, context, scopes }` | `string` (authorize URL) |
| `executeCallback` | `{ callbackObject, config, context }` | `{ token, expirationInSeconds?, refreshToken?, connectionIdentityName?, connectionIdentityImageURL?, overwrite?, userData? }` |
| `refreshToken` | `{ refreshToken, config, context }` | `{ token, expirationInSeconds?, refreshToken? }` |

## Using OAuth in handlers

`oauth` derives only from the `oauth-access-token` header → `oauth = { accessToken, token? }` (`token` =
`deserializeToken(accessToken)`). Per-op opt-in for extra scopes: set `oauth2Scopes: ['extra.scope']` on
any action, **polling** trigger, or dictionary.

`token` is the one slot in the handler bag left open rather than inferred: `setupOauth2` runs after
`createExtension` has already resolved the service shape, so a `deserializeToken` return cannot flow back
into every handler. Annotate it where you read it if the composite shape matters.

> Scopes declared on a **realtime** trigger are currently **not** collected into the authorize URL — the
> runtime unions `scopes` with the `oauth2Scopes` of actions, polling triggers and dictionaries only. Put a
> realtime trigger's scopes in the extension-level `scopes` instead.

Client id/secret are typically `configItems` marked `.shared()` (see [02-zod-types.md](02-zod-types.md)).
