Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 40 additions & 4 deletions packages/opencode/src/provider/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,46 @@ export interface Interface {
readonly authorize: (
input: {
providerID: ProviderID
} & AuthorizeInput,
) => Effect.Effect<Authorization | undefined, Error>
readonly callback: (input: { providerID: ProviderID } & CallbackInput) => Effect.Effect<void, Error>
}
method: number
inputs?: Record<string, string>
}) => Effect.Effect<Authorization | undefined, Error>
readonly callback: (input: { providerID: ProviderID; method: number; code?: string }) => Effect.Effect<void, Error>
}

interface State {
hooks: Record<ProviderID, Hook>
pending: Map<ProviderID, AuthOAuthResult>
}

export class Service extends Context.Service<Service, Interface>()("@opencode/ProviderAuth") {}

export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> = Layer.effect(
Service,
Effect.gen(function* () {
const auth = yield* Auth.Service
const plugin = yield* Plugin.Service
const state = yield* InstanceState.make<State>(
Effect.fn("ProviderAuth.state")(function* () {
const plugins = yield* plugin.list()

const hooks: Record<ProviderID, Hook> = {}

for (const item of plugins) {
const auth = item.auth
if (!auth?.provider) continue

const id = ProviderID.make(auth.provider)
if (id === "openai" && hooks[id]) continue
hooks[id] = auth
}

return {
hooks,
pending: new Map<ProviderID, AuthOAuthResult>(),
}

}),
)

interface State {
hooks: Record<ProviderID, Hook>
Expand Down
44 changes: 44 additions & 0 deletions packages/opencode/test/plugin/auth-override.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,50 @@ describe("plugin.auth-override", () => {
expect(copilot[0].label).toBe("Test Override Auth")
expect(plainMethods[ProviderID.make("github-copilot")][0].label).not.toBe("Test Override Auth")
}, 30000) // Increased timeout for plugin installation

test("user plugin does not override built-in openai auth", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
const pluginDir = path.join(dir, ".opencode", "plugin")
await fs.mkdir(pluginDir, { recursive: true })

await Bun.write(
path.join(pluginDir, "custom-openai-auth.ts"),
[
"export default {",
' id: "demo.custom-openai-auth",',
" server: async () => ({",
" auth: {",
' provider: "openai",',
" methods: [",
' { type: "api", label: "Test Override Auth" },',
" ],",
" loader: async () => ({ access: 'test-token' }),",
" },",
" }),",
"}",
"",
].join("\n"),
)
},
})

const methods = await Instance.provide({
directory: tmp.path,
fn: async () => {
return Effect.runPromise(
ProviderAuth.Service.use((svc) => svc.methods()).pipe(Effect.provide(ProviderAuth.defaultLayer)),
)
},
})

const openai = methods[ProviderID.make("openai")]
expect(openai).toBeDefined()
expect(openai.some((x) => x.label === "ChatGPT Pro/Plus (browser)")).toBe(true)
expect(openai.some((x) => x.label === "ChatGPT Pro/Plus (headless)")).toBe(true)
expect(openai.some((x) => x.label === "Manually enter API Key")).toBe(true)
expect(openai.some((x) => x.label === "Test Override Auth")).toBe(false)
}, 30000)
})

const file = path.join(import.meta.dir, "../../src/plugin/index.ts")
Expand Down
Loading