From 595774e7ee7d1bfcb2e7e8ea125a4859d5afb4bf Mon Sep 17 00:00:00 2001 From: nicodes Date: Sat, 10 Oct 2026 05:26:43 +0200 Subject: [PATCH] Support Clerk publishing keys in the public CLI --- README.md | 7 +++++++ account.go | 3 +-- internal/registry/client.go | 4 ++-- internal/registry/client_test.go | 18 ++++++++++++++++++ internal/registry/device.go | 2 +- 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 62c8762..258d222 100644 --- a/README.md +++ b/README.md @@ -238,6 +238,13 @@ the marketplace where it is: termcade publish https://github.com/you/mygame v1.0.0 mygame.tcade ``` +For automated publishing, log in and run `termcade keys new `. +The API asks Termcade's Clerk tenant to create an `ak_…` key restricted to that +handle. Set it as `TERMCADE_TOKEN` when running `termcade publish`. Manage keys +with `termcade keys list` and `termcade keys revoke `. Existing `tck_…` keys +remain usable during the transition. Publishing keys cannot sign in or manage +accounts; browser-approved `termcade login` continues to use separate CLI sessions. + The registry fetches that asset once, validates it against the same manifest rules the arcade enforces, reads the game's identity and version out of the manifest inside it, and records its digest. Nothing you pass here asserts diff --git a/account.go b/account.go index bdd3a51..6ac1364 100644 --- a/account.go +++ b/account.go @@ -95,8 +95,7 @@ func cmdKeys(args []string) error { if err != nil { return err } - // Printed once because it exists once. The registry stores a hash and - // cannot produce this again, so anything that loses it needs a new key. + // Reveal the Clerk-issued secret once; lists never return secrets. fmt.Printf("created %q, publishing as %s\n\n %s\n\n", key.Name, key.Username, key.Token) fmt.Fprintln(os.Stderr, "that token is shown once and cannot be recovered — put it somewhere safe now") diff --git a/internal/registry/client.go b/internal/registry/client.go index cbaf9eb..7d9cf45 100644 --- a/internal/registry/client.go +++ b/internal/registry/client.go @@ -265,7 +265,7 @@ func (c *Client) doContext(ctx context.Context, method, path string, body, out a } func (c *Client) authorizationValue() string { - if strings.HasPrefix(c.token, "tcc_") || strings.HasPrefix(c.token, "tck_") { + if strings.HasPrefix(c.token, "tcc_") || strings.HasPrefix(c.token, "tck_") || strings.HasPrefix(c.token, "ak_") { return "Bearer " + c.token } return c.token @@ -621,7 +621,7 @@ type Key struct { Token string `json:"token,omitempty"` } -// CreateKey mints a publish key scoped to one handle. +// CreateKey requests a Clerk publishing key scoped to one handle. func (c *Client) CreateKey(name, username string) (Key, error) { var out Key body := map[string]string{"name": name, "username": username} diff --git a/internal/registry/client_test.go b/internal/registry/client_test.go index e3ccd3b..0253ef7 100644 --- a/internal/registry/client_test.go +++ b/internal/registry/client_test.go @@ -173,3 +173,21 @@ func TestResolveSendsTheABIAndNoVersion(t *testing.T) { t.Errorf("resolve still asks for a version: %v", stub.resolveQuery) } } + +func TestClerkPublishingKeyUsesBearerWithoutBecomingLogin(t *testing.T) { + token := "ak_" + strings.Repeat("A", 48) + if !IsPublishKey(token) || IsCLIToken(token) { + t.Fatal("publishing key classified as login or refused") + } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v1/publish" || r.Header.Get("Authorization") != "Bearer "+token { + t.Error("publishing key sent incorrectly") + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"id":"owner/game","version":"1.0.0"}`)) + })) + defer server.Close() + if _, err := New(server.URL, token).Publish("https://github.com/owner/game", "v1.0.0", "game.tcade"); err != nil { + t.Fatal(err) + } +} diff --git a/internal/registry/device.go b/internal/registry/device.go index 80df487..939ab3c 100644 --- a/internal/registry/device.go +++ b/internal/registry/device.go @@ -28,7 +28,7 @@ var ( userCodeRE = regexp.MustCompile(`^[ABCDEFGHJKMNPQRSTUVWXYZ23456789]{4}-[ABCDEFGHJKMNPQRSTUVWXYZ23456789]{4}$`) deviceCodeRE = regexp.MustCompile(`^tcd_[A-Za-z0-9_-]{43}$`) cliTokenRE = regexp.MustCompile(`^tcc_[A-Za-z0-9_-]{43}$`) - publishKeyRE = regexp.MustCompile(`^tck_[A-Za-z0-9_-]{43}$`) + publishKeyRE = regexp.MustCompile(`^(tck_[A-Za-z0-9_-]{43}|ak_[A-Za-z0-9_-]{20,512})$`) ) func IsCLIToken(value string) bool { return cliTokenRE.MatchString(value) }