/** * Sign in with Turn Zero for a builder's application (XTX-17): the plane's * OAuth 2.1 authorization-code leg with PKCE, run by the application's * server as a public client the plane registers dynamically (cloud:MCP-02), * under the scope `tzmesh.application`. The application's end user signs in * with their own Turn Zero account; the first time, the plane shows one line * naming the application and its callback's host, and after that nothing. * The code exchanges for a `tzmesh` token at `chat`, issued to the * application (cloud:MAPI-14), which the application spends at once on the * `human` call for the user's own participant (XTX-16) and drops. * * The module keeps nothing: `beginTzmeshSignIn` answers the verifier and * the state with the address, and the application keeps both for that one * sign-in, in its own session store, until `finishTzmeshSignIn` reads the * callback. It mints no credential of its own; the plane mints the token. */ /** The plane's origin on production, whose metadata names its endpoints. */ export const TZMESH_PLANE_ORIGIN = 'https://turnzero.ai'; /** The messaging resource on production, the resource a sign-in names. */ export const TZMESH_RESOURCE = 'https://tzmesh.ai/mcp'; /** The scope a builder's application names (cloud:MCP-02). */ export const TZMESH_APPLICATION_SCOPE = 'tzmesh.application'; /** A fetch-shaped function: the global `fetch` serves, and a test hands the * double's `plane.fetch`. */ export type TzmeshSignInFetch = ( url: string, init?: { method?: string; headers?: Record; body?: string }, ) => Promise<{ ok: boolean; status: number; json(): Promise }>; /** Where the sign-in runs: the plane's origin and the messaging resource, * production's where omitted, and the fetch the module calls with. */ export interface TzmeshSignInConfig { plane?: string; resource?: string; fetch?: TzmeshSignInFetch; } /** One sign-in begun: the address the user's browser goes to, and the * verifier and state the application keeps until the callback. */ export interface TzmeshSignInStart { url: string; verifier: string; state: string; } /** The token the exchange answered, issued to the application for the * user's account at `chat` under ` at ` (cloud:MCP-02). */ export interface TzmeshSignInToken { token: string; level: string; label: string; expiresAt: string | null; tokenId: string | null; } /** A sign-in that did not complete: `error` is the plane's OAuth error name, * `access_denied` where the user cancelled or the account may not send messages, * `state_mismatch` where the callback's state is not the one begun, and * `unexpected` where the plane or the callback answered outside the * protocol. */ export class TzmeshSignInError extends Error { readonly error: string; readonly status: number | null; constructor(error: string, detail: string, status: number | null = null) { super(detail); this.name = 'TzmeshSignInError'; this.error = error; this.status = status; } } interface Metadata { authorization_endpoint: string; token_endpoint: string; registration_endpoint?: string; } const base64url = (bytes: Uint8Array): string => { let text = ''; for (const byte of bytes) text += String.fromCharCode(byte); return btoa(text).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); }; const randomText = (bytes: number): string => { const buffer = new Uint8Array(bytes); crypto.getRandomValues(buffer); return base64url(buffer); }; /** The PKCE challenge of a verifier: the SHA-256 digest, base64url. */ export async function tzmeshChallengeOf(verifier: string): Promise { const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier)); return base64url(new Uint8Array(digest)); } const settings = (config: TzmeshSignInConfig): { plane: string; resource: string; fetch: TzmeshSignInFetch } => ({ plane: (config.plane ?? TZMESH_PLANE_ORIGIN).replace(/\/+$/, ''), resource: config.resource ?? TZMESH_RESOURCE, fetch: config.fetch ?? ((url, init) => fetch(url, init)), }); async function readJson(response: { ok: boolean; status: number; json(): Promise }): Promise> { const body = await response.json().catch(() => null); return body !== null && typeof body === 'object' ? (body as Record) : {}; } async function metadata(plane: string, fetchFn: TzmeshSignInFetch): Promise { const response = await fetchFn(`${plane}/.well-known/oauth-authorization-server`); const body = await readJson(response); if (!response.ok || typeof body.authorization_endpoint !== 'string' || typeof body.token_endpoint !== 'string') { throw new TzmeshSignInError('unexpected', 'The plane answered no authorization-server metadata.', response.status); } return body as unknown as Metadata; } /** * Register the application as a public client once, naming the one callback * the plane may return the user to, an exact HTTPS address. Keep the * `clientId` it answers in the application's configuration, and register * again where the plane answers `invalid_client`; the name and the * callback's host are what the user's first sign-in shows. */ export async function registerTzmeshClient(config: TzmeshSignInConfig, request: { name: string; redirectUri: string }): Promise<{ clientId: string }> { const { plane, fetch: fetchFn } = settings(config); const meta = await metadata(plane, fetchFn); if (meta.registration_endpoint === undefined) throw new TzmeshSignInError('unexpected', 'The plane names no registration endpoint.'); const response = await fetchFn(meta.registration_endpoint, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ client_name: request.name, redirect_uris: [request.redirectUri], grant_types: ['authorization_code'], response_types: ['code'], token_endpoint_auth_method: 'none', }), }); const body = await readJson(response); if (!response.ok || typeof body.client_id !== 'string') { throw new TzmeshSignInError(typeof body.error === 'string' ? body.error : 'unexpected', 'The plane did not register the client.', response.status); } return { clientId: body.client_id }; } /** * Begin one sign-in: answers the plane's authorization address, which the * application redirects the user's browser to, with the verifier and the * state the application keeps for that sign-in alone. */ export async function beginTzmeshSignIn(config: TzmeshSignInConfig, request: { clientId: string; redirectUri: string }): Promise { const { plane, resource, fetch: fetchFn } = settings(config); const meta = await metadata(plane, fetchFn); const verifier = randomText(48); const state = randomText(24); const url = new URL(meta.authorization_endpoint); url.searchParams.set('response_type', 'code'); url.searchParams.set('client_id', request.clientId); url.searchParams.set('redirect_uri', request.redirectUri); url.searchParams.set('code_challenge', await tzmeshChallengeOf(verifier)); url.searchParams.set('code_challenge_method', 'S256'); url.searchParams.set('state', state); url.searchParams.set('resource', resource); url.searchParams.set('scope', TZMESH_APPLICATION_SCOPE); return { url: url.toString(), verifier, state }; } /** * Finish the sign-in at the application's callback: reads the callback's * query, checks its state against the one begun before anything else, and * exchanges the code for the token. The application then spends the token * on `human` and drops it with the verifier. */ export async function finishTzmeshSignIn( config: TzmeshSignInConfig, request: { clientId: string; redirectUri: string; verifier: string; state: string; query: URLSearchParams | Record }, ): Promise { const { plane, resource, fetch: fetchFn } = settings(config); const query = request.query instanceof URLSearchParams ? request.query : new URLSearchParams(request.query); if (query.get('state') !== request.state) throw new TzmeshSignInError('state_mismatch', 'The callback carries a state this sign-in did not begin.'); const refused = query.get('error'); if (refused !== null) throw new TzmeshSignInError(refused, 'The sign-in did not complete at the plane.'); const code = query.get('code'); if (code === null || code === '') throw new TzmeshSignInError('unexpected', 'The callback carries no code.'); const meta = await metadata(plane, fetchFn); const form = new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: request.redirectUri, client_id: request.clientId, code_verifier: request.verifier, resource, }); const response = await fetchFn(meta.token_endpoint, { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body: form.toString() }); const body = await readJson(response); if (!response.ok || typeof body.access_token !== 'string') { throw new TzmeshSignInError(typeof body.error === 'string' ? body.error : 'unexpected', 'The plane did not exchange the code for a token.', response.status); } return { token: body.access_token, level: typeof body.level === 'string' ? body.level : 'chat', label: typeof body.label === 'string' ? body.label : '', expiresAt: typeof body.expires_at === 'string' ? body.expires_at : null, tokenId: typeof body.token_id === 'string' ? body.token_id : null, }; }