# zidane/oauth

> The pieces zidane's own sign-ins are built from, for OAuth flows written outside the core.

Import: `import { … } from 'zidane/oauth'`. Source: [src/oauth-entry.ts](https://github.com/Tahul/zidane/blob/main/src/oauth-entry.ts).

Use `zidane/oauth` to write a sign-in for an extension's provider (`ProviderDescriptor.oauthProvider`) that behaves like the built-in ones: the loopback callback server and its default page, PKCE, the race between the browser redirect and a pasted redirect URL, and Google's token endpoint. To sign in for a client of a remote host too, start the server with `listen: false` when `listensForCallback(callbacks)` is false, wait with `awaitAuthorizationCode()`, and declare `supportsRemoteCallback: true` on the provider. It is a subpath rather than part of the root barrel so `node:http` stays out of every `zidane` import.

```ts
import type { OAuthLoginCallbacks } from 'zidane/oauth'
import { awaitAuthorizationCode, generatePkce, listensForCallback, renderDefaultCallbackPage, startCallbackServer } from 'zidane/oauth'

async function authorize(callbacks: OAuthLoginCallbacks): Promise<{ code: string, verifier: string, redirectUri: string }> {
  const { verifier, challenge } = await generatePkce()
  const state = crypto.randomUUID()
  const server = await startCallbackServer({
    port: 8787,
    path: '/callback',
    expectedState: state,
    providerName: 'Example',
    renderPage: renderDefaultCallbackPage,
    successMessage: 'You can close this tab.',
    ...(listensForCallback(callbacks) ? {} : { listen: false }),
  })
  try {
    const url = new URL('https://auth.example.com/authorize')
    url.searchParams.set('redirect_uri', server.redirectUri)
    url.searchParams.set('code_challenge', challenge)
    url.searchParams.set('code_challenge_method', 'S256')
    url.searchParams.set('state', state)
    callbacks.onAuth({ url: url.toString() })
    return { code: await awaitAuthorizationCode(server, state, callbacks), verifier, redirectUri: server.redirectUri }
  }
  finally {
    server.close()
  }
}
```

## Functions

### `awaitAuthorizationCode`

Function.

The authorization code from whichever lands first: the loopback redirect,
a pasted redirect URL (`onManualCodeInput`: a browser on another machine,
or a remote sign-in), or, with neither, an `onPrompt` asking for it. A
pasted URL whose `state` doesn't match is refused.

```ts
function awaitAuthorizationCode(
  server: CallbackServerHandle,
  state: string,
  callbacks: OAuthLoginCallbacks,
): Promise<string>
```

**Parameters**

- `server` (`CallbackServerHandle`): The flow's callback server (or its no-listen stub).
- `state` (`string`): The `state` the authorization request carried.
- `callbacks` (`OAuthLoginCallbacks`): The flow's login callbacks.

**Returns** `Promise<string>`

Source: [src/chat/oauth-page/server.ts:147](https://github.com/Tahul/zidane/blob/main/src/chat/oauth-page/server.ts#L147)

### `canSignInRemotely`

Function.

Whether a provider can sign in for a client of a remote host without
listening on the host's machine: it honors `callbackServer: false`
(marked by `honorsRemoteCallback`, or declaring
`supportsRemoteCallback: true` — the form a flow from another copy of
zidane, like an extension's, can use), or it never listens
(`usesCallbackServer: false`: paste, device or polling flows). Others
(pi-ai's stock providers, a third party's) would bind a loopback port on
the server, which the client's browser can't reach.

```ts
function canSignInRemotely(
  provider: { supportsRemoteCallback?: boolean; usesCallbackServer?: boolean },
): boolean
```

**Parameters**

- `provider` (`{ supportsRemoteCallback?: boolean; usesCallbackServer?: boolean }`): The OAuth provider to check.

**Returns** `boolean`

Source: [src/chat/oauth-page/server.ts:89](https://github.com/Tahul/zidane/blob/main/src/chat/oauth-page/server.ts#L89)

### `escapeHtmlForCallbackPage`

Function.

The default page's escaping, for renderers that embed provider-supplied text.

```ts
function escapeHtmlForCallbackPage(value: string): string
```

**Parameters**

- `value` (`string`): Text to put in HTML.

**Returns** `string`

Source: [src/chat/oauth-page/render.ts:22](https://github.com/Tahul/zidane/blob/main/src/chat/oauth-page/render.ts#L22)

### `generatePkce`

Function.

A fresh PKCE verifier and its S256 challenge, for one authorization request.

```ts
function generatePkce(): Promise<PkcePair>
```

**Returns** `Promise<PkcePair>`

Source: [src/chat/oauth-page/pkce.ts:26](https://github.com/Tahul/zidane/blob/main/src/chat/oauth-page/pkce.ts#L26)

### `googleTokenExpiry`

Function.

When a freshly minted access token should count as expired.

```ts
function googleTokenExpiry(expiresIn: number): number
```

**Parameters**

- `expiresIn` (`number`): Lifetime Google reported, in seconds.

**Returns** `number`

Source: [src/providers/google-oauth.ts:82](https://github.com/Tahul/zidane/blob/main/src/providers/google-oauth.ts#L82)

### `googleTokenRequest`

Function.

POST a grant to Google's token endpoint.

```ts
function googleTokenRequest(
  body: Record<string, string>,
  signal?: AbortSignal,
): Promise<GoogleTokenResponse>
```

**Parameters**

- `body` (`Record<string, string>`): The grant's form fields.
- `signal?` (`AbortSignal`): Cancels the request; it also times out after 30 seconds.

**Returns** `Promise<GoogleTokenResponse>`

Source: [src/providers/google-oauth.ts:56](https://github.com/Tahul/zidane/blob/main/src/providers/google-oauth.ts#L56)

### `listensForCallback`

Function.

Whether a provider should bind its loopback callback listener for these callbacks.

```ts
function listensForCallback(callbacks: OAuthLoginCallbacks): boolean
```

**Parameters**

- `callbacks` (`OAuthLoginCallbacks`): The login callbacks; `callbackServer: false` means no listener.

**Returns** `boolean`

Source: [src/chat/oauth-page/server.ts:98](https://github.com/Tahul/zidane/blob/main/src/chat/oauth-page/server.ts#L98)

### `parseRedirectInput`

Function.

Split a pasted redirect URL, query string (`?code=…&state=…`) or bare
code into its parts.

```ts
function parseRedirectInput(input: string): { code?: string; state?: string }
```

**Parameters**

- `input` (`string`): What the user pasted.

**Returns** `{ code?: string; state?: string }`

Source: [src/chat/oauth-page/server.ts:122](https://github.com/Tahul/zidane/blob/main/src/chat/oauth-page/server.ts#L122)

### `refreshGoogleToken`

Function.

Mint a fresh access token from a stored grant, with the client it was
minted with. Keeps every stored field; Google keeps the refresh token.

```ts
function refreshGoogleToken(
  credentials: OAuthCredentials,
): Promise<GoogleOAuthCredentials>
```

**Parameters**

- `credentials` (`OAuthCredentials`): The stored grant.

**Returns** `Promise<GoogleOAuthCredentials>`

Source: [src/providers/google-oauth.ts:92](https://github.com/Tahul/zidane/blob/main/src/providers/google-oauth.ts#L92)

### `startCallbackServer`

Function.

Start the loopback HTTP callback server. The returned handle is the
caller's contract — they `await waitForCode()`, race it against manual
paste, then `close()` in a `finally`.

```ts
function startCallbackServer(opts: CallbackServerOptions): Promise<CallbackServerHandle>
```

**Parameters**

- `opts` (`CallbackServerOptions`): Port, path, page renderer and listen behaviour.

**Returns** `Promise<CallbackServerHandle>`

Source: [src/chat/oauth-page/server.ts:255](https://github.com/Tahul/zidane/blob/main/src/chat/oauth-page/server.ts#L255)

## Constants

### `renderDefaultCallbackPage`

Constant.

The page every zidane sign-in ends on; pass another renderer to replace it.

```ts
const renderDefaultCallbackPage: OAuthCallbackPageRenderer
```

Source: [src/chat/oauth-page/render.ts:15](https://github.com/Tahul/zidane/blob/main/src/chat/oauth-page/render.ts#L15)

## Interfaces

### `CallbackServerHandle`

Interface.

`zidane/oauth`: the building blocks zidane's own sign-ins use, for OAuth
flows written outside the core (an extension's
`ProviderDescriptor.oauthProvider`): the loopback callback server with the
default callback page, PKCE, the redirect-or-paste race, and Google's token
endpoint. A flow built on them signs in remotely too: pass `listen:
listensForCallback(callbacks)` to [`startCallbackServer`](https://zidane.si/docs/reference/oauth.md#startCallbackServer), wait with
[`awaitAuthorizationCode`](https://zidane.si/docs/reference/oauth.md#awaitAuthorizationCode), and declare `supportsRemoteCallback: true`.

A subpath rather than the root barrel, so `node:http` stays off the graph
of every `zidane` import. Extensions loaded by discovery resolve it to the
host's own module (see `registerExtensionHostModules`).

```ts
interface CallbackServerHandle
```

**Properties**

- `cancelWait` (`() => void`): Unblock `waitForCode` early (e.g. manual paste won the race).
- `close` (`() => void`): Close the listener. Idempotent.
- `redirectUri` (`string`): Fully-qualified redirect URI to advertise to the provider.
- `waitForCode` (`() => Promise<{ code: string; state?: string } | null>`): Resolves when the redirect lands (with `{ code, state? }`) or when
  `cancelWait` is invoked (with `null`). Never throws.

Source: [src/chat/oauth-page/server.ts:102](https://github.com/Tahul/zidane/blob/main/src/chat/oauth-page/server.ts#L102)

### `CallbackServerOptions`

Interface.

`zidane/oauth`: the building blocks zidane's own sign-ins use, for OAuth
flows written outside the core (an extension's
`ProviderDescriptor.oauthProvider`): the loopback callback server with the
default callback page, PKCE, the redirect-or-paste race, and Google's token
endpoint. A flow built on them signs in remotely too: pass `listen:
listensForCallback(callbacks)` to [`startCallbackServer`](https://zidane.si/docs/reference/oauth.md#startCallbackServer), wait with
[`awaitAuthorizationCode`](https://zidane.si/docs/reference/oauth.md#awaitAuthorizationCode), and declare `supportsRemoteCallback: true`.

A subpath rather than the root barrel, so `node:http` stays off the graph
of every `zidane` import. Extensions loaded by discovery resolve it to the
host's own module (see `registerExtensionHostModules`).

```ts
interface CallbackServerOptions
```

**Properties**

- `expectedState` (`string`): Expected `state` value. The server rejects any callback whose `state`
  doesn't match, exactly. For Anthropic this is the PKCE verifier; for
  Codex it's a random 16-byte hex string.
- `listen?` (`boolean`): `false`: bind nothing, return the stub handle (`waitForCode` resolves
  `null` at once) for `redirectUri`. A remote sign-in: the browser
  redirects to the CLIENT's loopback, and the code comes back through the
  provider's manual-input path (see [`ZidaneOAuthLoginCallbacks`](https://zidane.si/docs/reference/oauth.md#ZidaneOAuthLoginCallbacks)).
- `onListenError?` (`'reject' | 'resolveWithStub'`): How to react when `server.listen` itself fails (typically `EADDRINUSE`
  when another instance is mid-flow). `"reject"` propagates the error to
  the awaiting `start` caller; `"resolveWithStub"` returns a degraded
  handle whose `waitForCode` immediately resolves to `null`, so the
  caller can fall back to manual paste. Anthropic uses `"reject"`,
  Codex uses `"resolveWithStub"` to match pi-ai's per-provider behavior.
- `path` (`string`): Path component of the redirect URI (e.g. `/callback`, `/auth/callback`).
- `port` (`number`): Loopback port. Must match the `redirect_uri` registered with the provider.
- `providerName` (`string`): Display name passed through to [`OAuthCallbackPageRenderer`](https://zidane.si/docs/reference/oauth.md#OAuthCallbackPageRenderer).
- `renderPage` (`OAuthCallbackPageRenderer`): HTML renderer for both success + error pages.
- `successMessage` (`string`): Success-page body shown after a clean redirect.

Source: [src/chat/oauth-page/server.ts:23](https://github.com/Tahul/zidane/blob/main/src/chat/oauth-page/server.ts#L23)

### `GoogleOAuthCredentials`

Interface.

A "Sign in with Google" grant, as persisted in the credentials file.

```ts
interface GoogleOAuthCredentials extends OAuthCredentials
```

**Properties**

- `clientId` (`string`): OAuth client the grant was minted with; refreshing needs both halves.
- `clientSecret` (`string`): Secret of that OAuth client.
- `email?` (`string`): Signed-in Google account.
- `projectId` (`string`): Google Cloud project the grant bills.

Source: [src/providers/google-oauth.ts:27](https://github.com/Tahul/zidane/blob/main/src/providers/google-oauth.ts#L27)

### `GoogleTokenResponse`

Interface.

What Google's token endpoint returns.

```ts
interface GoogleTokenResponse
```

**Properties**

- `access_token` (`string`): Bearer token for API calls.
- `expires_in` (`number`): Seconds until `access_token` expires.
- `id_token?` (`string`): OpenID Connect token, sent when an `openid` scope was granted.
- `refresh_token?` (`string`): Long-lived token; Google may omit it on refresh, and the stored one is kept.

Source: [src/providers/google-oauth.ts:39](https://github.com/Tahul/zidane/blob/main/src/providers/google-oauth.ts#L39)

### `OAuthCallbackPage`

Interface.

The page a browser lands on when an OAuth sign-in comes back to this
computer (a provider's or an MCP server's): one status, one line, and the
provider's error when there is one. Shared by every flow so they all end
on the same screen. A leaf module: `chat/oauth-page` and `mcp` both read it.

Styled like the design system's empty state (`EmptyState` in zidane-ui),
with the default theme's tokens (`ui/src/theme`) for each appearance. No
stylesheet is loaded: the page is served once, from a loopback port.

```ts
interface OAuthCallbackPage
```

**Properties**

- `details?` (`string`): The provider's own error (`error_description`), shown in mono.
- `kind` (`'error' | 'success'`): Whether the sign-in worked; picks the icon, title and colours.
- `message` (`string`): One sentence under the title: what to do now.
- `provider` (`string`): What was signed in to: a provider (`Anthropic`) or an MCP server's name.

Source: [src/oauth-callback-page.ts:12](https://github.com/Tahul/zidane/blob/main/src/oauth-callback-page.ts#L12)

### `PkcePair`

Interface.

`zidane/oauth`: the building blocks zidane's own sign-ins use, for OAuth
flows written outside the core (an extension's
`ProviderDescriptor.oauthProvider`): the loopback callback server with the
default callback page, PKCE, the redirect-or-paste race, and Google's token
endpoint. A flow built on them signs in remotely too: pass `listen:
listensForCallback(callbacks)` to [`startCallbackServer`](https://zidane.si/docs/reference/oauth.md#startCallbackServer), wait with
[`awaitAuthorizationCode`](https://zidane.si/docs/reference/oauth.md#awaitAuthorizationCode), and declare `supportsRemoteCallback: true`.

A subpath rather than the root barrel, so `node:http` stays off the graph
of every `zidane` import. Extensions loaded by discovery resolve it to the
host's own module (see `registerExtensionHostModules`).

```ts
interface PkcePair
```

**Properties**

- `challenge` (`string`): SHA-256(verifier) sent on the authorize URL.
- `verifier` (`string`): Random verifier used in the token-exchange step.

Source: [src/chat/oauth-page/pkce.ts:18](https://github.com/Tahul/zidane/blob/main/src/chat/oauth-page/pkce.ts#L18)

## Types

### `OAuthCallbackPageRenderer`

Type.

`zidane/oauth`: the building blocks zidane's own sign-ins use, for OAuth
flows written outside the core (an extension's
`ProviderDescriptor.oauthProvider`): the loopback callback server with the
default callback page, PKCE, the redirect-or-paste race, and Google's token
endpoint. A flow built on them signs in remotely too: pass `listen:
listensForCallback(callbacks)` to [`startCallbackServer`](https://zidane.si/docs/reference/oauth.md#startCallbackServer), wait with
[`awaitAuthorizationCode`](https://zidane.si/docs/reference/oauth.md#awaitAuthorizationCode), and declare `supportsRemoteCallback: true`.

A subpath rather than the root barrel, so `node:http` stays off the graph
of every `zidane` import. Extensions loaded by discovery resolve it to the
host's own module (see `registerExtensionHostModules`).

```ts
type OAuthCallbackPageRenderer = (page: OAuthCallbackPage) => string
```

Source: [src/oauth-callback-page.ts:23](https://github.com/Tahul/zidane/blob/main/src/oauth-callback-page.ts#L23)

### `ZidaneOAuthLoginCallbacks`

Type.

pi-ai's login callbacks plus zidane's `callbackServer: false`: the
redirect lands on another machine (a client of a remote host), which
forwards the full callback URL through `onManualCodeInput`. zidane's own
providers then skip their loopback listener; others ignore the flag.

```ts
type ZidaneOAuthLoginCallbacks = OAuthLoginCallbacks & { callbackServer?: false }
```

Source: [src/chat/oauth-page/server.ts:64](https://github.com/Tahul/zidane/blob/main/src/chat/oauth-page/server.ts#L64)

## Re-exports

These names are documented on the entry point that owns them.

- .bun: `OAuthCredentials`, `OAuthLoginCallbacks`
- [zidane/providers](https://zidane.si/docs/reference/providers.md): [`OAuthProviderInterface`](https://zidane.si/docs/reference/providers.md#OAuthProviderInterface)
