Summary
Every successful e2a login mints a new API key via store.CreateAPIKey(ctx, userID, "CLI login") regardless of whether the user already has prior keys. There's no dedup, no replacement, no expiration. After running login a few times, a user has multiple keys all literally named "CLI login".
Sample outcome after three e2a login runs from different machines (or the same machine after a config wipe):
e2a_aaa… CLI login (active)
e2a_bbb… CLI login (active)
e2a_ccc… CLI login (active)
Concrete pain
- Indistinguishable in the dashboard / future
keys list. When a user wants to revoke "the key on the laptop I lost," they can't tell which row that is.
- Stale keys remain valid forever. No
expires_at field in the schema; revocation requires explicit user action. A key from a stolen device or a past colleague's machine is live until manually revoked.
- Sprawl over time. Power users who re-clone configs or work across machines accumulate dozens of identically-named entries.
Surfaced during fresh-user e2e testing for the launch.
How comparable tools handle it
gcloud uses OAuth refresh tokens, not API keys. gcloud auth login <account> updates the entry in place — re-login doesn't accumulate credentials. Different model end-to-end, but the user-facing behavior is "one credential per account, replaced on re-auth."
gh (GitHub CLI) issues an access token via OAuth. Re-running gh auth login invalidates the prior token on the same scope; tokens are auto-named gh-cli for <hostname>. User-facing behavior is "one credential per device, with a meaningful name."
stripe CLI issues a device-scoped key, named after the host.
Proposed paths
Path A — Revoke-on-login (smallest change)
On e2a login:
- Read existing config; if
api_key is set, capture its prefix (or call a whoami endpoint to get its ID)
- After OAuth completes and the new key is minted, revoke the old one
- Save the new key to
~/.e2a/config.json
Result: at most one CLI key per machine that's been using e2a login. Server-side change minimal — uses the existing key-revocation endpoint.
Path B — Hostname-aware names + per-name dedup (recommended)
On e2a login:
- CLI passes
os.hostname() as the key name hint: "CLI login on joshzhang-MBP-2026" instead of the generic "CLI login"
- Server checks for existing keys with the same
(user_id, name) pair and revokes them before minting the new one
- Each machine ends up with exactly one CLI key, distinctively named in the dashboard
Result: dashboard's "API keys" view becomes meaningful. User can identify and revoke a specific machine without guessing.
Path C — OAuth refresh-token model
Drop the long-lived API key for the CLI entirely. Issue an OAuth-style access token (~1hr) plus a refresh token. SDKs and CLI handle 401 → refresh → retry.
Better security model. Significant refactor: every SDK auth flow changes, all API consumers need refresh logic, the API-key-in-Authorization-header pattern becomes a parallel auth path.
My take
Path B. It's the right user-mental-model fit ("one device = one key, with a name I recognize") and it's well under a day of work:
- CLI: pass
os.hostname() in the key-creation request
- API: when
(user_id, name) collides on create, auto-revoke prior key
- No SDK changes, no breaking auth changes for existing users
Path A is simpler but leaves the dashboard cluttered with identically-named keys. Path C is the architecturally right answer but costs more than its payback for an OSS launch.
Belt-and-suspenders worth considering on top of B: add a last_used_at column (already exists per migrations/001_init.sql) and run a periodic worker that revokes keys with last_used_at < now() - 90 days. Bounded blast radius for forgotten keys without bothering active users.
Priority
Not blocking launch. Should land before the project sees enough adoption that users start having multiple devices and forget which key is which — meaning soon-after-launch, not immediately.
Summary
Every successful
e2a loginmints a new API key viastore.CreateAPIKey(ctx, userID, "CLI login")regardless of whether the user already has prior keys. There's no dedup, no replacement, no expiration. After running login a few times, a user has multiple keys all literally named"CLI login".Sample outcome after three
e2a loginruns from different machines (or the same machine after a config wipe):Concrete pain
keys list. When a user wants to revoke "the key on the laptop I lost," they can't tell which row that is.expires_atfield in the schema; revocation requires explicit user action. A key from a stolen device or a past colleague's machine is live until manually revoked.Surfaced during fresh-user e2e testing for the launch.
How comparable tools handle it
gclouduses OAuth refresh tokens, not API keys.gcloud auth login <account>updates the entry in place — re-login doesn't accumulate credentials. Different model end-to-end, but the user-facing behavior is "one credential per account, replaced on re-auth."gh(GitHub CLI) issues an access token via OAuth. Re-runninggh auth logininvalidates the prior token on the same scope; tokens are auto-namedgh-cli for <hostname>. User-facing behavior is "one credential per device, with a meaningful name."stripeCLI issues a device-scoped key, named after the host.Proposed paths
Path A — Revoke-on-login (smallest change)
On
e2a login:api_keyis set, capture its prefix (or call awhoamiendpoint to get its ID)~/.e2a/config.jsonResult: at most one CLI key per machine that's been using
e2a login. Server-side change minimal — uses the existing key-revocation endpoint.Path B — Hostname-aware names + per-name dedup (recommended)
On
e2a login:os.hostname()as the key name hint:"CLI login on joshzhang-MBP-2026"instead of the generic"CLI login"(user_id, name)pair and revokes them before minting the new oneResult: dashboard's "API keys" view becomes meaningful. User can identify and revoke a specific machine without guessing.
Path C — OAuth refresh-token model
Drop the long-lived API key for the CLI entirely. Issue an OAuth-style access token (~1hr) plus a refresh token. SDKs and CLI handle 401 → refresh → retry.
Better security model. Significant refactor: every SDK auth flow changes, all API consumers need refresh logic, the API-key-in-Authorization-header pattern becomes a parallel auth path.
My take
Path B. It's the right user-mental-model fit ("one device = one key, with a name I recognize") and it's well under a day of work:
os.hostname()in the key-creation request(user_id, name)collides on create, auto-revoke prior keyPath A is simpler but leaves the dashboard cluttered with identically-named keys. Path C is the architecturally right answer but costs more than its payback for an OSS launch.
Belt-and-suspenders worth considering on top of B: add a
last_used_atcolumn (already exists permigrations/001_init.sql) and run a periodic worker that revokes keys withlast_used_at < now() - 90 days. Bounded blast radius for forgotten keys without bothering active users.Priority
Not blocking launch. Should land before the project sees enough adoption that users start having multiple devices and forget which key is which — meaning soon-after-launch, not immediately.