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 <dan-myles@users.noreply.github.com> --------- Co-authored-by: YeonGyu-Kim <code.yeon.gyu@gmail.com> Co-authored-by: dan-myles <dan-myles@users.noreply.github.com>
This commit is contained in:
parent
6151d1cb5e
commit
2f9004f076
@ -98,6 +98,7 @@ import {
|
|||||||
getOpenCodeVersion,
|
getOpenCodeVersion,
|
||||||
isOpenCodeVersionAtLeast,
|
isOpenCodeVersionAtLeast,
|
||||||
OPENCODE_NATIVE_AGENTS_INJECTION_VERSION,
|
OPENCODE_NATIVE_AGENTS_INJECTION_VERSION,
|
||||||
|
injectServerAuthIntoClient,
|
||||||
} from "./shared";
|
} from "./shared";
|
||||||
import { loadPluginConfig } from "./plugin-config";
|
import { loadPluginConfig } from "./plugin-config";
|
||||||
import { createModelCacheState } from "./plugin-state";
|
import { createModelCacheState } from "./plugin-state";
|
||||||
@ -107,6 +108,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
log("[OhMyOpenCodePlugin] ENTRY - plugin loading", {
|
log("[OhMyOpenCodePlugin] ENTRY - plugin loading", {
|
||||||
directory: ctx.directory,
|
directory: ctx.directory,
|
||||||
});
|
});
|
||||||
|
injectServerAuthIntoClient(ctx.client);
|
||||||
// Start background tmux check immediately
|
// Start background tmux check immediately
|
||||||
startTmuxCheck();
|
startTmuxCheck();
|
||||||
|
|
||||||
|
|||||||
@ -39,3 +39,4 @@ export * from "./connected-providers-cache"
|
|||||||
export * from "./session-utils"
|
export * from "./session-utils"
|
||||||
export * from "./tmux"
|
export * from "./tmux"
|
||||||
export * from "./model-suggestion-retry"
|
export * from "./model-suggestion-retry"
|
||||||
|
export * from "./opencode-server-auth"
|
||||||
|
|||||||
95
src/shared/opencode-server-auth.test.ts
Normal file
95
src/shared/opencode-server-auth.test.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { getServerBasicAuthHeader, injectServerAuthIntoClient } from "./opencode-server-auth"
|
||||||
|
|
||||||
|
describe("opencode-server-auth", () => {
|
||||||
|
let originalEnv: Record<string, string | undefined>
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalEnv = {
|
||||||
|
OPENCODE_SERVER_PASSWORD: process.env.OPENCODE_SERVER_PASSWORD,
|
||||||
|
OPENCODE_SERVER_USERNAME: process.env.OPENCODE_SERVER_USERNAME,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const [key, value] of Object.entries(originalEnv)) {
|
||||||
|
if (value !== undefined) {
|
||||||
|
process.env[key] = value
|
||||||
|
} else {
|
||||||
|
delete process.env[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given no server password #when building auth header #then returns undefined", () => {
|
||||||
|
delete process.env.OPENCODE_SERVER_PASSWORD
|
||||||
|
|
||||||
|
const result = getServerBasicAuthHeader()
|
||||||
|
|
||||||
|
expect(result).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given server password without username #when building auth header #then uses default username", () => {
|
||||||
|
process.env.OPENCODE_SERVER_PASSWORD = "secret"
|
||||||
|
delete process.env.OPENCODE_SERVER_USERNAME
|
||||||
|
|
||||||
|
const result = getServerBasicAuthHeader()
|
||||||
|
|
||||||
|
expect(result).toBe("Basic b3BlbmNvZGU6c2VjcmV0")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given server password and username #when building auth header #then uses provided username", () => {
|
||||||
|
process.env.OPENCODE_SERVER_PASSWORD = "secret"
|
||||||
|
process.env.OPENCODE_SERVER_USERNAME = "dan"
|
||||||
|
|
||||||
|
const result = getServerBasicAuthHeader()
|
||||||
|
|
||||||
|
expect(result).toBe("Basic ZGFuOnNlY3JldA==")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given server password #when injecting into client #then updates client headers", () => {
|
||||||
|
process.env.OPENCODE_SERVER_PASSWORD = "secret"
|
||||||
|
delete process.env.OPENCODE_SERVER_USERNAME
|
||||||
|
|
||||||
|
let receivedConfig: { headers: Record<string, string> } | undefined
|
||||||
|
const client = {
|
||||||
|
_client: {
|
||||||
|
setConfig: (config: { headers: Record<string, string> }) => {
|
||||||
|
receivedConfig = config
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
injectServerAuthIntoClient(client)
|
||||||
|
|
||||||
|
expect(receivedConfig).toEqual({
|
||||||
|
headers: {
|
||||||
|
Authorization: "Basic b3BlbmNvZGU6c2VjcmV0",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given server password #when client has no _client #then throws error", () => {
|
||||||
|
process.env.OPENCODE_SERVER_PASSWORD = "secret"
|
||||||
|
const client = {}
|
||||||
|
|
||||||
|
expect(() => injectServerAuthIntoClient(client)).toThrow(
|
||||||
|
"[opencode-server-auth] OPENCODE_SERVER_PASSWORD is set but SDK client structure is incompatible"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given server password #when client._client has no setConfig #then throws error", () => {
|
||||||
|
process.env.OPENCODE_SERVER_PASSWORD = "secret"
|
||||||
|
const client = { _client: {} }
|
||||||
|
|
||||||
|
expect(() => injectServerAuthIntoClient(client)).toThrow(
|
||||||
|
"[opencode-server-auth] OPENCODE_SERVER_PASSWORD is set but SDK client._client.setConfig is not a function"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given no server password #when client is invalid #then does not throw", () => {
|
||||||
|
delete process.env.OPENCODE_SERVER_PASSWORD
|
||||||
|
const client = {}
|
||||||
|
|
||||||
|
expect(() => injectServerAuthIntoClient(client)).not.toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
65
src/shared/opencode-server-auth.ts
Normal file
65
src/shared/opencode-server-auth.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* Builds HTTP Basic Auth header from environment variables.
|
||||||
|
*
|
||||||
|
* @returns Basic Auth header string, or undefined if OPENCODE_SERVER_PASSWORD is not set
|
||||||
|
*/
|
||||||
|
export function getServerBasicAuthHeader(): string | undefined {
|
||||||
|
const password = process.env.OPENCODE_SERVER_PASSWORD
|
||||||
|
if (!password) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const username = process.env.OPENCODE_SERVER_USERNAME ?? "opencode"
|
||||||
|
const token = Buffer.from(`${username}:${password}`, "utf8").toString("base64")
|
||||||
|
|
||||||
|
return `Basic ${token}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects HTTP Basic Auth header into the OpenCode SDK client.
|
||||||
|
*
|
||||||
|
* This function accesses the SDK's internal `_client.setConfig()` method.
|
||||||
|
* While `_client` has an underscore prefix (suggesting internal use), this is actually
|
||||||
|
* a stable public API from `@hey-api/openapi-ts` generated client:
|
||||||
|
* - `setConfig()` MERGES headers (does not replace existing ones)
|
||||||
|
* - This is the documented way to update client config at runtime
|
||||||
|
*
|
||||||
|
* @see https://github.com/sst/opencode/blob/main/packages/sdk/js/src/gen/client/client.gen.ts
|
||||||
|
* @throws {Error} If OPENCODE_SERVER_PASSWORD is set but client structure is incompatible
|
||||||
|
*/
|
||||||
|
export function injectServerAuthIntoClient(client: unknown): void {
|
||||||
|
const auth = getServerBasicAuthHeader()
|
||||||
|
if (!auth) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runtime type guard for SDK client structure
|
||||||
|
if (
|
||||||
|
typeof client !== "object" ||
|
||||||
|
client === null ||
|
||||||
|
!("_client" in client) ||
|
||||||
|
typeof (client as { _client: unknown })._client !== "object" ||
|
||||||
|
(client as { _client: unknown })._client === null
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"[opencode-server-auth] OPENCODE_SERVER_PASSWORD is set but SDK client structure is incompatible. " +
|
||||||
|
"This may indicate an OpenCode SDK version mismatch."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const internal = (client as { _client: { setConfig?: (config: { headers: Record<string, string> }) => void } })
|
||||||
|
._client
|
||||||
|
|
||||||
|
if (typeof internal.setConfig !== "function") {
|
||||||
|
throw new Error(
|
||||||
|
"[opencode-server-auth] OPENCODE_SERVER_PASSWORD is set but SDK client._client.setConfig is not a function. " +
|
||||||
|
"This may indicate an OpenCode SDK version mismatch."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal.setConfig({
|
||||||
|
headers: {
|
||||||
|
Authorization: auth,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user