client.go

  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}