feat: add env provision command and login mismatch safety#189
Conversation
Add `workos env provision`, a credentials-only subcommand that calls provisionUnclaimedEnvironment() directly (no auth, no code-gen). It emits credentials on stdout (JSON is the agent credential channel), stores the result as a local active unclaimed env so a follow-up `env claim` works, and never writes to the project directory or any .env file. Failures (incl. 429) surface as structured errors with no fallback to login. Rework auth login staging provisioning to stop silent cross-account switches. provisionStagingEnvironment now takes the authenticated account, detects a mismatch against the prior active env (ownerEmail wins over clientId), and on mismatch writes the new account's Staging under a distinct key instead of clobbering the active slot. runLogin prompts (human) / warns (agent-CI) / emits structured JSON, and always ends with an explicit "Now using" line. Stretch: persist ownerEmail/ownerUserId on environment records and carry them through markEnvironmentClaimed; add setActiveEnvironment(). Regression tests added first and confirmed failing on pre-fix code (env provision, mismatch/in-place-clobber guard, "Now using", owner fields).
Greptile SummaryThis PR adds unclaimed environment provisioning and safer staging selection during login. The main changes are:
Confidence Score: 4/5Mostly safe, with one contained JSON-mode login issue to fix before merging. Core provisioning and mismatch handling are covered, but
What T-Rex did
Important Files Changed
|
| if (isJsonMode()) { | ||
| outputJson({ | ||
| status: 'ok', | ||
| account: { email: account.email ?? null, userId: account.userId }, | ||
| activeEnvironment: provision.activeEnvironment ?? null, | ||
| mismatch: provision.mismatch, | ||
| }); |
There was a problem hiding this comment.
JSON output is corrupted
auth login --json still writes human prompts to stdout before this outputJson call: lines 211-213 print the verification URL and user code with console.log. Consumers parsing stdout as JSON receive those prompt lines before the JSON object, so agent and JSON-mode login workflows can fail before they see the structured mismatch fields.
Artifacts
Repro: focused Vitest harness that runs auth login in JSON mode with mocked auth and staging APIs
- Contains supporting evidence from the run (text/typescript; charset=utf-8).
Repro: verbose test output showing prompt text before the final JSON object on stdout
- Keeps the command output available without making the summary code-heavy.
Repro: parse-attempt log showing captured stdout is not valid JSON
- Keeps the command output available without making the summary code-heavy.
Ran code and verified through T-Rex
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/commands/login.ts
Line: 258-264
Comment:
**JSON output is corrupted**
`auth login --json` still writes human prompts to stdout before this `outputJson` call: lines 211-213 print the verification URL and user code with `console.log`. Consumers parsing stdout as JSON receive those prompt lines before the JSON object, so agent and JSON-mode login workflows can fail before they see the structured mismatch fields.
How can I resolve this? If you propose a fix, please make it concise.| function isMismatch( | ||
| prior: EnvironmentConfig | undefined, | ||
| account: { email?: string }, | ||
| staging: { clientId: string }, | ||
| ): boolean { | ||
| if (!prior) return false; | ||
| if (prior.ownerEmail && account.email) return prior.ownerEmail !== account.email; | ||
| if (prior.clientId) return prior.clientId !== staging.clientId; | ||
| return false; | ||
| } |
There was a problem hiding this comment.
🔍 clientId-based mismatch detection may false-positive across environment types
The isMismatch function (src/commands/login.ts:81-90) falls back to comparing prior.clientId !== staging.clientId when neither side has ownerEmail. This works correctly when comparing two staging environments, but if the active environment is a production or manually-added sandbox env, its clientId will always differ from the staging credentials' clientId — even when both belong to the same WorkOS account. This would cause a false-positive mismatch, prompting the user to confirm an environment switch that isn't actually cross-account.
The new ownerEmail field added in this PR mitigates this for environments provisioned via auth login going forward, since email comparison takes priority. The clientId fallback only fires for legacy or manually-added environments without ownerEmail. The comment at src/commands/login.ts:78-80 acknowledges this limitation ('When neither is available we cannot tell — return false'), but the clientId path can actively misidentify same-account environments as mismatches rather than returning the safe false default.
Was this helpful? React with 👍 or 👎 to provide feedback.
| // Persist as an unclaimed env (parity with install) — NEVER writes to the project dir. | ||
| const config = getOrCreateConfig(); | ||
| config.environments['unclaimed'] = { | ||
| name: 'unclaimed', | ||
| type: 'unclaimed', | ||
| apiKey: result.apiKey, | ||
| clientId: result.clientId, | ||
| claimToken: result.claimToken, | ||
| }; | ||
| config.activeEnvironment = 'unclaimed'; | ||
| saveConfig(config); |
There was a problem hiding this comment.
🔍 env provision unconditionally overwrites existing unclaimed environment and repoints active env
The runEnvProvision function at src/commands/env.ts:154-162 always writes to config.environments['unclaimed'] and always sets config.activeEnvironment = 'unclaimed', regardless of what was previously configured. If a user runs env provision twice without claiming the first environment, the first environment's claim token is permanently lost. This is consistent with the existing tryProvisionUnclaimedEnv in src/lib/unclaimed-env-provision.ts:58-66, so it's not a regression, but it contrasts with the careful mismatch/clobber-prevention logic added to provisionStagingEnvironment in this same PR. The unconditional active-env repoint also silently switches away from any prior active environment without warning.
Was this helpful? React with 👍 or 👎 to provide feedback.
|
Closing in favor of a single combined PR from nicknisi/akshay. |
What
workos env provision: provisions an unclaimed staging environment and returns credentials only —apiKey/clientId/claimTokenvia--json, no auth required, no code-gen, and no writes to the project directory (it callsprovisionUnclaimedEnvironmentdirectly, not the installer path that rewrites.env.localand repoints the active env).UnclaimedEnvApiErrorfailures (e.g. 429) surface as structured errors.auth loginno longer silently repoints the active environment to a different account's staging. Mismatches are detected via ownerEmail/clientId comparison; human mode prompts, agent/JSON mode warns and preserves the existing env under a distinctstaging-Nkey. Every login now ends with an explicitNow using: <env> (<email>)line.setActiveEnvironmenthelper.Why
From the friction log: agents ran
installin throwaway directories just to extract credentials because provisioning was coupled to AI code-gen; andauth loginsilently switched the active environment to a different account's staging mid-session, which the user only discovered later.Testing
env.spec.ts; clientId-mismatch / no-prior-env / same-account cases inlogin.spec.ts;config-store.spec.tscoverage for the new fields.Notes
Commit is unsigned (headless 1Password limitation) — no signed-commit rule on the repo; squash merge re-signs.
Stack 5/7 (
akshay-friction-log): merge after #188, then rebase ontomain.