How secrets are managed, how OAuth tokens are acquired and stored, and what each channel module can actually do.
Single source of truth for all credentials. Machine token on the VPS. Secrets flow from Bitwarden → .env via sync script. OAuth-generated tokens write back to Bitwarden automatically.
# Token stored at (root read-only, 600 permissions): /opt/cmo-analytics/.bws_token # SDK: bitwarden_sdk (Python, installed in venv) # Known secret IDs registry: /opt/cmo-analytics/data/bws_known_ids.json
# Pull all known secrets → .env (run this to refresh credentials) python3 scripts/bws_sync.py # Register a new secret added via Bitwarden dashboard python3 scripts/bws_sync.py --add=<bitwarden-secret-uuid> # Push a programmatically-generated credential → Bitwarden + .env python3 scripts/bws_sync.py --push=LINKEDIN_ACCESS_TOKEN=eyJhbG...
vault.bitwarden.com → Secrets Manager → your project → New Secret. Name it exactly as you want it in .env (e.g., GOOGLE_ADS_TOKEN).
Each secret has a UUID. Copy it from the secret detail view.
python3 scripts/bws_sync.py --add=<uuid> — pulls value into .env immediately and registers the UUID so future syncs include it.
OAuth 2.0 with authorization code flow. Callback handled by a local server on port 3849, proxied by Caddy at meetkai.xyz/linkedin/callback. Token written to Bitwarden SM + .env on success.
LinkedIn Developer Portal → your app → Auth → OAuth 2.0 settings → add: https://meetkai.xyz/linkedin/callback
python3 scripts/linkedin_oauth.py --start — starts server on 3849, prints the authorization URL.
Authorize the app. LinkedIn redirects to the callback. Token is exchanged and stored automatically.
LINKEDIN_ACCESS_TOKEN, LINKEDIN_REFRESH_TOKEN, LINKEDIN_TOKEN_EXPIRES — written to both Bitwarden SM and .env.
# LinkedIn tokens expire in 60 days. Refresh before expiry: python3 scripts/linkedin_oauth.py --refresh # Check current token status: cmo linkedin status
# API version pinned to 202501 (LinkedIn-Version header) # All posts use /rest/posts (v2/shares is deprecated) GET /v2/userinfo ← profile + person URN POST /rest/posts ← text post or article share GET /rest/posts?author=&q=author ← recent posts GET /v2/networkSizes/urn:li:person:{id} ← follower count GET /v2/organizationalEntityShareStatistics ← page analytics (needs Community Management API)
openid, profile, email ← basic identity w_member_social ← post on personal profile ✅ r_basicprofile ← read profile ✅ # Pending Community Management API approval: r_organization_social ← page analytics w_organization_social ← post to LinkedIn Pages
# LinkedIn cmo linkedin status # Token validity + expiry cmo linkedin profile # Profile + URN cmo linkedin post --text="..." # Post to personal profile cmo linkedin post-article --url=URL [--text="..."] cmo linkedin recent # Last 10 posts cmo linkedin followers # Follower count cmo linkedin analytics --days=30 # Page stats (needs org scope) # Meta Ads cmo meta_ads campaigns # All campaigns + status cmo meta_ads spend --days=7 # Spend summary cmo meta_ads performance --days=7 # Impressions, CTR, CPL, ROAS cmo meta_ads dashboard # Full overview # Resend cmo resend_report domains # 9 verified domains + health cmo resend_report recent --limit=20 # Sent emails + delivery status cmo resend_report stats --limit=100 # By-site delivery breakdown # Loops cmo loops dashboard # Account status cmo loops find [email protected] # Find contact cmo loops add --email=x --first_name=Y # Add/update contact cmo loops event --email=x --event=name # Trigger automation cmo loops send --email=x --transaction_id=id # Send transactional email # Instantly cmo instantly campaigns # List campaigns cmo instantly stats --all # Stats across all campaigns cmo instantly stats --campaign_id=ID # Single campaign stats
/opt/cmo-analytics/ ├── .bws_token # Bitwarden SM machine token (600, root only) ├── .env # All credentials — synced from Bitwarden ├── data/ │ └── bws_known_ids.json # {uuid: key_name} registry for sync └── scripts/ ├── bws_sync.py # Bitwarden SM ↔ .env sync (pull + push) ├── linkedin_oauth.py # OAuth flow + token refresh ├── linkedin.py # LinkedIn CLI module ├── meta_ads.py # Meta Ads CLI module ├── resend_report.py # Resend delivery reporting ├── loops.py # Loops contact + event management └── instantly.py # Instantly cold email analytics /etc/caddy/Caddyfile └── meetkai.xyz /linkedin/callback → 127.0.0.1:3849 # OAuth callback