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:
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)
+ }
+}