Skip to content

feat: move authentication from PHP to Go - #195

Open
pjcdawkins wants to merge 34 commits into
mainfrom
claude/auth-php-go-migration-0354ec
Open

pjcdawkins wants to merge 34 commits into
mainfrom
claude/auth-php-go-migration-0354ec

Conversation

@pjcdawkins

@pjcdawkins pjcdawkins commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Moves authentication from the legacy PHP CLI to Go. Go becomes the only component that stores or refreshes credentials; PHP gets tokens and auth state from Go.

Go

  • config.Auth() reads the auth keys (api.token, api.token_file, api.access_token, api.session_id, the auth URLs and client ID, disable_credential_helpers, skip_ssl, disable_locks) from the embedded config, the user's config.yaml and env vars, with the legacy CLI's precedence and casting.
  • internal/auth/store saves one entry per session ID in the system keychain (go-keyring, no cgo) or in <writable dir>/auth/<id>.json. The backend is chosen once, at the first save. No credential-helper binaries are downloaded.
  • auth.Manager resolves tokens and refreshes them under a blocking flock per session, held for the whole read-refresh-write sequence. The stored entry is re-read under the lock, so a rotated refresh token is never sent twice. Tokens are refreshed 2 minutes before expiry. invalid_grant clears the session; invalid_request and transient errors keep it.
  • Native auth:browser-login (login), auth:api-token-login, auth:logout (logout) and auth:token, with the legacy options and messages. Browser login uses a net/http listener on 127.0.0.1, on a port assigned by the system, with PKCE.
  • A hidden auth:internal token|status command for PHP. It writes only JSON, never prompts, and exits with code 3 when login is required.
  • A one-time migration from the legacy storage, via a hidden PHP export command, recorded in auth/.migrated. The legacy copies are then deleted, except SSH certificates.

PHP

  • Api gets tokens from Go (GoAuth) through the OAuth middleware's refresh hook, passing the rejected token after a 401. Login state, session IDs and stored API tokens also come from Go.
  • New hidden commands: auth:export-sessions [--delete] and auth:post-login (SSH host keys, certificate and config). Go prints the login messages and deletes the legacy cache and SSH files on login and logout.
  • Removed: the PHP login, logout and token commands, the OAuth listener, the refresh lock and the API token storage.
  • auth:info stays in PHP, as a client of Go tokens.

Tests

The existing auth and session integration tests pass against the Go implementation. One expectation changed: legacy session files are now migrated and deleted. New integration tests cover migration, Go and PHP commands in parallel across several token expiries (one refresh per expiry, no refresh-token reuse), a process killed while refreshing, 5xx errors on refresh, step-up challenges, a PHP command getting a new token after a 401, accepting the login prompt in PHP and Go commands, the effects of login and logout, and session switches.

Keychain storage needs manual testing on macOS, Windows and GNOME (including a locked keyring), as CI has no keychain.

🤖 Generated with Claude Code

pjcdawkins and others added 12 commits October 1, 2026 07:53
Add Config.Auth, which reads the auth keys (api.token, api.token_file,
api.access_token, api.session_id, the API/auth URLs, the client ID,
disable_credential_helpers, skip_ssl and disable_locks) from the embedded
config, the user's config.yaml and env vars, with the legacy CLI's
precedence and boolean casting. It also reads the session-id file and
applies the URL defaults under api.auth_url.

Validate session IDs with the legacy CLI's rules, and add the
browser_login config key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Store one entry per session ID in the system keychain (via go-keyring,
without cgo) or in a file under <writable dir>/auth/. The backend is
chosen when a session is first saved and recorded in the session file,
so a later keychain failure returns an error rather than silently
switching to a file. Keychain calls have a 10s timeout.

On Linux the keychain is only used under the legacy CLI's conditions
(a display, GNOME, not in a snap or container), with a check for the
Secret Service D-Bus name in place of the libsecret check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add auth.Manager, which replaces the token source that ran the legacy
CLI's auth:token and auth:info commands. It resolves tokens with the
legacy CLI's precedence (a stored API token, api.token, api.token_file,
then api.access_token, then the stored session), and exchanges API
tokens with the api_token grant under an api-token-<hash> session ID.

Refreshes run under a blocking flock on <writable dir>/auth/<id>.lock,
held for the whole read-refresh-write sequence, plus a mutex per
session within the process. The stored entry is re-read under the
lock, so a rotated refresh token is never sent again. Tokens are
refreshed 2 minutes before they expire. invalid_grant clears the
session; invalid_request (concurrent use) keeps it. Connection errors
before sending are retried twice, and timeouts or 5xx errors once.

Add a one-time migration from the legacy CLI's storage, through a
hidden export command, recorded in a marker file.

The Transport retries once on 401, and returns a LoginRequiredError
for step-up challenges (RFC 9470).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add native auth:browser-login (login), auth:api-token-login,
auth:logout (logout) and auth:token commands, with the legacy CLI's
options and messages. They are listed and abbreviated alongside the
legacy commands, and replace them in the list output.

Browser login serves the OAuth 2.0 redirect from a net/http listener on
127.0.0.1:5000-5010 (PKCE S256), using the browser_login page template.
After a login, Go runs the legacy CLI's hidden auth:post-login command
for SSH certificates and config; after a logout, auth:post-logout.

Add the hidden auth:internal command (token, status), through which
the legacy CLI gets tokens and auth state. It writes only JSON to
stdout, never prompts, and skips update checks. Exit code 3 means login
is required. The legacy CLI is given the wrapper's path in
<PREFIX>WRAPPER_EXECUTABLE.

init now uses the Go token source, and offers a browser login when
needed, as the legacy CLI's AutoLoginListener did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The legacy CLI no longer stores or refreshes credentials. It runs the
Go wrapper's hidden auth:internal command (found via
<PREFIX>WRAPPER_EXECUTABLE) for tokens and auth state, and caches the
results in memory. The OAuth 2.0 middleware gets a new token from Go
when its copy expires (2 minutes early) or after a 401, passing the
rejected token. A login prompt now runs the Go browser login.

Add hidden commands for the Go wrapper:
- auth:export-sessions [--delete] exports (or deletes) sessions and
  API tokens from the old keychain and file storage, for the one-time
  migration. It reads the keychain only if the credential helper is
  already installed, and never calls back into Go.
- auth:post-login generates SSH certificates and config, as
  Login::finalize() did.
- auth:post-logout flushes the cache and deletes the sessions' SSH
  certificates and config.

Remove the PHP auth:browser-login, auth:api-token-login, auth:logout
and auth:token commands, the OAuth listener, the refresh lock, and the
API token storage. PHP unit tests use a stub for the Go wrapper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Print each invalid token error once, and validate --max-age with the
legacy CLI's message.

Update the logout --other integration test: sessions written in the
legacy CLI's format are now migrated, and the legacy copies deleted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add integration tests for the Go auth stack:
- migration of legacy sessions and API tokens, and deletion of the
  legacy copies (keeping SSH certificates)
- Go and PHP commands in parallel across several short token
  lifetimes: one refresh per expiry and no refresh token reuse
- a process killed while refreshing does not block others
- 5xx errors on refresh keep the session
- a step-up challenge in a PHP command
- the auth:internal JSON output

Extend the mock auth server with token lifetimes, refresh counts and
failures, and the mock API with step-up challenges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Keep lock files when deleting all sessions, as another process may
  hold a lock on them.
- Allow a new login to replace a session whose keychain cannot be used.
- Bound authorization code exchange and revocation requests by a 30s
  timeout, as for refreshes.
- In the export, let keychain credentials take precedence over stale
  files, as the legacy CLI only used files without a keychain.
- Export file sessions whose IDs start with "cli-", and only delete
  their JSON file, as its directory can hold another session's SSH
  certificates.
- Open URLs on Windows with rundll32, which handles "&" in URLs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning

Changes suggested — 🟡 5 warnings · 🔵 2 minor points

🔍 Full review · 59 files reviewed

Verification
  • Manager.refresh re-reads the entry with Store.Load after taking both the in-process mutex and the flock, so a refresh token is never sent twice from parallel processes.
  • Migrator.Run re-checks the marker after taking .migrate.lock, and auth:export-sessions does not call back into Go, so the export cannot deadlock on that lock.
  • saveLogin releases the session lock (inside Manager.Save) before running the auth:post-login hook, so the nested PHP→auth:internal call does not block on it.
  • The Go authorize URL and code exchange match the removed PHP listener: same params, offline_access scope, space-joined amr, %20 encoding, and Basic auth with the client ID and an empty secret.

New unit tests cover the Manager (refresh, concurrency, lock timeout, migration, transport), the store and config.Auth. New integration tests in integration-tests/auth_go_test.go cover migration, parallel refreshes, a crash during refresh, 5xx retries and step-up. These run in ci.yml (make test, make integration-test) on Linux only. The Windows job only runs TestWindows in internal/legacy, and no test covers keychain backends, legacy global flags on the native auth commands, or disabled-command config.

Review details
  • Commit: a5767fa
  • Model: claude-opus-5-5

Review 1 of 10 for this pull request · View the full run

Comment thread commands/auth.go
Comment thread commands/root.go
Comment thread commands/root.go
Comment thread internal/auth/migrate.go Outdated
Comment thread internal/auth/migrate.go Outdated
Comment thread internal/auth/store/store.go Outdated
Comment thread internal/auth/store/store.go Outdated
- Retry a failed legacy export or delete after an hour, with a warning,
  instead of failing every auth command or starting PHP on every run.
- Fall back to a file when a new session is too big for the keychain.
- Retry replacing session files on Windows while another process has
  them open, so a rotated refresh token is not lost.
- Accept the legacy global options -n/--no, --ansi and --no-ansi on the
  native auth commands.
- Respect disabled_commands and wrapped_disabled_commands for them.
- Do not panic on a --browser value of only whitespace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@upsun-dispatch
upsun-dispatch Bot dismissed their stale review October 1, 2026 08:37

Superseded: the latest Upsun Dispatch review no longer requests changes.

Check the legacy session files by name before running the export. If
there is no legacy storage, the migration is marked done without
starting PHP. If a failed export is pending but every legacy session is
already in the Go store (e.g. the user logged in again), only the
deletion is left. With a credential helper installed, keychain
sessions cannot be listed, so the export is still retried.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning

Changes suggested — 🟡 1 warning

🔁 Incremental · 2 files reviewed

Verification
  • The globs in legacyState (sess-*/sess-*.json with a matching directory name, and sess-cli-*/api-token) match the ones in ExportSessionsCommand::sessionFiles/apiTokenFiles.
  • legacyState skips api-token- IDs and IDs that fail ValidateSessionID, the same as importSessions, so a session the import would skip never keeps the state at unknown.
  • The credential-helper check uses the same &lt;writable dir>/credential-helper[.exe] path as PHP's Manager::getExecutablePath, so keychain sessions keep the export running.
  • When the export is pending and the state is legacyImported, the import is skipped and the delete still runs. If the delete fails, the marker changes to DeletePending with a retry delay, so no command loops on the early-retry case.

This change adds TestMigrator_SkipsUnneededExports and updates TestMigrator to create legacy files. Both use a stubbed Export and the same home directory for Go and PHP, so neither covers a case where PHP's home directory differs from Go's.

Review details

Review 3 of 10 for this pull request · View the full run

Comment thread internal/auth/migrate.go
pjcdawkins and others added 3 commits October 1, 2026 21:12
auth:post-login now only sets up SSH (host keys, a certificate and SSH
config). Go prints the login messages and account summary, and deletes
the legacy API cache and SSH files on login and logout itself, so
auth:post-logout is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After getClient(false), the connector held an expired placeholder
token until the first request, so code reading the token from the
session (e.g. getMyUserId() decoding the JWT) did not see it. Replace
it whenever a client is requested with login.

The middleware's placeholder refresh token now identifies its access
token, so a token that was replaced locally is not mistaken for one
the API rejected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… switches

Add integration tests for:
- accepting the login prompt in a PHP command (which runs the Go
  login) and in a Go command
- a login replacing a session: revocation, the new tokens, and the
  SSH certificate
- a PHP command getting a new token after a 401
- logout --all revoking every session and keeping locks
- logging out after auth:api-token-login
- PHP and Go using the same session after session:switch

The mock auth server can issue unique access tokens, the mock API can
reject a token, and it serves /me.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@upsun-dispatch

upsun-dispatch Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

📋 PR Summary

This PR moves authentication out of the legacy PHP CLI and into Go. Go now stores and refreshes credentials: it uses the system keychain or JSON files, refreshes under a per-session lock, provides native login, logout and token commands, and migrates sessions from the legacy storage once. PHP now gets tokens and auth state from Go through a hidden auth:internal command. The latest push makes the internal PHP commands auth:export-sessions and auth:post-login show as hidden in the command list, and adds a test that they are hidden and can't be run by abbreviation.

Changes
Layer / File(s) Summary
Go auth core
internal/auth/manager.go Resolves and refreshes tokens under a blocking per-session flock. It re-reads the stored entry under the lock and refreshes 2 minutes before expiry.
internal/auth/store/store.go Stores one entry per session, in the keychain or in auth/&lt;id>.json. The backend is chosen at the first save.
internal/auth/migrate.go One-time migration from the legacy PHP storage, recorded in auth/.migrated.
Go commands
commands/auth_login.go Native browser login (local listener with PKCE) and API token login.
commands/auth.go Adds auth:token and the hidden auth:internal token|status command used by PHP.
Go config
internal/config/auth.go Reads auth config keys with the legacy CLI's precedence and casting.
PHP client
legacy/src/Service/GoAuth.php PHP client that gets tokens and auth state from Go.
legacy/src/Service/Api.php Gets tokens from Go through the OAuth refresh hook. The old storage and refresh-lock code is removed.
PHP internal commands
legacy/src/Command/Auth/ExportSessionsCommand.php Hidden command that exports legacy sessions for migration. It now sets hiddenInList so it stays out of the list.
legacy/src/Command/Auth/PostLoginCommand.php Hidden command that sets up SSH after login. It now sets hiddenInList.
Tests
integration-tests/auth_go_test.go Tests for the Go auth commands. Adds a test that the internal PHP commands are listed as hidden and that the auth:ex abbreviation doesn't resolve.

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning

Changes suggested — 🟡 1 warning · 1 still open

🔁 Incremental · 13 files reviewed

Outstanding from earlier reviews:

  • 🟡 #4155102652 — internal/auth/migrate.go:122: Where the home directories differ, migration is silently skipped forever and users are logged out. — legacyState still reads legacy files under filepath.Dir(m.Store.Dir), from Go's home directory, and writes an empty marker when it finds nothing there. legacy.go still passes PHP no home directory.
Verification
  • The cleanup paths cache, .session/sess-cli-&lt;id> and ssh/session.config match PHP's CacheFactory, Config::getSessionIdSlug and SshConfig::getCliSshDir when the home directories agree.
  • Session IDs passed to clearLegacySessionFiles have already been checked by ValidateSessionID (^[a-z0-9_-]+$), so dropping PHP's validateSessionId does not allow path traversal.
  • refreshFromGo marks a token as rejected only when the middleware's go:&lt;token> placeholder matches the unexpired static goToken. This matches GuzzleMiddleware::acquireAccessToken, which passes the current refresh token to on_refresh_start.
  • The --other logout path still leaves ssh/session.config alone, because others excludes the current session, as in the removed PostLogoutCommand.

This push adds integration tests in integration-tests/auth_flows_test.go: login prompts in PHP and Go, a login replacing an existing session, a refresh after a rejected token, logout --all removing .session, logout after an API-token login, and session switching. They use a fake curl-based browser and are skipped on Windows. The Windows home-directory mismatch above has no test.

Review details

Review 4 of 10 for this pull request · View the full run

Comment thread commands/auth_cleanup.go
Go and PHP share the writable dir: Go now keeps its credentials there
and cleans up the legacy CLI's files in it. Resolve it the same way:

- The home dir comes from <PREFIX>HOME, then HOME, then USERPROFILE.
  On Windows HOME can differ from USERPROFILE, e.g. in MSYS2.
- If the writable dir cannot be written, e.g. on an application
  container, use <temp dir>/<tmp_sub_dir>.

Otherwise the migration could miss the legacy sessions, and logout
could leave the legacy SSH certificate and API cache in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning

Changes suggested — 🟡 1 warning

🔁 Incremental · 2 files reviewed

Verification
  • HomeDir now checks &lt;PREFIX>HOME, then HOME, then USERPROFILE, the same order as PHP Config::getHomeDirectory().
  • migrate.go legacyState and auth_cleanup.go both get their directory from WritableUserDir, so both now follow the home directory PHP resolves.
  • When the home directory is unwritable, the fallback os.TempDir()/&lt;TempSubDir> matches PHP's sys_get_temp_dir()/tmp_sub_dir.
  • In TestWritableUserDir_ReadOnlyHome, the chmod-restore cleanup is registered after t.TempDir, so it runs before the directory is removed.

This push adds internal/config/dir_test.go. It covers the order HomeDir checks its environment variables in and the temp fallback for a read-only home directory (Unix only). Nothing covers Windows HOME≠USERPROFILE end to end, or failures other than permissions, such as a full disk.

Review details

Review 5 of 10 for this pull request · View the full run

Comment thread internal/config/dir.go Outdated
pjcdawkins and others added 2 commits October 1, 2026 21:27
Fall back to the temp dir only when the directory is not writable by
permissions (access W_OK, or the read-only attribute on Windows), as
PHP's Filesystem::canWrite does, rather than on any write failure. On
a full disk or with Windows ACLs, Go and PHP otherwise chose different
directories.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note

Reviewed — No blocking findings · 🔵 1 minor point

🔁 Incremental · 1 file reviewed

🔵 Minor point

  • internal/config/dir_test.go:70 — The new "a file in place of the directory" subtest has no Windows skip, unlike the "read-only home" case. It redirects the temp directory only through TMPDIR. On Windows, os.TempDir() reads TMP/TEMP/USERPROFILE and ignores TMPDIR. So WritableUserDir returns %TMP%\example-cli-tmp, and the assertion filepath.Join(tmp, "example-cli-tmp") fails whenever this test runs on Windows. Setting TMP as well, or skipping on Windows, fixes it.
Verification
  • canWrite returns false when a regular file is at the writable dir path (info.IsDir() check), which matches PHP's getWritableUserDir !is_dir fallback.
  • HomeDir checks &lt;PREFIX>HOME, then HOME, then USERPROFILE, the same order as PHP's Config::getHomeDirectory().
  • browserCommand now requires strings.TrimSpace(browserOption) != "" before it indexes fields[0], so an option made only of whitespace can no longer cause an empty slice index.

This push only changes TestWritableUserDir_TempFallback in internal/config. The ci.yml Go test job runs it on Linux. The Windows job runs only TestWindows in internal/legacy, so the new subtest never runs on Windows.

Review details

Review 7 of 10 for this pull request · View the full run

pjcdawkins and others added 2 commits October 1, 2026 21:35
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hide auth:export-sessions and auth:post-login with the AsCommand
attribute instead of hiddenInList. Both keep them out of the list, but
only the attribute excludes them from abbreviation matching, so e.g.
"auth:ex" no longer prints exported credentials.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning

Changes suggested — 🟡 1 warning

🔁 Incremental · 2 files reviewed

Verification
  • Both commands still run by exact name: Symfony's hidden attribute only stops abbreviation matching, and Go calls them by full name.
  • Plain list (without --all) still leaves both commands out, because ApplicationDescription checks the LazyCommand's attribute-based isHidden().
  • Go abbreviation is unaffected by hidden turning false: a single match on a non-native legacy command is never expanded by Go.

This commit adds and changes no tests. Nothing in the repository checks that these commands stay hidden in completion or in list --all --format=json.

Review details

Review 9 of 10 for this pull request · View the full run

Comment thread legacy/src/Command/Auth/ExportSessionsCommand.php
CommandBase::isHidden() ignores the AsCommand attribute, so completion
and "list --all --format=json" showed the commands once loaded. Keep
$hiddenInList as well as the attribute, which stops abbreviations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@upsun-dispatch
upsun-dispatch Bot dismissed their stale review October 1, 2026 21:55

Superseded: the latest Upsun Dispatch review no longer requests changes.

pjcdawkins and others added 4 commits October 2, 2026 01:18
If the server consumed the refresh token but the response was lost, a
retry sends the same single-use token again, which fails with
invalid_grant and logs the user out. Refresh requests are now retried
only when they were not sent. API token exchanges keep one retry after
sending, as the API token is reusable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The config schema omitted api.token, api.token_file, api.access_token
and api.disable_locks, so these were discarded from the embedded config
or CLI_CONFIG_FILE, unlike in the legacy CLI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the previous session could not be logged out because the keychain
was unusable, only the user session was forgotten. Re-entering the same
API token then saved to its existing api-token-* session, still marked
as keychain-backed, and failed again. The recovery is moved to
Manager.LogoutToReplace, which also forgets the session for the new API
token.

Also read the test auth server's counters under its lock, fixing a data
race in the lost-response test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refresh requests are no longer retried once sent, so a single server
error fails the command, and the kept session refreshes on the next run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pjcdawkins and others added 7 commits October 2, 2026 17:47
The browser login listened on the first free port between 5000 and
5010, a limit carried over from the legacy CLI. It now listens on port
0, so the system assigns a free port. The auth server accepts any port
in loopback redirect URIs (RFC 8252, section 7.3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The legacy CLI passed an access token that the API rejected to
`auth:internal token --rejected <token>`, exposing it in process
listings. A token rejected by one endpoint may still be valid. The
`--rejected` flag is now a boolean, and the token is read from stdin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A keychain write or deletion that timed out kept running after the
session lock was released, so it could overwrite or delete newer
credentials. Changes to an existing keychain entry are now waited for,
with a notice after the timeout. Reads, and the first write that
chooses the backend, keep the timeout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a refresh failed with invalid_grant, or an expired token could not
be refreshed, only the credentials were deleted. The legacy CLI also
deleted the API cache and the session's SSH certificate and config, as
`auth:logout` does. Manager.OnLoggedOut now runs the same cleanup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The root pre-run ignores --quiet when --verbose or --debug is also set,
but errors were still suppressed, e.g. `auth:token -qv --unknown`
printed nothing. Errors now use the same condition, via isQuiet().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DeleteAll removed every file in the auth directory after deleting each
session, including the files of sessions whose keychain deletion
failed. The secrets were then stranded in the keychain. Those session
files are now kept, so that the deletion can be retried.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On macOS the CLI opens URLs with `open`, which the fake browser helpers
did not stub, so login tests opened a real browser or waited for the
login timeout. The helpers now stub both `open` and `xdg-open`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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