diff --git a/src/copilot-api-resolver.internal.ts b/src/copilot-api-resolver.internal.ts new file mode 100644 index 000000000..2968a0364 --- /dev/null +++ b/src/copilot-api-resolver.internal.ts @@ -0,0 +1,43 @@ +/** + * Parse a provider base URL into a URL object, handling missing schemes. + * Returns undefined if the input is empty or unparseable. + */ +function parseProviderBaseUrl(providerBaseUrl: string | undefined): URL | undefined { + const trimmed = providerBaseUrl?.trim(); + if (!trimmed) return undefined; + + const candidate = trimmed.includes('://') + ? trimmed + : `https://${trimmed}`; + + try { + return new URL(candidate); + } catch { + return undefined; + } +} + +/** + * Derive a Copilot API target hostname from COPILOT_PROVIDER_BASE_URL. + * Returns undefined when the value is empty or not a valid URL/host. + */ +export function deriveCopilotApiTargetFromProviderBaseUrl( + providerBaseUrl: string | undefined +): string | undefined { + return parseProviderBaseUrl(providerBaseUrl)?.hostname || undefined; +} + +/** + * Derive a Copilot API base-path prefix from COPILOT_PROVIDER_BASE_URL. + * Returns undefined when the value is empty, invalid, or has no path. + */ +export function deriveCopilotApiBasePathFromProviderBaseUrl( + providerBaseUrl: string | undefined +): string | undefined { + const url = parseProviderBaseUrl(providerBaseUrl); + if (!url) return undefined; + + const pathname = url.pathname.replace(/\/+$/, ''); + if (!pathname || pathname === '/') return undefined; + return pathname.startsWith('/') ? pathname : `/${pathname}`; +} diff --git a/src/copilot-api-resolver.test-utils.ts b/src/copilot-api-resolver.test-utils.ts index a41fccd81..7191ef553 100644 --- a/src/copilot-api-resolver.test-utils.ts +++ b/src/copilot-api-resolver.test-utils.ts @@ -1,5 +1,13 @@ +import { + deriveCopilotApiBasePathFromProviderBaseUrl, + deriveCopilotApiTargetFromProviderBaseUrl, +} from './copilot-api-resolver.internal'; + /** - * Test-only re-export of internal helpers from copilot-api-resolver. + * Test-only helpers for copilot-api-resolver. * Tests should import from this file, not directly from the production module. */ -export { copilotApiResolverTestHelpers } from './copilot-api-resolver'; +export const copilotApiResolverTestHelpers = { + deriveCopilotApiTargetFromProviderBaseUrl, + deriveCopilotApiBasePathFromProviderBaseUrl, +}; diff --git a/src/copilot-api-resolver.test.ts b/src/copilot-api-resolver.test.ts index 324390827..234b4f685 100644 --- a/src/copilot-api-resolver.test.ts +++ b/src/copilot-api-resolver.test.ts @@ -1,3 +1,4 @@ +import * as copilotApiResolverModule from './copilot-api-resolver'; import { resolveCopilotApiKey, resolveCopilotApiRouting, @@ -10,6 +11,10 @@ const { } = copilotApiResolverTestHelpers; describe('resolveCopilotApiKey', () => { + it('should not expose test helpers from the production module API', () => { + expect(copilotApiResolverModule).not.toHaveProperty('copilotApiResolverTestHelpers'); + }); + it('should return COPILOT_API_KEY when set', () => { const env = { COPILOT_API_KEY: 'key123' }; expect(resolveCopilotApiKey(env)).toBe('key123'); diff --git a/src/copilot-api-resolver.ts b/src/copilot-api-resolver.ts index d7c20f0f1..52ffba427 100644 --- a/src/copilot-api-resolver.ts +++ b/src/copilot-api-resolver.ts @@ -1,3 +1,8 @@ +import { + deriveCopilotApiBasePathFromProviderBaseUrl, + deriveCopilotApiTargetFromProviderBaseUrl, +} from './copilot-api-resolver.internal'; + /** * Resolve the Copilot BYOK key from supported environment variables. * COPILOT_API_KEY takes precedence over COPILOT_PROVIDER_API_KEY. @@ -8,50 +13,6 @@ export function resolveCopilotApiKey( return env.COPILOT_API_KEY || env.COPILOT_PROVIDER_API_KEY; } -/** - * Parse a provider base URL into a URL object, handling missing schemes. - * Returns undefined if the input is empty or unparseable. - */ -function parseProviderBaseUrl(providerBaseUrl: string | undefined): URL | undefined { - const trimmed = providerBaseUrl?.trim(); - if (!trimmed) return undefined; - - const candidate = trimmed.includes('://') - ? trimmed - : `https://${trimmed}`; - - try { - return new URL(candidate); - } catch { - return undefined; - } -} - -/** - * Derive a Copilot API target hostname from COPILOT_PROVIDER_BASE_URL. - * Returns undefined when the value is empty or not a valid URL/host. - */ -function deriveCopilotApiTargetFromProviderBaseUrl( - providerBaseUrl: string | undefined -): string | undefined { - return parseProviderBaseUrl(providerBaseUrl)?.hostname || undefined; -} - -/** - * Derive a Copilot API base-path prefix from COPILOT_PROVIDER_BASE_URL. - * Returns undefined when the value is empty, invalid, or has no path. - */ -function deriveCopilotApiBasePathFromProviderBaseUrl( - providerBaseUrl: string | undefined -): string | undefined { - const url = parseProviderBaseUrl(providerBaseUrl); - if (!url) return undefined; - - const pathname = url.pathname.replace(/\/+$/, ''); - if (!pathname || pathname === '/') return undefined; - return pathname.startsWith('/') ? pathname : `/${pathname}`; -} - /** * Resolve Copilot target/base-path routing for BYOK provider-style env vars. * @@ -82,10 +43,3 @@ export function resolveCopilotApiRouting( copilotApiBasePathFromProviderBaseUrl, }; } - -/** @internal Exposed only for unit tests — not part of the public API. */ -// ts-prune-ignore-next -export const copilotApiResolverTestHelpers = { - deriveCopilotApiTargetFromProviderBaseUrl, - deriveCopilotApiBasePathFromProviderBaseUrl, -};