CLI Development Guide
Purpose: Guide for developers working on the kg TypeScript CLI client
Related ADRs: - ADR-027: User Management API - Server-side authentication - ADR-029: CLI Theory of Operation - Command structure - ADR-031: Encrypted API Key Storage - API key management - ADR-054: OAuth Client Management - OAuth flows
Design Principles
- Modular Architecture: Separate concerns into reusable TypeScript modules
- Security First: No plaintext password storage, secure token management
- User Experience: Remember username, persistent token storage, graceful error handling
- Multi-Client Support: Modules designed for CLI, MCP server, and future clients
- Backwards Compatibility: Existing API key authentication continues to work
Architecture Layers
┌─────────────────────────────────────────────────────────────┐
│ CLI Commands Layer │
│ kg login, kg logout, kg admin user [commands] │
└──────────────────────┬──────────────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────────────┐
│ Auth Client Layer │
│ AuthClient: login(), logout(), validateToken() │
└──────────────────────┬──────────────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────────────┐
│ Token Manager Layer │
│ TokenManager: store(), retrieve(), validate(), clear() │
└──────────────────────┬──────────────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────────────┐
│ Config Storage Layer │
│ ConfigManager: Auth token storage (~/.config/kg/) │
└─────────────────────────────────────────────────────────────┘
Key modules:
- client/src/lib/auth/token-manager.ts - JWT token lifecycle
- client/src/lib/auth/auth-client.ts - HTTP client for auth endpoints
- client/src/lib/auth/challenge.ts - Re-auth prompt for sensitive ops
- client/src/lib/config.ts - Config file management (extended for auth)
- client/src/api/client.ts - Main API client (with auth interceptors)
Authentication Flow
Login Flow
1. User runs: kg login
2. Prompt for username (remember from last login)
3. Prompt for password (hidden input, not stored)
4. POST /auth/login with credentials
5. Receive JWT token + user info
6. Store token in ~/.config/kg/config.json
7. Store expiration timestamp (not just duration)
8. Store username + role for quick access
Token Usage
1. User runs any command: kg search query "concept"
2. TokenManager checks for valid token
3. If expired → prompt re-login
4. If valid → add to request: Authorization: Bearer <token>
5. API validates token → returns data
6. If 401 → token expired, prompt re-login
Challenge Flow (Sensitive Operations)
For destructive operations (delete user, reset database):
1. User runs: kg admin user delete <id>
2. System prompts: "This is a sensitive operation. Re-enter password:"
3. Pre-fill username, prompt password only
4. Validate credentials → get fresh token
5. Execute operation with fresh token
6. Discard fresh token (keep original session)
Token Storage Format
Location: ~/.config/kg/config.json
{
"auth": {
"token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "bearer",
"expires_at": 1760230000,
"username": "admin",
"role": "admin"
},
"username": "admin",
"api_url": "http://localhost:8000"
}
Key points:
- Store expiration timestamp (Unix timestamp), not duration
- Store username and role for quick access (avoid JWT decode)
- NEVER store passwords
- Backwards compatible with secret (API key) field
Adding Authentication to Commands
Pattern 1: Protected Command (Requires Auth)
import { TokenManager } from '../lib/auth/token-manager';
import { KnowledgeGraphClient } from '../api/client';
async function myCommand() {
const config = ConfigManager.load();
const tokenManager = new TokenManager(config);
// Check authentication
if (!tokenManager.isLoggedIn()) {
console.error('Not logged in. Run: kg login');
process.exit(1);
}
// Create client with token
const client = new KnowledgeGraphClient(config, tokenManager);
// Make API call (token added automatically)
const result = await client.myApiCall();
}
Pattern 2: Optional Auth (Public Endpoint)
async function myCommand() {
const config = ConfigManager.load();
const tokenManager = new TokenManager(config);
// Create client (will use token if available)
const client = new KnowledgeGraphClient(config, tokenManager);
// API call works with or without auth
const result = await client.publicApiCall();
}
Pattern 3: Sensitive Operation (Challenge Required)
import { AuthChallenge } from '../lib/auth/challenge';
async function deleteUser(userId: number) {
const config = ConfigManager.load();
const tokenManager = new TokenManager(config);
const authClient = new AuthClient(config.api_url);
// Require challenge for sensitive operation
const challenge = new AuthChallenge(authClient, tokenManager);
const freshToken = await challenge.challenge({
reason: 'Delete user account',
username: tokenManager.getUsername() || undefined,
allowCancel: true
});
if (!freshToken) {
console.log('Operation cancelled');
return;
}
// Execute with fresh token
await authClient.deleteUser(freshToken.access_token, userId);
console.log('User deleted');
}
Token Expiration Handling
Automatic Detection
Tokens are checked before each request: - 5-minute buffer: Token considered expired 5 minutes before actual expiration - Interceptor: Axios interceptor catches 401 responses - User prompt: Clear error message directing user to re-login
User Experience
# Token valid - command succeeds
$ kg search query "concept"
Found 5 concepts...
# Token expired - prompt re-login
$ kg search query "concept"
Error: Authentication expired. Please run: kg login
# User logs in again
$ kg login
Username: admin
Password: ********
Logged in successfully as admin (role: admin)
# Command works again
$ kg search query "concept"
Found 5 concepts...
Security Considerations
What We Store
- ✅ JWT token (time-limited, signed by server)
- ✅ Token expiration timestamp
- ✅ Username and role (decoded from JWT)
- ✅ API URL
What We DON'T Store
- ❌ Passwords (NEVER stored, only used during login)
- ❌ Private keys (if using OAuth, keys stay server-side)
- ❌ Unencrypted secrets
File Permissions
- Config file:
~/.config/kg/config.jsonset to 0600 (owner read/write only) - Automatic permission check on write
- Warning if permissions too permissive
Token Security
- Short-lived: 60 minutes default (configurable server-side)
- Single-use sensitive ops: Challenge flow generates fresh token
- Stateless: Server validates signature, no revocation (use short expiry)
- Future: Refresh tokens for longer sessions (ADR-054)
Authentication Priority
When multiple auth methods are available:
1. JWT token (if logged in via kg login)
→ Authorization: Bearer <jwt_token>
2. API key (if configured via kg config set secret)
→ X-API-Key: <api_key>
3. None (unauthenticated)
→ Some endpoints allow public access
Implementation: Request interceptor in KnowledgeGraphClient checks in this order.
Testing Authentication Code
Unit Tests
Test individual modules in isolation:
// Test token expiration logic
describe('TokenManager', () => {
it('should detect expired tokens', () => {
const expired = { expires_at: Math.floor(Date.now() / 1000) - 100 };
expect(tokenManager.isTokenExpired(expired)).toBe(true);
});
});
Integration Tests
Test full authentication flow:
# Test login flow
kg login
# Enter credentials
kg admin user list # Should work
kg logout
kg admin user list # Should fail with auth error
Manual Testing
# 1. Fresh login
rm ~/.config/kg/config.json
kg login
# 2. Token persistence
kg admin user list # Should work without re-login
# 3. Token expiration (wait 60+ minutes or modify config)
kg admin user list # Should prompt re-login
# 4. Challenge flow
kg admin user delete <id> # Should prompt password
Common Patterns
Reading Current User Info
const tokenManager = new TokenManager(config);
const username = tokenManager.getUsername();
const role = tokenManager.getRole();
if (role === 'admin') {
// Admin-only functionality
}
Handling 401 Errors Gracefully
try {
const result = await client.someApiCall();
} catch (error) {
if (error.response?.status === 401) {
console.error('Authentication expired. Run: kg login');
process.exit(1);
}
throw error;
}
Prompting for Re-authentication
import { AuthChallenge } from '../lib/auth/challenge';
const challenge = new AuthChallenge(authClient, tokenManager);
const token = await challenge.challenge({
reason: 'Perform sensitive operation',
allowCancel: true
});
if (!token) {
// User cancelled
return;
}
Future Enhancements
OAuth 2.0 Support (ADR-054)
- Device Authorization Grant for CLI
- Better for headless environments
- Longer-lived sessions with refresh tokens
Refresh Tokens
- Extend sessions without re-entering password
- Background token refresh
- Revocable at server-side
MCP Server Integration
- Share token management modules
- Same security model across clients
Quick Reference
Key Files:
- client/src/lib/auth/ - Authentication modules
- client/src/cli/login.ts - Login command
- client/src/cli/admin/user.ts - User management commands
- client/src/api/client.ts - Main API client
Key Commands:
- kg login - Authenticate user
- kg logout - Clear stored token
- kg admin user list - List users (admin only)
- kg admin user create - Create user (admin only)
Config Location:
- ~/.config/kg/config.json (Linux/macOS)
- %APPDATA%\kg\config.json (Windows)
Token Lifespan:
- Default: 60 minutes
- Buffer: 5 minutes (55 minutes effective)
- Configurable server-side: JWT_EXPIRATION_MINUTES