From bb272e5303043252297d6b2fed1139e8378a3e7a Mon Sep 17 00:00:00 2001 From: akarineren Date: Sat, 25 Jul 2026 21:31:52 +0900 Subject: [PATCH] Add tokitoki verify key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Checks the stored API key against the server's verify endpoint. An invalid key is a definite answer — exit 0 with valid:false — so callers can tell it apart from a check that could not run. Editor plugins use this for their key-status commands; the verification logic lives in the CLI once instead of in every front-end. --- cmd/tokitoki/main.go | 33 ++++++++++++++++++++++++++++++ internal/deviceauth/deviceauth.go | 34 +++++++++++++++++++++++++++++++ pkg/agentlib/agentlib.go | 14 +++++++++++++ 3 files changed, 81 insertions(+) diff --git a/cmd/tokitoki/main.go b/cmd/tokitoki/main.go index 6282636..2273480 100644 --- a/cmd/tokitoki/main.go +++ b/cmd/tokitoki/main.go @@ -63,6 +63,9 @@ func run(args []string) int { if len(args) > 0 && args[0] == "get" { return runGet(args[1:]) } + if len(args) > 0 && args[0] == "verify" { + return runVerify(args[1:]) + } if len(args) > 0 && args[0] == "__service-run" { return runServiceWorker(args[1:]) } @@ -272,6 +275,35 @@ func runGet(args []string) int { return 0 } +func runVerify(args []string) int { + if len(args) != 1 || args[0] != "key" { + fmt.Fprintln(os.Stderr, "usage: tokitoki verify key") + return 2 + } + + logger := defaultLogger() + client, err := agentlib.New(agentlib.Options{Logger: logger}) + if err != nil { + return fail(logger, err) + } + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + // An invalid key is a definite answer, not a failure: exit 0 with + // valid:false so callers can tell it apart from "could not check". + valid, err := client.VerifyAPIKey(ctx) + if err != nil { + return fail(logger, err) + } + if err := writeJSON(os.Stdout, map[string]any{"ok": true, "valid": valid}); err != nil { + return fail(logger, err) + } + return 0 +} + func runUpdate(args []string) int { if len(args) != 0 { fmt.Fprintln(os.Stderr, "usage: tokitoki update") @@ -522,6 +554,7 @@ Usage: tokitoki set key tokitoki get key tokitoki get dashboard-url + tokitoki verify key tokitoki heartbeat --entity FILE [options] tokitoki version tokitoki update diff --git a/internal/deviceauth/deviceauth.go b/internal/deviceauth/deviceauth.go index 114a825..ae22ba6 100644 --- a/internal/deviceauth/deviceauth.go +++ b/internal/deviceauth/deviceauth.go @@ -43,3 +43,37 @@ func DashboardURL(ctx context.Context, baseURL, apiKey string) (string, error) { } return decoded.URL, nil } + +// VerifyKey asks the server whether apiKey is currently valid. A definite +// server answer returns (true, nil) or (false, nil); anything else — network +// failure, server error — is an error, so callers can tell "invalid key" +// apart from "could not check". +func VerifyKey(ctx context.Context, baseURL, apiKey string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+"/api/auth/api-key/verify", nil) + if err != nil { + return false, err + } + req.Header.Set("Authorization", "Bearer "+apiKey) + req.Header.Set("User-Agent", buildinfo.UserAgent()) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return false, fmt.Errorf("verify API key: %w", err) + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK: + var decoded struct { + Valid bool `json:"valid"` + } + if err := json.NewDecoder(resp.Body).Decode(&decoded); err != nil { + return false, fmt.Errorf("verify API key: %w", err) + } + return decoded.Valid, nil + case http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("verify API key: server returned %s", resp.Status) + } +} diff --git a/pkg/agentlib/agentlib.go b/pkg/agentlib/agentlib.go index 31633f9..66d68f2 100644 --- a/pkg/agentlib/agentlib.go +++ b/pkg/agentlib/agentlib.go @@ -225,6 +225,20 @@ func (c *Client) DashboardURL(ctx context.Context) (string, error) { return deviceauth.DashboardURL(ctx, usageupload.BaseURL(), apiKey) } +// VerifyAPIKey checks the stored API key against the server. A definite +// answer returns (true, nil) or (false, nil); network or server trouble is +// an error so callers can tell "invalid" apart from "could not check". +func (c *Client) VerifyAPIKey(ctx context.Context) (bool, error) { + apiKey, err := c.GetAPIKey() + if err != nil { + return false, err + } + if ctx == nil { + ctx = context.Background() + } + return deviceauth.VerifyKey(ctx, usageupload.BaseURL(), apiKey) +} + // Sync scans selected provider directories and uploads newly discovered events. func (c *Client) Sync(ctx context.Context, options SyncOptions) error { providerDirs := normalizeProviderDirs(options.ProviderDirs)