Send API requests to an origin of their own
Deliverable: https://app.basecamp.com/2914079/buckets/48804061/todos/10477329816
Rung H1 of hey2's launch is one staff identity's CLI reading at api.hey.com, with a token from app.hey.com's issuer, which hey2 verifies from the shared tables. The CLI keys everything on one base URL, so HEY_BASE_URL=https://api.hey.com loses the login and takes the next one from an issuer it shouldn't use.
What it does
api_url (HEY_API_URL, or hey config set api_url) moves the SDK's requests and nothing else. hey.Config.BaseURL is cfg.APIOrigin(); everything else still reads cfg.BaseURL.
| Goes to | |
|---|---|
API requests, hey mcp's included | API origin |
hey auth login, the code exchange | base URL |
| Refresh | the credentials' TokenEndpoint, else base URL |
| The credential key, the account default | base URL |
/cable | base URL |
draftAppURL, a page a person opens | base URL |
With no api_url, nothing changes, in output included.
hey auth login # at app.hey.com, if not signed in already
hey config set api_url https://api.hey.com # or HEY_API_URL=https://api.hey.com for one process
hey auth status # names both
hey box view imbox
hey config set api_url "" # the way back down
Saved, it's api_urls in the global config.json, keyed by login origin as account_defaults is, so a token from another server never goes to it: not under HEY_BASE_URL, --base-url or a repository's trusted base_url. --base-url drops HEY_API_URL too. A repository's .hey/config.json can't set it at all. It must be an origin: HTTPS or localhost, with no path, query or userinfo.
A 401 from the API origin refreshes at the login origin and resends once. A 401 that persists ends as an auth failure with the login kept; invalid_grant at the login origin clears it. hey2's challenge naming its own issuer is ignored, since the CLI does no discovery. hey2's not_admitted 403 refreshes nothing.
Absolute URLs from the server. No SDK change. The SDK strips credentials on a redirect off the origin and doesn't take a 401 from that hop as its own. next_history_url, posting_changes_url and a label's Link give up only their cursor, asked of the API origin. Attachments are relative paths only, and a blob on another origin is refused.
The second commit is older than this feature: /oauth/tokens answering a 307 or 308 made Go resend the form, refresh token included, to the Location. The Manager's client now follows none, as the SDK's own OAuth client doesn't. A 3xx is an error that leaves the login in place. Any redirect there was already wrong, so this changes nothing for a working server.
What it doesn't
- No allow-list of API hosts. A typo sends the token to the typo, as a typo in
base_urldoes today; the setting is the owner's own, and no repository file can set it. /cablestays on the base URL, withHEY_CABLE_URLstill overriding it.hey watchtakes its doorbell from app.hey.com and reads its changes from the API origin.- Inline images and blobs named absolutely on the login origin aren't fetched with
api_urlset. The fetcher refuses them, so the TUI draws no image. It fails closed, and reaching them would take a second SDK client, which H1 doesn't need. hey mcp's draftapp_urlcomes from theLocationHEY answers with, so at api.hey.com it would name a page there that doesn't exist. It's a write, so it's H2's, and worth settling before then.hey draft's own link uses the base URL and is right.
Tests
internal/cmd/api_origin_test.go runs the real command tree against two fake servers, a login origin and an API origin, each recording everything it hears:
- The token is sent to the API origin, and the login origin hears nothing (stored login and
HEY_TOKEN). - Refresh hits only the login origin, on expiry, with no stored endpoint and on an API 401, and the API origin never hears
/oauth. - Credentials stay keyed on the login origin after sign-in and after refresh, and ones stored under the API origin aren't found.
hey auth login --no-browseris driven to the end by answering its callback.- A redirect off the API origin carries no token or cookie and spends no refresh, and a page named on the login origin is read at the API origin.
Pointing initSDK back at BaseURL, or building the Manager from APIOrigin(), fails most of these. TestTokenEndpointRedirectsAreNotFollowed fails without the second commit.
Run here: make fmt-check vet test-unit test-sync-skills tidy-check check-surface check-release-lockstep, and golangci-lint v2.14.0 with 0 issues. make lint can't find golangci-lint under this repo's .mise.toml on this machine, so I ran the pinned binary directly. Nothing here has run against the real api.hey.com, which isn't serving yet (hey2 #1315 is open).
Summary by cubic
Adds an api_url setting that sends the SDK's API requests to a separate origin — api.hey.com for hey2's launch — while signing in, token refresh, credential storage, /cable, and draft links stay at the base URL. With no api_url set, nothing changes.
Details
HEY_API_URLorhey config set api_url <origin>routes every API request there, and a 401 from it refreshes at the login origin exactly once before failing.- The saved value is keyed by canonical login origin in the global config, so a token from another server — by
HEY_BASE_URL,--base-url, or a repository's.hey/config.json— never goes there; conflicting keys for one login origin are refused, and a repository's file can't set it or fail loading on a bad one. - The value must be an origin: HTTPS, or localhost for development, with no path, query, fragment, or userinfo — empty query/fragment and escaped paths included.
- A redirect (307/308) from the token endpoint is no longer followed, so a refresh token or authorization code can't be resent to the redirect target; the 3xx is now an error that keeps the login.