Skip to content

feat(accounts): hold and replay a request when the server asks for step-up - #22

Merged
pyramation merged 1 commit into
mainfrom
feat/auth-step-up
Aug 9, 2026
Merged

feat(accounts): hold and replay a request when the server asks for step-up#22
pyramation merged 1 commit into
mainfrom
feat/auth-step-up

Conversation

@pyramation

Copy link
Copy Markdown
Contributor

Summary

Two things that both came out of pointing dcrypt at a real auth plane: the endpoint field made you guess, and a step-up demand looked like a plain failure.

Endpoint. normalizeEndpoint() runs before the SDK client is built, so what a human types becomes the URL the SDK needs:

auth.example.com        → https://auth.example.com/graphql
auth.localhost:3000     → http://auth.localhost:3000/graphql   # local ⇒ no TLS
https://x.com/api/v2/gql→ unchanged                            # an explicit path is kept

A 404 is then explained rather than surfaced raw — no GraphQL endpoint at <url> — try <url>/graphql, or, when the URL already ends in /graphql, "check the host and that the auth plane is running".

Step-up. The server raises typed exceptions (STEP_UP_REQUIRED_PASSWORD / _MFA / _FRESH_AUTH); those become a StepUpRequiredError carrying the factor, and nothing else does. AccountManager holds the request rather than rebuilding it:

withStepUp(endpoint, token, proof, run):
  try { return run(client) }
  catch (e) {
    if !(e instanceof StepUpRequiredError) throw e   // no generic retry
    if e.kind === 'mfa'  client.verifyTotp(proof.totpCode)   // never the password
    else                 client.verifyPassword(proof.password)
    return run(client)                               // exactly once more
  }

With no proof the typed error propagates — that is what lets a UI collect one and call again. createApiKey/revokeApiKey take an optional StepUpProof { password?, totpCode? }, which is transient: it is never written to the vault and never logged.

The desktop screen keeps the failed closure itself, so the replay cannot pick up state that moved on while the dialog was open:

catch (error) {
  const kind = stepUpKind(message(error));
  kind ? setHeld({ kind, work }) : toast.error(message(error));
}
// dialog confirm → run(held.work, stepUpProof(held.kind, value))

An IPC rejection reaches the renderer as a string, so shared/step-up.ts keys off the server's own code rather than any wording of ours. The CLI gets the same behaviour: dcrypt account key create|revoke prompts for the named factor and re-runs the identical call.

Tests cover the demand propagating when no proof is given, password and MFA each replaying once, a refused proof failing without a second attempt, and revoke keeping the local key until the server has accepted it.

Link to Devin session: https://app.devin.ai/sessions/04636534e07048089ffb6b78142e12cd
Requested by: @pyramation

…ep-up

Normalize the auth endpoint (a bare host gets /graphql, local hosts get
http) and explain a 404 instead of surfacing it raw. On a typed
STEP_UP_REQUIRED_* failure the request is held, the named factor is
collected, verified through the SDK, and the identical request replayed
once — no other failure retries.
@pyramation pyramation self-assigned this Aug 9, 2026
@devin-ai-integration

Copy link
Copy Markdown

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@pyramation
pyramation merged commit 50f3fcb into main Aug 9, 2026
5 checks 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