prime8dev

prime8dev
git clone https://git.prime8.dev/prime8dev.git
Log | Files | Refs | README | LICENSE

commit cfde566beecdf2785bc50a2df0c6cdd170cd767b
parent abd633dc0c80ca5325ab839cbf5987eced716a66
Author: Damian Myrda <damian@prime8.dev>
Date:   Wed, 21 Oct 2026 18:14:24 +0000

feat(wakatime): support oauth access tokens via WAKATIME_TOKEN

Diffstat:
Menv.example | 3++-
Minternal/external/wakatime.go | 24+++++++++++++++++++++---
Minternal/external/wakatime_test.go | 21+++++++++++++++++++++
3 files changed, 44 insertions(+), 4 deletions(-)

diff --git a/env.example b/env.example @@ -5,4 +5,5 @@ TERM=xterm-256color COLORTERM=truecolor GIT_REPOS_DIR=/srv/git/repos GIT_CLONE_BASE=https://prime8.dev -WAKATIME_API_KEY=your_wakatime_api_key_here +# OAuth token from https://wakatime.com/oauth (API keys are write-only since 2026-11-01) +WAKATIME_TOKEN=waka_tok_your_token_here diff --git a/internal/external/wakatime.go b/internal/external/wakatime.go @@ -26,7 +26,7 @@ func NewWakaTime(c *cache.Cache) *WakaTime { Timeout: 10 * time.Second, }, baseURL: "https://wakatime.com/api/v1", - apiKey: strings.TrimSpace(os.Getenv("WAKATIME_API_KEY")), + apiKey: wakaTimeCredential(), cache: c, } } @@ -293,8 +293,7 @@ func (s *WakaTime) makeRequest(path string) ([]byte, error) { return nil, err } - auth := base64.StdEncoding.EncodeToString([]byte(s.apiKey)) - req.Header.Set("Authorization", fmt.Sprintf("Basic %s", auth)) + req.Header.Set("Authorization", authHeader(s.apiKey)) resp, err := s.httpClient.Do(req) if err != nil { @@ -308,3 +307,22 @@ func (s *WakaTime) makeRequest(path string) ([]byte, error) { return io.ReadAll(resp.Body) } + +// wakaTimeCredential prefers WAKATIME_TOKEN (an OAuth access token from +// wakatime.com/oauth) over WAKATIME_API_KEY: since 2026-11-01 API keys are +// write-only and only work for plugins sending heartbeats. +func wakaTimeCredential() string { + if t := strings.TrimSpace(os.Getenv("WAKATIME_TOKEN")); t != "" { + return t + } + return strings.TrimSpace(os.Getenv("WAKATIME_API_KEY")) +} + +// authHeader sends OAuth tokens (waka_tok_...) as a bearer token and legacy +// API keys as HTTP Basic. +func authHeader(cred string) string { + if strings.HasPrefix(cred, "waka_tok_") { + return "Bearer " + cred + } + return "Basic " + base64.StdEncoding.EncodeToString([]byte(cred)) +} diff --git a/internal/external/wakatime_test.go b/internal/external/wakatime_test.go @@ -39,3 +39,24 @@ func TestFetchTotalWithoutKey(t *testing.T) { t.Fatalf("got %v, %v", stats, err) } } + +func TestAuthHeader(t *testing.T) { + if got := authHeader("waka_tok_abc"); got != "Bearer waka_tok_abc" { + t.Fatalf("token: got %q", got) + } + if got := authHeader("waka_123"); got != "Basic d2FrYV8xMjM=" { + t.Fatalf("key: got %q", got) + } +} + +func TestWakaTimeCredentialPrefersToken(t *testing.T) { + t.Setenv("WAKATIME_API_KEY", "waka_key") + t.Setenv("WAKATIME_TOKEN", " waka_tok_x ") + if got := wakaTimeCredential(); got != "waka_tok_x" { + t.Fatalf("got %q", got) + } + t.Setenv("WAKATIME_TOKEN", "") + if got := wakaTimeCredential(); got != "waka_key" { + t.Fatalf("fallback: got %q", got) + } +}