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