Skip to content

fix(cli): correct the login token URL, and name the real cause of edge 401s - #18

Merged
albertcmiller1 merged 1 commit into
mainfrom
fix/login-url-and-401-diagnosis
Aug 21, 2026
Merged

fix(cli): correct the login token URL, and name the real cause of edge 401s#18
albertcmiller1 merged 1 commit into
mainfrom
fix/login-url-and-401-diagnosis

Conversation

@albertcmiller1

Copy link
Copy Markdown
Contributor

Three bugs surfaced while confirming that PATs don't authenticate against deployed backends.

1. yertle login printed a URL that 401s

It appended /settings to the API base URL. Tokens are minted in the web app:

https://api.dev.yertle.com/settings (what login printed) 401
https://dev.yertle.com/settings (where the page is) 200

Every first-time user was sent to a 401 page during their first interaction with the tool. It's been wrong since the command was written.

_web_url_for() now derives the web host, and declines to guess rather than being confidently wrong:

https://api.dev.yertle.com   -> https://dev.yertle.com/settings
https://api.yertle.com       -> https://yertle.com/settings
http://localhost:8000        -> http://localhost:3000/settings
https://weird.internal       -> "…from the Settings page of your Yertle web app"

--web-url overrides it.

2. The 401 message named three causes, none of them the real one

Deployed hosts sit behind API Gateway's Cognito JWT authorizer. A yrt_ PAT isn't a JWT, so it's rejected structurally, before Lambda runs and before any lookup — the www-authenticate header says error_description="token contains an invalid number of segments". Telling users their token might be "revoked or expired" sends them off to debug a token that is perfectly valid. (I watched exactly that happen.)

Before / after against api.dev.yertle.com with a freshly minted PAT:

- 401 Unauthorized — token rejected by the backend. Likely causes: the token was
-   issued by a different backend..., the token was revoked, or it has expired.

+ 401 Unauthorized — rejected before reaching the Yertle backend.
+   This host sits behind a JWT authorizer that does not accept personal access tokens:
+   a `yrt_` token isn't a JWT, so it's refused on sight and never looked up.
+   Your token is almost certainly fine — PATs currently work only against a
+   locally-run backend.

UnexpectedStatus exposes only status_code and content — no headers — so detection keys off the body shape: API Gateway returns {"message": …}, FastAPI returns {"detail": …}. That's precisely the difference observed between dev and localhost. App-level 401s keep the original message, verified against a real bad token on localhost.

3. login accepted an empty token

It would persist {"token": ""} — a config that looks populated but resolves as unauthenticated, since resolve() treats "" as absent. Now refuses and saves nothing.

Found by nearly clobbering my own config while testing (1).

Testing

make check green (93 tests, +12). All twelve new tests verified to fail against the previous implementation. Both messages checked against live backends — dev for the edge rejection, localhost for the app-level one.

Note

This makes the failure legible; it does not fix it. PATs still cannot authenticate against any deployed environment — that's the infrastructure gap, tracked separately.

🤖 Generated with Claude Code

…e 401s

Three problems surfaced while confirming that PATs don't authenticate against
deployed backends.

1. `yertle login` printed the wrong URL. It appended `/settings` to the *API*
   base URL, but tokens are minted in the web app:

       https://api.dev.yertle.com/settings -> 401
       https://dev.yertle.com/settings     -> 200

   Every first-time user was sent to a 401 page during their first
   interaction with the tool. `_web_url_for` now derives the web host by
   stripping an `api.` prefix, maps localhost:8000 -> :3000 for local dev, and
   returns None rather than guessing when the mapping isn't obvious — in which
   case the prompt stays vague instead of confidently wrong. `--web-url`
   overrides it.

2. The 401 message listed three causes, none of which was the real one.
   Deployed hosts sit behind API Gateway's Cognito JWT authorizer, which
   rejects a `yrt_` PAT before Lambda runs — it isn't a JWT, so it fails
   structurally ("token contains an invalid number of segments" in the
   www-authenticate header) and is never looked up. Telling the user their
   token may be "revoked or expired" sends them to debug a token that is
   perfectly valid.

   `UnexpectedStatus` carries only status_code and content, so detection uses
   the body shape: API Gateway returns {"message": ...}, FastAPI returns
   {"detail": ...} — the difference we observed against dev vs localhost.
   App-level 401s keep the original message.

3. `yertle login` accepted an empty token, persisting a config that looks
   populated but resolves as unauthenticated, since `resolve()` treats "" as
   absent. Now refuses and saves nothing. Found by nearly clobbering my own
   config while testing (1).

Twelve tests, verified to fail against the previous implementation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@albertcmiller1
albertcmiller1 merged commit 7bfad01 into main Aug 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant