Skip to content

zidane/oauth

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

import { … } from 'zidane/oauth'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.

TypeScript
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#

awaitAuthorizationCodeFunction#

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.

TypeScript
function awaitAuthorizationCode(
  server: CallbackServerHandle,
  state: string,
  callbacks: OAuthLoginCallbacks,
): Promise<string>
Parameters
NameTypeDescription
serverCallbackServerHandle

The flow's callback server (or its no-listen stub).

statestring

The state the authorization request carried.

callbacksOAuthLoginCallbacks

The flow's login callbacks.

Promise<string>

src/chat/oauth-page/server.ts:147

canSignInRemotelyFunction#

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.

TypeScript
function canSignInRemotely(
  provider: { supportsRemoteCallback?: boolean; usesCallbackServer?: boolean },
): boolean
Parameters
NameTypeDescription
provider{ supportsRemoteCallback?: boolean; usesCallbackServer?: boolean }

The OAuth provider to check.

boolean

src/chat/oauth-page/server.ts:89

escapeHtmlForCallbackPageFunction#

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

TypeScript
function escapeHtmlForCallbackPage(value: string): string
Parameters
NameTypeDescription
valuestring

Text to put in HTML.

string

src/chat/oauth-page/render.ts:22

generatePkceFunction#

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

TypeScript
function generatePkce(): Promise<PkcePair>
Promise<PkcePair>

src/chat/oauth-page/pkce.ts:26

googleTokenExpiryFunction#

When a freshly minted access token should count as expired.

TypeScript
function googleTokenExpiry(expiresIn: number): number
Parameters
NameTypeDescription
expiresInnumber

Lifetime Google reported, in seconds.

number

src/providers/google-oauth.ts:82

googleTokenRequestFunction#

POST a grant to Google's token endpoint.

TypeScript
function googleTokenRequest(
  body: Record<string, string>,
  signal?: AbortSignal,
): Promise<GoogleTokenResponse>
Parameters
NameTypeDescription
bodyRecord<string, string>

The grant's form fields.

signal?AbortSignal

Cancels the request; it also times out after 30 seconds.

src/providers/google-oauth.ts:56

listensForCallbackFunction#

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

TypeScript
function listensForCallback(callbacks: OAuthLoginCallbacks): boolean
Parameters
NameTypeDescription
callbacksOAuthLoginCallbacks

The login callbacks; callbackServer: false means no listener.

boolean

src/chat/oauth-page/server.ts:98

parseRedirectInputFunction#

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

TypeScript
function parseRedirectInput(input: string): { code?: string; state?: string }
Parameters
NameTypeDescription
inputstring

What the user pasted.

{ code?: string; state?: string }

src/chat/oauth-page/server.ts:122

refreshGoogleTokenFunction#

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.

TypeScript
function refreshGoogleToken(
  credentials: OAuthCredentials,
): Promise<GoogleOAuthCredentials>
Parameters
NameTypeDescription
credentialsOAuthCredentials

The stored grant.

src/providers/google-oauth.ts:92

startCallbackServerFunction#

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.

TypeScript
function startCallbackServer(opts: CallbackServerOptions): Promise<CallbackServerHandle>
Parameters
NameTypeDescription
optsCallbackServerOptions

Port, path, page renderer and listen behaviour.

src/chat/oauth-page/server.ts:255

Constants#

renderDefaultCallbackPageConstant#

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

TypeScript
const renderDefaultCallbackPage: OAuthCallbackPageRenderer

src/chat/oauth-page/render.ts:15

Interfaces#

CallbackServerHandleInterface#

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, wait with 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).

TypeScript
interface CallbackServerHandle
CallbackServerHandle properties
NameTypeDescription
cancelWait() => void

Unblock waitForCode early (e.g. manual paste won the race).

close() => void

Close the listener. Idempotent.

redirectUristring

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.

src/chat/oauth-page/server.ts:102

CallbackServerOptionsInterface#

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, wait with 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).

TypeScript
interface CallbackServerOptions
CallbackServerOptions properties
NameTypeDescription
expectedStatestring

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).

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.

pathstring

Path component of the redirect URI (e.g. /callback, /auth/callback).

portnumber

Loopback port. Must match the redirect_uri registered with the provider.

providerNamestring

Display name passed through to OAuthCallbackPageRenderer.

renderPageOAuthCallbackPageRenderer

HTML renderer for both success + error pages.

successMessagestring

Success-page body shown after a clean redirect.

src/chat/oauth-page/server.ts:23

GoogleOAuthCredentialsInterface#

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

TypeScript
interface GoogleOAuthCredentials extends OAuthCredentials
GoogleOAuthCredentials properties
NameTypeDescription
clientIdstring

OAuth client the grant was minted with; refreshing needs both halves.

clientSecretstring

Secret of that OAuth client.

email?string

Signed-in Google account.

projectIdstring

Google Cloud project the grant bills.

src/providers/google-oauth.ts:27

GoogleTokenResponseInterface#

What Google's token endpoint returns.

TypeScript
interface GoogleTokenResponse
GoogleTokenResponse properties
NameTypeDescription
access_tokenstring

Bearer token for API calls.

expires_innumber

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.

src/providers/google-oauth.ts:39

OAuthCallbackPageInterface#

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.

TypeScript
interface OAuthCallbackPage
OAuthCallbackPage properties
NameTypeDescription
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.

messagestring

One sentence under the title: what to do now.

providerstring

What was signed in to: a provider (Anthropic) or an MCP server's name.

src/oauth-callback-page.ts:12

PkcePairInterface#

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, wait with 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).

TypeScript
interface PkcePair
PkcePair properties
NameTypeDescription
challengestring

SHA-256(verifier) sent on the authorize URL.

verifierstring

Random verifier used in the token-exchange step.

src/chat/oauth-page/pkce.ts:18

Types#

OAuthCallbackPageRendererType#

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, wait with 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).

TypeScript
type OAuthCallbackPageRenderer = (page: OAuthCallbackPage) => string

src/oauth-callback-page.ts:23

ZidaneOAuthLoginCallbacksType#

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.

TypeScript
type ZidaneOAuthLoginCallbacks = OAuthLoginCallbacks & { callbackServer?: false }

src/chat/oauth-page/server.ts:64

Re-exports#

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

  • OAuthCredentials
  • OAuthLoginCallbacks