What happens
If the team lookup fails during fx login, the sign-in reports success and quietly gives you a session scoped to your personal account.
There is no way to tell it happened. Against a fake issuer returning 500 for /v2/teams:
- exit code
0
- stdout byte-identical to a healthy login, including
Signed in to Vercel.
- stderr empty
auth.json written with no team_slug and no team_id
A healthy login and a broken one differ only by two absent fields in a file most people never open.
Why it matters
Credential.gatewayTeam() returns null without those fields, so no teamId is attached to gateway requests. The comment on fetchCreditsWithFetch in src/builtins/gateway.zig says /v1/credits "rejects it outright unless the request names one", and team-scoped private models stop being visible. The session persists that way, so it stays broken until the next login.
Cause
Both sign-in paths in src/core/auth/login_flow.zig do:
var teams = fetchTeams(alloc, token.access_token, issuer_url) catch std.ArrayList(Team).empty;
once in completeSignIn (the in-app sign-in) and once in runLogin (the CLI). Every failure collapses into an empty list, which is exactly what an account with genuinely no teams produces, so nothing downstream can tell the two apart. selectTeam returns null for an empty list, no picker appears, and the success message prints unconditionally.
A related case reaches the same end without any error at all: parseTeams skips entries whose id or slug is not a string, so a 200 whose entries drift from the expected shape also yields an empty list, silently.
Reproduce
Point FX_E2E_OAUTH_ISSUER_URL at a local issuer that serves the OAuth device flow normally but returns 500 for /v2/teams, then run fx login. Compare stdout and ~/.fx/auth.json against the same run where /v2/teams returns a team. A 200 with body {"teams":[{"id":123,"slug":"acme"}]} reproduces the silent variant.
Note
The sibling path treats this failure as significant: loadTeamSelection, used by fx teams, calls the same fetchTeams with try and fails loudly. Only the sign-in paths swallow it.
I have a fix and will open a PR shortly.
What happens
If the team lookup fails during
fx login, the sign-in reports success and quietly gives you a session scoped to your personal account.There is no way to tell it happened. Against a fake issuer returning 500 for
/v2/teams:0Signed in to Vercel.auth.jsonwritten with noteam_slugand noteam_idA healthy login and a broken one differ only by two absent fields in a file most people never open.
Why it matters
Credential.gatewayTeam()returns null without those fields, so noteamIdis attached to gateway requests. The comment onfetchCreditsWithFetchinsrc/builtins/gateway.zigsays/v1/credits"rejects it outright unless the request names one", and team-scoped private models stop being visible. The session persists that way, so it stays broken until the next login.Cause
Both sign-in paths in
src/core/auth/login_flow.zigdo:once in
completeSignIn(the in-app sign-in) and once inrunLogin(the CLI). Every failure collapses into an empty list, which is exactly what an account with genuinely no teams produces, so nothing downstream can tell the two apart.selectTeamreturns null for an empty list, no picker appears, and the success message prints unconditionally.A related case reaches the same end without any error at all:
parseTeamsskips entries whoseidorslugis not a string, so a200whose entries drift from the expected shape also yields an empty list, silently.Reproduce
Point
FX_E2E_OAUTH_ISSUER_URLat a local issuer that serves the OAuth device flow normally but returns500for/v2/teams, then runfx login. Compare stdout and~/.fx/auth.jsonagainst the same run where/v2/teamsreturns a team. A200with body{"teams":[{"id":123,"slug":"acme"}]}reproduces the silent variant.Note
The sibling path treats this failure as significant:
loadTeamSelection, used byfx teams, calls the samefetchTeamswithtryand fails loudly. Only the sign-in paths swallow it.I have a fix and will open a PR shortly.