Skip to content

Commit 600f71c

Browse files
authored
Merge pull request #7 from moderation-api/cli-key-on-api-domain
login: use the key exchange on the API domain
2 parents 35c5b01 + 5e35cae commit 600f71c

4 files changed

Lines changed: 41 additions & 23 deletions

File tree

‎internal/custom/custom_test.go‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,15 +331,15 @@ func TestExchangeForOrganizationKey(t *testing.T) {
331331
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
332332
gotAuth = r.Header.Get("Authorization")
333333
switch r.URL.Path {
334-
case "/api/cli/key":
334+
case "/v1/cli/key":
335335
_, _ = io.WriteString(w, `{"apiKey":"sk_org","organizationId":"org_1","organizationName":"Acme"}`)
336336
default:
337337
http.NotFound(w, r)
338338
}
339339
}))
340340
defer srv.Close()
341341

342-
got, err := exchangeForOrganizationKey(context.Background(), srv.Client(), srv.URL, "at-1", "laptop")
342+
got, err := exchangeForOrganizationKey(context.Background(), srv.Client(), srv.URL+"/v1/cli/key", "at-1", "laptop")
343343
if err != nil {
344344
t.Fatal(err)
345345
}
@@ -352,3 +352,17 @@ func TestExchangeForOrganizationKey(t *testing.T) {
352352
t.Fatalf("404 should mean not available, got %v", err)
353353
}
354354
}
355+
356+
func TestKeyExchangeURLIsTheTokenAudience(t *testing.T) {
357+
t.Setenv("MODAPI_KEY_EXCHANGE_URL", "")
358+
t.Setenv("MODAPI_OAUTH_RESOURCE", "")
359+
cfg := loadSettings()
360+
if cfg.KeyExchangeURL != "https://api.moderationapi.com/v1/cli/key" || cfg.Resource != cfg.KeyExchangeURL {
361+
t.Fatalf("got exchange %q, resource %q", cfg.KeyExchangeURL, cfg.Resource)
362+
}
363+
364+
t.Setenv("MODAPI_KEY_EXCHANGE_URL", "https://staging.example.com/v1/cli/key")
365+
if cfg := loadSettings(); cfg.Resource != "https://staging.example.com/v1/cli/key" {
366+
t.Fatalf("resource %q does not follow the exchange URL", cfg.Resource)
367+
}
368+
}

‎internal/custom/exchange.go‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,29 @@ import (
1010
"net/http"
1111
)
1212

13-
// exchangeResult is the dashboard's answer: the same shape as the MCP key
14-
// exchange (POST /api/mcp/key).
13+
// exchangeResult is the key exchange's answer: the same shape as the MCP key
14+
// exchange.
1515
type exchangeResult struct {
1616
APIKey string `json:"apiKey"`
1717
OrganizationID string `json:"organizationId"`
1818
OrganizationName string `json:"organizationName"`
1919
}
2020

21-
// errExchangeNotAvailable means the dashboard has no CLI key exchange yet.
21+
// errExchangeNotAvailable means the API has no CLI key exchange yet.
2222
var errExchangeNotAvailable = errors.New("this Moderation API account cannot sign in from the CLI yet")
2323

2424
// exchangeForOrganizationKey trades a Clerk OAuth access token for an
2525
// organization key of the organization the user picked on the consent screen.
26-
// The dashboard makes one key per user and device name.
27-
func exchangeForOrganizationKey(ctx context.Context, client *http.Client, dashboardURL, accessToken, deviceName string) (*exchangeResult, error) {
28-
if err := requireHTTPS(dashboardURL); err != nil {
26+
// The API makes one key per user and device name.
27+
func exchangeForOrganizationKey(ctx context.Context, client *http.Client, keyExchangeURL, accessToken, deviceName string) (*exchangeResult, error) {
28+
if err := requireHTTPS(keyExchangeURL); err != nil {
2929
return nil, err
3030
}
3131
payload, err := json.Marshal(map[string]string{"deviceName": deviceName})
3232
if err != nil {
3333
return nil, err
3434
}
35-
req, err := http.NewRequestWithContext(ctx, http.MethodPost, dashboardURL+"/api/cli/key", bytes.NewReader(payload))
35+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, keyExchangeURL, bytes.NewReader(payload))
3636
if err != nil {
3737
return nil, err
3838
}
@@ -42,7 +42,7 @@ func exchangeForOrganizationKey(ctx context.Context, client *http.Client, dashbo
4242

4343
res, err := client.Do(req)
4444
if err != nil {
45-
return nil, fmt.Errorf("could not reach the Moderation API dashboard: %w", err)
45+
return nil, fmt.Errorf("could not reach the Moderation API: %w", err)
4646
}
4747
defer res.Body.Close()
4848
body, _ := io.ReadAll(io.LimitReader(res.Body, 1<<20))
@@ -61,6 +61,6 @@ func exchangeForOrganizationKey(ctx context.Context, client *http.Client, dashbo
6161
case result.Message != "":
6262
return nil, errors.New(result.Message)
6363
default:
64-
return nil, fmt.Errorf("the dashboard returned %s for the CLI sign-in", res.Status)
64+
return nil, fmt.Errorf("the Moderation API returned %s for the CLI sign-in", res.Status)
6565
}
6666
}

‎internal/custom/login.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ func runLogin(cmd *cobra.Command, _ []string) error {
9999
}
100100

101101
hostname, _ := os.Hostname()
102-
exchanged, err := exchangeForOrganizationKey(ctx, httpClient, cfg.DashboardURL, tokens.AccessToken, hostname)
102+
exchanged, err := exchangeForOrganizationKey(ctx, httpClient, cfg.KeyExchangeURL, tokens.AccessToken, hostname)
103103
if errors.Is(err, errExchangeNotAvailable) {
104104
return withHints(failure(output.ReasonCLIUnavailable, "%v", err),
105105
"Set "+organizationKeyEnv+" to an organization key from the dashboard's API keys page")

‎internal/custom/settings.go‎

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ var OAuthClientID = "4Cis1Jh7mS6MINJg"
2020
const (
2121
defaultIssuer = "https://clerk.moderationapi.com"
2222
defaultDashboardURL = "https://dash.moderationapi.com"
23-
// defaultResource is the audience the CLI asks Clerk to put in its access
24-
// token; the dashboard's CLI key exchange accepts only this audience.
25-
defaultResource = "https://dash.moderationapi.com/api/cli/key"
23+
// defaultKeyExchangeURL trades the Clerk access token for an organization
24+
// key. It is also the audience the CLI asks Clerk to put in that token: the
25+
// key exchange accepts only this audience.
26+
defaultKeyExchangeURL = "https://api.moderationapi.com/v1/cli/key"
2627

2728
// projectKeyEnv and webhookSecretEnv are the variables every SDK reads.
2829
projectKeyEnv = "MODAPI_SECRET_KEY"
@@ -36,19 +37,22 @@ const (
3637
var loginScopes = []string{"openid", "profile", "email", "user:org:read"}
3738

3839
type settings struct {
39-
Issuer string
40-
ClientID string
41-
Resource string
42-
DashboardURL string
40+
Issuer string
41+
ClientID string
42+
Resource string
43+
KeyExchangeURL string
44+
DashboardURL string
4345
}
4446

4547
// loadSettings reads the sign-in settings. The env vars exist for staging and tests.
4648
func loadSettings() settings {
49+
keyExchangeURL := envOr("MODAPI_KEY_EXCHANGE_URL", defaultKeyExchangeURL)
4750
return settings{
48-
Issuer: envOr("MODAPI_OAUTH_ISSUER", defaultIssuer),
49-
ClientID: envOr("MODAPI_OAUTH_CLIENT_ID", OAuthClientID),
50-
Resource: envOr("MODAPI_OAUTH_RESOURCE", defaultResource),
51-
DashboardURL: strings.TrimSuffix(envOr("MODAPI_DASHBOARD_URL", defaultDashboardURL), "/"),
51+
Issuer: envOr("MODAPI_OAUTH_ISSUER", defaultIssuer),
52+
ClientID: envOr("MODAPI_OAUTH_CLIENT_ID", OAuthClientID),
53+
Resource: envOr("MODAPI_OAUTH_RESOURCE", keyExchangeURL),
54+
KeyExchangeURL: keyExchangeURL,
55+
DashboardURL: strings.TrimSuffix(envOr("MODAPI_DASHBOARD_URL", defaultDashboardURL), "/"),
5256
}
5357
}
5458

0 commit comments

Comments
 (0)