Repository navigation
fix(cli)!: store finite Deepgram credentials - #133
Conversation
dg-coreylweathers
left a comment
There was a problem hiding this comment.
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_passwordfor the oldjwt.*andrefresh-token.*entries sits in the sametryasset_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
PasswordDeleteErrorwhen the entry doesn't exist, which is every first login. Theexceptthen 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(ignorePasswordDeleteError) and keep the file fallback only for a failedset_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_tokenand nodg_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_tokencomes back, store nothing and print "Rundg updateanddg loginagain". Minimal: release only alongside the server change.
Should-fix
- [S1] An expired key gives a bare 401. Expected: a "run
dg login" prompt oncedg_token_expires_athas passed. Observed:get_api_keychecks 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: passatauth.py:188swallows 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 aBREAKING 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
left a comment
There was a problem hiding this comment.
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 loginshows the user the code to enter and opens the verification page. - Observed: the server now returns a
user_codealongsidedevice_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 printsuser_code, so the user lands on "Enter the code" with nothing to enter. - Fix: print
user_codeprominently (for exampleEnter code: ABCD-EFGH) and open the plainverification_urireturned by/device/code. Add a test asserting both.
- Expected:
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
left a comment
There was a problem hiding this comment.
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.
urlencodeis still called in_poll_for_token(auth.py:599), but its import was removed. Expected:dg logincompletes. Observed: "Error during device flow: name 'urlencode' is not defined", nothing stored (also the red Lint/mypy). Fix: restorefrom urllib.parse import urlencode, and add a test that runs_poll_for_tokenagainst a mockedclient.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. Rundg loginagain." Fix: treat an expired profile credential as absent (ideally delete theapi-key.{profile}keyring entry) and fall through; raise only if nothing else yields a key. Add a test.
Should-fix
- S1:
_store_tokencallscreate_profilewithoutbase_url, resetting custom URLs. Pass the existingbase_urllikelogin_with_api_keydoes. - S2:
dg whoamiprints "✓ Authenticated" anddg loginsays "Already logged in" for an expired credential. Share one validity check. - S3: the verification URL no longer carries
device_code, so login depends onuser_code, and prints "Enter code: None" if it's absent. Supportverification_uri_completeand print the code only when present. - S4: a timezone-naive expiry raises
TypeError, which isn't caught (auth.py:193 catches onlyValueError). - S5: existing JWT sessions now require re-login, and legacy server responses are rejected. Please mark this breaking (
fix(cli)!:or aBREAKING CHANGE:footer) with a "rundg loginonce 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.
BREAKING CHANGE: Existing browser-login sessions must run dg login once after upgrading.
a3034a5 to
5972207
Compare
dg-coreylweathers
left a comment
There was a problem hiding this comment.
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 —
urlencodeimport restored, and_poll_for_tokennow has a test instead of being mocked out. - B2 (env fallback): fixed — an expired profile credential is skipped and deleted,
DEEPGRAM_API_KEYis used if set, and the "rundg loginagain" error only fires when nothing else is available. Tested. - S1–S5: fixed —
base_urlpreserved;whoamianddg logintreat expired credentials as logged out (andwhoamishows the expiry);verification_uri_completepreferred 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_keyon expiry. whoamiprints 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.
Summary
dg_tokenreturned by browser device sign-in as the CLI API key and persist its server-provided expiry.user_code; openverification_uri_completewhen provided, otherwise the plain verification URI.admin.Breaking change
Existing browser-login sessions must run
dg loginonce after upgrading.Validation
make qualityuv run pytestuvbuilds plustwine checkfordeepctl,deepctl-core,deepctl-cmd-login,deepctl-cmd-init, anddeepctl-cmd-debug-toolkit