AUTH-6735: retry/recovery for Unauthorized dashboard config - #219
AUTH-6735: retry/recovery for Unauthorized dashboard config#219nicknisi wants to merge 2 commits into
Conversation
…nfig The installer called the WorkOS API to configure dashboard settings (redirect URI, CORS origin, homepage URL) and, on a 401 "Unauthorized", dropped straight to "configure it manually" with no recovery path (AUTH-6735). Worse, the 401 was string-matched from the error message, so a bare "Unauthorized" body was not even classified correctly. - Carry the HTTP status on a new DashboardConfigError so 401 is detected exactly. - On 401, explain the likely cause (expired/revoked key, wrong environment) and offer recovery before giving up: re-authenticate via the OAuth device flow (fresh staging credentials) or paste a different API key, then retry the dashboard configuration with the new key. - Bound recovery to 2 attempts; decline or repeated failure falls back to manual instructions that now list the exact settings and values to apply in the dashboard. - Return the API key that succeeded so callers write the working key to env files instead of the rejected one (agent runner, state machine, go/dotnet/ruby integrations). - Non-interactive modes (agent/CI/JSON) skip the prompt and get the explanation plus specific manual instructions.
Greptile SummaryThe PR adds bounded recovery and retry behavior when dashboard auto-configuration receives a 401, then propagates credentials recovered through re-authentication across installer paths.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains; the previously reported re-authentication credential-pairing issue is addressed by returning and propagating both credentials from the newly selected environment. Important Files Changed
|
| // If 401 recovery re-authenticated, continue with the key that worked. | ||
| if (outcome) apiKey = outcome.apiKey; |
There was a problem hiding this comment.
🔴 After signing in again, projects are set up with a mismatched key and app identifier
Only the replacement API key is carried forward after recovery (apiKey = outcome.apiKey at src/lib/agent-runner.ts:79) while the previously fetched client identifier is left untouched, so the credentials written into the project belong to two different WorkOS environments.
Impact: Sign-in in the generated app fails because the app identifier and the key no longer belong to the same WorkOS environment.
Mechanism: re-auth returns a full credential pair but only the key is propagated
promptForUnauthorizedRecovery (src/lib/workos-management.ts:203-216) calls fetchStagingCredentials, which returns { clientId, apiKey }, persists both via saveStagingCredentials, but returns only staging.apiKey. AutoConfigOutcome (src/lib/workos-management.ts:24-32) likewise only exposes apiKey.
Every caller then combines the fresh key with the stale clientId obtained earlier from getOrAskForWorkOSCredentials:
src/lib/agent-runner.ts:89-100—writeEnvLocal({ WORKOS_API_KEY: apiKey (fresh), WORKOS_CLIENT_ID: clientId (stale) })src/integrations/go/index.ts:147-157— same pattern for.envsrc/lib/run-with-core.ts:332-340—writeEnvLocalwith freshapiKeyandcredentials.clientIdsrc/integrations/dotnet/index.ts:186—config.environment.getEnvVars(apiKey, clientId)
The same mismatch occurs on the "Enter a different API key" branch (src/lib/workos-management.ts:192-198), where the pasted key may belong to another environment entirely.
Prompt for agents
When dashboard auto-config recovers from a 401 by re-authenticating, the recovery path (promptForUnauthorizedRecovery in src/lib/workos-management.ts) obtains a complete credential pair from fetchStagingCredentials (clientId + apiKey) but only surfaces the apiKey through AutoConfigOutcome. All callers (src/lib/agent-runner.ts, src/lib/run-with-core.ts configureEnvironment, src/integrations/go|dotnet|ruby) then write the fresh apiKey together with the previously-obtained, now-stale clientId, producing a credential pair from two different WorkOS environments and breaking sign-in in the generated app. Consider having the recovery hook and AutoConfigOutcome carry an optional clientId (undefined for the pasted-key branch, where the caller should at least be warned that the client ID may no longer match), and have each caller use it when present.
Was this helpful? React with 👍 or 👎 to provide feedback.
| }); | ||
| // If 401 recovery re-authenticated, write the key that worked. | ||
| if (outcome) apiKey = outcome.apiKey; |
There was a problem hiding this comment.
🟡 Installer hands the rejected key to the code-generating agent even after recovery
The refreshed key is stored only in a local variable (apiKey = outcome.apiKey at src/lib/run-with-core.ts:331) and never written back to the shared installer state, so the later install step still runs with the key WorkOS already rejected.
Impact: The install step operates with an invalid key, so key-dependent work during installation can fail even though recovery appeared to succeed.
Mechanism: state machine context credentials are not updated after 401 recovery
In configureEnvironment (src/lib/run-with-core.ts:322-340) the recovered key is used for writeEnvLocal, but context.credentials.apiKey is unchanged. The subsequent runAgent actor builds agentOptions from credentials?.apiKey (src/lib/run-with-core.ts:348-355), so the framework installer receives the stale key. Because options.apiKey/options.clientId are set, callerHandledConfig is true in src/lib/agent-runner.ts:68, so no re-configuration or env rewrite happens, and the stale key is passed as workOSApiKey to initializeAgent (src/lib/agent-runner.ts:114-121). The .env.local on disk and the key given to the agent therefore disagree.
Prompt for agents
In src/lib/run-with-core.ts, the configureEnvironment actor recovers a fresh API key from autoConfigureWorkOSEnvironment's 401 recovery but only uses it locally for writeEnvLocal. The state machine context's credentials.apiKey stays stale, and the runAgent actor later passes credentials?.apiKey to the framework installer (which forwards it to initializeAgent as workOSApiKey). Consider returning the recovered key from the configureEnvironment actor and assigning it into the machine context (an output/assign on the invoke done transition) so downstream steps use the key that actually worked.
Was this helpful? React with 👍 or 👎 to provide feedback.
| try { | ||
| const { ensureAuthenticated } = await import('./ensure-auth.js'); | ||
| const auth = await ensureAuthenticated(); | ||
| if (!auth.authenticated) return null; | ||
|
|
||
| const { getAccessToken, saveStagingCredentials } = await import('./credentials.js'); | ||
| const token = getAccessToken(); | ||
| if (!token) return null; | ||
|
|
||
| const { fetchStagingCredentials } = await import('./staging-api.js'); | ||
| const staging = await fetchStagingCredentials(token); | ||
| saveStagingCredentials(staging); | ||
| ui.log.success('Re-authenticated with WorkOS'); | ||
| return staging.apiKey; |
There was a problem hiding this comment.
🔍 "Re-authenticate" may be a no-op when the OAuth token is still valid
The most common cause of a 401 here (staging API key revoked/rotated or belonging to another environment) does not necessarily invalidate the CLI's OAuth session. ensureAuthenticated returns { authenticated: true } immediately when the access token is unexpired (src/lib/ensure-auth.ts:74-78) without any interactive re-login, so this branch simply re-fetches staging credentials with the same token. If the server returns the same apiKey, the same-key guard at src/lib/workos-management.ts:333 skips the retry and the user falls through to manual instructions — after being shown a green "Re-authenticated with WorkOS" message, which is misleading. Worth confirming that the staging credentials endpoint provisions/rotates a usable key in that scenario; otherwise consider forcing a fresh login or messaging the no-change case.
Was this helpful? React with 👍 or 👎 to provide feedback.
…smatch
Re-authentication during dashboard auto-config 401 recovery may select a
different WorkOS account or environment, but only the new API key was
surfaced while callers kept the original client ID — producing project
config with credentials from two different environments that fail auth
at runtime.
- AutoConfigOutcome now carries an optional clientId; the recovery hook
returns { apiKey, clientId? } (clientId present only for re-auth)
- promptForUnauthorizedRecovery surfaces both staging credentials on
re-auth, and warns on the pasted-key branch that the client ID may no
longer match
- agent-runner, run-with-core, go, dotnet, and ruby integrations adopt
the recovered clientId when present, falling back to the original
- run-with-core writes recovered credentials back to the shared machine
context so runAgent hands the agent the working key, not the rejected
one
Refs: AUTH-6735
bosun task: AUTH-6735: retry/recovery for Unauthorized dashboard config
Task id: task-msh07tdh-1fj3
Shape: ship
Project: workos/cli