Files
bear-cli/lib/bear_cli/credentials.ex
T
alex 8cc8c41416 feat: 實作 bear CLI PAT 模式 MVP(login/whoami/token/logout/status)
- 新增 Elixir + Mix escript 專案(Req 0.5+、Jason)
- login --token <PAT>:以 PAT 驗證 /userinfo 後寫入憑證檔(0600)
- whoami:GET /userinfo 顯示身分 claims(支援 --json)
- token:印出 access token 供 pipe(--refresh 在 PAT 模式忽略)
- logout:清除本機憑證;status:純本機判定
- 憑證檔採原子寫入(rename)、0600、O_CREAT|O_EXCL 防 symlink
- 全域選項:--issuer/--config/--json/-v/-h/--version
- 29 個單元測試;mix precommit(compile --warnings-as-errors + format + test)
- 更新 README.md 與 docs/commands.md 反映 PAT 模式 MVP

Device Flow 登入待伺服器端 P1(bear 倉庫)完成後再接。issue #2
2026-08-30 14:24:51 +08:00

88 lines
2.2 KiB
Elixir
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
defmodule BearCli.Credentials do
@moduledoc """
本機憑證檔的讀寫(0600,原子寫入、防 symlink 攻擊)。
憑證檔結構(JSON):
{"issuer": "https://alterminal.com", "access_token": "<PAT>", "email": "..."}
PAT 模式沒有 refresh token,`access_token` 即個人存取權杖本身。
"""
alias BearCli.Config
@doc """
載入憑證,回傳 `{:ok, map}` 或 `:error`(不存在/JSON 損毀)。
"""
def load(path \\ Config.credentials_path()) do
case File.read(path) do
{:ok, content} ->
case Jason.decode(content) do
{:ok, map} when is_map(map) -> {:ok, map}
_ -> :error
end
{:error, _} ->
:error
end
end
@doc """
寫入憑證(原子、0600)。建立父目錄(0700)。回傳 `:ok` 或 `{:error, reason}`。
"""
def save(map, path \\ Config.credentials_path()) do
json = Jason.encode!(map)
with :ok <- ensure_parent_dir(path),
{:ok, tmp} <- write_temp(path, json) do
# rename(2) 會原子地取代目標(包含取代 symlink 本身,而非其指向的檔案)。
File.rename(tmp, path)
else
{:error, _} = error -> error
end
end
@doc """
刪除憑證檔;檔案不存在也算成功。
"""
def delete(path \\ Config.credentials_path()) do
case File.rm(path) do
:ok -> :ok
{:error, :enoent} -> :ok
{:error, reason} -> {:error, reason}
end
end
defp ensure_parent_dir(path) do
dir = Path.dirname(path)
File.mkdir_p(dir)
# 目錄權限 0700 為 best-effort(例如憑證路徑落在 /tmp 等共享目錄時無法 chmod,
# 忽略即可;真正的保護是憑證檔本身的 0600)。
_ = File.chmod(dir, 0o700)
:ok
end
defp write_temp(path, json) do
dir = Path.dirname(path)
tmp = Path.join(dir, ".credentials.#{System.unique_integer([:positive])}.tmp")
result =
with {:ok, io} <- File.open(tmp, [:write, :exclusive, :binary]),
:ok <- IO.binwrite(io, json),
:ok <- File.close(io),
:ok <- File.chmod(tmp, 0o600) do
{:ok, tmp}
end
case result do
{:ok, _} = ok ->
ok
{:error, _} = error ->
_ = File.rm(tmp)
error
end
end
end