-
Notifications
You must be signed in to change notification settings - Fork 12
systems auth
DroidProxy keeps each provider's OAuth credentials as JSON files in ~/.cli-proxy-api/. The auth subsystem models those files as typed accounts, scans the directory on demand, watches it for external changes, and lets the UI enable, disable, or delete individual accounts.
Ran, Nik
The auth subsystem answers three questions for the rest of the app:
- Which providers are authenticated, and with which accounts? (
AuthManager.checkAuthStatus) - Where do credential files live? (
AuthPaths.authDirectory) - When does the on-disk credential set change, so the UI can refresh? (
AuthDirectoryMonitor)
Credential files are written by the backend's OAuth login flows (driven by Backend supervisor). The auth subsystem only reads, toggles, and deletes them; it never performs the OAuth handshake itself.
| Path | Role |
|---|---|
src/Sources/AuthStatus.swift |
ServiceType, AuthAccount, ServiceAccounts, and the AuthManager observable object. |
src/Sources/AuthPaths.swift |
Single source of truth for the auth directory (~/.cli-proxy-api). |
src/Sources/AuthDirectoryMonitor.swift |
Debounced DispatchSource file watcher on the auth directory. |
~/.cli-proxy-api/*.json |
One credential file per authenticated account. |
| Abstraction | Description |
|---|---|
ServiceType |
String-backed CaseIterable enum: claude, codex, antigravity, kimi, cursor. init?(authFileType:) maps a file's type field to a case; antigravity, gemini, and gemini-cli all map to .antigravity. displayName gives the UI label (Claude Code, Codex, Antigravity, Kimi, Cursor). |
AuthAccount |
One account parsed from a credential file. Fields: id (filename), email?, login?, type, expired: Date?, filePath, isDisabled. isExpired is computed (expired < Date()). displayName falls back email → login → id. Equality is by id. |
ServiceAccounts |
Groups all AuthAccounts for one ServiceType. hasAccounts is !accounts.isEmpty. |
AuthManager |
ObservableObject exposing @Published serviceAccounts: [ServiceType: ServiceAccounts]. Owns scanning, toggling, and deletion. |
AuthPaths |
Namespace whose authDirectory resolves to ~/.cli-proxy-api. |
AuthDirectoryMonitor |
Debounced DispatchSourceFileSystemObject watcher that calls an onChange closure after the directory changes. |
Each *.json file in the auth directory carries the fields the parser reads:
-
type— provider type string (e.g.claude,codex,antigravity,gemini,kimi,cursor). -
email— account email, when present. -
login— login handle (used by Copilot-style accounts). -
expired— ISO 8601 expiry timestamp. -
disabled— bool; whentruethe backend skips the account.
checkAuthStatus() lists ~/.cli-proxy-api via FileManager.contentsOfDirectory. On error it publishes an empty account set and returns. Otherwise it iterates over files with the json extension, calls parseAccount on each, and groups results by ServiceType into a fresh dictionary, which it assigns to serviceAccounts on the main thread.
parseAccount(from:) reads the file, decodes JSON, requires a type string that maps to a known ServiceType, and builds an AuthAccount. The expired field is parsed by parseExpiry, which tries two ISO8601DateFormatters in order: one with fractional seconds (.withInternetDateTime, .withFractionalSeconds) and one without (.withInternetDateTime).
AuthDirectoryMonitor.start() opens the auth directory with O_EVTONLY and creates a DispatchSource.makeFileSystemObjectSource with an event mask of [.write, .delete, .rename] on the main queue. Each event schedules a refresh; scheduleRefresh() cancels any pending work item and posts a new one after debounceInterval, which logs and invokes onChange. stop() cancels the pending refresh and the source (the cancel handler closes the file descriptor); deinit calls stop().
sequenceDiagram
participant FS as ~/.cli-proxy-api (filesystem)
participant M as AuthDirectoryMonitor
participant AM as AuthManager
participant UI as SettingsView
FS->>M: write / delete / rename event
M->>M: scheduleRefresh() (cancel + debounce)
Note over M: wait debounceInterval
M->>AM: onChange() -> checkAuthStatus()
AM->>FS: list *.json, parseAccount each
FS-->>AM: credential files
AM->>AM: publish serviceAccounts (main thread)
AM-->>UI: @Published update re-renders ServiceRow
toggleAccountDisabled(_:) reads the account's file, flips the disabled bool, and writes it back atomically with .sortedKeys. Before disabling a currently-enabled account it counts the enabled accounts for that ServiceType and refuses (enabledCount > 1 guard) so the last enabled account for a provider cannot be disabled. After a successful write it calls checkAuthStatus().
deleteAccount(_:) removes the credential file via FileManager.removeItem and then calls checkAuthStatus(). Both the toggle and delete paths re-scan so serviceAccounts reflects disk state immediately, in addition to the monitor firing from the file change.
-
SettingsView(src/Sources/SettingsView.swift) observesAuthManagerand renders aServiceRowper provider, with the expanded account list drivingtoggleAccountDisabledanddeleteAccount. It also runs its ownAuthDirectoryMonitorto refresh while the window is open. -
AppDelegate(src/Sources/AppDelegate.swift) runs anAuthDirectoryMonitorfor the lifetime of the app so the menu reflects credential changes. -
OAuthUsageTracker(src/Sources/OAuthUsageTracker.swift) consumes the parsed accounts to fetch quota windows. See OAuth usage tracker. -
ServerManager(src/Sources/ServerManager.swift) login flows write the credential files that the monitor detects andcheckAuthStatusthen parses. See Backend supervisor.
-
Support a new provider: add a
ServiceTypecase, map its authtypestrings ininit?(authFileType:), and give it adisplayName. -
Read a new credential field: add it to
AuthAccountand populate it inparseAccount. -
Accept another expiry format: add an
ISO8601DateFormatterto thedateFormattersarray used byparseExpiry. -
Change watch sensitivity: adjust the
eventMaskordebounceIntervalpassed toAuthDirectoryMonitor. -
Move the auth directory: change
AuthPaths.authDirectory(the single source of truth).
| File | Role |
|---|---|
src/Sources/AuthStatus.swift |
ServiceType, AuthAccount, ServiceAccounts, AuthManager. |
src/Sources/AuthPaths.swift |
Auth directory location. |
src/Sources/AuthDirectoryMonitor.swift |
Debounced directory watcher. |
src/Sources/NotificationNames.swift |
authDirectoryChanged constant posted after login. |