index.ts
services/oauth/index.ts
199
Lines
6554
Bytes
1
Exports
6
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with integrations. It contains 199 lines, 6 detected imports, and 1 detected exports.
Important relationships
- services/oauth/auth-code-listener.ts
- services/oauth/client.ts
- services/oauth/crypto.ts
- services/oauth/getOauthProfile.ts
- commands/add-dir/index.ts
- commands/agents/index.ts
- commands/branch/index.ts
- commands/bridge/index.ts
- commands/btw/index.ts
- commands/chrome/index.ts
- commands/clear/index.ts
- commands/color/index.ts
Detected exports
OAuthService
Keywords
authcodelistenerresponseautomaticclientcodemanualmanualauthcoderesolveroptionsflowauthorizationcode
Detected imports
src/services/analytics/index.js../../utils/browser.js./auth-code-listener.js./client.js./crypto.js./types.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
import { logEvent } from 'src/services/analytics/index.js'
import { openBrowser } from '../../utils/browser.js'
import { AuthCodeListener } from './auth-code-listener.js'
import * as client from './client.js'
import * as crypto from './crypto.js'
import type {
OAuthProfileResponse,
OAuthTokenExchangeResponse,
OAuthTokens,
RateLimitTier,
SubscriptionType,
} from './types.js'
/**
* OAuth service that handles the OAuth 2.0 authorization code flow with PKCE.
*
* Supports two ways to get authorization codes:
* 1. Automatic: Opens browser, redirects to localhost where we capture the code
* 2. Manual: User manually copies and pastes the code (used in non-browser environments)
*/
export class OAuthService {
private codeVerifier: string
private authCodeListener: AuthCodeListener | null = null
private port: number | null = null
private manualAuthCodeResolver: ((authorizationCode: string) => void) | null =
null
constructor() {
this.codeVerifier = crypto.generateCodeVerifier()
}
async startOAuthFlow(
authURLHandler: (url: string, automaticUrl?: string) => Promise<void>,
options?: {
loginWithClaudeAi?: boolean
inferenceOnly?: boolean
expiresIn?: number
orgUUID?: string
loginHint?: string
loginMethod?: string
/**
* Don't call openBrowser(). Caller takes both URLs via authURLHandler
* and decides how/where to open them. Used by the SDK control protocol
* (claude_authenticate) where the SDK client owns the user's display,
* not this process.
*/
skipBrowserOpen?: boolean
},
): Promise<OAuthTokens> {
// Create OAuth callback listener and start it
this.authCodeListener = new AuthCodeListener()
this.port = await this.authCodeListener.start()
// Generate PKCE values and state
const codeChallenge = crypto.generateCodeChallenge(this.codeVerifier)
const state = crypto.generateState()
// Build auth URLs for both automatic and manual flows
const opts = {
codeChallenge,
state,
port: this.port,
loginWithClaudeAi: options?.loginWithClaudeAi,
inferenceOnly: options?.inferenceOnly,
orgUUID: options?.orgUUID,
loginHint: options?.loginHint,
loginMethod: options?.loginMethod,
}
const manualFlowUrl = client.buildAuthUrl({ ...opts, isManual: true })
const automaticFlowUrl = client.buildAuthUrl({ ...opts, isManual: false })
// Wait for either automatic or manual auth code
const authorizationCode = await this.waitForAuthorizationCode(
state,
async () => {
if (options?.skipBrowserOpen) {
// Hand both URLs to the caller. The automatic one still works
// if the caller opens it on the same host (localhost listener
// is running); the manual one works from anywhere.
await authURLHandler(manualFlowUrl, automaticFlowUrl)
} else {
await authURLHandler(manualFlowUrl) // Show manual option to user
await openBrowser(automaticFlowUrl) // Try automatic flow
}
},
)
// Check if the automatic flow is still active (has a pending response)
const isAutomaticFlow = this.authCodeListener?.hasPendingResponse() ?? false
logEvent('tengu_oauth_auth_code_received', { automatic: isAutomaticFlow })
try {
// Exchange authorization code for tokens
const tokenResponse = await client.exchangeCodeForTokens(
authorizationCode,
state,
this.codeVerifier,
this.port!,
!isAutomaticFlow, // Pass isManual=true if it's NOT automatic flow
options?.expiresIn,
)
// Fetch profile info (subscription type and rate limit tier) for the
// returned OAuthTokens. Logout and account storage are handled by the
// caller (installOAuthTokens in auth.ts).
const profileInfo = await client.fetchProfileInfo(
tokenResponse.access_token,
)
// Handle success redirect for automatic flow
if (isAutomaticFlow) {
const scopes = client.parseScopes(tokenResponse.scope)
this.authCodeListener?.handleSuccessRedirect(scopes)
}
return this.formatTokens(
tokenResponse,
profileInfo.subscriptionType,
profileInfo.rateLimitTier,
profileInfo.rawProfile,
)
} catch (error) {
// If we have a pending response, send an error redirect before closing
if (isAutomaticFlow) {
this.authCodeListener?.handleErrorRedirect()
}
throw error
} finally {
// Always cleanup
this.authCodeListener?.close()
}
}
private async waitForAuthorizationCode(
state: string,
onReady: () => Promise<void>,
): Promise<string> {
return new Promise((resolve, reject) => {
// Set up manual auth code resolver
this.manualAuthCodeResolver = resolve
// Start automatic flow
this.authCodeListener
?.waitForAuthorization(state, onReady)
.then(authorizationCode => {
this.manualAuthCodeResolver = null
resolve(authorizationCode)
})
.catch(error => {
this.manualAuthCodeResolver = null
reject(error)
})
})
}
// Handle manual flow callback when user pastes the auth code
handleManualAuthCodeInput(params: {
authorizationCode: string
state: string
}): void {
if (this.manualAuthCodeResolver) {
this.manualAuthCodeResolver(params.authorizationCode)
this.manualAuthCodeResolver = null
// Close the auth code listener since manual input was used
this.authCodeListener?.close()
}
}
private formatTokens(
response: OAuthTokenExchangeResponse,
subscriptionType: SubscriptionType | null,
rateLimitTier: RateLimitTier | null,
profile?: OAuthProfileResponse,
): OAuthTokens {
return {
accessToken: response.access_token,
refreshToken: response.refresh_token,
expiresAt: Date.now() + response.expires_in * 1000,
scopes: client.parseScopes(response.scope),
subscriptionType,
rateLimitTier,
profile,
tokenAccount: response.account
? {
uuid: response.account.uuid,
emailAddress: response.account.email_address,
organizationUuid: response.organization?.uuid,
}
: undefined,
}
}
// Clean up any resources (like the local server)
cleanup(): void {
this.authCodeListener?.close()
this.manualAuthCodeResolver = null
}
}