Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
80 changes: 80 additions & 0 deletions lua/opencode/auth.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
local config = require('opencode.config')

local M = {}

local cache = nil

--- Resolve a credential value that may be a string or a function returning a string.
--- Returns nil for nil, empty string, or function errors.
---@param val string | (fun(): string | nil) | nil
---@return string | nil
local function resolve_credential(val)
if type(val) == 'function' then
local ok, result = pcall(val)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should credential function errors be surfaced here instead of being treated as missing credentials and cached?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although not major. This could be a nice to have

if ok and result and result ~= '' then
return result
end
return nil
end
if val and val ~= '' then
return val
end
return nil
end

--- Resolve and cache credentials from config + env vars.
---@return string|nil password
---@return string username
local function ensure_resolved()
if cache then
return cache.password, cache.username
end

local password = resolve_credential(config.server.password)
or vim.env.OPENCODE_SERVER_PASSWORD
local username = resolve_credential(config.server.username)
or vim.env.OPENCODE_SERVER_USERNAME
or 'opencode'

cache = {
password = password,
username = username,
}

return cache.password, cache.username
end

--- Reset cached credentials. Call after changing config values.
function M.clear_cache()
cache = nil
end

--- Resolve credentials and return Authorization headers for HTTP Basic Auth.
--- Returns an empty table if no password is configured (server doesn't require auth).
---@return table<string, string> headers
function M.get_auth_headers()
local password, username = ensure_resolved()
if not password then
return {}
end

local encoded = vim.base64.encode(username .. ':' .. password)
return { ['Authorization'] = 'Basic ' .. encoded }
end

--- Resolve credentials and return environment variables for a spawned server.
--- Returns an empty table if no password is configured.
---@return table<string, string> env
function M.get_env()
local password, username = ensure_resolved()
if not password then
return {}
end

return {
OPENCODE_SERVER_PASSWORD = password,
OPENCODE_SERVER_USERNAME = username,
}
end

return M
2 changes: 2 additions & 0 deletions lua/opencode/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ M.defaults = {
auto_kill = true,
path_map = nil,
reverse_path_map = nil,
username = nil,
password = nil,
},
-- stylua: ignore
keymap = {
Expand Down
4 changes: 4 additions & 0 deletions lua/opencode/opencode_server.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ local safe_call = util.safe_call
local Promise = require('opencode.promise')
local config = require('opencode.config')
local curl = require('opencode.curl')
local auth = require('opencode.auth')

--- @class OpencodeServer
--- @field job any The vim.system job handle
Expand Down Expand Up @@ -95,6 +96,7 @@ function OpencodeServer.health_check(url, timeout_ms)
curl.request({
url = url,
method = 'GET',
headers = auth.get_auth_headers(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another thing I noticed: curl.request() includes the Authorization header in its debug log, which may expose the Basic Auth credentials.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this could be an issue. I will have a look

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#467

I created an issue so I don't forget to have a look

timeout = timeout_ms or 2000,
proxy = '',
callback = function(response)
Expand Down Expand Up @@ -176,6 +178,7 @@ function OpencodeServer.request_graceful_shutdown(base_url)
curl.request({
url = shutdown_url,
method = 'POST',
headers = auth.get_auth_headers(),
timeout = 1000,
proxy = '',
callback = function(response)
Expand Down Expand Up @@ -272,6 +275,7 @@ function OpencodeServer:spawn(opts)
self.mode = 'serve'
self.job = vim.system(cmd, {
cwd = opts.cwd,
env = auth.get_env(),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't sure if the credential settings should also extend to the server that opencode.nvim starts. Since it was so small I figured it wouldn't hurt adding it and then seeing if it's appropriate or not.

stdout = function(err, data)
if err then
fail_startup(err)
Expand Down
4 changes: 3 additions & 1 deletion lua/opencode/server_job.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ local port_mapping = require('opencode.port_mapping')
local log = require('opencode.log')
local config = require('opencode.config')
local util = require('opencode.util')
local auth = require('opencode.auth')

local M = {}
M.requests = {}
Expand Down Expand Up @@ -75,7 +76,7 @@ function M.call_api(url, method, body)
local opts = {
url = url,
method = method or 'GET',
headers = { ['Content-Type'] = 'application/json' },
headers = vim.tbl_extend('force', { ['Content-Type'] = 'application/json' }, auth.get_auth_headers()),
proxy = '',
callback = function(response)
remove_from_requests()
Expand Down Expand Up @@ -128,6 +129,7 @@ function M.stream_api(url, method, body, on_chunk)
local opts = {
url = url,
method = method or 'GET',
headers = auth.get_auth_headers(),
proxy = '',
stream = function(err, chunk)
on_chunk(chunk)
Expand Down
2 changes: 2 additions & 0 deletions lua/opencode/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@
---@field auto_kill boolean -- Kill spawned servers when nvim exits (default: true)
---@field path_map (string | fun(host_path: string): string) | nil -- Map host paths to server paths
---@field reverse_path_map (fun(server_path: string): string) | nil -- Map server paths back to host paths
---@field username? string | fun(): string | nil -- Username for Basic auth. Falls back to OPENCODE_SERVER_USERNAME env var, then "opencode"
---@field password? string | fun(): string | nil -- Password for Basic auth. Falls back to OPENCODE_SERVER_PASSWORD env var
Comment on lines +209 to +210

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decided to add support for a function because my opencode server key is kept in a file, and I figured some neovim users would appreciate a more lazy approach to loading opencode credentials.


---@class OpencodeUIFloatConfig
---@field width number # Width in columns, or ratio when <= 1 (default: 0.95)
Expand Down
Loading