Skip to content

Send API requests to an origin of their own - #616

Open
jeremy wants to merge 3 commits into
mainfrom
api-origin
Open

jeremy wants to merge 3 commits into
mainfrom
api-origin

Conversation

@jeremy

@jeremy jeremy commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

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_url does today; the setting is the owner's own, and no repository file can set it.
  • /cable stays on the base URL, with HEY_CABLE_URL still overriding it. hey watch takes 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_url set. 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 draft app_url comes from the Location HEY 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-browser is 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_URL or hey 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.

Written for commit 993792a. Summary will update on new commits.

View guided diff Turn on auto-fix

HEY's API is about to be served at api.hey.com while signing in stays at
app.hey.com, whose tokens api.hey.com reads from the shared tables. The CLI keys
everything on one base URL, so pointing it at api.hey.com lost the login and
took the next one from an issuer it should not use.

api_url (HEY_API_URL) moves what the SDK talks to and nothing else. Signing in,
refreshing, the credential key, /cable and draft links stay on the base URL.
Saved, it is keyed by login origin as account_defaults is, so no other server's
token goes there, and a repository's .hey/config.json has no say in it.
A 307 or 308 from /oauth/tokens made Go resend the form, refresh token or code in
it, to wherever the Location named. With an API origin beside the login origin
that is a live way for a refresh token to leave the issuer. The SDK's own OAuth
client already follows none, for the same reason. The 3xx is now the answer, an
error that leaves the login in place.
@jeremy
jeremy requested a review from a team as a code owner November 5, 2026 01:56
Copilot AI balanced review requested due to automatic review settings November 5, 2026 01:56
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-05T02:10:44.569863Z 993792a New commits
🔒 Security Review ✅ Completed 2026-11-05T02:12:37.451537Z 993792a New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@jeremy

jeremy commented Nov 5, 2026

Copy link
Copy Markdown
Member Author

A review of this branch aimed at a token at the wrong origin, a refresh at the API origin and credentials under the wrong key raised four things. Three are fixed, one is declined.

  1. Fixed. A 307 or 308 from /oauth/tokens resent the form, refresh token or code included, to the Location, API origin included. It's older than this PR, but it breaks the boundary this PR states. The Manager's client now follows no redirect, in the second commit, with TestTokenEndpointRedirectsAreNotFollowed covering both statuses on refresh and sign-in. The test fails without the fix.
  2. Declined. An invalid HEY_API_URL fails Load() before --base-url can drop it, and an invalid saved value stops hey config set api_url "" from repairing it. Both are true. The failure is a refusal that names the bad value, before any request goes out, so no token moves. base_url already behaves this way: an invalid HEY_BASE_URL fails before --base-url applies. A bad saved value takes a hand edit, since config set validates, and a hand edit is how to undo it. Deferring validation would leave an unvalidated destination in cfg between Load() and the flag, which is a worse shape for this setting.
  3. Fixed. The redirected-401 test seeded only credentials that couldn't refresh, so its "no refresh" assertion couldn't fail. It now includes a sign-in with a refresh token and a working token endpoint, and asserts one attempt at the API origin and an error from the command.
  4. Fixed. The paging test passed even with the label's Link removed. Both listings now have a distinct second page: the command must succeed, the second page must appear in the output, and each cursor must reach the API origin.

Found sound in the same review: the credential lookup stays on the base URL, the SDK strips bearer and cookie on an off-origin redirect and doesn't refresh on that hop's 401, repository config can't supply the API origin, and /cable and draft links stay on the base URL. That review read the code without running the tests. The tests were run separately, as the PR says.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 14 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread docs/cli.md Resolved
Comment thread internal/auth/auth_test.go Resolved
Comment thread internal/config/config.go Resolved
Comment thread internal/config/config.go Resolved
Comment thread internal/config/config.go Resolved
Comment thread docs/architecture/auth-and-setup.md Resolved
Comment thread docs/cli.md Resolved

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

API URL validation still accepts empty query/fragment delimiters and encoded paths, allowing SDK blob/image requests to target unintended resources instead of enforcing origin-only endpoints.

1 open issue
What changed in this PR

Separates the SDK’s API origin from the login origin, allowing hey-cli to use api.hey.com while keeping authentication, credential storage, and browser links on the base URL.

Changes:

  • Adds validated, login-origin-scoped api_url configuration and the HEY_API_URL override.
  • Routes SDK requests to the API origin and exposes it in configuration and authentication status.
  • Blocks token-endpoint redirects and adds tests for routing, refresh, credential isolation, and pagination.

[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

File Description
internal/​config/​config.go Adds API origin loading, validation, persistence, and precedence.
internal/​config/​config_test.go Tests origin scoping, overrides, validation, and persistence.
internal/​cmd/​sdk.go Configures the SDK with the API origin.
internal/​cmd/​root.go Passes the effective API origin to SDK initialization.
internal/​cmd/​help_topics.go Documents HEY_API_URL in environment help.
internal/​cmd/​draft.go Clarifies in a comment that draft links use the base URL.
internal/​cmd/​config.go Adds API URL setting and display support.
internal/​cmd/​auth.go Includes the configured API URL in authentication status.
internal/​cmd/​api_origin_test.go Exercises separate login and API origins through real commands.
internal/​auth/​auth.go Prevents token-endpoint redirects.
internal/​auth/​auth_test.go Tests redirect refusal during refresh and sign-in.
docs/​cli.md Documents API origin configuration and reset behavior.
docs/​architecture/​auth-and-setup.md Explains routing and credential boundaries.
AGENTS.md Records API origin and storage guidance.

Review effort: Balanced

Comment thread internal/config/config.go Resolved
A malformed api_urls key in a repository's .hey/config.json failed loading even
though that file has no say in them, so they are normalized for the global file
only. Two keys for one login origin are now refused, as account_defaults are,
rather than one winning by map order. An API URL can no longer carry an empty
query or fragment, or a path behind an escape. The docs say what HEY_TOKEN and
HEY_API_URL do beside the setting.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 993792a44d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/config/config.go
Comment on lines +215 to +217
if env := os.Getenv("HEY_API_URL"); env != "" {
if apiErr := validateAPIURL(env); apiErr != nil {
return nil, apiErr

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore HEY_API_URL before validating an explicit base URL

When HEY_API_URL is malformed and the invocation supplies --base-url, config.Load rejects the environment value before the root command applies the flag. The command therefore exits even though --base-url is documented to ignore HEY_API_URL in docs/cli.md:67; skip or defer this validation when the explicit base-URL flag will discard the setting.

Useful? React with 👍 / 👎.

Comment thread internal/config/config.go
Comment on lines +778 to 780
func validateAPIURL(apiURL string) error {
u, err := parseServerURL("API URL", apiURL)
if err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject non-HTTP schemes for localhost API URLs

validateAPIURL delegates scheme validation to a helper that exempts localhost from every scheme check, so values such as ftp://localhost or file://localhost are accepted and persisted. The SDK uses Go's HTTP client and cannot issue requests with those schemes, leaving every data command to fail at runtime; the localhost exception should allow plain http, not arbitrary protocols.

Useful? React with 👍 / 👎.

Comment thread internal/cmd/root.go
// that credential any more than it outlives a logout.
authMgr.OnCredentialCleared(func() { clearHTTPCache(cmd.ErrOrStderr()) })
initSDK(authMgr, cfg.BaseURL)
initSDK(authMgr, cfg.APIOrigin())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep MCP draft links on the login origin

When a separate API origin returns an absolute Location for a draft save, routing the MCP SDK through cfg.APIOrigin() causes internal/mcpserver/drafts.go:draftLink to expose an app_url on that API host. That host serves API responses rather than the browser draft page, so a person following the MCP result gets a broken link; build this link from cfg.BaseURL, as the regular draft commands do.

AGENTS.md reference: AGENTS.md:L43-L45

Useful? React with 👍 / 👎.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants