diff --git a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/AuthenticationError.kt b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/AuthenticationError.kt index 1a9d1c571983..3535471ab4be 100644 --- a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/AuthenticationError.kt +++ b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/AuthenticationError.kt @@ -16,7 +16,6 @@ sealed class AuthenticationError { "network-error" -> context.deserialize(element, NetworkAuthError::class.java) "invalid-access-token" -> context.deserialize(element, InvalidAccessTokenError::class.java) "enterprise-user-logged-into-dotcom" -> context.deserialize(element, EnterpriseUserDotComError::class.java) - "auth-config-error" -> context.deserialize(element, AuthConfigError::class.java) else -> throw Exception("Unknown discriminator ${element}") } } @@ -51,14 +50,3 @@ data class EnterpriseUserDotComError( } } -data class AuthConfigError( - val title: String? = null, - val message: String, - val type: TypeEnum, // Oneof: auth-config-error -) : AuthenticationError() { - - enum class TypeEnum { - @SerializedName("auth-config-error") `Auth-config-error`, - } -} - diff --git a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/CodyContextFilterItem.kt b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/CodyContextFilterItem.kt index d54012c2afbb..471920ca5ab1 100644 --- a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/CodyContextFilterItem.kt +++ b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/CodyContextFilterItem.kt @@ -1,15 +1,8 @@ @file:Suppress("FunctionName", "ClassName", "unused", "EnumEntryName", "UnusedImport") package com.sourcegraph.cody.agent.protocol_generated; -import com.google.gson.annotations.SerializedName; - data class CodyContextFilterItem( - val repoNamePattern: RepoNamePatternEnum, // Oneof: .* + val repoNamePattern: String, val filePathPatterns: List? = null, -) { - - enum class RepoNamePatternEnum { - @SerializedName(".*") Wildcard, - } -} +) diff --git a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Constants.kt b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Constants.kt index 5e6294c2ffc5..515d5e785346 100644 --- a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Constants.kt +++ b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Constants.kt @@ -2,7 +2,6 @@ package com.sourcegraph.cody.agent.protocol_generated; object Constants { - const val Wildcard = ".*" const val Applied = "Applied" const val Applying = "Applying" const val Automatic = "Automatic" @@ -16,7 +15,6 @@ object Constants { const val agentic = "agentic" const val ask = "ask" const val assistant = "assistant" - const val `auth-config-error` = "auth-config-error" const val authenticated = "authenticated" const val autocomplete = "autocomplete" const val balanced = "balanced" diff --git a/agent/scripts/reverse-proxy.py b/agent/scripts/reverse-proxy.py deleted file mode 100755 index bba319477c4c..000000000000 --- a/agent/scripts/reverse-proxy.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python3 - -from aiohttp import web, ClientSession -from urllib.parse import urlparse -import argparse -import asyncio -import re - -async def proxy_handler(request): - async with ClientSession(auto_decompress=False) as session: - print(f'Request to: {request.url}') - - # Modify headers here - headers = dict(request.headers) - - # Reset the Host header to use target server host instead of the proxy host - if 'Host' in headers: - headers['Host'] = urlparse(target_url).netloc.split(':')[0] - - # 'chunked' encoding results in error 400 from Cloudflare, removing it still keeps response chunked anyway - if 'Transfer-Encoding' in headers: - del headers['Transfer-Encoding'] - - # Use value of 'Authorization: Bearer' to fill 'X-Forwarded-User' and remove 'Authorization' header - if 'Authorization' in headers: - match = re.match('Bearer (.*)', headers['Authorization']) - if match: - headers['X-Forwarded-User'] = match.group(1) - del headers['Authorization'] - - # Forward the request to target - async with session.request( - method=request.method, - url=f'{target_url}{request.path_qs}', - headers=headers, - data=await request.read() - ) as response: - proxy_response = web.StreamResponse( - status=response.status, - headers=response.headers - ) - - await proxy_response.prepare(request) - - # Stream the response back - async for chunk in response.content.iter_chunks(): - await proxy_response.write(chunk[0]) - - await proxy_response.write_eof() - return proxy_response - -app = web.Application() -app.router.add_route('*', '/{path_info:.*}', proxy_handler) - -""" -Reverse Proxy Server for testing External Auth Providers in Cody - -This script implements a simple reverse proxy server to facilitate testing of external authentication providers -with Cody. It's role is to simulate simulate HTTP authentication proxy setups. It handles incoming requests by: -- Forwarding them to a target Sourcegraph instance -- Converting Bearer tokens from Authorization headers into X-Forwarded-User headers -- Managing request/response streaming -- Handling header modifications required for Cloudflare compatibility - -Target Sourcegraph instance needs to be configured to use HTTP authentication proxies -as described in https://sourcegraph.com/docs/admin/auth#http-authentication-proxies -""" -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='External auth provider test proxy server') - parser.add_argument('target_url', help='Target Sourcegraph instance URL to proxy to') - parser.add_argument('proxy_port', type=int, nargs='?', default=5555, - help='Port for the proxy server (default: %(default)s)') - - args = parser.parse_args() - - target_url = args.target_url.rstrip('/') - port = args.proxy_port - - print(f'Starting proxy server on port {port} targeting {target_url}...') - web.run_app(app, port=port) diff --git a/agent/scripts/simple-external-auth-provider.py b/agent/scripts/simple-external-auth-provider.py deleted file mode 100755 index 11209b7ed4f5..000000000000 --- a/agent/scripts/simple-external-auth-provider.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -import json -import time -from datetime import datetime - -def generate_credentials(): - current_epoch = int(time.time()) + 100 - - credentials = { - "headers": { - "Authorization": "Bearer SomeUser", - "Expiration": current_epoch, - }, - "expiration": current_epoch - } - - # Print JSON to stdout - print(json.dumps(credentials)) - -if __name__ == "__main__": - generate_credentials() diff --git a/agent/src/AgentWorkspaceConfiguration.ts b/agent/src/AgentWorkspaceConfiguration.ts index 58e504f1e970..ac8df4a37045 100644 --- a/agent/src/AgentWorkspaceConfiguration.ts +++ b/agent/src/AgentWorkspaceConfiguration.ts @@ -116,7 +116,7 @@ export class AgentWorkspaceConfiguration implements vscode.WorkspaceConfiguratio function mergeWithBaseConfig(config: any) { for (const [key, value] of Object.entries(config)) { - if (typeof value === 'object' && !Array.isArray(value)) { + if (typeof value === 'object') { const existing = _.get(baseConfig, key) ?? {} const merged = _.merge(existing, value) _.set(baseConfig, key, merged) diff --git a/agent/src/agent.ts b/agent/src/agent.ts index 42f8b2fe8171..1bad13eda865 100644 --- a/agent/src/agent.ts +++ b/agent/src/agent.ts @@ -1486,10 +1486,11 @@ export class Agent extends MessageHandler implements ExtensionClient { config: ExtensionConfiguration, params?: { forceAuthentication: boolean } ): Promise { - const isAuthChange = vscode_shim.isTokenOrEndpointChange(config) + const isAuthChange = vscode_shim.isAuthenticationChange(config) vscode_shim.setExtensionConfiguration(config) - // If this is an token or endpoint change we need to save them prior to firing events that update the clients + // If this is an authentication change we need to reauthenticate prior to firing events + // that update the clients try { if ((isAuthChange || params?.forceAuthentication) && config.serverEndpoint) { await authProvider.validateAndStoreCredentials( @@ -1499,9 +1500,7 @@ export class Agent extends MessageHandler implements ExtensionClient { }, auth: { serverEndpoint: config.serverEndpoint, - credentials: config.accessToken - ? { token: config.accessToken, source: 'paste' } - : undefined, + accessToken: config.accessToken ?? null, }, clientState: { anonymousUserID: config.anonymousUserID ?? null, diff --git a/agent/src/cli/command-auth/AuthenticatedAccount.ts b/agent/src/cli/command-auth/AuthenticatedAccount.ts index a631d11827e8..9a228e3fb224 100644 --- a/agent/src/cli/command-auth/AuthenticatedAccount.ts +++ b/agent/src/cli/command-auth/AuthenticatedAccount.ts @@ -42,10 +42,7 @@ export class AuthenticatedAccount { ): Promise { const graphqlClient = SourcegraphGraphQLAPIClient.withStaticConfig({ configuration: { telemetryLevel: 'agent' }, - auth: { - credentials: { token: options.accessToken }, - serverEndpoint: options.endpoint, - }, + auth: { accessToken: options.accessToken, serverEndpoint: options.endpoint }, clientState: { anonymousUserID: null }, }) const userInfo = await graphqlClient.getCurrentUserInfo() diff --git a/agent/src/cli/command-auth/command-login.ts b/agent/src/cli/command-auth/command-login.ts index dacd412bcd33..2cc9aedb745d 100644 --- a/agent/src/cli/command-auth/command-login.ts +++ b/agent/src/cli/command-auth/command-login.ts @@ -123,7 +123,7 @@ async function loginAction( : await captureAccessTokenViaBrowserRedirect(serverEndpoint, spinner) const client = SourcegraphGraphQLAPIClient.withStaticConfig({ configuration: { telemetryLevel: 'agent' }, - auth: { credentials: { token: options.accessToken }, serverEndpoint: serverEndpoint }, + auth: { accessToken: token, serverEndpoint: serverEndpoint }, clientState: { anonymousUserID: null }, }) const userInfo = await client.getCurrentUserInfo() @@ -256,7 +256,7 @@ async function promptUserAboutLoginMethod(spinner: Ora, options: LoginOptions): try { const client = SourcegraphGraphQLAPIClient.withStaticConfig({ configuration: { telemetryLevel: 'agent' }, - auth: { credentials: { token: options.accessToken }, serverEndpoint: options.endpoint }, + auth: { accessToken: options.accessToken, serverEndpoint: options.endpoint }, clientState: { anonymousUserID: null }, }) const userInfo = await client.getCurrentUserInfo() diff --git a/agent/src/cli/command-bench/command-bench.ts b/agent/src/cli/command-bench/command-bench.ts index 98dbc1025df5..17da06851ff5 100644 --- a/agent/src/cli/command-bench/command-bench.ts +++ b/agent/src/cli/command-bench/command-bench.ts @@ -331,7 +331,7 @@ export const benchCommand = new commander.Command('bench') setStaticResolvedConfigurationWithAuthCredentials({ configuration: { customHeaders: {} }, auth: { - credentials: { token: options.srcAccessToken }, + accessToken: options.srcAccessToken, serverEndpoint: options.srcEndpoint, }, }) diff --git a/agent/src/cli/command-bench/llm-judge.ts b/agent/src/cli/command-bench/llm-judge.ts index cf6f6c3b076d..8df9d4510c2f 100644 --- a/agent/src/cli/command-bench/llm-judge.ts +++ b/agent/src/cli/command-bench/llm-judge.ts @@ -16,10 +16,7 @@ export class LlmJudge { localStorage.setStorage('noop') setStaticResolvedConfigurationWithAuthCredentials({ configuration: { customHeaders: undefined }, - auth: { - credentials: { token: options.srcAccessToken }, - serverEndpoint: options.srcEndpoint, - }, + auth: { accessToken: options.srcAccessToken, serverEndpoint: options.srcEndpoint }, }) setClientCapabilities({ configuration: {}, agentCapabilities: undefined }) this.client = new SourcegraphNodeCompletionsClient() diff --git a/agent/src/cli/scip-codegen/emitters/KotlinEmitter.ts b/agent/src/cli/scip-codegen/emitters/KotlinEmitter.ts index e3d5b67b3d56..7814d66d849c 100644 --- a/agent/src/cli/scip-codegen/emitters/KotlinEmitter.ts +++ b/agent/src/cli/scip-codegen/emitters/KotlinEmitter.ts @@ -265,7 +265,7 @@ export class KotlinFormatter extends Formatter { } override formatFieldName(name: string): string { - const escaped = name.replace('.*', 'Wildcard').replace(':', '_').replace('/', '_') + const escaped = name.replace(':', '_').replace('/', '_') const isKeyword = this.options.reserved.has(escaped) const needsBacktick = isKeyword || !/^[a-zA-Z0-9_]+$/.test(escaped) // Replace all non-alphanumeric characters with underscores diff --git a/agent/src/local-e2e/helpers.ts b/agent/src/local-e2e/helpers.ts index f6a9d64569b1..703a4817c061 100644 --- a/agent/src/local-e2e/helpers.ts +++ b/agent/src/local-e2e/helpers.ts @@ -96,10 +96,7 @@ export class LocalSGInstance { // for checking the LLM configuration section. this.gqlclient = SourcegraphGraphQLAPIClient.withStaticConfig({ configuration: { customHeaders: headers, telemetryLevel: 'agent' }, - auth: { - credentials: { token: this.params.accessToken }, - serverEndpoint: this.params.serverEndpoint, - }, + auth: { accessToken: this.params.accessToken, serverEndpoint: this.params.serverEndpoint }, clientState: { anonymousUserID: null }, }) } diff --git a/agent/src/vscode-shim.ts b/agent/src/vscode-shim.ts index c2d258b6d20d..9b856be75ae2 100644 --- a/agent/src/vscode-shim.ts +++ b/agent/src/vscode-shim.ts @@ -136,8 +136,7 @@ export let extensionConfiguration: ExtensionConfiguration | undefined export function setExtensionConfiguration(newConfig: ExtensionConfiguration): void { extensionConfiguration = newConfig } - -export function isTokenOrEndpointChange(newConfig: ExtensionConfiguration): boolean { +export function isAuthenticationChange(newConfig: ExtensionConfiguration): boolean { if (!extensionConfiguration) { return true } diff --git a/lib/shared/src/auth/types.ts b/lib/shared/src/auth/types.ts index cca6b6bd6d3a..1a5bc31babf9 100644 --- a/lib/shared/src/auth/types.ts +++ b/lib/shared/src/auth/types.ts @@ -62,15 +62,7 @@ export interface EnterpriseUserDotComError { enterprise: string } -export interface AuthConfigError extends AuthenticationErrorMessage { - type: 'auth-config-error' -} - -export type AuthenticationError = - | NetworkAuthError - | InvalidAccessTokenError - | EnterpriseUserDotComError - | AuthConfigError +export type AuthenticationError = NetworkAuthError | InvalidAccessTokenError | EnterpriseUserDotComError export interface AuthenticationErrorMessage { title?: string @@ -98,8 +90,6 @@ export function getAuthErrorMessage(error: AuthenticationError): AuthenticationE "in through your organization's enterprise instance instead. If you need assistance " + 'please contact your Sourcegraph admin.', } - case 'auth-config-error': - return error } } diff --git a/lib/shared/src/configuration.ts b/lib/shared/src/configuration.ts index 10397e508d46..fc38f13a51be 100644 --- a/lib/shared/src/configuration.ts +++ b/lib/shared/src/configuration.ts @@ -17,19 +17,8 @@ export type TokenSource = 'redirect' | 'paste' */ export interface AuthCredentials { serverEndpoint: string - credentials: HeaderCredential | TokenCredential | undefined - error?: any -} - -export interface HeaderCredential { - // We use function instead of property to prevent accidential top level serialization - we never want to store this data - getHeaders(): Record - expiration: number | undefined -} - -export interface TokenCredential { - token: string - source?: TokenSource + accessToken: string | null + tokenSource?: TokenSource | undefined } export interface AutoEditsTokenLimit { @@ -82,19 +71,6 @@ export interface AgenticContextConfiguration { } } -export interface ExternalAuthCommand { - commandLine: readonly string[] - environment?: Record - shell?: string - timeout?: number - windowsHide?: boolean -} - -export interface ExternalAuthProvider { - endpoint: string - executable: ExternalAuthCommand -} - interface RawClientConfiguration { net: NetConfiguration codebase?: string @@ -189,8 +165,6 @@ interface RawClientConfiguration { */ overrideServerEndpoint?: string | undefined overrideAuthToken?: string | undefined - - authExternalProviders: ExternalAuthProvider[] } /** diff --git a/lib/shared/src/configuration/auth-resolver.test.ts b/lib/shared/src/configuration/auth-resolver.test.ts deleted file mode 100644 index 9b779fe707e8..000000000000 --- a/lib/shared/src/configuration/auth-resolver.test.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { describe, expect, test } from 'vitest' -import { type HeaderCredential, type TokenSource, isWindows } from '..' -import { resolveAuth } from './auth-resolver' -import type { ClientSecrets } from './resolver' - -class TempClientSecrets implements ClientSecrets { - constructor(readonly store: Map) {} - - getToken(endpoint: string): Promise { - return Promise.resolve(this.store.get(endpoint)?.[0]) - } - getTokenSource(endpoint: string): Promise { - return Promise.resolve(this.store.get(endpoint)?.[1]) - } -} - -describe('auth-resolver', () => { - test('resolve with serverEndpoint and credentials overrides', async () => { - const auth = await resolveAuth( - 'sourcegraph.com', - { - authExternalProviders: [], - overrideServerEndpoint: 'my-endpoint.com', - overrideAuthToken: 'my-token', - }, - new TempClientSecrets(new Map([['sourcegraph.com/', ['sgp_212323123', 'paste']]])) - ) - - expect(auth.serverEndpoint).toBe('my-endpoint.com/') - expect(auth.credentials).toEqual({ token: 'my-token' }) - }) - - test('resolve with serverEndpoint override', async () => { - const auth = await resolveAuth( - 'sourcegraph.com', - { - authExternalProviders: [], - overrideServerEndpoint: 'my-endpoint.com', - overrideAuthToken: undefined, - }, - new TempClientSecrets(new Map([['my-endpoint.com/', ['sgp_212323123', 'paste']]])) - ) - - expect(auth.serverEndpoint).toBe('my-endpoint.com/') - expect(auth.credentials).toEqual({ token: 'sgp_212323123', source: 'paste' }) - }) - - test('resolve with token override', async () => { - const auth = await resolveAuth( - 'sourcegraph.com', - { - authExternalProviders: [], - overrideServerEndpoint: undefined, - overrideAuthToken: 'my-token', - }, - new TempClientSecrets(new Map([['sourcegraph.com/', ['sgp_777777777', 'paste']]])) - ) - - expect(auth.serverEndpoint).toBe('sourcegraph.com/') - expect(auth.credentials).toEqual({ token: 'my-token' }) - }) - - test('resolve custom auth provider', async () => { - const futureEpoch = Date.UTC(2050) / 1000 - const credentialsJson = JSON.stringify({ - headers: { Authorization: 'token X' }, - expiration: futureEpoch, - }) - - const auth = await resolveAuth( - 'sourcegraph.com', - { - authExternalProviders: [ - { - endpoint: 'https://my-server.com', - executable: { - commandLine: [ - isWindows() ? `echo ${credentialsJson}` : `echo '${credentialsJson}'`, - ], - shell: isWindows() ? process.env.ComSpec : '/bin/bash', - timeout: 5000, - windowsHide: true, - }, - }, - ], - overrideServerEndpoint: 'https://my-server.com', - overrideAuthToken: undefined, - }, - new TempClientSecrets(new Map()) - ) - - expect(auth.serverEndpoint).toBe('https://my-server.com/') - - const headerCredential = auth.credentials as HeaderCredential - expect(headerCredential.expiration).toBe(futureEpoch) - expect(headerCredential.getHeaders()).toStrictEqual({ - Authorization: 'token X', - }) - - expect(JSON.stringify(headerCredential)).not.toContain('token X') - }) - - test('resolve custom auth provider error handling - bad JSON', async () => { - const auth = await resolveAuth( - 'sourcegraph.com', - { - authExternalProviders: [ - { - endpoint: 'https://my-server.com', - executable: { - commandLine: ['echo x'], - shell: isWindows() ? process.env.ComSpec : '/bin/bash', - timeout: 5000, - windowsHide: true, - }, - }, - ], - overrideServerEndpoint: 'https://my-server.com', - overrideAuthToken: undefined, - }, - new TempClientSecrets(new Map()) - ) - - expect(auth.serverEndpoint).toBe('https://my-server.com/') - - expect(auth.credentials).toBe(undefined) - expect(auth.error.message).toContain('Failed to execute external auth command: Unexpected token') - }) - - test('resolve custom auth provider error handling - bad expiration', async () => { - const expiredEpoch = Date.UTC(2020) / 1000 - const credentialsJson = JSON.stringify({ - headers: { Authorization: 'token X' }, - expiration: expiredEpoch, - }) - - const auth = await resolveAuth( - 'sourcegraph.com', - { - authExternalProviders: [ - { - endpoint: 'https://my-server.com', - executable: { - commandLine: [ - isWindows() ? `echo ${credentialsJson}` : `echo '${credentialsJson}'`, - ], - shell: isWindows() ? process.env.ComSpec : '/bin/bash', - timeout: 5000, - windowsHide: true, - }, - }, - ], - overrideServerEndpoint: 'https://my-server.com', - overrideAuthToken: undefined, - }, - new TempClientSecrets(new Map()) - ) - - expect(auth.serverEndpoint).toBe('https://my-server.com/') - - expect(auth.credentials).toBe(undefined) - expect(auth.error.message).toContain( - 'Credentials expiration cannot be set to a date in the past' - ) - }) -}) diff --git a/lib/shared/src/configuration/auth-resolver.ts b/lib/shared/src/configuration/auth-resolver.ts deleted file mode 100644 index 688d255c4e5c..000000000000 --- a/lib/shared/src/configuration/auth-resolver.ts +++ /dev/null @@ -1,119 +0,0 @@ -import type { - AuthCredentials, - ClientConfiguration, - ExternalAuthCommand, - ExternalAuthProvider, -} from '../configuration' -import type { ClientSecrets } from './resolver' - -export function normalizeServerEndpointURL(url: string): string { - return url.endsWith('/') ? url : `${url}/` -} - -async function executeCommand(cmd: ExternalAuthCommand): Promise { - if (typeof process === 'undefined' || !process.version) { - throw new Error('Command execution is only supported in Node.js environments') - } - - const { exec } = await import('node:child_process') - const { promisify } = await import('node:util') - const execAsync = promisify(exec) - - const command = cmd.commandLine.join(' ') - - // No need to check error code, promisify causes exec to throw in case of errors - const { stdout } = await execAsync(command, { - shell: cmd.shell, - timeout: cmd.timeout, - windowsHide: cmd.windowsHide, - env: { ...process.env, ...cmd.environment }, - }) - - return stdout.trim() -} - -interface HeaderCredentialResult { - headers: Record - expiration?: number | undefined -} - -async function getExternalProviderAuthResult( - serverEndpoint: string, - authExternalProviders: readonly ExternalAuthProvider[] -): Promise { - const externalProvider = authExternalProviders.find( - provider => normalizeServerEndpointURL(provider.endpoint) === serverEndpoint - ) - - if (externalProvider) { - const result = await executeCommand(externalProvider.executable) - return JSON.parse(result) - } - - return undefined -} - -export async function resolveAuth( - endpoint: string, - configuration: Pick< - ClientConfiguration, - 'authExternalProviders' | 'overrideServerEndpoint' | 'overrideAuthToken' - >, - clientSecrets: ClientSecrets -): Promise { - const { authExternalProviders, overrideServerEndpoint, overrideAuthToken } = configuration - const serverEndpoint = normalizeServerEndpointURL(overrideServerEndpoint || endpoint) - - try { - if (overrideAuthToken) { - return { credentials: { token: overrideAuthToken }, serverEndpoint } - } - - const credentials = await getExternalProviderAuthResult( - serverEndpoint, - authExternalProviders - ).catch(error => { - throw new Error(`Failed to execute external auth command: ${error.message || error}`) - }) - - if (credentials) { - if (credentials?.expiration) { - const expirationMs = credentials?.expiration * 1000 - if (expirationMs < Date.now()) { - throw new Error( - 'Credentials expiration cannot be set to a date in the past: ' + - `${new Date(expirationMs)} (${credentials.expiration})` - ) - } - } - return { - credentials: { - expiration: credentials?.expiration, - getHeaders() { - return credentials.headers - }, - }, - serverEndpoint, - } - } - - const token = await clientSecrets.getToken(serverEndpoint).catch(error => { - throw new Error( - `Failed to get access token for endpoint ${serverEndpoint}: ${error.message || error}` - ) - }) - - return { - credentials: token - ? { token, source: await clientSecrets.getTokenSource(serverEndpoint) } - : undefined, - serverEndpoint, - } - } catch (error) { - return { - credentials: undefined, - serverEndpoint, - error, - } - } -} diff --git a/lib/shared/src/configuration/resolver.ts b/lib/shared/src/configuration/resolver.ts index a6df637a102a..331309fbfc7f 100644 --- a/lib/shared/src/configuration/resolver.ts +++ b/lib/shared/src/configuration/resolver.ts @@ -1,19 +1,16 @@ -import { Observable, Subject, map } from 'observable-fns' -import type { AuthCredentials, ClientConfiguration, TokenSource } from '../configuration' +import { Observable, map } from 'observable-fns' +import type { AuthCredentials, ClientConfiguration } from '../configuration' import { logError } from '../logger' import { - combineLatest, distinctUntilChanged, firstValueFrom, fromLateSetSource, promiseToObservable, - startWith, } from '../misc/observable' import { skipPendingOperation, switchMapReplayOperation } from '../misc/observableOperation' import type { DefaultsAndUserPreferencesByEndpoint } from '../models/modelsService' import { DOTCOM_URL } from '../sourcegraph-api/environments' import { type PartialDeep, type ReadonlyDeep, isError } from '../utils' -import { resolveAuth } from './auth-resolver' /** * The input from various sources that is needed to compute the {@link ResolvedConfiguration}. @@ -30,7 +27,6 @@ export interface ConfigurationInput { export interface ClientSecrets { getToken(endpoint: string): Promise - getTokenSource(endpoint: string): Promise } export interface ClientState { @@ -81,41 +77,42 @@ async function resolveConfiguration({ clientSecrets, clientState, reinstall: { isReinstalling, onReinstall }, -}: ConfigurationInput): Promise { +}: ConfigurationInput): Promise { const isReinstall = await isReinstalling() if (isReinstall) { await onReinstall() } - - const serverEndpoint = + // we allow for overriding the server endpoint from config if we haven't + // manually signed in somewhere else + const serverEndpoint = normalizeServerEndpointURL( clientConfiguration.overrideServerEndpoint || - clientState.lastUsedEndpoint || - DOTCOM_URL.toString() + (clientState.lastUsedEndpoint ?? DOTCOM_URL.toString()) + ) - try { - const auth = await resolveAuth(serverEndpoint, clientConfiguration, clientSecrets) - const cred = auth.credentials - if (cred !== undefined && 'expiration' in cred && cred.expiration !== undefined) { - const expireInMs = cred.expiration * 1000 - Date.now() - setInterval(() => _refreshConfigRequests.next(), expireInMs) - } - return { configuration: clientConfiguration, clientState, auth, isReinstall } - } catch (error) { - // We don't want to throw here, because that would cause the observable to terminate and - // all callers receiving no further config updates. - logError('resolveConfiguration', `Error resolving configuration: ${error}`) - const auth = { - credentials: undefined, - serverEndpoint, - error: error, - } - return { configuration: clientConfiguration, clientState, auth, isReinstall } + // We must not throw here, because that would result in the `resolvedConfig` observable + // terminating and all callers receiving no further config updates. + const loadTokenFn = () => + clientSecrets.getToken(serverEndpoint).catch(error => { + logError( + 'resolveConfiguration', + `Failed to get access token for endpoint ${serverEndpoint}: ${error}` + ) + return null + }) + const accessToken = clientConfiguration.overrideAuthToken || ((await loadTokenFn()) ?? null) + return { + configuration: clientConfiguration, + clientState, + auth: { accessToken, serverEndpoint }, + isReinstall, } } -const _resolvedConfig = fromLateSetSource() +export function normalizeServerEndpointURL(url: string): string { + return url.endsWith('/') ? url : `${url}/` +} -const _refreshConfigRequests = new Subject() +const _resolvedConfig = fromLateSetSource() /** * Set the observable that will be used to provide the global {@link resolvedConfig}. This should be @@ -123,8 +120,8 @@ const _refreshConfigRequests = new Subject() */ export function setResolvedConfigurationObservable(input: Observable): void { _resolvedConfig.setSource( - combineLatest(input, _refreshConfigRequests.pipe(startWith(undefined))).pipe( - switchMapReplayOperation(([input]) => promiseToObservable(resolveConfiguration(input))), + input.pipe( + switchMapReplayOperation(input => promiseToObservable(resolveConfiguration(input))), skipPendingOperation(), map(value => { if (isError(value)) { diff --git a/lib/shared/src/experimentation/FeatureFlagProvider.test.ts b/lib/shared/src/experimentation/FeatureFlagProvider.test.ts index f3bb5c50e9a5..43bea01c31d3 100644 --- a/lib/shared/src/experimentation/FeatureFlagProvider.test.ts +++ b/lib/shared/src/experimentation/FeatureFlagProvider.test.ts @@ -24,7 +24,7 @@ describe('FeatureFlagProvider', () => { beforeAll(() => { vi.useFakeTimers() mockResolvedConfig({ - auth: { credentials: undefined, serverEndpoint: 'https://example.com' }, + auth: { accessToken: null, serverEndpoint: 'https://example.com' }, }) mockAuthStatus(AUTH_STATUS_FIXTURE_AUTHED) }) diff --git a/lib/shared/src/index.ts b/lib/shared/src/index.ts index e84be69ebd63..2bbc4ba09e0d 100644 --- a/lib/shared/src/index.ts +++ b/lib/shared/src/index.ts @@ -405,7 +405,7 @@ export { checkVersion, } from './sourcegraph-api/siteVersion' export { configOverwrites } from './models/configOverwrites' -export { isS2, isWorkspaceInstance } from './sourcegraph-api/environments' +export { isS2 } from './sourcegraph-api/environments' export { createGitDiff } from './editor/create-git-diff' export { serialize, deserialize } from './lexicalEditor/atMentionsSerializer' diff --git a/lib/shared/src/models/sync.ts b/lib/shared/src/models/sync.ts index deadde784ae8..6abe7f8ab9e1 100644 --- a/lib/shared/src/models/sync.ts +++ b/lib/shared/src/models/sync.ts @@ -473,7 +473,11 @@ async function fetchServerSideModels( ): Promise { // Fetch the data via REST API. // NOTE: We may end up exposing this data via GraphQL, it's still TBD. - const client = new RestClient(config.auth, config.configuration.customHeaders) + const client = new RestClient( + config.auth.serverEndpoint, + config.auth.accessToken ?? undefined, + config.configuration.customHeaders + ) return await client.getAvailableModels(signal) } diff --git a/lib/shared/src/sourcegraph-api/completions/browserClient.ts b/lib/shared/src/sourcegraph-api/completions/browserClient.ts index 8475b4f3aa74..65b8d7ec8996 100644 --- a/lib/shared/src/sourcegraph-api/completions/browserClient.ts +++ b/lib/shared/src/sourcegraph-api/completions/browserClient.ts @@ -4,7 +4,6 @@ import { dependentAbortController } from '../../common/abortController' import { currentResolvedConfig } from '../../configuration/resolver' import { isError } from '../../utils' import { addClientInfoParams, addCodyClientIdentificationHeaders } from '../client-name-version' -import { addAuthHeaders } from '../utils' import { CompletionsResponseBuilder } from './CompletionsResponseBuilder' import { type CompletionRequestParameters, SourcegraphCompletionsClient } from './client' @@ -40,9 +39,10 @@ export class SourcegraphBrowserCompletionsClient extends SourcegraphCompletionsC ...requestParams.customHeaders, } as HeadersInit) addCodyClientIdentificationHeaders(headersInstance) - addAuthHeaders(config.auth, headersInstance, url) headersInstance.set('Content-Type', 'application/json; charset=utf-8') - + if (config.auth.accessToken) { + headersInstance.set('Authorization', `token ${config.auth.accessToken}`) + } const parameters = new URLSearchParams(globalThis.location.search) const trace = parameters.get('trace') if (trace) { @@ -132,8 +132,9 @@ export class SourcegraphBrowserCompletionsClient extends SourcegraphCompletionsC ...requestParams.customHeaders, }) addCodyClientIdentificationHeaders(headersInstance) - addAuthHeaders(auth, headersInstance, url) - + if (auth.accessToken) { + headersInstance.set('Authorization', `token ${auth.accessToken}`) + } if (new URLSearchParams(globalThis.location.search).get('trace')) { headersInstance.set('X-Sourcegraph-Should-Trace', 'true') } diff --git a/lib/shared/src/sourcegraph-api/environments.ts b/lib/shared/src/sourcegraph-api/environments.ts index cf143935fc23..c4f171c6838e 100644 --- a/lib/shared/src/sourcegraph-api/environments.ts +++ b/lib/shared/src/sourcegraph-api/environments.ts @@ -41,25 +41,3 @@ export function isS2(arg: Pick | undefined | string): bo // TODO: Update to live link https://linear.app/sourcegraph/issue/CORE-535/cody-clients-migrate-ctas-to-live-links export const DOTCOM_WORKSPACE_UPGRADE_URL = new URL('https://sourcegraph.com/cody/manage') export const SG_WORKSPACES_URL = new URL('https://workspaces.sourcegraph.com') - -export const Workspaces_Host_Prod = '.sourcegraph.app' -export const Workspaces_Host_Dev = '.sourcegraphdev.app' - -// 🚨 SECURITY: This is used to validate a set of URLs we will allow to be passed in -// to the editor in the URL handler. -export function isWorkspaceInstance(authStatus: Pick | undefined): boolean -export function isWorkspaceInstance(url: string): boolean -export function isWorkspaceInstance(arg: Pick | undefined | string): boolean { - const url = typeof arg === 'string' ? arg : arg?.endpoint - if (url === undefined) { - return false - } - try { - return ( - new URL(url).host.endsWith(Workspaces_Host_Prod) || - new URL(url).host.endsWith(Workspaces_Host_Dev) - ) - } catch { - return false - } -} diff --git a/lib/shared/src/sourcegraph-api/graphql/client.ts b/lib/shared/src/sourcegraph-api/graphql/client.ts index c45873c7d9b4..4e8046bfbbbe 100644 --- a/lib/shared/src/sourcegraph-api/graphql/client.ts +++ b/lib/shared/src/sourcegraph-api/graphql/client.ts @@ -17,7 +17,6 @@ import { addTraceparent, wrapInActiveSpan } from '../../tracing' import { isError } from '../../utils' import { addCodyClientIdentificationHeaders } from '../client-name-version' import { isAbortError } from '../errors' -import { addAuthHeaders } from '../utils' import { type GraphQLResultCache, ObservableInvalidatedGraphQLResultCacheFactory } from './cache' import { BUILTIN_PROMPTS_QUERY, @@ -1616,23 +1615,23 @@ export class SourcegraphGraphQLAPIClient { const headers = new Headers(config.configuration?.customHeaders as HeadersInit | undefined) headers.set('Content-Type', 'application/json; charset=utf-8') + if (config.auth.accessToken) { + headers.set('Authorization', `token ${config.auth.accessToken}`) + } if (config.clientState.anonymousUserID && !process.env.CODY_WEB_DONT_SET_SOME_HEADERS) { headers.set('X-Sourcegraph-Actor-Anonymous-UID', config.clientState.anonymousUserID) } - const url = new URL( - buildGraphQLUrl({ - request: query, - baseUrl: config.auth.serverEndpoint, - }) - ) - addTraceparent(headers) addCodyClientIdentificationHeaders(headers) - addAuthHeaders(config.auth, headers, url) const queryName = query.match(QUERY_TO_NAME_REGEXP)?.[1] + const url = buildGraphQLUrl({ + request: query, + baseUrl: config.auth.serverEndpoint, + }) + const { abortController, timeoutSignal } = dependentAbortControllerWithTimeout(signal) return wrapInActiveSpan(`graphql.fetch${queryName ? `.${queryName}` : ''}`, () => fetch(url, { @@ -1643,7 +1642,7 @@ export class SourcegraphGraphQLAPIClient { }) .then(verifyResponseCode) .then(response => response.json() as T) - .catch(catchHTTPError(url.href, timeoutSignal)) + .catch(catchHTTPError(url, timeoutSignal)) ) } @@ -1669,15 +1668,17 @@ export class SourcegraphGraphQLAPIClient { const headers = new Headers(config.configuration?.customHeaders as HeadersInit | undefined) headers.set('Content-Type', 'application/json; charset=utf-8') + if (config.auth.accessToken) { + headers.set('Authorization', `token ${config.auth.accessToken}`) + } if (config.clientState.anonymousUserID && !process.env.CODY_WEB_DONT_SET_SOME_HEADERS) { headers.set('X-Sourcegraph-Actor-Anonymous-UID', config.clientState.anonymousUserID) } - const url = new URL(urlPath, config.auth.serverEndpoint) - addTraceparent(headers) addCodyClientIdentificationHeaders(headers) - addAuthHeaders(config.auth, headers, url) + + const url = new URL(urlPath, config.auth.serverEndpoint).href const { abortController, timeoutSignal } = dependentAbortControllerWithTimeout(signal) return wrapInActiveSpan(`httpapi.fetch${queryName ? `.${queryName}` : ''}`, () => @@ -1689,7 +1690,7 @@ export class SourcegraphGraphQLAPIClient { }) .then(verifyResponseCode) .then(response => response.json() as T) - .catch(catchHTTPError(url.href, timeoutSignal)) + .catch(catchHTTPError(url, timeoutSignal)) ) } } diff --git a/lib/shared/src/sourcegraph-api/rest/client.ts b/lib/shared/src/sourcegraph-api/rest/client.ts index e433a2ba1f7c..551ada9c70c0 100644 --- a/lib/shared/src/sourcegraph-api/rest/client.ts +++ b/lib/shared/src/sourcegraph-api/rest/client.ts @@ -1,6 +1,5 @@ import type { ServerModelConfiguration } from '../../models/modelsService' -import { type AuthCredentials, addAuthHeaders } from '../..' import { fetch } from '../../fetch' import { logError } from '../../logger' import { addTraceparent, wrapInActiveSpan } from '../../tracing' @@ -21,12 +20,14 @@ import { verifyResponseCode } from '../graphql/client' */ export class RestClient { /** - * Creates a new REST client to interact with a Sourcegraph instance. - * @param auth Authentication credentials containing endpoint URL and access token - * @param customHeaders Additional headers for requests (used by Cody Web to ensure proper auth flow) + * @param endpointUrl URL to the sourcegraph instance, e.g. "https://sourcegraph.acme.com". + * @param accessToken User access token to contact the sourcegraph instance. + * @param customHeaders Custom headers (primary is used by Cody Web case when Sourcegraph client + * providers set of custom headers to make sure that auth flow will work properly */ constructor( - private auth: AuthCredentials, + private endpointUrl: string, + private accessToken: string | undefined, private customHeaders: Record | undefined ) {} @@ -34,15 +35,15 @@ export class RestClient { // "name" is a developer-friendly term to label the request's trace span. private getRequest(name: string, urlSuffix: string, signal?: AbortSignal): Promise { const headers = new Headers(this.customHeaders) - - const endpoint = new URL(this.auth.serverEndpoint) - endpoint.pathname = urlSuffix - const url = endpoint.href - + if (this.accessToken) { + headers.set('Authorization', `token ${this.accessToken}`) + } addCodyClientIdentificationHeaders(headers) - addAuthHeaders(this.auth, headers, endpoint) addTraceparent(headers) + const endpoint = new URL(this.endpointUrl) + endpoint.pathname = urlSuffix + const url = endpoint.href return wrapInActiveSpan(`rest-api.${name}`, () => fetch(url, { method: 'GET', diff --git a/lib/shared/src/sourcegraph-api/utils.ts b/lib/shared/src/sourcegraph-api/utils.ts index 8a778f8c4516..a6d161b27903 100644 --- a/lib/shared/src/sourcegraph-api/utils.ts +++ b/lib/shared/src/sourcegraph-api/utils.ts @@ -2,9 +2,6 @@ // of a character, returns the remaining bytes of the partial character in a // new buffer. Note! This assumes that the prefix of buf *is* valid UTF8--it // only examines the bytes of the last character in the buffer and assumes it - -import type { AuthCredentials } from '..' - // will find an initial byte before the start of the buffer. export function toPartialUtf8String(buf: Buffer): { str: string; buf: Buffer } { if (buf.length === 0) { @@ -35,18 +32,3 @@ export function toPartialUtf8String(buf: Buffer): { str: string; buf: Buffer } { buf: Buffer.from(buf.slice(lastValidByteOffsetExclusive)), } } - -export function addAuthHeaders(auth: AuthCredentials, headers: Headers, url: URL): void { - // We want to be sure we sent authorization headers only to the valid endpoint - if (auth.credentials && url.host === new URL(auth.serverEndpoint).host) { - if ('token' in auth.credentials) { - headers.set('Authorization', `token ${auth.credentials.token}`) - } else if (typeof auth.credentials.getHeaders === 'function') { - for (const [key, value] of Object.entries(auth.credentials.getHeaders())) { - headers.set(key, value) - } - } else { - console.error('Cannot add headers: neither token nor headers found') - } - } -} diff --git a/vscode/package.json b/vscode/package.json index f3f0bc8c3421..2a14f2005db5 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -1312,53 +1312,6 @@ "~/.mitmproxy/mitmproxy-ca-cert.pem", "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----" ] - }, - "cody.auth.externalProviders": { - "type": "array", - "markdownDescription": "Configure external authentication providers for Cody requests. Each provider consists of a command that generates HTTP headers used for authentication for a given endpoint.\n\n**How it works:**\n1. The specified command outputs a JSON object with header-value pairs\n2. These headers are included in authenticated Cody requests to the specified endpoint\n3. HTTP authentication proxy need to be used to enable custom authentication flows (e.g. JWT tokens, Oath2, etc)\n\nSee [HTTP Authentication Proxies](https://sourcegraph.com/docs/admin/auth#http-authentication-proxies) for proxy configuration.", - "items": { - "type": "object", - "required": ["endpoint", "executable"], - "properties": { - "endpoint": { - "type": "string", - "description": "The endpoint URL of the Sourcegraph instance" - }, - "executable": { - "type": "object", - "required": ["commandLine"], - "properties": { - "commandLine": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Command line arguments to execute the command." - }, - "environment": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "description": "Environment variables to set when executing the command." - }, - "shell": { - "type": "string", - "description": "If true, runs command inside of a shell. Uses '/bin/sh' on Unix, and process.env.ComSpec on Windows. A different shell can be specified as a string." - }, - "timeout": { - "type": "number", - "description": "Timeout for executing the command in milliseconds." - }, - "windowsHide": { - "type": "boolean", - "description": "Whether to hide the console window that would normally be created for the child process on Windows." - } - } - } - } - }, - "default": [] } } }, diff --git a/vscode/src/auth/auth.ts b/vscode/src/auth/auth.ts index 7a75f03efd65..3f490c9b16d5 100644 --- a/vscode/src/auth/auth.ts +++ b/vscode/src/auth/auth.ts @@ -12,17 +12,14 @@ import { cenv, clientCapabilities, currentAuthStatus, - currentResolvedConfig, getAuthErrorMessage, getCodyAuthReferralCode, graphqlClient, isDotCom, isError, isNetworkLikeError, - isWorkspaceInstance, telemetryRecorder, } from '@sourcegraph/cody-shared' -import { resolveAuth } from '@sourcegraph/cody-shared/src/configuration/auth-resolver' import { isSourcegraphToken } from '../chat/protocol' import { newAuthStatus } from '../chat/utils' import { logDebug } from '../output-channel-logger' @@ -41,28 +38,6 @@ interface LoginMenuItem { type AuthMenuType = 'signin' | 'switch' -/** - * Handles trying to directly sign-in or add to an enterprise instance. - * First tries to sign in with the current token, if it's valid. Otherwise, - * opens the sign-in flow and has user confirm. - */ -async function showEnterpriseInstanceUrlFlow(endpoint: string): Promise { - const { configuration } = await currentResolvedConfig() - const auth = await resolveAuth(endpoint, configuration, secretStorage) - - const authStatus = await authProvider.validateAndStoreCredentials(auth, 'store-if-valid') - - if (!authStatus?.authenticated) { - const instanceUrl = await showInstanceURLInputBox(endpoint) - if (!instanceUrl) { - return - } - authProvider.setAuthPendingToEndpoint(instanceUrl) - redirectToEndpointLogin(instanceUrl) - } else { - await showAuthResultMessage(endpoint, authStatus) - } -} /** * Show a quickpick to select the endpoint to sign into. */ @@ -108,20 +83,27 @@ export async function showSignInMenu( break } default: { - // Auto log user if token for the selected instance was found in secret or custom provider is configured + // Auto log user if token for the selected instance was found in secret const selectedEndpoint = item.uri - const { configuration } = await currentResolvedConfig() - const auth = await resolveAuth(selectedEndpoint, configuration, secretStorage) - - let authStatus = await authProvider.validateAndStoreCredentials(auth, 'store-if-valid') - + const token = await secretStorage.getToken(selectedEndpoint) + const tokenSource = await secretStorage.getTokenSource(selectedEndpoint) + let authStatus = token + ? await authProvider.validateAndStoreCredentials( + { serverEndpoint: selectedEndpoint, accessToken: token, tokenSource }, + 'store-if-valid' + ) + : undefined if (!authStatus?.authenticated) { - const token = await showAccessTokenInputBox(selectedEndpoint) - if (!token) { + const newToken = await showAccessTokenInputBox(selectedEndpoint) + if (!newToken) { return } authStatus = await authProvider.validateAndStoreCredentials( - { serverEndpoint: selectedEndpoint, credentials: { token, source: 'paste' } }, + { + serverEndpoint: selectedEndpoint, + accessToken: newToken, + tokenSource: 'paste', + }, 'store-if-valid' ) } @@ -160,12 +142,12 @@ async function showAuthMenu(type: AuthMenuType): Promise { /** * Show a VS Code input box to ask the user to enter a Sourcegraph instance URL. */ -async function showInstanceURLInputBox(url?: string): Promise { +async function showInstanceURLInputBox(title: string): Promise { const result = await vscode.window.showInputBox({ - title: 'Connect to a Sourcegraph instance', + title, prompt: 'Enter the URL of the Sourcegraph instance. For example, https://sourcegraph.example.com.', placeHolder: 'https://sourcegraph.example.com', - value: url ?? 'https://', + value: 'https://', password: false, ignoreFocusOut: true, // valide input to ensure the user is not entering a token as URL @@ -246,12 +228,12 @@ const LoginMenuOptionItems = [ ] async function signinMenuForInstanceUrl(instanceUrl: string): Promise { - const token = await showAccessTokenInputBox(instanceUrl) - if (!token) { + const accessToken = await showAccessTokenInputBox(instanceUrl) + if (!accessToken) { return } const authStatus = await authProvider.validateAndStoreCredentials( - { serverEndpoint: instanceUrl, credentials: { token, source: 'paste' } }, + { serverEndpoint: instanceUrl, accessToken: accessToken, tokenSource: 'paste' }, 'store-if-valid' ) telemetryRecorder.recordEvent('cody.auth.signin.token', 'clicked', { @@ -323,28 +305,14 @@ export async function tokenCallbackHandler(uri: vscode.Uri): Promise { closeAuthProgressIndicator() const params = new URLSearchParams(uri.query) - const token = params.get('code') || params.get('token') const endpoint = currentAuthStatus().endpoint - - // If we were provided an instance URL then it means we are - // request the user setup auth with a different sourcegraph instance - // We want to prompt them to switch to this instance and if needed - // start the auth flow - const instanceHost = params.get('instance') - const instanceUrl = instanceHost ? new URL(instanceHost).origin : undefined - if (instanceUrl && isWorkspaceInstance(instanceUrl)) { - // Prompt the user to switch/setup with the new instance - await showEnterpriseInstanceUrlFlow(instanceUrl) - return - } - if (!token || !endpoint) { return } const authStatus = await authProvider.validateAndStoreCredentials( - { serverEndpoint: endpoint, credentials: { token, source: 'redirect' } }, + { serverEndpoint: endpoint, accessToken: token, tokenSource: 'redirect' }, 'store-if-valid' ) telemetryRecorder.recordEvent('cody.auth.fromCallback.web', 'succeeded', { @@ -441,26 +409,8 @@ export async function validateCredentials( signal?: AbortSignal, clientConfig?: CodyClientConfig ): Promise { - if (config.auth.error !== undefined) { - logDebug( - 'auth', - `Failed to authenticate to ${config.auth.serverEndpoint} due to configuration error`, - config.auth.error - ) - return { - authenticated: false, - endpoint: config.auth.serverEndpoint, - pendingValidation: false, - error: { - type: 'auth-config-error', - title: 'Auth config error', - message: config.auth.error?.message ?? config.auth.error, - }, - } - } - // An access token is needed except for Cody Web, which uses cookies. - if (!config.auth.credentials && !clientCapabilities().isCodyWeb) { + if (!config.auth.accessToken && !clientCapabilities().isCodyWeb) { return { authenticated: false, endpoint: config.auth.serverEndpoint, pendingValidation: false } } diff --git a/vscode/src/auth/token-receiver.ts b/vscode/src/auth/token-receiver.ts index 490d8ac92a5d..0a93195690fe 100644 --- a/vscode/src/auth/token-receiver.ts +++ b/vscode/src/auth/token-receiver.ts @@ -14,7 +14,7 @@ const FIVE_MINUTES = 5 * 60 * 1000 // the user follow a redirect. export function startTokenReceiver( endpoint: string, - onNewToken: (credentials: Pick) => void, + onNewToken: (credentials: Pick) => void, timeout = FIVE_MINUTES ): Promise { const endpointUrl = new URL(endpoint) @@ -46,10 +46,7 @@ export function startTokenReceiver( 'accessToken' in json && typeof json.accessToken === 'string' ) { - onNewToken({ - serverEndpoint: endpoint, - credentials: { token: json.accessToken, source: 'redirect' }, - }) + onNewToken({ serverEndpoint: endpoint, accessToken: json.accessToken }) res.writeHead(200, headers) res.write('ok') diff --git a/vscode/src/autoedits/adapters/cody-gateway.test.ts b/vscode/src/autoedits/adapters/cody-gateway.test.ts index d776761602a3..f3fcbdb4dd09 100644 --- a/vscode/src/autoedits/adapters/cody-gateway.test.ts +++ b/vscode/src/autoedits/adapters/cody-gateway.test.ts @@ -33,7 +33,7 @@ describe('CodyGatewayAdapter', () => { mockResolvedConfig({ configuration: {}, auth: { - credentials: { token: 'sgp_local_f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0' }, + accessToken: 'sgp_local_f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0', serverEndpoint: DOTCOM_URL.toString(), }, }) diff --git a/vscode/src/autoedits/adapters/cody-gateway.ts b/vscode/src/autoedits/adapters/cody-gateway.ts index eab2caec7575..8e4100195eca 100644 --- a/vscode/src/autoedits/adapters/cody-gateway.ts +++ b/vscode/src/autoedits/adapters/cody-gateway.ts @@ -33,12 +33,7 @@ export class CodyGatewayAdapter implements AutoeditsModelAdapter { private async getApiKey(): Promise { const resolvedConfig = await currentResolvedConfig() - // TODO (pkukielka): Check if fastpath should support custom auth providers and how - const accessToken = - resolvedConfig.auth.credentials && 'token' in resolvedConfig.auth.credentials - ? resolvedConfig.auth.credentials.token - : null - const fastPathAccessToken = dotcomTokenToGatewayToken(accessToken) + const fastPathAccessToken = dotcomTokenToGatewayToken(resolvedConfig.auth.accessToken) if (!fastPathAccessToken) { autoeditsOutputChannelLogger.logError('getApiKey', 'FastPath access token is not available') throw new Error('FastPath access token is not available') diff --git a/vscode/src/autoedits/autoedits-provider.test.ts b/vscode/src/autoedits/autoedits-provider.test.ts index 2faf7dcb30c1..ed40c64b3270 100644 --- a/vscode/src/autoedits/autoedits-provider.test.ts +++ b/vscode/src/autoedits/autoedits-provider.test.ts @@ -60,7 +60,7 @@ describe('AutoeditsProvider', () => { mockResolvedConfig({ configuration: {}, auth: { - credentials: { token: 'sgp_local_f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0' }, + accessToken: 'sgp_local_f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0', serverEndpoint: DOTCOM_URL.toString(), }, }) diff --git a/vscode/src/chat/agentic/DeepCody.test.ts b/vscode/src/chat/agentic/DeepCody.test.ts index a3be8fac7c09..1eba1d683a22 100644 --- a/vscode/src/chat/agentic/DeepCody.test.ts +++ b/vscode/src/chat/agentic/DeepCody.test.ts @@ -57,7 +57,7 @@ describe('DeepCody', () => { } as any) beforeEach(async () => { - mockResolvedConfig({ configuration: {}, auth: { serverEndpoint: DOTCOM_URL.toString() } }) + mockResolvedConfig({ configuration: {} }) mockClientCapabilities(CLIENT_CAPABILITIES_FIXTURE) mockAuthStatus(codyProAuthStatus) localStorageData = {} diff --git a/vscode/src/chat/chat-view/ChatController.ts b/vscode/src/chat/chat-view/ChatController.ts index 99a2c094afd4..5adeff324f05 100644 --- a/vscode/src/chat/chat-view/ChatController.ts +++ b/vscode/src/chat/chat-view/ChatController.ts @@ -1,5 +1,4 @@ import { - type AuthCredentials, type AuthStatus, type BillingCategory, type BillingProduct, @@ -72,7 +71,6 @@ import * as vscode from 'vscode' import { type Span, context } from '@opentelemetry/api' import { captureException } from '@sentry/core' import type { SubMessage } from '@sourcegraph/cody-shared/src/chat/transcript/messages' -import { resolveAuth } from '@sourcegraph/cody-shared/src/configuration/auth-resolver' import type { TelemetryEventParameters } from '@sourcegraph/telemetry' import { Subject, map } from 'observable-fns' import type { URI } from 'vscode-uri' @@ -409,6 +407,10 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv ) break case 'auth': { + if (message.authKind === 'callback' && message.endpoint) { + redirectToEndpointLogin(message.endpoint) + break + } if (message.authKind === 'simplified-onboarding') { const endpoint = DOTCOM_URL.href @@ -448,28 +450,19 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv } break } - if ( - (message.authKind === 'signin' || message.authKind === 'callback') && - message.endpoint - ) { + if (message.authKind === 'signin' && message.endpoint) { try { const { endpoint, value: token } = message - let auth: AuthCredentials | undefined = undefined - - if (token) { - auth = { credentials: { token, source: 'paste' }, serverEndpoint: endpoint } - } else { - const { configuration } = await currentResolvedConfig() - auth = await resolveAuth(endpoint, configuration, secretStorage) + const credentials = { + serverEndpoint: endpoint, + accessToken: token || (await secretStorage.getToken(endpoint)) || null, + tokenSource: token ? 'paste' : await secretStorage.getTokenSource(endpoint), } - - if (!auth || !auth.credentials) { - return redirectToEndpointLogin(endpoint) + if (!credentials.accessToken) { + return redirectToEndpointLogin(credentials.serverEndpoint) } - - await authProvider.validateAndStoreCredentials(auth, 'always-store') + await authProvider.validateAndStoreCredentials(credentials, 'always-store') } catch (error) { - void vscode.window.showErrorMessage(`Authentication failed: ${error}`) this.postError(new Error(`Authentication failed: ${error}`)) } break @@ -505,7 +498,7 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv const authStatus = await authProvider.validateAndStoreCredentials( { serverEndpoint: DOTCOM_URL.href, - credentials: { token }, + accessToken: token, }, 'store-if-valid' ) diff --git a/vscode/src/chat/chat-view/prompt.test.ts b/vscode/src/chat/chat-view/prompt.test.ts index d16635f7aa06..b7652c1efa8a 100644 --- a/vscode/src/chat/chat-view/prompt.test.ts +++ b/vscode/src/chat/chat-view/prompt.test.ts @@ -8,7 +8,6 @@ import { type ModelsData, contextFiltersProvider, createModel, - graphqlClient, mockAuthStatus, mockClientCapabilities, mockResolvedConfig, @@ -25,7 +24,6 @@ import { DefaultPrompter } from './prompt' describe('DefaultPrompter', () => { beforeEach(() => { vi.spyOn(contextFiltersProvider, 'isUriIgnored').mockResolvedValue(false) - vi.spyOn(graphqlClient, 'fetchSourcegraphAPI').mockResolvedValue(true) mockAuthStatus(AUTH_STATUS_FIXTURE_AUTHED) mockResolvedConfig({ configuration: {} }) mockClientCapabilities(CLIENT_CAPABILITIES_FIXTURE) diff --git a/vscode/src/completions/default-client.ts b/vscode/src/completions/default-client.ts index 992b174d5642..7b3912745d6c 100644 --- a/vscode/src/completions/default-client.ts +++ b/vscode/src/completions/default-client.ts @@ -14,8 +14,6 @@ import { RateLimitError, type SerializedCodeCompletionsParams, TracedError, - addAuthHeaders, - addCodyClientIdentificationHeaders, addTraceparent, contextFiltersProvider, createSSEIterator, @@ -51,8 +49,8 @@ class DefaultCodeCompletionsClient implements CodeCompletionsClient { const { auth, configuration } = await currentResolvedConfig() const query = new URLSearchParams(getClientInfoParams()) - const url = new URL(`/.api/completions/code?${query.toString()}`, auth.serverEndpoint) - const log = autocompleteLifecycleOutputChannelLogger?.startCompletion(params, url.href) + const url = new URL(`/.api/completions/code?${query.toString()}`, auth.serverEndpoint).href + const log = autocompleteLifecycleOutputChannelLogger?.startCompletion(params, url) const { signal } = abortController return tracer.startActiveSpan( @@ -71,8 +69,9 @@ class DefaultCodeCompletionsClient implements CodeCompletionsClient { // Force HTTP connection reuse to reduce latency. // c.f. https://github.com/microsoft/vscode/issues/173861 headers.set('Content-Type', 'application/json; charset=utf-8') - addCodyClientIdentificationHeaders(headers) - addAuthHeaders(auth, headers, url) + if (auth.accessToken) { + headers.set('Authorization', `token ${auth.accessToken}`) + } if (tracingFlagEnabled) { headers.set('X-Sourcegraph-Should-Trace', '1') diff --git a/vscode/src/completions/nodeClient.ts b/vscode/src/completions/nodeClient.ts index cd561d66491f..4f2fc6500492 100644 --- a/vscode/src/completions/nodeClient.ts +++ b/vscode/src/completions/nodeClient.ts @@ -13,7 +13,6 @@ import { NetworkError, RateLimitError, SourcegraphCompletionsClient, - addAuthHeaders, addClientInfoParams, addCodyClientIdentificationHeaders, currentResolvedConfig, @@ -96,13 +95,13 @@ export class SourcegraphNodeCompletionsClient extends SourcegraphCompletionsClie // responses afterwards. 'Accept-Encoding': 'gzip;q=0', 'X-Sourcegraph-Interaction-ID': interactionId || '', + ...(auth.accessToken ? { Authorization: `token ${auth.accessToken}` } : null), ...configuration?.customHeaders, ...requestParams.customHeaders, ...getTraceparentHeaders(), Connection: 'keep-alive', }) addCodyClientIdentificationHeaders(headers) - addAuthHeaders(auth, headers, url) const request = requestFn( url, @@ -300,6 +299,7 @@ export class SourcegraphNodeCompletionsClient extends SourcegraphCompletionsClie const headers = new Headers({ 'Content-Type': 'application/json', 'Accept-Encoding': 'gzip;q=0', + ...(auth.accessToken ? { Authorization: `token ${auth.accessToken}` } : null), ...configuration.customHeaders, ...requestParams.customHeaders, ...getTraceparentHeaders(), @@ -307,7 +307,6 @@ export class SourcegraphNodeCompletionsClient extends SourcegraphCompletionsClie }) addCodyClientIdentificationHeaders(headers) - addAuthHeaders(auth, headers, url) const response = await fetch(url.toString(), { method: 'POST', diff --git a/vscode/src/completions/providers/fireworks.ts b/vscode/src/completions/providers/fireworks.ts index 50bc43ec82e8..8f8548dc5b25 100644 --- a/vscode/src/completions/providers/fireworks.ts +++ b/vscode/src/completions/providers/fireworks.ts @@ -133,12 +133,7 @@ class FireworksProvider extends Provider { typeof process !== 'undefined' if (canFastPathBeUsed) { - // TODO (pkukielka): Check if fastpath should support custom auth providers and how - const accessToken = - config.auth.credentials && 'token' in config.auth.credentials - ? config.auth.credentials.token - : null - const fastPathAccessToken = dotcomTokenToGatewayToken(accessToken) + const fastPathAccessToken = dotcomTokenToGatewayToken(config.auth.accessToken) const localFastPathAccessToken = process.env.NODE_ENV === 'development' diff --git a/vscode/src/configuration.test.ts b/vscode/src/configuration.test.ts index ec2d03661e1e..edaf96e05bca 100644 --- a/vscode/src/configuration.test.ts +++ b/vscode/src/configuration.test.ts @@ -138,8 +138,6 @@ describe('getConfiguration', () => { return false case 'cody.agentic.context.experimentalOptions': return { shell: { allow: ['git'] } } - case 'cody.auth.externalProviders': - return [] default: assert(false, `unexpected key: ${key}`) } @@ -208,7 +206,6 @@ describe('getConfiguration', () => { overrideAuthToken: undefined, overrideServerEndpoint: undefined, - authExternalProviders: [], } satisfies ClientConfiguration) }) }) diff --git a/vscode/src/configuration.ts b/vscode/src/configuration.ts index ea3572ef1b9e..caacb4b313d7 100644 --- a/vscode/src/configuration.ts +++ b/vscode/src/configuration.ts @@ -129,8 +129,6 @@ export function getConfiguration( */ agenticContextExperimentalOptions: config.get(CONFIG_KEY.agenticContextExperimentalOptions, {}), - authExternalProviders: config.get(CONFIG_KEY.authExternalProviders, []), - /** * Hidden settings for internal use only. */ diff --git a/vscode/src/local-context/rewrite-keyword-query.test.ts b/vscode/src/local-context/rewrite-keyword-query.test.ts index e0958e7fc089..099ab22236e5 100644 --- a/vscode/src/local-context/rewrite-keyword-query.test.ts +++ b/vscode/src/local-context/rewrite-keyword-query.test.ts @@ -30,9 +30,8 @@ describe('rewrite-query', () => { mockResolvedConfig({ configuration: { customHeaders: {} }, auth: { - credentials: { - token: TESTING_CREDENTIALS.dotcom.token ?? TESTING_CREDENTIALS.dotcom.redactedToken, - }, + accessToken: + TESTING_CREDENTIALS.dotcom.token ?? TESTING_CREDENTIALS.dotcom.redactedToken, serverEndpoint: TESTING_CREDENTIALS.dotcom.serverEndpoint, }, }) diff --git a/vscode/src/main.ts b/vscode/src/main.ts index c22275e114cb..e35e42dfc7e3 100644 --- a/vscode/src/main.ts +++ b/vscode/src/main.ts @@ -679,11 +679,8 @@ async function registerTestCommands( } }), // Access token - this is only used in configuration tests - vscode.commands.registerCommand('cody.test.token', async (serverEndpoint, token) => - authProvider.validateAndStoreCredentials( - { credentials: { token }, serverEndpoint }, - 'always-store' - ) + vscode.commands.registerCommand('cody.test.token', async (serverEndpoint, accessToken) => + authProvider.validateAndStoreCredentials({ serverEndpoint, accessToken }, 'always-store') ) ) } diff --git a/vscode/src/notifications/setup-notification.ts b/vscode/src/notifications/setup-notification.ts index e131eaf3c433..0c0812aea15a 100644 --- a/vscode/src/notifications/setup-notification.ts +++ b/vscode/src/notifications/setup-notification.ts @@ -8,7 +8,7 @@ import { telemetryRecorder } from '@sourcegraph/cody-shared' import { showActionNotification } from '.' export const showSetupNotification = async (auth: AuthCredentials): Promise => { - if (auth.serverEndpoint && auth.credentials) { + if (auth.serverEndpoint && auth.accessToken) { // User has already attempted to configure Cody. // Regardless of if they are authenticated or not, we don't want to prompt them. return diff --git a/vscode/src/services/AuthProvider.test.ts b/vscode/src/services/AuthProvider.test.ts index 3f6db32795f8..2ff7fd5a7ab0 100644 --- a/vscode/src/services/AuthProvider.test.ts +++ b/vscode/src/services/AuthProvider.test.ts @@ -81,7 +81,7 @@ describe('AuthProvider', () => { const { values, clearValues } = readValuesFrom(authStatus) resolvedConfig.next({ configuration: {}, - auth: { serverEndpoint: 'https://example.com/', credentials: { token: 't' } }, + auth: { serverEndpoint: 'https://example.com/', accessToken: 't' }, clientState: { anonymousUserID: '123' }, } satisfies PartialDeep as ResolvedConfiguration) @@ -108,7 +108,7 @@ describe('AuthProvider', () => { validateCredentialsMock.mockReturnValue(asyncValue(authedAuthStatusBob, 10)) resolvedConfig.next({ configuration: {}, - auth: { serverEndpoint: 'https://other.example.com/', credentials: { token: 't2' } }, + auth: { serverEndpoint: 'https://other.example.com/', accessToken: 't2' }, clientState: { anonymousUserID: '123' }, } satisfies PartialDeep as ResolvedConfiguration) await vi.advanceTimersByTimeAsync(1) @@ -156,7 +156,7 @@ describe('AuthProvider', () => { const { values, clearValues } = readValuesFrom(authStatus) resolvedConfig.next({ configuration: {}, - auth: { serverEndpoint: 'https://example.com/', credentials: { token: 't' } }, + auth: { serverEndpoint: 'https://example.com/', accessToken: 't' }, clientState: { anonymousUserID: '123' }, } satisfies PartialDeep as ResolvedConfiguration) @@ -176,7 +176,7 @@ describe('AuthProvider', () => { const promise = authProvider.validateAndStoreCredentials( { configuration: {}, - auth: { serverEndpoint: 'https://other.example.com/', credentials: { token: 't2' } }, + auth: { serverEndpoint: 'https://other.example.com/', accessToken: 't2' }, clientState: { anonymousUserID: '123' }, }, 'always-store' @@ -212,7 +212,7 @@ describe('AuthProvider', () => { const { values, clearValues } = readValuesFrom(authStatus) resolvedConfig.next({ configuration: {}, - auth: { serverEndpoint: 'https://example.com/', credentials: { token: 't' } }, + auth: { serverEndpoint: 'https://example.com/', accessToken: 't' }, clientState: { anonymousUserID: '123' }, } satisfies PartialDeep as ResolvedConfiguration) diff --git a/vscode/src/services/AuthProvider.ts b/vscode/src/services/AuthProvider.ts index 3b3731dffd39..0ea7e60d503c 100644 --- a/vscode/src/services/AuthProvider.ts +++ b/vscode/src/services/AuthProvider.ts @@ -15,6 +15,7 @@ import { distinctUntilChanged, clientCapabilities as getClientCapabilities, isAbortError, + normalizeServerEndpointURL, resolvedConfig as resolvedConfig_, setAuthStatusObservable as setAuthStatusObservable_, startWith, @@ -22,7 +23,6 @@ import { telemetryRecorder, withLatestFrom, } from '@sourcegraph/cody-shared' -import { normalizeServerEndpointURL } from '@sourcegraph/cody-shared/src/configuration/auth-resolver' import isEqual from 'lodash/isEqual' import { Observable, Subject } from 'observable-fns' import * as vscode from 'vscode' diff --git a/vscode/src/services/LocalStorageProvider.ts b/vscode/src/services/LocalStorageProvider.ts index 6b5f2689377e..f947a585540b 100644 --- a/vscode/src/services/LocalStorageProvider.ts +++ b/vscode/src/services/LocalStorageProvider.ts @@ -115,26 +115,26 @@ class LocalStorage implements LocalStorageForModelPreferences { * would give an inconsistent view of the state. */ public async saveEndpointAndToken( - auth: Pick + credentials: Pick ): Promise { - if (!auth.serverEndpoint) { + if (!credentials.serverEndpoint) { return } // Do not save an access token as the last-used endpoint, to prevent user mistakes. - if (isSourcegraphToken(auth.serverEndpoint)) { + if (isSourcegraphToken(credentials.serverEndpoint)) { return } - const serverEndpoint = new URL(auth.serverEndpoint).href + const serverEndpoint = new URL(credentials.serverEndpoint).href // Pass `false` to avoid firing the change event until we've stored all of the values. await this.set(this.LAST_USED_ENDPOINT, serverEndpoint, false) await this.addEndpointHistory(serverEndpoint, false) - if (auth.credentials && 'token' in auth.credentials) { + if (credentials.accessToken) { await secretStorage.storeToken( serverEndpoint, - auth.credentials.token, - auth.credentials.source + credentials.accessToken, + credentials.tokenSource ) } this.onChange.fire() diff --git a/vscode/src/services/UpstreamHealthProvider.ts b/vscode/src/services/UpstreamHealthProvider.ts index a5fb5be15675..8667dd2bc75c 100644 --- a/vscode/src/services/UpstreamHealthProvider.ts +++ b/vscode/src/services/UpstreamHealthProvider.ts @@ -1,6 +1,5 @@ import { type BrowserOrNodeResponse, - addAuthHeaders, addCodyClientIdentificationHeaders, addTraceparent, currentResolvedConfig, @@ -95,10 +94,11 @@ class UpstreamHealthProvider implements vscode.Disposable { addTraceparent(sharedHeaders) addCodyClientIdentificationHeaders(sharedHeaders) - const url = new URL('/healthz', auth.serverEndpoint) const upstreamHeaders = new Headers(sharedHeaders) - addAuthHeaders(auth, upstreamHeaders, url) - + if (auth.accessToken) { + upstreamHeaders.set('Authorization', `token ${auth.accessToken}`) + } + const url = new URL('/healthz', auth.serverEndpoint) const upstreamResult = await wrapInActiveSpan('upstream-latency.upstream', span => { span.setAttribute('sampled', true) return measureLatencyToUri(upstreamHeaders, url.toString()) diff --git a/vscode/src/services/open-telemetry/CodyTraceExport.ts b/vscode/src/services/open-telemetry/CodyTraceExport.ts index 57380f09f040..b0e0d482daab 100644 --- a/vscode/src/services/open-telemetry/CodyTraceExport.ts +++ b/vscode/src/services/open-telemetry/CodyTraceExport.ts @@ -1,7 +1,6 @@ import type { ExportResult } from '@opentelemetry/core' import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http' import type { ReadableSpan } from '@opentelemetry/sdk-trace-base' -import { type AuthCredentials, addAuthHeaders } from '@sourcegraph/cody-shared' const MAX_TRACE_RETAIN_MS = 60 * 1000 @@ -11,16 +10,15 @@ export class CodyTraceExporter extends OTLPTraceExporter { constructor({ traceUrl, - auth, + accessToken, isTracingEnabled, - }: { traceUrl: string; auth: AuthCredentials | null; isTracingEnabled: boolean }) { - const headers = new Headers() - if (auth) addAuthHeaders(auth, headers, new URL(traceUrl)) - + }: { traceUrl: string; accessToken: string | null; isTracingEnabled: boolean }) { super({ url: traceUrl, httpAgentOptions: { rejectUnauthorized: false }, - headers: Object.fromEntries(headers.entries()), + headers: { + ...(accessToken ? { Authorization: `token ${accessToken}` } : {}), + }, }) this.isTracingEnabled = isTracingEnabled } diff --git a/vscode/src/services/open-telemetry/OpenTelemetryService.node.ts b/vscode/src/services/open-telemetry/OpenTelemetryService.node.ts index 53c5920a5756..49ba2d4ac170 100644 --- a/vscode/src/services/open-telemetry/OpenTelemetryService.node.ts +++ b/vscode/src/services/open-telemetry/OpenTelemetryService.node.ts @@ -79,7 +79,7 @@ export class OpenTelemetryService { new CodyTraceExporter({ traceUrl, isTracingEnabled: this.isTracingEnabled, - auth, + accessToken: auth.accessToken, }) ) ) diff --git a/vscode/src/services/open-telemetry/trace-sender.ts b/vscode/src/services/open-telemetry/trace-sender.ts index 0b7f0973a217..d7e7bb410d82 100644 --- a/vscode/src/services/open-telemetry/trace-sender.ts +++ b/vscode/src/services/open-telemetry/trace-sender.ts @@ -1,4 +1,5 @@ -import { addAuthHeaders, currentResolvedConfig, fetch } from '@sourcegraph/cody-shared' +import { currentResolvedConfig } from '@sourcegraph/cody-shared' +import fetch from 'node-fetch' import { logDebug, logError } from '../../output-channel-logger' /** @@ -21,19 +22,18 @@ export const TraceSender = { */ async function doSendTraceData(spanData: any): Promise { const { auth } = await currentResolvedConfig() - if (!auth.credentials) { + if (!auth.accessToken) { logError('TraceSender', 'Cannot send trace data: not authenticated') throw new Error('Not authenticated') } - const traceUrl = new URL('/-/debug/otlp/v1/traces', auth.serverEndpoint) - - const headers = new Headers({ 'Content-Type': 'application/json' }) - addAuthHeaders(auth, headers, traceUrl) - + const traceUrl = new URL('/-/debug/otlp/v1/traces', auth.serverEndpoint).toString() const response = await fetch(traceUrl, { method: 'POST', - headers: headers, + headers: { + 'Content-Type': 'application/json', + ...(auth.accessToken ? { Authorization: `token ${auth.accessToken}` } : {}), + }, body: spanData, }) diff --git a/vscode/src/testutils/mocks.ts b/vscode/src/testutils/mocks.ts index 30b72ebb5cdf..3a8f84f2fe8e 100644 --- a/vscode/src/testutils/mocks.ts +++ b/vscode/src/testutils/mocks.ts @@ -948,5 +948,4 @@ export const DEFAULT_VSCODE_SETTINGS = { experimentalGuardrailsTimeoutSeconds: undefined, overrideAuthToken: undefined, overrideServerEndpoint: undefined, - authExternalProviders: [], } satisfies ClientConfiguration diff --git a/vscode/webviews/AppWrapperForTest.tsx b/vscode/webviews/AppWrapperForTest.tsx index 1f7b9bab2ec7..c01146a6934e 100644 --- a/vscode/webviews/AppWrapperForTest.tsx +++ b/vscode/webviews/AppWrapperForTest.tsx @@ -115,10 +115,7 @@ export const AppWrapperForTest: FunctionComponent<{ children: ReactNode }> = ({ detectIntent: () => Observable.of(), resolvedConfig: () => Observable.of({ - auth: { - credentials: { token: 'abc' }, - serverEndpoint: 'https://example.com', - }, + auth: { accessToken: 'abc', serverEndpoint: 'https://example.com' }, configuration: { autocomplete: true, devModels: [{ model: 'my-model', provider: 'my-provider' }],