| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import { OAuthTokenEndpointError } from '@maka/runtime/oauth-login'; |
| import { createProxiedFetchTransport } from '@maka/runtime/network/scoped-fetch-transport'; |
| import { |
| exchangeCodexDeviceAuthorizationCode, |
| pollCodexDeviceAuthorization, |
| startCodexDeviceAuthorization, |
| } from '@maka/runtime/codex-oauth-enrollment'; |
| import { OAuthDeviceAuthorizationExpiredError } from '@maka/runtime/oauth-provider-contracts'; |
| import { |
| GitHubCopilotEntitlementError, |
| GitHubCopilotEntitlementUnavailableError, |
| pollGitHubCopilotDeviceAuthorization, |
| startGitHubCopilotDeviceAuthorization, |
| verifyGitHubCopilotModelEntitlement, |
| } from '@maka/runtime/github-copilot-oauth-enrollment'; |
| import { |
| pollXaiDeviceAuthorization, |
| startXaiDeviceAuthorization, |
| } from '@maka/runtime/xai-oauth-enrollment'; |
| import { |
| serializeOAuthSubscriptionTokens, |
| type OAuthSubscriptionTokens, |
| } from '@maka/runtime/subscription-credentials'; |
| import { |
| RuntimePolicyStoreError, |
| type RuntimePolicyStoresWriter, |
| } from '@maka/storage/runtime-policy-stores'; |
| import { |
| decodeOAuthPresentationResult, |
| OAUTH_PRESENTATION_SERVICE_ID, |
| OAUTH_PRESENTATION_SERVICE_VERSION, |
| type OAuthLoginFailureCode, |
| type OAuthLoginProjection, |
| type OAuthLoginProvider, |
| type OAuthLoginTarget, |
| type OAuthPresentationRequest, |
| type OAuthPresentationResult, |
| type OperationOutcome, |
| } from '../protocol/index.js'; |
| import type { RuntimeHostResidency } from './host-kernel.js'; |
| import { HostOAuthExecutionAuthority } from './oauth-execution-authority.js'; |
| import { |
| ClientCapabilityInvocationError, |
| type HostClientCapabilityCoordinator, |
| } from './client-capability-coordinator.js'; |
| import type { OAuthOperationHandlerMap } from './operation-dispatcher.js'; |
| import type { RuntimePolicyActivationGate } from './runtime-policy-activation-gate.js'; |
| import { toRuntimePolicyProxy } from './runtime-policy-proxy.js'; |
| |
| const MAX_TERMINAL_ATTEMPTS = 256; |
| |
| export class HostOAuthFatalError extends Error { |
| constructor( |
| message: string, |
| readonly fatalCause: unknown, |
| ) { |
| super(message, { cause: fatalCause }); |
| this.name = 'HostOAuthFatalError'; |
| } |
| } |
| |
| export interface HostOAuthCoordinatorInput { |
| readonly runtimePolicy: RuntimePolicyStoresWriter; |
| readonly oauthCredentials?: HostOAuthExecutionAuthority; |
| readonly activation: RuntimePolicyActivationGate; |
| readonly clientCapabilities: HostClientCapabilityCoordinator; |
| readonly isProviderEnabled: (provider: OAuthLoginProvider) => boolean; |
| readonly acquireResidency: () => RuntimeHostResidency; |
| readonly invalidateBackends: () => Promise<void>; |
| readonly onFatal: (error: HostOAuthFatalError) => void; |
| readonly now?: () => number; |
| readonly startXaiAuthorization?: typeof startXaiDeviceAuthorization; |
| readonly pollXaiAuthorization?: typeof pollXaiDeviceAuthorization; |
| readonly startGitHubCopilotAuthorization?: typeof startGitHubCopilotDeviceAuthorization; |
| readonly pollGitHubCopilotAuthorization?: typeof pollGitHubCopilotDeviceAuthorization; |
| readonly verifyGitHubCopilotEntitlement?: typeof verifyGitHubCopilotModelEntitlement; |
| readonly startCodexAuthorization?: typeof startCodexDeviceAuthorization; |
| readonly pollCodexAuthorization?: typeof pollCodexDeviceAuthorization; |
| readonly exchangeCodexCode?: typeof exchangeCodexDeviceAuthorizationCode; |
| readonly createFetchTransport?: typeof createProxiedFetchTransport; |
| } |
| |
| type OAuthLoginAdmission = Extract< |
| Awaited<ReturnType<RuntimePolicyStoresWriter['operations']['beginInteractiveOAuthLogin']>>, |
| { readonly kind: 'ready' } |
| >; |
| |
| interface ActiveLoginAttempt { |
| readonly kind: 'active'; |
| readonly attemptId: string; |
| readonly target: OAuthLoginTarget; |
| readonly connection: OAuthLoginProjection['connection']; |
| readonly initiatingConnectionId: string; |
| readonly provider: OAuthLoginProvider; |
| readonly ticket: OAuthLoginAdmission; |
| readonly abort: AbortController; |
| readonly residency: RuntimeHostResidency; |
| phase: OAuthLoginProjection['phase']; |
| failure?: OAuthLoginFailureCode; |
| cancellationDeferred: boolean; |
| cancelRequested: boolean; |
| settlement: Promise<void>; |
| } |
| |
| interface TerminalLoginAttempt { |
| readonly kind: 'terminal'; |
| readonly target: OAuthLoginTarget; |
| readonly projection: OAuthLoginProjection; |
| } |
| |
| type LoginAttemptRecord = ActiveLoginAttempt | TerminalLoginAttempt; |
| |
| /** Host-owned OAuth enrollment and presentation authority. */ |
| export class HostOAuthCoordinator { |
| readonly handlers: OAuthOperationHandlerMap = { |
| 'oauth.login.start': (input, context) => this.#start(input, context.connectionId), |
| 'oauth.login.query': (input) => this.#query(input.attemptId), |
| 'oauth.login.cancel': (input) => this.#cancel(input.attemptId), |
| 'oauth.enrollment.query': (input) => this.#enrollment(input.provider), |
| }; |
| |
| readonly #runtimePolicy: RuntimePolicyStoresWriter; |
| readonly #oauthCredentials: HostOAuthExecutionAuthority; |
| readonly #activation: RuntimePolicyActivationGate; |
| readonly #clientCapabilities: HostClientCapabilityCoordinator; |
| readonly #isProviderEnabled: (provider: OAuthLoginProvider) => boolean; |
| readonly #acquireResidency: () => RuntimeHostResidency; |
| readonly #invalidateBackends: () => Promise<void>; |
| readonly #onFatal: (error: HostOAuthFatalError) => void; |
| readonly #now: () => number; |
| readonly #startXaiAuthorization: typeof startXaiDeviceAuthorization; |
| readonly #pollXaiAuthorization: typeof pollXaiDeviceAuthorization; |
| readonly #startGitHubCopilotAuthorization: typeof startGitHubCopilotDeviceAuthorization; |
| readonly #pollGitHubCopilotAuthorization: typeof pollGitHubCopilotDeviceAuthorization; |
| readonly #verifyGitHubCopilotEntitlement: typeof verifyGitHubCopilotModelEntitlement; |
| readonly #startCodexAuthorization: typeof startCodexDeviceAuthorization; |
| readonly #pollCodexAuthorization: typeof pollCodexDeviceAuthorization; |
| readonly #exchangeCodexCode: typeof exchangeCodexDeviceAuthorizationCode; |
| readonly #createFetchTransport: typeof createProxiedFetchTransport; |
| readonly #attempts = new Map<string, LoginAttemptRecord>(); |
| #activeAttempt: ActiveLoginAttempt | undefined; |
| /** |
| * Serializes oauth.login.start admissions so concurrent starts cannot dual-open |
| * interactive logins around the active-attempt conflict check. |
| */ |
| #startGate: Promise<void> = Promise.resolve(); |
| #admissionClosed = false; |
| #closeTask: Promise<void> | undefined; |
| |
| constructor(input: HostOAuthCoordinatorInput) { |
| this.#runtimePolicy = input.runtimePolicy; |
| this.#oauthCredentials = |
| input.oauthCredentials ?? new HostOAuthExecutionAuthority(input.runtimePolicy); |
| this.#activation = input.activation; |
| this.#clientCapabilities = input.clientCapabilities; |
| this.#isProviderEnabled = input.isProviderEnabled; |
| this.#acquireResidency = input.acquireResidency; |
| this.#invalidateBackends = input.invalidateBackends; |
| this.#onFatal = input.onFatal; |
| this.#now = input.now ?? Date.now; |
| this.#startXaiAuthorization = input.startXaiAuthorization ?? startXaiDeviceAuthorization; |
| this.#pollXaiAuthorization = input.pollXaiAuthorization ?? pollXaiDeviceAuthorization; |
| this.#startGitHubCopilotAuthorization = |
| input.startGitHubCopilotAuthorization ?? startGitHubCopilotDeviceAuthorization; |
| this.#pollGitHubCopilotAuthorization = |
| input.pollGitHubCopilotAuthorization ?? pollGitHubCopilotDeviceAuthorization; |
| this.#verifyGitHubCopilotEntitlement = |
| input.verifyGitHubCopilotEntitlement ?? verifyGitHubCopilotModelEntitlement; |
| this.#startCodexAuthorization = input.startCodexAuthorization ?? startCodexDeviceAuthorization; |
| this.#pollCodexAuthorization = input.pollCodexAuthorization ?? pollCodexDeviceAuthorization; |
| this.#exchangeCodexCode = input.exchangeCodexCode ?? exchangeCodexDeviceAuthorizationCode; |
| this.#createFetchTransport = input.createFetchTransport ?? createProxiedFetchTransport; |
| } |
| |
| beginDrain(): void { |
| if (this.#admissionClosed) return; |
| this.#admissionClosed = true; |
| if (this.#activeAttempt) { |
| this.#requestCancellation( |
| this.#activeAttempt, |
| new DOMException('Runtime Host is draining', 'AbortError'), |
| ); |
| } |
| } |
| |
| close(): Promise<void> { |
| this.#closeTask ??= this.#closeOnce(); |
| return this.#closeTask; |
| } |
| |
| async #start( |
| input: { readonly attemptId: string; readonly target: OAuthLoginTarget }, |
| initiatingConnectionId: string, |
| ): Promise<OperationOutcome<'oauth.login.start'>> { |
| const existing = this.#attempts.get(input.attemptId); |
| if (existing) { |
| if (!sameOAuthLoginTarget(existing.target, input.target)) { |
| return invalidRequest('OAuth attemptId is already bound to another connection'); |
| } |
| return { ok: true, result: projection(existing) }; |
| } |
| |
| // Claim the start gate before any await so concurrent admissions queue. |
| let releaseGate!: () => void; |
| const previousGate = this.#startGate; |
| this.#startGate = new Promise<void>((resolve) => { |
| releaseGate = resolve; |
| }); |
| await previousGate.catch(() => undefined); |
| try { |
| const again = this.#attempts.get(input.attemptId); |
| if (again) { |
| if (!sameOAuthLoginTarget(again.target, input.target)) { |
| return invalidRequest('OAuth attemptId is already bound to another connection'); |
| } |
| return { ok: true, result: projection(again) }; |
| } |
| let durable: Awaited< |
| ReturnType<RuntimePolicyStoresWriter['operations']['queryInteractiveOAuthLogin']> |
| >; |
| try { |
| durable = await this.#runtimePolicy.operations.queryInteractiveOAuthLogin(input.attemptId); |
| } catch (error) { |
| if (error instanceof RuntimePolicyStoreError) { |
| return persistenceFailure('OAuth login receipt query failed'); |
| } |
| throw error; |
| } |
| if (durable.kind === 'authenticated') { |
| if (!sameOAuthLoginTarget(durable.target, input.target)) { |
| return invalidRequest('OAuth attemptId is already bound to another connection'); |
| } |
| const terminal = authenticatedAttempt(input.target, input.attemptId, durable.connection); |
| this.#attempts.set(input.attemptId, terminal); |
| this.#pruneTerminalAttempts(); |
| return { ok: true, result: terminal.projection }; |
| } |
| if (this.#activeAttempt) { |
| return operationConflict('Another OAuth login is already in progress'); |
| } |
| if (this.#admissionClosed) return hostDraining(); |
| return await this.#prepareStart(input, initiatingConnectionId); |
| } finally { |
| releaseGate(); |
| } |
| } |
| |
| async #prepareStart( |
| input: { readonly attemptId: string; readonly target: OAuthLoginTarget }, |
| initiatingConnectionId: string, |
| ): Promise<OperationOutcome<'oauth.login.start'>> { |
| let admitted: Awaited< |
| ReturnType<RuntimePolicyStoresWriter['operations']['beginInteractiveOAuthLogin']> |
| >; |
| try { |
| admitted = await this.#runtimePolicy.operations.beginInteractiveOAuthLogin(input); |
| } catch (error) { |
| if (error instanceof RuntimePolicyStoreError) { |
| return persistenceFailure('OAuth login admission failed'); |
| } |
| throw error; |
| } |
| if (admitted.kind === 'connection_not_found') { |
| return notFound('OAuth connection was not found'); |
| } |
| if (admitted.kind === 'catalog_full') { |
| return operationConflict('OAuth Connection capacity is exhausted'); |
| } |
| if (admitted.kind === 'slug_taken') { |
| return slugTaken('OAuth Connection slug is already in use'); |
| } |
| if (admitted.kind === 'attempt_conflict') { |
| return invalidRequest('OAuth attemptId is already bound to another connection'); |
| } |
| if (admitted.kind === 'authenticated') { |
| const terminal = authenticatedAttempt(input.target, input.attemptId, admitted.connection); |
| this.#attempts.set(input.attemptId, terminal); |
| this.#pruneTerminalAttempts(); |
| return { ok: true, result: terminal.projection }; |
| } |
| if (admitted.kind !== 'ready') { |
| return invalidRequest('Connection cannot start an interactive OAuth login'); |
| } |
| if (this.#admissionClosed) return hostDraining(); |
| if (!this.#isProviderEnabled(admitted.connection.providerType)) { |
| return operationUnavailable('OAuth enrollment is disabled for this provider'); |
| } |
| if ( |
| !this.#clientCapabilities.hasService( |
| initiatingConnectionId, |
| OAUTH_PRESENTATION_SERVICE_ID, |
| OAUTH_PRESENTATION_SERVICE_VERSION, |
| ) |
| ) { |
| return { |
| ok: false, |
| error: { |
| code: 'capability_unavailable', |
| message: 'Initiating Client cannot present this OAuth login', |
| }, |
| }; |
| } |
| const attempt: ActiveLoginAttempt = { |
| kind: 'active', |
| attemptId: input.attemptId, |
| target: input.target, |
| connection: admitted.identity, |
| initiatingConnectionId, |
| provider: admitted.connection.providerType, |
| ticket: admitted, |
| abort: new AbortController(), |
| residency: this.#acquireResidency(), |
| phase: 'awaiting_authorization', |
| cancellationDeferred: false, |
| cancelRequested: false, |
| settlement: Promise.resolve(), |
| }; |
| this.#attempts.set(attempt.attemptId, attempt); |
| this.#activeAttempt = attempt; |
| attempt.settlement = this.#runLogin(attempt); |
| observe(attempt.settlement); |
| return { ok: true, result: projection(attempt) }; |
| } |
| |
| async #query(attemptId: string): Promise<OperationOutcome<'oauth.login.query'>> { |
| const attempt = this.#attempts.get(attemptId); |
| if (attempt) return { ok: true, result: projection(attempt) }; |
| let durable: Awaited< |
| ReturnType<RuntimePolicyStoresWriter['operations']['queryInteractiveOAuthLogin']> |
| >; |
| try { |
| durable = await this.#runtimePolicy.operations.queryInteractiveOAuthLogin(attemptId); |
| } catch (error) { |
| if (error instanceof RuntimePolicyStoreError) { |
| return persistenceFailure('OAuth login receipt query failed'); |
| } |
| throw error; |
| } |
| if (durable.kind === 'not_found') return notFound('OAuth login was not found'); |
| const terminal = authenticatedAttempt(durable.target, attemptId, durable.connection); |
| this.#attempts.set(attemptId, terminal); |
| this.#pruneTerminalAttempts(); |
| return { ok: true, result: terminal.projection }; |
| } |
| |
| async #cancel(attemptId: string): Promise<OperationOutcome<'oauth.login.cancel'>> { |
| const attempt = this.#attempts.get(attemptId); |
| if (!attempt) return this.#query(attemptId); |
| if (attempt.kind === 'active') { |
| this.#requestCancellation(attempt, new DOMException('OAuth login cancelled', 'AbortError')); |
| } |
| return { ok: true, result: projection(attempt) }; |
| } |
| |
| // The Host owns the enrollment gate: whether a provider may begin an |
| // interactive login is this Host's answer, and only the Host has it. Surfaces |
| // read it to avoid presenting a primary action that a default install refuses. |
| #enrollment(provider: OAuthLoginProvider): Promise<OperationOutcome<'oauth.enrollment.query'>> { |
| return Promise.resolve({ |
| ok: true, |
| result: { provider, enabled: this.#isProviderEnabled(provider) }, |
| }); |
| } |
| |
| #requestCancellation(attempt: ActiveLoginAttempt, reason: Error): void { |
| attempt.cancelRequested = true; |
| if (attempt.cancellationDeferred) return; |
| attempt.phase = 'cancelled'; |
| attempt.abort.abort(reason); |
| } |
| |
| async #runLogin(attempt: ActiveLoginAttempt): Promise<void> { |
| let transport: ReturnType<typeof createProxiedFetchTransport> | undefined; |
| try { |
| transport = createProxiedFetchTransport( |
| toRuntimePolicyProxy( |
| attempt.ticket.networkProxy, |
| attempt.ticket.secretMaterial.networkProxy?.secret, |
| ), |
| ); |
| // Switched rather than defaulted: routing any future provider into an |
| // unrelated device flow must be a compiler error, not a silent default. |
| const tokens = await this.#runProviderLogin(attempt, transport.fetch); |
| attempt.abort.signal.throwIfAborted(); |
| attempt.cancellationDeferred = true; |
| attempt.phase = 'committing'; |
| await this.#activation.runMutation(async () => { |
| const completion = await this.#runtimePolicy.operations.completeInteractiveOAuthLogin( |
| attempt.ticket.ticket, |
| serializeOAuthSubscriptionTokens(tokens), |
| ); |
| if (completion.kind === 'slug_taken') { |
| throw new LoginFailure('slug_taken'); |
| } |
| if (completion.kind !== 'committed') { |
| throw new LoginFailure( |
| completion.changed.includes('connection') ? 'connection_changed' : 'credential_changed', |
| ); |
| } |
| await this.#invalidateAfterCredentialMutation(); |
| }); |
| attempt.phase = 'authenticated'; |
| } catch (error) { |
| if (!attempt.cancellationDeferred && attempt.abort.signal.aborted) { |
| attempt.phase = 'cancelled'; |
| } else { |
| attempt.phase = 'failed'; |
| attempt.failure = loginFailureCode(error); |
| if (isCommitOutcomeUnknown(error)) { |
| this.#onFatal(new HostOAuthFatalError('OAuth login commit outcome is unknown', error)); |
| } |
| } |
| } finally { |
| if (transport) await transport.close().catch(() => undefined); |
| if (this.#activeAttempt === attempt) this.#activeAttempt = undefined; |
| attempt.residency.release(); |
| if (this.#attempts.get(attempt.attemptId) === attempt) { |
| this.#attempts.set(attempt.attemptId, terminalAttempt(attempt)); |
| this.#pruneTerminalAttempts(); |
| } |
| } |
| } |
| |
| async #runCodexDeviceLogin(attempt: ActiveLoginAttempt, fetchFn: typeof fetch) { |
| const authorization = await this.#startCodexAuthorization({ |
| fetchFn, |
| signal: attempt.abort.signal, |
| now: this.#now, |
| }); |
| await this.#present(attempt, { |
| method: 'open_external', |
| url: authorization.verificationUrl, |
| stateHint: authorization.userCode, |
| }); |
| attempt.phase = 'exchanging'; |
| const grant = await this.#pollCodexAuthorization({ |
| authorization, |
| fetchFn, |
| signal: attempt.abort.signal, |
| now: this.#now, |
| onPollAdmission: () => { |
| attempt.cancellationDeferred = true; |
| }, |
| onPollRetry: () => { |
| attempt.cancellationDeferred = false; |
| if (attempt.cancelRequested) { |
| this.#requestCancellation( |
| attempt, |
| new DOMException('OAuth login cancelled', 'AbortError'), |
| ); |
| } |
| }, |
| }); |
| return this.#exchangeCodexCode({ |
| grant, |
| fetchFn, |
| signal: attempt.abort.signal, |
| now: this.#now, |
| }); |
| } |
| |
| #runProviderLogin( |
| attempt: ActiveLoginAttempt, |
| fetchFn: typeof fetch, |
| ): Promise<OAuthSubscriptionTokens> { |
| switch (attempt.provider) { |
| case 'xai-oauth': |
| return this.#runXaiLogin(attempt, fetchFn); |
| case 'openai-codex': |
| return this.#runCodexDeviceLogin(attempt, fetchFn); |
| case 'github-copilot': |
| return this.#runGitHubCopilotLogin(attempt, fetchFn); |
| } |
| } |
| |
| async #runGitHubCopilotLogin(attempt: ActiveLoginAttempt, fetchFn: typeof fetch) { |
| const authorization = await this.#startGitHubCopilotAuthorization({ |
| fetchFn, |
| signal: attempt.abort.signal, |
| now: this.#now, |
| }); |
| await this.#present(attempt, { |
| method: 'open_external', |
| url: authorization.verificationUrl, |
| stateHint: authorization.userCode, |
| }); |
| attempt.phase = 'exchanging'; |
| const tokens = await this.#pollGitHubCopilotAuthorization({ |
| authorization, |
| fetchFn, |
| signal: attempt.abort.signal, |
| now: this.#now, |
| onPollAdmission: () => { |
| attempt.cancellationDeferred = true; |
| }, |
| onPollRetry: () => { |
| attempt.cancellationDeferred = false; |
| if (attempt.cancelRequested) { |
| this.#requestCancellation( |
| attempt, |
| new DOMException('OAuth login cancelled', 'AbortError'), |
| ); |
| } |
| }, |
| }); |
| // A GitHub account is not a Copilot subscription. Adopt the account only |
| // once the provider says it can reach a model, so the commit below never |
| // stores a credential the connection cannot use. |
| await this.#verifyGitHubCopilotEntitlement({ tokens, fetchFn }); |
| return tokens; |
| } |
| |
| async #runXaiLogin(attempt: ActiveLoginAttempt, fetchFn: typeof fetch) { |
| const authorization = await this.#startXaiAuthorization({ |
| fetchFn, |
| signal: attempt.abort.signal, |
| now: this.#now, |
| }); |
| await this.#present(attempt, { |
| method: 'open_external', |
| url: authorization.verificationUrl, |
| stateHint: authorization.userCode, |
| }); |
| attempt.phase = 'exchanging'; |
| return this.#pollXaiAuthorization({ |
| authorization, |
| fetchFn, |
| signal: attempt.abort.signal, |
| now: this.#now, |
| onPollAdmission: () => { |
| attempt.cancellationDeferred = true; |
| }, |
| onPollRetry: () => { |
| attempt.cancellationDeferred = false; |
| if (attempt.cancelRequested) { |
| this.#requestCancellation( |
| attempt, |
| new DOMException('OAuth login cancelled', 'AbortError'), |
| ); |
| } |
| }, |
| }); |
| } |
| |
| async #present( |
| attempt: ActiveLoginAttempt, |
| request: OAuthPresentationRequest, |
| ): Promise<OAuthPresentationResult> { |
| const { method, ...input } = request; |
| let result: Awaited<ReturnType<HostClientCapabilityCoordinator['callService']>>; |
| try { |
| result = await this.#clientCapabilities.callService({ |
| connectionId: attempt.initiatingConnectionId, |
| serviceId: OAUTH_PRESENTATION_SERVICE_ID, |
| version: OAUTH_PRESENTATION_SERVICE_VERSION, |
| method, |
| input, |
| signal: attempt.abort.signal, |
| }); |
| } catch (error) { |
| if (error instanceof ClientCapabilityInvocationError) { |
| throw new LoginFailure('capability_unavailable'); |
| } |
| throw error; |
| } |
| try { |
| return decodeOAuthPresentationResult(method, result); |
| } catch { |
| throw new LoginFailure('authorization_failed'); |
| } |
| } |
| |
| async #invalidateAfterCredentialMutation(): Promise<void> { |
| try { |
| await this.#invalidateBackends(); |
| } catch (error) { |
| const fatal = new HostOAuthFatalError( |
| 'OAuth login committed but backend invalidation failed', |
| error, |
| ); |
| this.#onFatal(fatal); |
| throw fatal; |
| } |
| } |
| |
| #pruneTerminalAttempts(): void { |
| const terminalIds = [...this.#attempts] |
| .filter(([, attempt]) => attempt.kind === 'terminal') |
| .map(([attemptId]) => attemptId); |
| for (const attemptId of terminalIds.slice(0, -MAX_TERMINAL_ATTEMPTS)) { |
| this.#attempts.delete(attemptId); |
| } |
| } |
| |
| async #closeOnce(): Promise<void> { |
| this.beginDrain(); |
| const active = this.#activeAttempt; |
| if (active) await active.settlement; |
| } |
| } |
| |
| class LoginFailure extends Error { |
| constructor(readonly code: OAuthLoginFailureCode) { |
| super(code); |
| } |
| } |
| |
| function projection(attempt: LoginAttemptRecord): OAuthLoginProjection { |
| if (attempt.kind === 'terminal') return attempt.projection; |
| return { |
| attemptId: attempt.attemptId, |
| connection: attempt.connection, |
| phase: attempt.phase, |
| ...(attempt.phase === 'failed' ? { failure: attempt.failure ?? 'internal_failure' } : {}), |
| }; |
| } |
| |
| function terminalAttempt(attempt: ActiveLoginAttempt): TerminalLoginAttempt { |
| return Object.freeze({ |
| kind: 'terminal', |
| target: attempt.target, |
| projection: Object.freeze(projection(attempt)), |
| }); |
| } |
| |
| function authenticatedAttempt( |
| target: OAuthLoginTarget, |
| attemptId: string, |
| connection: OAuthLoginProjection['connection'], |
| ): TerminalLoginAttempt { |
| return Object.freeze({ |
| kind: 'terminal', |
| target: structuredClone(target), |
| projection: Object.freeze({ |
| attemptId, |
| connection: structuredClone(connection), |
| phase: 'authenticated', |
| }), |
| }); |
| } |
| |
| function sameOAuthLoginTarget(actual: OAuthLoginTarget, expected: OAuthLoginTarget): boolean { |
| return ( |
| actual.kind === expected.kind && |
| (actual.kind === 'create' |
| ? expected.kind === 'create' && |
| actual.providerType === expected.providerType && |
| actual.slug === expected.slug && |
| actual.name === expected.name |
| : expected.kind === 'existing' && actual.connectionId === expected.connectionId) |
| ); |
| } |
| |
| function loginFailureCode(error: unknown): OAuthLoginFailureCode { |
| if (error instanceof LoginFailure) return error.code; |
| if (error instanceof RuntimePolicyStoreError) return 'persistence_failed'; |
| // The account authorized the grant and the provider then refused it: the |
| // login worked, the subscription behind it did not. |
| if (error instanceof GitHubCopilotEntitlementError) return 'provider_rejected'; |
| // The provider never answered the entitlement question. Nothing is known |
| // about the subscription, so this is a login that did not complete — the |
| // user retries, they do not go looking for a plan they already have. |
| if (error instanceof GitHubCopilotEntitlementUnavailableError) return 'authorization_failed'; |
| // A local device window that elapsed without approval is a timeout, not |
| // a provider rejection of the account. |
| if (error instanceof OAuthDeviceAuthorizationExpiredError) return 'authorization_failed'; |
| if (error instanceof OAuthTokenEndpointError) { |
| return error.category === 'invalid_grant' || error.category === 'invalid_token' |
| ? 'provider_rejected' |
| : 'authorization_failed'; |
| } |
| return 'internal_failure'; |
| } |
| |
| function isCommitOutcomeUnknown(error: unknown): error is RuntimePolicyStoreError { |
| return error instanceof RuntimePolicyStoreError && error.code === 'commit_outcome_unknown'; |
| } |
| |
| function invalidRequest(message: string) { |
| return { ok: false, error: { code: 'invalid_request', message } } as const; |
| } |
| |
| function notFound(message: string) { |
| return { ok: false, error: { code: 'not_found', message } } as const; |
| } |
| |
| function persistenceFailure(message: string) { |
| return { ok: false, error: { code: 'persistence_failed', message } } as const; |
| } |
| |
| function operationUnavailable(message: string) { |
| return { ok: false, error: { code: 'operation_unavailable', message } } as const; |
| } |
| |
| function operationConflict(message: string) { |
| return { ok: false, error: { code: 'operation_conflict', message } } as const; |
| } |
| |
| function slugTaken(message: string) { |
| return { ok: false, error: { code: 'slug_taken', message } } as const; |
| } |
| |
| function hostDraining(): OperationOutcome<'oauth.login.start'> { |
| return { |
| ok: false, |
| error: { code: 'host_draining', message: 'Runtime Host is draining' }, |
| }; |
| } |
| |
| function observe(task: Promise<unknown>): void { |
| void task.catch(() => undefined); |
| } |