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.
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.
function awaitAuthorizationCode(
server: CallbackServerHandle,
state: string,
callbacks: OAuthLoginCallbacks,
): Promise<string>Parameters
| Name | Type | Description |
|---|---|---|
| server | CallbackServerHandle | The flow's callback server (or its no-listen stub). |
| state | string | The |
| callbacks | OAuthLoginCallbacks | The flow's login callbacks. |
Returns
Promise<string>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.
function canSignInRemotely(
provider: { supportsRemoteCallback?: boolean; usesCallbackServer?: boolean },
): booleanParameters
| Name | Type | Description |
|---|---|---|
| provider | { supportsRemoteCallback?: boolean; usesCallbackServer?: boolean } | The OAuth provider to check. |
Returns
booleanescapeHtmlForCallbackPageFunction#
The default page's escaping, for renderers that embed provider-supplied text.
function escapeHtmlForCallbackPage(value: string): stringParameters
| Name | Type | Description |
|---|---|---|
| value | string | Text to put in HTML. |
Returns
stringgeneratePkceFunction#
googleTokenExpiryFunction#
When a freshly minted access token should count as expired.
function googleTokenExpiry(expiresIn: number): numberParameters
| Name | Type | Description |
|---|---|---|
| expiresIn | number | Lifetime Google reported, in seconds. |
Returns
numbergoogleTokenRequestFunction#
POST a grant to Google's token endpoint.
function googleTokenRequest(
body: Record<string, string>,
signal?: AbortSignal,
): Promise<GoogleTokenResponse>Parameters
| Name | Type | Description |
|---|---|---|
| body | Record<string, string> | The grant's form fields. |
| signal? | AbortSignal | Cancels the request; it also times out after 30 seconds. |
Returns
Promise<GoogleTokenResponse>listensForCallbackFunction#
Whether a provider should bind its loopback callback listener for these callbacks.
function listensForCallback(callbacks: OAuthLoginCallbacks): booleanParameters
| Name | Type | Description |
|---|---|---|
| callbacks | OAuthLoginCallbacks | The login callbacks; |
Returns
booleanparseRedirectInputFunction#
Split a pasted redirect URL, query string (?code=…&state=…) or bare
code into its parts.
function parseRedirectInput(input: string): { code?: string; state?: string }Parameters
| Name | Type | Description |
|---|---|---|
| input | string | What the user pasted. |
Returns
{ code?: string; state?: string }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.
function refreshGoogleToken(
credentials: OAuthCredentials,
): Promise<GoogleOAuthCredentials>Parameters
| Name | Type | Description |
|---|---|---|
| credentials | OAuthCredentials | The stored grant. |
Returns
Promise<GoogleOAuthCredentials>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.
function startCallbackServer(opts: CallbackServerOptions): Promise<CallbackServerHandle>Parameters
| Name | Type | Description |
|---|---|---|
| opts | CallbackServerOptions | Port, path, page renderer and listen behaviour. |
Returns
Promise<CallbackServerHandle>Constants#
renderDefaultCallbackPageConstant#
The page every zidane sign-in ends on; pass another renderer to replace it.
const renderDefaultCallbackPage: OAuthCallbackPageRendererInterfaces#
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).
interface CallbackServerHandleProperties
| Name | Type | Description |
|---|---|---|
| cancelWait | () => void | Unblock |
| 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 |
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).
interface CallbackServerOptionsProperties
| Name | Type | Description |
|---|---|---|
| expectedState | string | Expected |
| listen? | boolean |
|
| onListenError? | 'reject' | 'resolveWithStub' | How to react when |
| path | string | Path component of the redirect URI (e.g. |
| port | number | Loopback port. Must match the |
| providerName | string | Display name passed through to |
| renderPage | OAuthCallbackPageRenderer | HTML renderer for both success + error pages. |
| successMessage | string | Success-page body shown after a clean redirect. |
GoogleOAuthCredentialsInterface#
A "Sign in with Google" grant, as persisted in the credentials file.
interface GoogleOAuthCredentials extends OAuthCredentialsProperties
| Name | Type | Description |
|---|---|---|
| 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. |
GoogleTokenResponseInterface#
What Google's token endpoint returns.
interface GoogleTokenResponseProperties
| Name | Type | Description |
|---|---|---|
| access_token | string | Bearer token for API calls. |
| expires_in | number | Seconds until |
| id_token? | string | OpenID Connect token, sent when an |
| refresh_token? | string | Long-lived token; Google may omit it on refresh, and the stored one is kept. |
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.
interface OAuthCallbackPageProperties
| Name | Type | Description |
|---|---|---|
| details? | string | The provider's own error ( |
| 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 ( |
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).
interface PkcePairProperties
| Name | Type | Description |
|---|---|---|
| challenge | string | SHA-256(verifier) sent on the authorize URL. |
| verifier | string | Random verifier used in the token-exchange step. |
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).
type OAuthCallbackPageRenderer = (page: OAuthCallbackPage) => stringZidaneOAuthLoginCallbacksType#
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.
type ZidaneOAuthLoginCallbacks = OAuthLoginCallbacks & { callbackServer?: false }Re-exports#
These names are documented on the entry point that owns them.
- OAuthCredentials
- OAuthLoginCallbacks