feat(cli): create organization access tokens through the dashboard

Merged
PI
17d ago
+1060-161

Summary

polar auth token creates an organization access token (OAT) for an app and prints it on stdout, for the caller to store wherever it belongs.

What

  • CLI โ€” polar auth token [--scope โ€ฆ]
    • Takes the organization and environment from the current session (auth whoami), including --local.
    • Opens /dashboard/<slug>/cli/token, waits on the existing loopback listener (127.0.0.1:3333), and prints the token, nothing else, on stdout.
    • The browser prompt goes to stderr, as does a hint for sandbox and local tokens on how to point the SDK at that environment. So echo "POLAR_ACCESS_TOKEN=$(polar auth token)" >> .env.local and polar auth token | gh secret set POLAR_ACCESS_TOKEN capture only the token. --json is refused.
  • Web โ€” /dashboard/[organization]/cli/token: full-screen page with the existing token form, prefilled with the CLI's name and scopes. Cancel notifies the CLI. Opened without a CLI key, it shows the token once to copy.
  • Refactors
    • utils/loopback.ts: the callback server is shared by auth login and auth token.
    • dashboardOrigin in services/api.ts: OAuth authorizationUrl and the token page resolve the dashboard the same way, including POLAR_DASHBOARD_URL and the guard against the hosted dashboard when POLAR_API_URL is set.
    • useSubmitOrganizationAccessToken: create-token logic, including the fresh-session retry, is shared by the settings modal and the new page.

Why

Creating an OAT requires a fresh web session. That stays: an OAuth session must not be able to mint an OAT, because the OAT can outlive it. But developers and agents setting up Polar in an app need a token from the CLI. The token prints on stdout, so an agent should redirect or pipe it rather than read it, to keep it out of its history.

How

The CLI generates a one-time RSA-OAEP key pair and a state, and passes the public key to the page. After the user creates the token in their web session, the page encrypts it with WebCrypto and redirects to the fixed loopback URL. The CLI checks state and decrypts.

  • The page only sends to 127.0.0.1:3333, so a crafted link cannot redirect the token.
  • The token travels encrypted, so browser history only holds ciphertext.
  • Delivery is a top-level navigation, like the OAuth login, so no CORS or CSP changes are needed.

Checklist

  • This PR addresses a single concern (one bug fix, one feature, one refactor)
  • The diff is reasonably sized and easy to review
  • New functionality is covered by tests
  • Linting and type checking pass
  • No unrelated changes or drive-by fixes are included

Verification

  • Run end to end in Chrome against a local stack: dev seed, bun src/cli.ts auth login --local, then bun src/cli.ts auth token.
  • The local stack is HTTP throughout, so the redirect from an HTTPS dashboard to http://127.0.0.1:3333 can only be checked on sandbox after deploy. auth login already makes the same jump in production (polar.sh โ†’ http://127.0.0.1:3333/oauth/callback); this page navigates with window.location rather than a server redirect, which browsers also treat as a top-level navigation.
  • If the CLI has stopped listening when the page redirects, the token is created but never delivered and has to be revoked in settings.

๐Ÿค– Generated with Claude Code