fix(sdk): use dynamic port instead of hardcoded 4096 (fixes #2311)
- TmuxSessionManager resolves server URL with priority: ctx.serverUrl.origin > getServerBaseUrl(client) > OPENCODE_PORT env > localhost:4096 - Inject resolved baseUrl into SDK client (root + session subclient) at startup so all session tools use the dynamic port - Extract shared getInternalClient utility to sdk-internal-client.ts; prefer candidate with getConfig/setConfig for correct v1/v2 detection - Support v1 (client._client) and v2 (client.client) SDK structures across opencode-server-auth.ts and opencode-http-api.ts - Use createMockContext factory for serverUrl overrides in tests
This commit is contained in:
@@ -78,9 +78,10 @@ const trackedSessions = new Set<string>()
|
||||
function createMockContext(overrides?: {
|
||||
sessionStatusResult?: { data?: Record<string, { type: string }> }
|
||||
sessionMessagesResult?: { data?: unknown[] }
|
||||
serverUrl?: URL | undefined
|
||||
}) {
|
||||
return {
|
||||
serverUrl: new URL('http://localhost:4096'),
|
||||
serverUrl: overrides && 'serverUrl' in overrides ? overrides.serverUrl : new URL('http://localhost:4096'),
|
||||
client: {
|
||||
session: {
|
||||
status: mock(async () => {
|
||||
@@ -226,6 +227,59 @@ describe('TmuxSessionManager', () => {
|
||||
// then
|
||||
expect(manager).toBeDefined()
|
||||
})
|
||||
|
||||
test('uses dynamic port from ctx.serverUrl instead of hardcoded 4096', async () => {
|
||||
// given - server running on dynamic port 55535 (not 4096)
|
||||
const dynamicPort = 55535
|
||||
const { TmuxSessionManager } = await import('./manager')
|
||||
const ctx = createMockContext({ serverUrl: new URL(`http://localhost:${dynamicPort}`) })
|
||||
const config: TmuxConfig = {
|
||||
enabled: true,
|
||||
layout: 'main-vertical',
|
||||
main_pane_size: 60,
|
||||
main_pane_min_width: 80,
|
||||
agent_pane_min_width: 40,
|
||||
}
|
||||
|
||||
// when
|
||||
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
|
||||
|
||||
// then - should use dynamic port, not hardcoded 4096
|
||||
const serverUrl = (manager as any).serverUrl as string
|
||||
expect(serverUrl).toContain(`:${dynamicPort}`)
|
||||
expect(serverUrl).not.toContain('4096')
|
||||
})
|
||||
|
||||
test('falls back to OPENCODE_PORT env var when ctx.serverUrl is undefined', async () => {
|
||||
// given - ctx.serverUrl is undefined, but OPENCODE_PORT is set
|
||||
const originalPort = process.env.OPENCODE_PORT
|
||||
const { TmuxSessionManager } = await import('./manager')
|
||||
const ctx = createMockContext({ serverUrl: undefined })
|
||||
const config: TmuxConfig = {
|
||||
enabled: true,
|
||||
layout: 'main-vertical',
|
||||
main_pane_size: 60,
|
||||
main_pane_min_width: 80,
|
||||
agent_pane_min_width: 40,
|
||||
}
|
||||
|
||||
try {
|
||||
process.env.OPENCODE_PORT = '8080'
|
||||
// when
|
||||
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
|
||||
|
||||
// then - should use env var port, not hardcoded 4096
|
||||
const serverUrl = (manager as any).serverUrl as string
|
||||
expect(serverUrl).toBe('http://localhost:8080')
|
||||
expect(serverUrl).not.toContain('4096')
|
||||
} finally {
|
||||
if (originalPort !== undefined) {
|
||||
process.env.OPENCODE_PORT = originalPort
|
||||
} else {
|
||||
delete process.env.OPENCODE_PORT
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('onSessionCreated', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
import type { TmuxConfig } from "../../config/schema"
|
||||
import type { TrackedSession, CapacityConfig, WindowState } from "./types"
|
||||
import { log, normalizeSDKResponse } from "../../shared"
|
||||
import { log, normalizeSDKResponse, getServerBaseUrl } from "../../shared"
|
||||
import {
|
||||
isInsideTmux as defaultIsInsideTmux,
|
||||
getCurrentPaneId as defaultGetCurrentPaneId,
|
||||
@@ -72,12 +72,28 @@ export class TmuxSessionManager {
|
||||
this.client = ctx.client
|
||||
this.tmuxConfig = tmuxConfig
|
||||
this.deps = deps
|
||||
const defaultPort = process.env.OPENCODE_PORT ?? "4096"
|
||||
try {
|
||||
this.serverUrl = ctx.serverUrl?.toString() ?? `http://localhost:${defaultPort}`
|
||||
} catch {
|
||||
this.serverUrl = `http://localhost:${defaultPort}`
|
||||
let serverUrl: string | null = null
|
||||
|
||||
if (ctx.serverUrl) {
|
||||
serverUrl = ctx.serverUrl.origin
|
||||
}
|
||||
|
||||
if (!serverUrl) {
|
||||
serverUrl = getServerBaseUrl(ctx.client)
|
||||
}
|
||||
|
||||
if (!serverUrl) {
|
||||
const envPort = process.env.OPENCODE_PORT
|
||||
if (envPort) {
|
||||
serverUrl = `http://localhost:${envPort}`
|
||||
}
|
||||
}
|
||||
|
||||
if (!serverUrl) {
|
||||
serverUrl = "http://localhost:4096"
|
||||
}
|
||||
|
||||
this.serverUrl = serverUrl
|
||||
this.sourcePaneId = deps.getCurrentPaneId()
|
||||
this.pollingManager = new TmuxPollingManager(
|
||||
this.client,
|
||||
|
||||
+7
-1
@@ -12,7 +12,7 @@ import { createPluginDispose, type PluginDispose } from "./plugin-dispose"
|
||||
import { loadPluginConfig } from "./plugin-config"
|
||||
import { createModelCacheState } from "./plugin-state"
|
||||
import { createFirstMessageVariantGate } from "./shared/first-message-variant"
|
||||
import { injectServerAuthIntoClient, log } from "./shared"
|
||||
import { injectServerAuthIntoClient, injectServerBaseUrlIntoClient, log } from "./shared"
|
||||
import { startTmuxCheck } from "./tools"
|
||||
|
||||
let activePluginDispose: PluginDispose | null = null
|
||||
@@ -24,6 +24,12 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
directory: ctx.directory,
|
||||
})
|
||||
|
||||
// Inject correct server baseUrl to ensure SDK client uses dynamic port
|
||||
// Priority: ctx.serverUrl > fallback to existing client baseUrl
|
||||
if (ctx.serverUrl) {
|
||||
injectServerBaseUrlIntoClient(ctx.client, ctx.serverUrl.toString())
|
||||
}
|
||||
|
||||
injectServerAuthIntoClient(ctx.client)
|
||||
startTmuxCheck()
|
||||
await activePluginDispose?.()
|
||||
|
||||
@@ -58,6 +58,41 @@ describe("getServerBaseUrl", () => {
|
||||
// then
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it("returns baseUrl from client.client.getConfig().baseUrl (v2 SDK)", () => {
|
||||
// given
|
||||
const mockClient = {
|
||||
client: {
|
||||
getConfig: () => ({ baseUrl: "https://v2-api.example.com" }),
|
||||
},
|
||||
}
|
||||
|
||||
// when
|
||||
const result = getServerBaseUrl(mockClient)
|
||||
|
||||
// then
|
||||
expect(result).toBe("https://v2-api.example.com")
|
||||
})
|
||||
|
||||
it("returns baseUrl from client.session.client.getConfig().baseUrl (v2 SDK session path)", () => {
|
||||
// given
|
||||
const mockClient = {
|
||||
_client: {
|
||||
getConfig: () => ({}),
|
||||
},
|
||||
session: {
|
||||
client: {
|
||||
getConfig: () => ({ baseUrl: "https://v2-session.example.com" }),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// when
|
||||
const result = getServerBaseUrl(mockClient)
|
||||
|
||||
// then
|
||||
expect(result).toBe("https://v2-session.example.com")
|
||||
})
|
||||
})
|
||||
|
||||
describe("patchPart", () => {
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
import { getServerBasicAuthHeader } from "./opencode-server-auth"
|
||||
import { log } from "./logger"
|
||||
import { isRecord } from "./record-type-guard"
|
||||
|
||||
type UnknownRecord = Record<string, unknown>
|
||||
|
||||
function getInternalClient(client: unknown): UnknownRecord | null {
|
||||
if (!isRecord(client)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const internal = client["_client"]
|
||||
return isRecord(internal) ? internal : null
|
||||
}
|
||||
import { getInternalClient, isRecord } from "./sdk-internal-client"
|
||||
|
||||
export function getServerBaseUrl(client: unknown): string | null {
|
||||
// Try client._client.getConfig().baseUrl
|
||||
@@ -29,12 +18,12 @@ export function getServerBaseUrl(client: unknown): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
// Try client.session._client.getConfig().baseUrl
|
||||
// Try client.session._client.getConfig().baseUrl (v1) or client.session.client.getConfig().baseUrl (v2)
|
||||
if (isRecord(client)) {
|
||||
const session = client["session"]
|
||||
if (isRecord(session)) {
|
||||
const internal = session["_client"]
|
||||
if (isRecord(internal)) {
|
||||
const internal = getInternalClient(session)
|
||||
if (internal) {
|
||||
const getConfig = internal["getConfig"]
|
||||
if (typeof getConfig === "function") {
|
||||
const config = getConfig()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
||||
import { getServerBasicAuthHeader, injectServerAuthIntoClient } from "./opencode-server-auth"
|
||||
import { getServerBasicAuthHeader, injectServerAuthIntoClient, injectServerBaseUrlIntoClient } from "./opencode-server-auth"
|
||||
|
||||
describe("opencode-server-auth", () => {
|
||||
let originalEnv: Record<string, string | undefined>
|
||||
@@ -32,25 +32,25 @@ describe("opencode-server-auth", () => {
|
||||
})
|
||||
|
||||
test("#given server password without username #when building auth header #then uses default username", () => {
|
||||
process.env.OPENCODE_SERVER_PASSWORD = "secret"
|
||||
process.env.OPENCODE_SERVER_PASSWORD = "test-password-placeholder"
|
||||
delete process.env.OPENCODE_SERVER_USERNAME
|
||||
|
||||
const result = getServerBasicAuthHeader()
|
||||
|
||||
expect(result).toBe("Basic b3BlbmNvZGU6c2VjcmV0")
|
||||
expect(result).toBe("Basic b3BlbmNvZGU6dGVzdC1wYXNzd29yZC1wbGFjZWhvbGRlcg==")
|
||||
})
|
||||
|
||||
test("#given server password and username #when building auth header #then uses provided username", () => {
|
||||
process.env.OPENCODE_SERVER_PASSWORD = "secret"
|
||||
process.env.OPENCODE_SERVER_PASSWORD = "test-password-placeholder"
|
||||
process.env.OPENCODE_SERVER_USERNAME = "dan"
|
||||
|
||||
const result = getServerBasicAuthHeader()
|
||||
|
||||
expect(result).toBe("Basic ZGFuOnNlY3JldA==")
|
||||
expect(result).toBe("Basic ZGFuOnRlc3QtcGFzc3dvcmQtcGxhY2Vob2xkZXI=")
|
||||
})
|
||||
|
||||
test("#given server password #when injecting into client #then updates client headers", () => {
|
||||
process.env.OPENCODE_SERVER_PASSWORD = "secret"
|
||||
process.env.OPENCODE_SERVER_PASSWORD = "test-password-placeholder"
|
||||
delete process.env.OPENCODE_SERVER_USERNAME
|
||||
|
||||
let receivedHeadersConfig: { headers: Record<string, string> } | undefined
|
||||
@@ -68,14 +68,14 @@ describe("opencode-server-auth", () => {
|
||||
|
||||
expect(receivedHeadersConfig).toEqual({
|
||||
headers: {
|
||||
Authorization: "Basic b3BlbmNvZGU6c2VjcmV0",
|
||||
Authorization: "Basic b3BlbmNvZGU6dGVzdC1wYXNzd29yZC1wbGFjZWhvbGRlcg==",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("#given server password #when injecting wraps internal fetch #then wrapped fetch adds Authorization header", async () => {
|
||||
//#given
|
||||
process.env.OPENCODE_SERVER_PASSWORD = "secret"
|
||||
process.env.OPENCODE_SERVER_PASSWORD = "test-password-placeholder"
|
||||
delete process.env.OPENCODE_SERVER_USERNAME
|
||||
|
||||
let receivedAuthorization: string | null = null
|
||||
@@ -112,12 +112,12 @@ describe("opencode-server-auth", () => {
|
||||
await currentConfig.fetch(new Request("http://example.com"))
|
||||
|
||||
//#then
|
||||
expect(receivedAuthorization ?? "").toBe("Basic b3BlbmNvZGU6c2VjcmV0")
|
||||
expect(receivedAuthorization ?? "").toBe("Basic b3BlbmNvZGU6dGVzdC1wYXNzd29yZC1wbGFjZWhvbGRlcg==")
|
||||
})
|
||||
|
||||
test("#given server password #when internal has _config.fetch but no setConfig #then fetch is wrapped and injects Authorization", async () => {
|
||||
//#given
|
||||
process.env.OPENCODE_SERVER_PASSWORD = "secret"
|
||||
process.env.OPENCODE_SERVER_PASSWORD = "test-password-placeholder"
|
||||
delete process.env.OPENCODE_SERVER_USERNAME
|
||||
|
||||
let receivedAuthorization: string | null = null
|
||||
@@ -141,12 +141,12 @@ describe("opencode-server-auth", () => {
|
||||
await internal._config.fetch(new Request("http://example.com"))
|
||||
|
||||
//#then
|
||||
expect(receivedAuthorization ?? "").toBe("Basic b3BlbmNvZGU6c2VjcmV0")
|
||||
expect(receivedAuthorization ?? "").toBe("Basic b3BlbmNvZGU6dGVzdC1wYXNzd29yZC1wbGFjZWhvbGRlcg==")
|
||||
})
|
||||
|
||||
test("#given server password #when client has top-level fetch #then fetch is wrapped and injects Authorization", async () => {
|
||||
//#given
|
||||
process.env.OPENCODE_SERVER_PASSWORD = "secret"
|
||||
process.env.OPENCODE_SERVER_PASSWORD = "test-password-placeholder"
|
||||
delete process.env.OPENCODE_SERVER_USERNAME
|
||||
|
||||
let receivedAuthorization: string | null = null
|
||||
@@ -164,12 +164,12 @@ describe("opencode-server-auth", () => {
|
||||
await client.fetch(new Request("http://example.com"))
|
||||
|
||||
//#then
|
||||
expect(receivedAuthorization ?? "").toBe("Basic b3BlbmNvZGU6c2VjcmV0")
|
||||
expect(receivedAuthorization ?? "").toBe("Basic b3BlbmNvZGU6dGVzdC1wYXNzd29yZC1wbGFjZWhvbGRlcg==")
|
||||
})
|
||||
|
||||
test("#given server password #when interceptors are available #then request interceptor injects Authorization", async () => {
|
||||
//#given
|
||||
process.env.OPENCODE_SERVER_PASSWORD = "secret"
|
||||
process.env.OPENCODE_SERVER_PASSWORD = "test-password-placeholder"
|
||||
delete process.env.OPENCODE_SERVER_USERNAME
|
||||
|
||||
let registeredInterceptor:
|
||||
@@ -200,7 +200,7 @@ describe("opencode-server-auth", () => {
|
||||
const result = await registeredInterceptor(request, {})
|
||||
|
||||
//#then
|
||||
expect(result.headers.get("Authorization")).toBe("Basic b3BlbmNvZGU6c2VjcmV0")
|
||||
expect(result.headers.get("Authorization")).toBe("Basic b3BlbmNvZGU6dGVzdC1wYXNzd29yZC1wbGFjZWhvbGRlcg==")
|
||||
})
|
||||
|
||||
test("#given no server password #when injecting into client with fetch #then does not wrap fetch", async () => {
|
||||
@@ -242,14 +242,14 @@ describe("opencode-server-auth", () => {
|
||||
})
|
||||
|
||||
test("#given server password #when client has no _client #then does not throw", () => {
|
||||
process.env.OPENCODE_SERVER_PASSWORD = "secret"
|
||||
process.env.OPENCODE_SERVER_PASSWORD = "test-password-placeholder"
|
||||
const client = {}
|
||||
|
||||
expect(() => injectServerAuthIntoClient(client)).not.toThrow()
|
||||
})
|
||||
|
||||
test("#given server password #when client._client has no setConfig #then does not throw", () => {
|
||||
process.env.OPENCODE_SERVER_PASSWORD = "secret"
|
||||
process.env.OPENCODE_SERVER_PASSWORD = "test-password-placeholder"
|
||||
const client = { _client: {} }
|
||||
|
||||
expect(() => injectServerAuthIntoClient(client)).not.toThrow()
|
||||
@@ -261,4 +261,88 @@ describe("opencode-server-auth", () => {
|
||||
|
||||
expect(() => injectServerAuthIntoClient(client)).not.toThrow()
|
||||
})
|
||||
|
||||
// v2 SDK compatibility tests
|
||||
test("#given server password #when injecting into v2 client #then updates client headers", () => {
|
||||
process.env.OPENCODE_SERVER_PASSWORD = "test-password-placeholder"
|
||||
delete process.env.OPENCODE_SERVER_USERNAME
|
||||
|
||||
let receivedHeadersConfig: { headers: Record<string, string> } | undefined
|
||||
const client = {
|
||||
client: {
|
||||
setConfig: (config: { headers?: Record<string, string> }) => {
|
||||
if (config.headers) {
|
||||
receivedHeadersConfig = { headers: config.headers }
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
injectServerAuthIntoClient(client)
|
||||
|
||||
expect(receivedHeadersConfig).toEqual({
|
||||
headers: {
|
||||
Authorization: "Basic b3BlbmNvZGU6dGVzdC1wYXNzd29yZC1wbGFjZWhvbGRlcg==",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("#given server password #when injecting into v2 client with fetch #then fetch is wrapped", async () => {
|
||||
process.env.OPENCODE_SERVER_PASSWORD = "test-password-placeholder"
|
||||
delete process.env.OPENCODE_SERVER_USERNAME
|
||||
|
||||
let receivedAuthorization: string | null = null
|
||||
const baseFetch = async (request: Request): Promise<Response> => {
|
||||
receivedAuthorization = request.headers.get("Authorization")
|
||||
return new Response("ok")
|
||||
}
|
||||
|
||||
type InternalConfig = {
|
||||
fetch?: (request: Request) => Promise<Response>
|
||||
headers?: Record<string, string>
|
||||
}
|
||||
|
||||
let currentConfig: InternalConfig = {
|
||||
fetch: baseFetch,
|
||||
headers: {},
|
||||
}
|
||||
|
||||
const client = {
|
||||
client: {
|
||||
getConfig: (): InternalConfig => ({ ...currentConfig }),
|
||||
setConfig: (config: InternalConfig): InternalConfig => {
|
||||
currentConfig = { ...currentConfig, ...config }
|
||||
return { ...currentConfig }
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
injectServerAuthIntoClient(client)
|
||||
if (!currentConfig.fetch) {
|
||||
throw new Error("expected fetch to be set")
|
||||
}
|
||||
await currentConfig.fetch(new Request("http://example.com"))
|
||||
|
||||
expect(receivedAuthorization ?? "").toBe("Basic b3BlbmNvZGU6dGVzdC1wYXNzd29yZC1wbGFjZWhvbGRlcg==")
|
||||
})
|
||||
|
||||
test("#given server password #when injecting baseUrl into v2 client #then updates baseUrl", () => {
|
||||
process.env.OPENCODE_SERVER_PASSWORD = "test-password-placeholder"
|
||||
delete process.env.OPENCODE_SERVER_USERNAME
|
||||
|
||||
let receivedBaseUrl: string | undefined
|
||||
const client = {
|
||||
client: {
|
||||
setConfig: (config: { baseUrl?: string }) => {
|
||||
if (config.baseUrl) {
|
||||
receivedBaseUrl = config.baseUrl
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
injectServerBaseUrlIntoClient(client, "http://localhost:3000")
|
||||
|
||||
expect(receivedBaseUrl).toBe("http://localhost:3000")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { log } from "./logger"
|
||||
import type { UnknownRecord } from "./sdk-internal-client"
|
||||
import { getInternalClient, isRecord } from "./sdk-internal-client"
|
||||
|
||||
/**
|
||||
* Builds HTTP Basic Auth header from environment variables.
|
||||
@@ -17,12 +19,6 @@ export function getServerBasicAuthHeader(): string | undefined {
|
||||
return `Basic ${token}`
|
||||
}
|
||||
|
||||
type UnknownRecord = Record<string, unknown>
|
||||
|
||||
function isRecord(value: unknown): value is UnknownRecord {
|
||||
return typeof value === "object" && value !== null
|
||||
}
|
||||
|
||||
function isRequestFetch(value: unknown): value is (request: Request) => Promise<Response> {
|
||||
return typeof value === "function"
|
||||
}
|
||||
@@ -38,15 +34,6 @@ function wrapRequestFetch(
|
||||
}
|
||||
}
|
||||
|
||||
function getInternalClient(client: unknown): UnknownRecord | null {
|
||||
if (!isRecord(client)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const internal = client["_client"]
|
||||
return isRecord(internal) ? internal : null
|
||||
}
|
||||
|
||||
function tryInjectViaSetConfigHeaders(internal: UnknownRecord, auth: string): boolean {
|
||||
const setConfig = internal["setConfig"]
|
||||
if (typeof setConfig !== "function") {
|
||||
@@ -188,3 +175,61 @@ export function injectServerAuthIntoClient(client: unknown): void {
|
||||
log("[opencode-server-auth] Failed to inject server auth", { message })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects/updates the server baseUrl in the OpenCode SDK client.
|
||||
*
|
||||
* This ensures the SDK client uses the correct server URL (dynamic port) instead of
|
||||
* any hardcoded default. Uses the same setConfig() API as auth injection.
|
||||
*
|
||||
* @param client - The SDK client to update
|
||||
* @param serverUrl - The correct server URL to use
|
||||
*/
|
||||
export function injectServerBaseUrlIntoClient(client: unknown, serverUrl: string): void {
|
||||
try {
|
||||
let injected = false
|
||||
|
||||
const internal = getInternalClient(client)
|
||||
if (internal) {
|
||||
const setConfig = internal["setConfig"]
|
||||
if (typeof setConfig === "function") {
|
||||
setConfig({ baseUrl: serverUrl })
|
||||
log("[opencode-server-auth] Updated client baseUrl", { serverUrl })
|
||||
injected = true
|
||||
}
|
||||
}
|
||||
|
||||
// Also inject into session subclient if it exists (mirrors getServerBaseUrl fallback path)
|
||||
if (isRecord(client)) {
|
||||
const session = client["session"]
|
||||
if (isRecord(session)) {
|
||||
const sessionInternal = getInternalClient(session)
|
||||
if (sessionInternal) {
|
||||
const setConfig = sessionInternal["setConfig"]
|
||||
if (typeof setConfig === "function") {
|
||||
setConfig({ baseUrl: serverUrl })
|
||||
log("[opencode-server-auth] Updated session client baseUrl", { serverUrl })
|
||||
injected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try to set baseUrl directly on client if it has setConfig
|
||||
if (!injected && isRecord(client)) {
|
||||
const setConfig = client["setConfig"]
|
||||
if (typeof setConfig === "function") {
|
||||
setConfig({ baseUrl: serverUrl })
|
||||
log("[opencode-server-auth] Updated client baseUrl (top-level)", { serverUrl })
|
||||
injected = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!injected) {
|
||||
log("[opencode-server-auth] Could not update client baseUrl - incompatible client structure")
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
log("[opencode-server-auth] Failed to inject server baseUrl", { message, serverUrl })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Shared utilities for accessing OpenCode SDK internal client structure.
|
||||
*
|
||||
* Provides safe access to the SDK's internal `_client` (v1) or `client` (v2) properties.
|
||||
*/
|
||||
|
||||
export type UnknownRecord = Record<string, unknown>
|
||||
|
||||
function isRecord(value: unknown): value is UnknownRecord {
|
||||
return typeof value === "object" && value !== null
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the internal SDK client from a wrapper client.
|
||||
*
|
||||
* Supports both v1 SDK (client._client) and v2 SDK (client.client).
|
||||
*
|
||||
* @param client - The SDK client wrapper
|
||||
* @returns The internal client record, or null if not accessible
|
||||
*/
|
||||
export function getInternalClient(client: unknown): UnknownRecord | null {
|
||||
if (!isRecord(client)) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Support both v1 SDK (client._client) and v2 SDK (client.client)
|
||||
// Prefer the candidate that has getConfig or setConfig (the actual transport client)
|
||||
const candidates = [client["_client"], client["client"]].filter(isRecord)
|
||||
for (const candidate of candidates) {
|
||||
if (typeof candidate["getConfig"] === "function" || typeof candidate["setConfig"] === "function") {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
return candidates[0] ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard for Record<string, unknown>.
|
||||
*
|
||||
* @param value - The value to check
|
||||
* @returns True if the value is a Record<string, unknown>
|
||||
*/
|
||||
export { isRecord }
|
||||
Reference in New Issue
Block a user