1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5// Package client provides access token management and Lunatask API client creation.
6package client
7
8import (
9 "errors"
10 "fmt"
11 "os"
12 "runtime/debug"
13
14 "git.secluded.site/go-lunatask"
15 "github.com/zalando/go-keyring"
16)
17
18const (
19 keyringService = "lunatask-mcp-server"
20 keyringUser = "access-token"
21
22 // EnvAccessToken is the environment variable for the access token.
23 EnvAccessToken = "LUNATASK_ACCESS_TOKEN"
24)
25
26// TokenSource indicates where the access token was loaded from.
27type TokenSource int
28
29// Token source constants.
30const (
31 TokenSourceNone TokenSource = iota // No token found
32 TokenSourceEnv // From environment variable
33 TokenSourceKeyring // From system keyring
34)
35
36func (s TokenSource) String() string {
37 switch s {
38 case TokenSourceEnv:
39 return "environment variable"
40 case TokenSourceKeyring:
41 return "system keyring"
42 case TokenSourceNone:
43 return "none"
44 default:
45 return "none"
46 }
47}
48
49// ErrNoToken indicates no access token is available.
50var ErrNoToken = errors.New(
51 "no access token found; run 'lunatask-mcp-server config' to configure, " +
52 "or set LUNATASK_ACCESS_TOKEN",
53)
54
55// New creates a Lunatask client using the access token from available sources.
56// Token priority: environment variable > system keyring.
57func New() (*lunatask.Client, error) {
58 token, _, err := GetToken()
59 if err != nil {
60 return nil, err
61 }
62
63 return lunatask.NewClient(token, lunatask.UserAgent("lunatask-mcp-server/"+version())), nil
64}
65
66// GetToken returns the access token and its source.
67// Token priority: environment variable > system keyring.
68// Returns ErrNoToken if no token is found.
69func GetToken() (string, TokenSource, error) {
70 // 1. Environment variable (highest priority for container deployments)
71 if token := os.Getenv(EnvAccessToken); token != "" {
72 return token, TokenSourceEnv, nil
73 }
74
75 // 2. System keyring
76 token, err := keyring.Get(keyringService, keyringUser)
77 if err == nil && token != "" {
78 return token, TokenSourceKeyring, nil
79 }
80
81 if err != nil && !errors.Is(err, keyring.ErrNotFound) {
82 return "", TokenSourceNone, fmt.Errorf("accessing system keyring: %w", err)
83 }
84
85 return "", TokenSourceNone, ErrNoToken
86}
87
88// HasKeyringToken checks if an access token is stored in the keyring.
89// Returns (true, nil) if found, (false, nil) if not found,
90// or (false, error) if there was a keyring access problem.
91func HasKeyringToken() (bool, error) {
92 _, err := keyring.Get(keyringService, keyringUser)
93 if err != nil {
94 if errors.Is(err, keyring.ErrNotFound) {
95 return false, nil
96 }
97
98 return false, fmt.Errorf("accessing system keyring: %w", err)
99 }
100
101 return true, nil
102}
103
104// SetKeyringToken stores the access token in the system keyring.
105func SetKeyringToken(token string) error {
106 if err := keyring.Set(keyringService, keyringUser, token); err != nil {
107 return fmt.Errorf("saving to keyring: %w", err)
108 }
109
110 return nil
111}
112
113// DeleteKeyringToken removes the access token from the system keyring.
114func DeleteKeyringToken() error {
115 if err := keyring.Delete(keyringService, keyringUser); err != nil {
116 if errors.Is(err, keyring.ErrNotFound) {
117 return nil
118 }
119
120 return fmt.Errorf("deleting from keyring: %w", err)
121 }
122
123 return nil
124}
125
126// HasEnvToken checks if an access token is set via environment variable.
127func HasEnvToken() bool {
128 return os.Getenv(EnvAccessToken) != ""
129}
130
131// version returns the module version from build info, or "dev" if unavailable.
132func version() string {
133 info, ok := debug.ReadBuildInfo()
134 if !ok || info.Main.Version == "" || info.Main.Version == "(devel)" {
135 return "dev"
136 }
137
138 return info.Main.Version
139}