Skip to content

Commit 2f9004f

Browse files
fix(auth): opencode desktop server unauthorized bugfix on subagent spawn (#1399)
* fix(auth): opencode desktop server unauthorized bugfix on subagent spawn * refactor(auth): add runtime guard and throw on SDK mismatch - Add JSDoc with SDK API documentation reference - Replace silent failure with explicit Error throw when OPENCODE_SERVER_PASSWORD is set but client structure is incompatible - Add runtime type guard for SDK client structure - Add tests for error cases (missing _client, missing setConfig) - Remove unrelated bun.lock changes Co-authored-by: dan-myles <[email protected]> --------- Co-authored-by: YeonGyu-Kim <[email protected]> Co-authored-by: dan-myles <[email protected]>
1 parent 6151d1c commit 2f9004f

File tree

4 files changed

+163
-0
lines changed

4 files changed

+163
-0
lines changed

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ import {
9898
getOpenCodeVersion,
9999
isOpenCodeVersionAtLeast,
100100
OPENCODE_NATIVE_AGENTS_INJECTION_VERSION,
101+
injectServerAuthIntoClient,
101102
} from "./shared";
102103
import { loadPluginConfig } from "./plugin-config";
103104
import { createModelCacheState } from "./plugin-state";
@@ -107,6 +108,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
107108
log("[OhMyOpenCodePlugin] ENTRY - plugin loading", {
108109
directory: ctx.directory,
109110
});
111+
injectServerAuthIntoClient(ctx.client);
110112
// Start background tmux check immediately
111113
startTmuxCheck();
112114

src/shared/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@ export * from "./connected-providers-cache"
3939
export * from "./session-utils"
4040
export * from "./tmux"
4141
export * from "./model-suggestion-retry"
42+
export * from "./opencode-server-auth"
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { getServerBasicAuthHeader, injectServerAuthIntoClient } from "./opencode-server-auth"
2+
3+
describe("opencode-server-auth", () => {
4+
let originalEnv: Record<string, string | undefined>
5+
6+
beforeEach(() => {
7+
originalEnv = {
8+
OPENCODE_SERVER_PASSWORD: process.env.OPENCODE_SERVER_PASSWORD,
9+
OPENCODE_SERVER_USERNAME: process.env.OPENCODE_SERVER_USERNAME,
10+
}
11+
})
12+
13+
afterEach(() => {
14+
for (const [key, value] of Object.entries(originalEnv)) {
15+
if (value !== undefined) {
16+
process.env[key] = value
17+
} else {
18+
delete process.env[key]
19+
}
20+
}
21+
})
22+
23+
test("#given no server password #when building auth header #then returns undefined", () => {
24+
delete process.env.OPENCODE_SERVER_PASSWORD
25+
26+
const result = getServerBasicAuthHeader()
27+
28+
expect(result).toBeUndefined()
29+
})
30+
31+
test("#given server password without username #when building auth header #then uses default username", () => {
32+
process.env.OPENCODE_SERVER_PASSWORD = "secret"
33+
delete process.env.OPENCODE_SERVER_USERNAME
34+
35+
const result = getServerBasicAuthHeader()
36+
37+
expect(result).toBe("Basic b3BlbmNvZGU6c2VjcmV0")
38+
})
39+
40+
test("#given server password and username #when building auth header #then uses provided username", () => {
41+
process.env.OPENCODE_SERVER_PASSWORD = "secret"
42+
process.env.OPENCODE_SERVER_USERNAME = "dan"
43+
44+
const result = getServerBasicAuthHeader()
45+
46+
expect(result).toBe("Basic ZGFuOnNlY3JldA==")
47+
})
48+
49+
test("#given server password #when injecting into client #then updates client headers", () => {
50+
process.env.OPENCODE_SERVER_PASSWORD = "secret"
51+
delete process.env.OPENCODE_SERVER_USERNAME
52+
53+
let receivedConfig: { headers: Record<string, string> } | undefined
54+
const client = {
55+
_client: {
56+
setConfig: (config: { headers: Record<string, string> }) => {
57+
receivedConfig = config
58+
},
59+
},
60+
}
61+
62+
injectServerAuthIntoClient(client)
63+
64+
expect(receivedConfig).toEqual({
65+
headers: {
66+
Authorization: "Basic b3BlbmNvZGU6c2VjcmV0",
67+
},
68+
})
69+
})
70+
71+
test("#given server password #when client has no _client #then throws error", () => {
72+
process.env.OPENCODE_SERVER_PASSWORD = "secret"
73+
const client = {}
74+
75+
expect(() => injectServerAuthIntoClient(client)).toThrow(
76+
"[opencode-server-auth] OPENCODE_SERVER_PASSWORD is set but SDK client structure is incompatible"
77+
)
78+
})
79+
80+
test("#given server password #when client._client has no setConfig #then throws error", () => {
81+
process.env.OPENCODE_SERVER_PASSWORD = "secret"
82+
const client = { _client: {} }
83+
84+
expect(() => injectServerAuthIntoClient(client)).toThrow(
85+
"[opencode-server-auth] OPENCODE_SERVER_PASSWORD is set but SDK client._client.setConfig is not a function"
86+
)
87+
})
88+
89+
test("#given no server password #when client is invalid #then does not throw", () => {
90+
delete process.env.OPENCODE_SERVER_PASSWORD
91+
const client = {}
92+
93+
expect(() => injectServerAuthIntoClient(client)).not.toThrow()
94+
})
95+
})

src/shared/opencode-server-auth.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* Builds HTTP Basic Auth header from environment variables.
3+
*
4+
* @returns Basic Auth header string, or undefined if OPENCODE_SERVER_PASSWORD is not set
5+
*/
6+
export function getServerBasicAuthHeader(): string | undefined {
7+
const password = process.env.OPENCODE_SERVER_PASSWORD
8+
if (!password) {
9+
return undefined
10+
}
11+
12+
const username = process.env.OPENCODE_SERVER_USERNAME ?? "opencode"
13+
const token = Buffer.from(`${username}:${password}`, "utf8").toString("base64")
14+
15+
return `Basic ${token}`
16+
}
17+
18+
/**
19+
* Injects HTTP Basic Auth header into the OpenCode SDK client.
20+
*
21+
* This function accesses the SDK's internal `_client.setConfig()` method.
22+
* While `_client` has an underscore prefix (suggesting internal use), this is actually
23+
* a stable public API from `@hey-api/openapi-ts` generated client:
24+
* - `setConfig()` MERGES headers (does not replace existing ones)
25+
* - This is the documented way to update client config at runtime
26+
*
27+
* @see https://github.com/sst/opencode/blob/main/packages/sdk/js/src/gen/client/client.gen.ts
28+
* @throws {Error} If OPENCODE_SERVER_PASSWORD is set but client structure is incompatible
29+
*/
30+
export function injectServerAuthIntoClient(client: unknown): void {
31+
const auth = getServerBasicAuthHeader()
32+
if (!auth) {
33+
return
34+
}
35+
36+
// Runtime type guard for SDK client structure
37+
if (
38+
typeof client !== "object" ||
39+
client === null ||
40+
!("_client" in client) ||
41+
typeof (client as { _client: unknown })._client !== "object" ||
42+
(client as { _client: unknown })._client === null
43+
) {
44+
throw new Error(
45+
"[opencode-server-auth] OPENCODE_SERVER_PASSWORD is set but SDK client structure is incompatible. " +
46+
"This may indicate an OpenCode SDK version mismatch."
47+
)
48+
}
49+
50+
const internal = (client as { _client: { setConfig?: (config: { headers: Record<string, string> }) => void } })
51+
._client
52+
53+
if (typeof internal.setConfig !== "function") {
54+
throw new Error(
55+
"[opencode-server-auth] OPENCODE_SERVER_PASSWORD is set but SDK client._client.setConfig is not a function. " +
56+
"This may indicate an OpenCode SDK version mismatch."
57+
)
58+
}
59+
60+
internal.setConfig({
61+
headers: {
62+
Authorization: auth,
63+
},
64+
})
65+
}

0 commit comments

Comments
 (0)