CLI Login Architecture¶
The CLI login system enables web-assisted authentication similar to gh auth login, allowing users to authenticate in the terminal by completing the flow in their browser.
Architecture¶
graph TB
subgraph CLI ["CLI (Terminal)"]
Config["~/.config/difflog/"]
Keychain["OS Keychain"]
Crypto[Web Crypto API]
end
subgraph Server ["Cloudflare Pages"]
API["/api/* Server Routes"]
KV["KV (ephemeral relay)"]
end
subgraph DB ["Cloudflare D1"]
Profiles[(profiles)]
Diffs[(diffs)]
end
subgraph Browser ["Web Browser"]
Storage[localStorage]
end
Config <--> Crypto
Keychain --- Config
Crypto <--> API
API <--> Profiles
API <--> Diffs
Browser -- "login relay" --> KV
KV -- "encrypted session" --> CLI
The CLI shares the same sync, encryption, and generation code as the web app. Profile metadata and diffs are stored as JSON files in ~/.config/difflog/, while API keys are stored in the OS keychain. Cloud sync uses the same D1-backed API endpoints as the web client.
Login Flow¶
sequenceDiagram
participant CLI as Terminal
participant KV as Cloudflare KV
participant Browser as Web Browser
participant LocalStorage as Browser Storage
CLI->>CLI: Generate code + expires
CLI->>CLI: Calculate verification hash
CLI->>Browser: Open /cli/auth?code=xxx&expires=yyy
CLI->>KV: Poll GET /api/cli/auth/{code}
Browser->>Browser: Calculate same verification hash
Browser->>Browser: Display 4-char verification code
Browser->>LocalStorage: Load profiles
Browser->>Browser: User selects profile
Browser->>Browser: Encrypt {profile, diffs} with code+expires
Browser->>KV: POST encrypted session
KV-->>CLI: Return encrypted session
CLI->>CLI: Decrypt with code+expires
CLI->>CLI: Save profile + diffs locally
Components¶
1. CLI Command (src/cli/commands/login.ts)¶
The login command supports two modes:
Interactive Web Login (default):
Direct Login (legacy):
Web Login Flow¶
- Generate ephemeral credentials:
- Code: 12 random hex characters from UUID
- Expires: current timestamp + 5 minutes
-
Verification code: last 4 chars of
SHA-256(code + expires) -
Open browser:
- URL:
{BASE}/cli/auth?code={code}&expires={expires} - Opens via
xdg-open(Linux) oropen(macOS) -
Prints verification code to stderr for user confirmation
-
Poll for session:
- GET
{BASE}/api/cli/auth/{code}every 2 seconds - Timeout after 5 minutes
-
Shows spinner in terminal:
| / - \ -
Decrypt session:
- Receives encrypted blob from KV
- Decrypts using code as password, expires as salt
- Saves profile + diffs to local config
2. Browser Auth Page (src/routes/cli/auth/+page.svelte)¶
Multi-step SPA that guides users through authentication:
Step 1: Verification¶
- Calculates verification code:
SHA-256(code + expires).slice(-4) - User confirms code matches terminal
- Prevents MITM attacks by requiring out-of-band verification
Step 2: Profile Selection¶
- Loads profiles from localStorage
- Displays cards with profile name, ID, and type (Local/Shared)
- Auto-selects if only one profile exists
Step 3: Encryption & Upload¶
- Collects profile metadata + diff history
- Encrypts payload using:
- Password:
code(from URL) - Salt:
base64(expires)(from URL) - POSTs encrypted session to KV relay
Step 4: Redirect¶
- Redirects to
/cli/successon successful upload - Success page shows checkmark and "Profile sent to CLI"
3. KV Relay API (src/routes/api/cli/auth/[code]/+server.ts)¶
Temporary storage for encrypted session data using Cloudflare Workers KV.
GET Endpoint (CLI polls)¶
Response:
- {pending: true} - session not yet uploaded
- {session: "..."} - encrypted session blob (auto-deletes after read)
Validation:
- Code must match /^[0-9a-f]{12}$/
- Returns 400 if invalid
POST Endpoint (Browser uploads)¶
TTL Calculation:
- Respects client-provided expiry timestamp
- Capped at 5 minutes to prevent abuse
- KV auto-deletes after TTL
4. Shared Layout (src/routes/cli/+layout.svelte)¶
Common layout for all /cli/* routes:
- diff·log branding
- "CLI Login" subtitle
- Container styles (.cli-auth, .cli-card, etc.)
- Global styles for child pages
Security Model¶
Encryption Flow¶
graph LR
subgraph CLI
Code1[12-char code]
Exp1[Expires timestamp]
Code1 --> Hash1[SHA-256]
Exp1 --> Hash1
Hash1 --> Verify1[Last 4 chars]
end
subgraph Browser
Code2[Same code from URL]
Exp2[Same expires from URL]
Code2 --> Hash2[SHA-256]
Exp2 --> Hash2
Hash2 --> Verify2[Last 4 chars]
Profile[Profile + Diffs]
Code2 --> Encrypt[AES-256-GCM]
Exp2 --> Salt[base64 salt]
Salt --> Encrypt
Profile --> Encrypt
Encrypt --> Blob[Encrypted blob]
end
subgraph KV
Blob --> Store[5-min TTL]
end
Store --> Decrypt[Decrypt with code+expires]
Decrypt --> Save[Save locally]
Security Properties¶
- End-to-end encryption: Server never sees plaintext credentials
- Ephemeral keys: Code and expires are single-use, auto-expire
- Tamper detection: Verification code binds code+expires together
- No password transmission: Profile data relayed directly, no server auth
- Time-bound: All sessions expire in 5 minutes maximum
Attack Resistance¶
| Attack | Mitigation |
|---|---|
| Code interception | Verification code prevents MITM (requires out-of-band confirmation) |
| URL tampering | Changing expires invalidates verification code |
| Replay attack | KV entry deleted after first read |
| Brute force | Code is 12 hex chars (2^48 entropy), expires in 5 minutes |
| Server compromise | Only encrypted blobs stored, useless without code+expires |
Data Flow¶
Payload Structure¶
interface RelayPayload {
profile: {
id: string;
name: string;
languages: string[];
frameworks: string[];
tools: string[];
topics: string[];
depth: 'quick' | 'standard' | 'deep';
customFocus: string;
};
diffs: Diff[];
}
Diffs include full history from browser's localStorage. No server sync occurs during login.
Encryption Details¶
- Algorithm: AES-256-GCM (same as standard encryption)
- Key derivation: PBKDF2 with 100,000 iterations
- Password: Raw code string (12 hex chars)
- Salt:
base64(TextEncoder.encode(String(expires))) - IV: Random 12 bytes prepended to ciphertext
Local Storage¶
CLI stores decrypted data in ~/.config/difflog/ (Linux/macOS) or %APPDATA%\difflog\ (Windows):
session.json- Profile ID + credentials (if shared profile)profile.json- Profile metadatadiffs.json- Diff history
API keys extracted during login are stored in the OS keychain — see CLI Key Storage Architecture for details.
Environment Variables¶
DIFFLOG_URL: Override base URL (default:https://difflog.dev)- Used for local development:
DIFFLOG_URL=http://localhost:8788 - CLI uses
localAwareFetchfor IPv6 fallback on localhost
Implementation Notes¶
Browser Compatibility¶
- Uses Web Crypto API (standard encryption)
- QR code generation via CDN (optional fallback)
- Works on mobile browsers for profile selection
Terminal Compatibility¶
- Raw mode for interactive input (
process.stdin.setRawMode) - Supports non-TTY piped input
- Ctrl+C handling (exit code 130)
- Spinner animation only when TTY
Cross-Platform¶
xdg-openon Linuxopenon macOS- Falls back gracefully if browser launch fails (prints URL)
Monitoring & Debugging¶
Successful Login¶
$ difflog login
Verification code: a3f7
Press Enter to open difflog.dev in your browser...
| Waiting for browser login... done
Logged in as John Doe. 42 diff(s) cached.
Common Errors¶
Connection failed:
- Check internet connection - Verify DIFFLOG_URL if testing locallyTimeout:
- User took >5 minutes to complete browser flow - Code expired in KVInvalid code:
- Malformed code in URL - Code already consumed (replay attempt)Future Enhancements¶
- QR code in terminal (for remote SSH sessions)
- Device name labeling ("Logged in on MacBook Pro")
- Multiple profile selection for teams
- Biometric confirmation on mobile
- WebSocket for instant relay (remove polling)