Replaces the flat FernetEncryptionWrapper + EncryptionConfig model with a
proper envelope encryption scheme:
- MasterSecretResolver: unified resolution order (env → file → keyring →
auto-generate) under a single AUTHSOME_MASTER_KEY env var; no separate
passphrase vs raw-key distinction — both go through Argon2id
- DekManager: generates a random 256-bit DEK, wraps it with an
Argon2id-derived KEK (AES-256-GCM), and stores the wrapped record in the
KV store under __vault_meta__:__dek__ so it works with any KV backend
- AesGcmEncryptionWrapper: drop-in BaseEncryptionWrapper using AES-256-GCM
per-value encryption via closure; replaces FernetEncryptionWrapper
- Vault: simplified — no longer owns crypto or lifecycle; receives an
already-encrypted AsyncKeyValue; close() removed (caller manages store)
- EncryptionConfig removed from ServerConfig and models __all__
- Health route updated to report crypto_source from Vault properties
- Tests rewritten with fixtures and SimpleStore (in-memory); no DiskStore
BREAKING CHANGE: existing Fernet-encrypted vaults cannot be read back;
migration requires re-importing credentials.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Entire-Checkpoint: a778dfa71075
Enables authenticated access to sites like X/Twitter and LinkedIn that
use browser session cookies rather than OAuth or API keys.
Core logic lives entirely in auth/ — BrowserFlow.run_login() reads
Chrome's on-disk SQLite cookie database via browser-cookie3 (macOS
Keychain / Linux GNOME Keyring / Windows DPAPI), opens the site in the
user's default browser if no valid session exists, then polls until the
required auth cookies appear. No separate Chrome profile, no Playwright.
- auth/browser_cookies.py: read_chrome_cookies(), cookies_are_valid(),
normalize_jsessionid() with lazy browser-cookie3 import
- auth/flows/browser.py: BrowserFlow (begin/resume/refresh) + static
run_login() for CLI use
- auth/models/: AuthType.BROWSER, FlowType.BROWSER, BrowserConfig,
ExtractRule, ConnectionRecord.credentials field
- server/: register flow, header rendering, export branch, BrowserAction
schema, _session_response wiring (~35 lines total)
- cli/main.py: 8-line elif block calling BrowserFlow.run_login()
- Bundled providers: x-browser, linkedin-browser
- 30 new tests in tests/auth/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 3753e27e5941
Resolved two conflicts:
- daemon_control.py: combined AUTHSOME_DAEMON_URL dynamic host/port
with main's removal of the "admin" prefix from the daemon serve
subprocess command
- admin.py: accepted deletion (daemon commands moved to top-level CLI
in main); applied is_port_occupied() and dynamic URL message changes
to main.py where those commands now live
Also adds .gitleaksignore for the PostHog public project API key in
analytics.py (intentionally committed, not a secret).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 85f9bdcbf1f4
All daemon-side functions (is_daemon_responsive, ensure_daemon,
wait_for_daemon_ready, daemon_status, is_port_occupied) were
hardcoding DEFAULT_DAEMON_URL / port 7998 instead of reading the
configured URL from AUTHSOME_DAEMON_URL.
- Add _resolved_host_port() helper that parses host/port from
resolve_daemon_url() so every call site stays consistent
- start_daemon() now passes --host/--port derived from the env to
the daemon serve subprocess and records them in daemon.json
- admin.py port-occupied messages now show the actual daemon URL
instead of the hardcoded "Port 7998" string
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Entire-Checkpoint: d29926b4634e