Skip to content

fix(cli)!: store finite Deepgram credentials - #133

Merged
GregHolmes merged 5 commits into
mainfrom
gh/store-finite-dx-id-token
Oct 14, 2026
Merged

GregHolmes merged 5 commits into
mainfrom
gh/store-finite-dx-id-token

Conversation

@GregHolmes

@GregHolmes GregHolmes commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Store the dg_token returned by browser device sign-in as the CLI API key and persist its server-provided expiry.
  • Display the typed device user_code; open verification_uri_complete when provided, otherwise the plain verification URI.
  • Remove legacy JWT/refresh credentials and require a new browser login when a finite credential expires.
  • Request standard OIDC scopes instead of admin.

Breaking change

Existing browser-login sessions must run dg login once after upgrading.

Validation

  • make quality
  • uv run pytest
  • Isolated uv builds plus twine check for deepctl, deepctl-core, deepctl-cmd-login, deepctl-cmd-init, and deepctl-cmd-debug-toolkit

@dg-coreylweathers dg-coreylweathers left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What this PR does: dg login stores the Deepgram API key that the device sign-in flow now returns, along with its expiry, and stops storing a short-lived token and refresh token.

Blocking

  • [B1] The API key is written to a world-readable config file on a normal login.
    • keyring.delete_password for the old jwt.* and refresh-token.* entries sits in the same try as set_password (packages/deepctl-core/src/deepctl_core/auth.py:651-664).
    • Expected: the key is stored only in the system keyring.
    • Observed: the macOS, Windows and SecretService/libsecret backends raise PasswordDeleteError when the entry doesn't exist, which is every first login. The except then also writes the key to ~/.config/deepctl/config.yaml (0644). The user sees "Could not store in keyring" and then "stored securely in system keyring".
    • Reproduced in Docker with a real keyring backend. The unit tests miss it because the mocked keyring never raises.
    • Fix: give the deletes their own try/except (ignore PasswordDeleteError) and keep the file fallback only for a failed set_password.
  • [B2] The new CLI doesn't work with the current sign-in server.
    • Expected: the CLI either stores a usable API key or tells the user what to do.
    • Observed: against a server that still returns refresh_token and no dg_token, the legacy branch (auth.py:681-697) stores the 15-minute JWT as the API key, and requests start failing a few minutes later.
    • Fix (lets the CLI ship first): if no dg_token comes back, store nothing and print "Run dg update and dg login again". Minimal: release only alongside the server change.

Should-fix

  • [S1] An expired key gives a bare 401. Expected: a "run dg login" prompt once dg_token_expires_at has passed. Observed: get_api_key checks the expiry only on the JWT path.
  • [S2] Upgraders with an old session get no message. Expected: a prompt to log in again. Observed: the refresh now returns 400, the except: pass at auth.py:188 swallows it, and the expired cached key is used silently. The refresh and exchange code is dead and can go.
  • [S3] Release notes hide the change. fix(cli): releases 0.3.4 under Bug Fixes. Because existing sessions need a re-login, fix(login)!: with a BREAKING CHANGE: footer gives 0.4.0 (bump-minor-pre-major).

Nits: device login drops a custom base_url; error copy names "dx-id" where users know "Deepgram sign-in"; the success message says "Finite API key"; the branch is one commit behind main; drop "Phase 0" from the PR body.

Gates (Docker, python 3.12 + uv): ruff format and check clean, mypy clean (121 files), 2207 passed / 12 skipped, build OK. A live dg login against the updated sign-in server stored dg_token, the 30-day expiry and project_id, and handled the authorization_pending poll.

Question: device-login keys get only usage:write and usage:read. Is that intended? If so, dg projects, dg keys, dg members and dg billing will return 403 for these users (not tested live).

@dg-coreylweathers dg-coreylweathers left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addendum: the device sign-in server now requires a typed code. This is another blocker to add to my review above.

  • [B3] CLI login can't complete against the updated sign-in server.
    • Expected: dg login shows the user the code to enter and opens the verification page.
    • Observed: the server now returns a user_code alongside device_code, and the verification page asks the user to type it. This PR still opens the old link built from the device code and never prints user_code, so the user lands on "Enter the code" with nothing to enter.
    • Fix: print user_code prominently (for example Enter code: ABCD-EFGH) and open the plain verification_uri returned by /device/code. Add a test asserting both.

The response also carries per-client lifetimes now (one day for the CLI), not 30 days, so read dg_expires_in as returned and adjust the "30-day" copy if any remains.

@dg-coreylweathers dg-coreylweathers left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes — two blockers, plus a release-timing hold.

What this PR does: moves dg login (browser flow) to the dx-id Phase 0 finite credential: it stores the expiring dg_token and its expiry, deletes the old JWT/refresh tokens, and requests openid profile email instead of admin.

Blocking

  • B1 — Browser login crashes. urlencode is still called in _poll_for_token (auth.py:599), but its import was removed. Expected: dg login completes. Observed: "Error during device flow: name 'urlencode' is not defined", nothing stored (also the red Lint/mypy). Fix: restore from urllib.parse import urlencode, and add a test that runs _poll_for_token against a mocked client.get (current tests mock it out entirely).
  • B2 — Expired login blocks DEEPGRAM_API_KEY. The expiry check at auth.py:185 raises before the env-var fallback. Expected: expired profile + env key set → env key used (main does this). Observed: "Your Deepgram credential has expired. Run dg login again." Fix: treat an expired profile credential as absent (ideally delete the api-key.{profile} keyring entry) and fall through; raise only if nothing else yields a key. Add a test.

Should-fix

  • S1: _store_token calls create_profile without base_url, resetting custom URLs. Pass the existing base_url like login_with_api_key does.
  • S2: dg whoami prints "✓ Authenticated" and dg login says "Already logged in" for an expired credential. Share one validity check.
  • S3: the verification URL no longer carries device_code, so login depends on user_code, and prints "Enter code: None" if it's absent. Support verification_uri_complete and print the code only when present.
  • S4: a timezone-naive expiry raises TypeError, which isn't caught (auth.py:193 catches only ValueError).
  • S5: existing JWT sessions now require re-login, and legacy server responses are rejected. Please mark this breaking (fix(cli)!: or a BREAKING CHANGE: footer) with a "run dg login once after upgrading" note.

Nits: unused current_profile (auth.py:149); the JWT refresh/exchange helpers are unreachable after one expiry cycle; the get_api_key docstring still describes JWT refresh; debug-toolkit and init swallow the expired error silently; nothing shows the expiry date.

Before release: please confirm the prod dx-id contract after #42 (dg_token/dg_expires_in, user_code, openid profile email accepted) and what permissions the finite key has (dg keys/dg projects may need more than member). The deepctl-core release should not publish until dx-id #42 is live in prod, or browser login fails with "did not return a finite credential".

I reproduced B1, B2 and the forced re-login in Docker: all fail at the PR head and pass on main.

@GregHolmes
GregHolmes force-pushed the gh/store-finite-dx-id-token branch from a3034a5 to 5972207 Compare October 13, 2026 14:48
@GregHolmes GregHolmes changed the title fix(cli): store finite dx-id credentials fix(cli)!: store finite Deepgram credentials Oct 13, 2026

@dg-coreylweathers dg-coreylweathers left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving the content — all findings from my earlier review are resolved. Please hold the deepctl-core release until dx-id #42 is live in prod.

What I re-checked at 35905c5

  • B1 (login crash): fixed — urlencode import restored, and _poll_for_token now has a test instead of being mocked out.
  • B2 (env fallback): fixed — an expired profile credential is skipped and deleted, DEEPGRAM_API_KEY is used if set, and the "run dg login again" error only fires when nothing else is available. Tested.
  • S1–S5: fixed — base_url preserved; whoami and dg login treat expired credentials as logged out (and whoami shows the expiry); verification_uri_complete preferred and the code printed only when present; naive timestamps handled; fix(cli)!: with a BREAKING CHANGE footer.
  • Nits: unused variable and dead JWT refresh code removed, docstring updated, debug-toolkit/init now show the expired-credential message.
  • Gates: ruff, mypy, and pytest (1804 passed) green in Docker; CI 42/42 green.

Optional follow-ups (non-blocking)

  • When the keyring is unavailable, an expired key in the config file is skipped but not cleared; consider clearing profile.api_key on expiry.
  • whoami prints the expiry as a raw ISO timestamp; a friendlier format would help.

Before release: the release-please PR (minor bump from !) should wait until dx-id #42 is deployed to prod; otherwise browser login fails with "did not return a finite credential". Worth a release-note line that browser-login keys now last one day, so dg login is a daily step.

@GregHolmes
GregHolmes merged commit c654944 into main Oct 14, 2026
62 checks passed
@GregHolmes
GregHolmes deleted the gh/store-finite-dx-id-token branch October 14, 2026 11:52
@github-actions github-actions Bot mentioned this pull request Oct 14, 2026
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.

2 participants