5720a63958645bf70e75e1c9f1ec87c03a67667b
Member disclosure, three findings (fixes 1-3 of 5): 1. WEBHOOK_TOKEN was both the Open Collective webhook URL token and the X-Admin-Token for every /admin/* route, so anything that learned the webhook URL (OC admins, OC logs, this app's access logs) gained portal admin. The URL token is forced by OC's generic webhooks, which cannot set headers — the reuse is not. Introduce ADMIN_TOKEN, header-only. ADMIN_TOKEN_LEGACY=1 (default) still accepts WEBHOOK_TOKEN and logs a warning, so the admin panel and the scheduler keep working; set ADMIN_TOKEN everywhere, then flip it to 0 and rotate. sweep.sh and the digest script now send ADMIN_TOKEN. 2. The credit-pack path fell back to the webhook payload's own amount when OC could not resolve the order, so a forged POST naming its own totalAmount minted credits for any slug. The payload is now a hint only: an unresolvable order is refused, logged loudly, and emailed to the operator for manual reconciliation. Transient OC failures hold instead of granting blind. 3. Lapsing did not revoke access: both deactivation sites (lapse webhook, nightly sweep) discarded the key token deactivate_member() returns. Member keys carry no max_budget of their own, so they inherit the team's — zero the team budget on deactivation, which blocks every key under it. Key deletion is deliberately NOT used: provisioning reuses the stored sk- token on reactivation, and get_or_create_team does not update an existing team's budget, so the two renewal paths now re-sync the budget explicitly. Failures are loud (log + alert email); deactivation still completes.
Member Portal — membership middleware for the Inference Cooperative
Reconciles three systems into one membership lifecycle:
| System | Role | Source of truth for |
|---|---|---|
| Open Collective | Billing | Who is a paying member |
| Cloudron | Identity/SSO | Who can log in |
| LiteLLM | Inference | Who can use models, and how much |
Architecture
Open Collective (billing)
│ webhook
▼
┌─────────────────────────────┐
│ Member Portal (this app) │
│ - OC webhook handler │
│ - Cloudron user management │
│ - LiteLLM key management │
│ - key-injection proxy │
└─────────────────────────────┘
▲
│ X-User-Email header (LibreChat forwards identity)
│
LibreChat ──→ Member Portal ──→ LiteLLM ──→ backend
The three responsibilities
- Webhook handler (
POST /webhook/opencollective) — listens for Open Collective membership events and activates/deactivates members. - Key injector (
/v1/*) — reads the member's email from theX-User-Emailheader, looks up their LiteLLM key, and forwards the request to the gateway with that key. - Admin (
/health,/) — health and status.
Configuration (environment variables)
| Variable | Purpose |
|---|---|
CLOUDRON_API |
Cloudron API origin (auto-set by Cloudron) |
CLOUDRON_TOKEN |
Cloudron API token (Read+Write) |
MEMBERS_GROUP_ID |
Cloudron group ID for members (grants chat access) |
LITELLM_BASE |
LiteLLM gateway URL |
LITELLM_MASTER_KEY |
LiteLLM master key |
OPENCOLLECTIVE_WEBHOOK_SECRET |
Optional webhook signature secret |
Tier budgets
Governance decision (set in Loomio). Defaults:
free— $2/month of tokensmember— $15/monthsupporter— $30/month
LibreChat wiring
Point LibreChat's custom endpoint at this portal (not directly at LiteLLM), and add the identity header:
endpoints:
custom:
- name: "Inference Cooperative"
apiKey: "${LITELLM_KEY}"
baseURL: "https://portal.inference.coop/v1"
headers:
X-User-Email: "{{LIBRECHAT_USER_EMAIL}}"
models:
default: ["deepseek-v4-flash", "gpt-oss-120b"]
fetch: true
Status
Scaffold — the three handlers are stubbed with the correct integration points. Remaining work before production:
- Verify Open Collective webhook event names + payload shape
→
order.processed(every payment),new member(first only),firstPaymentflag - Verify Cloudron user-creation API payload (role/group assignment)
→
POST /api/v1/userswith{username, email, displayName, role, active}→ group assignment viaPUT /api/v1/users/:userId/groupswith{groupIds: [...]} - Add a persistent store (SQLite) for email→key mapping
→
key/listreturns token strings (not objects), so we store email→token locally - Verify LiteLLM key/generate + key/list payload shapes against live gateway
→
key/generatereturns{key: "sk-..."};key/listreturns{keys: ["<token>", ...]} - Add HMAC signature verification for the OC webhook (if OC supports it)
- Wire the OIDC addon for admin access
Scheduled tasks (scheduler addon)
Two jobs run inside the container via CloudronManifest.json → addons.scheduler:
- membership_sweep (nightly 3am) —
sweep.shcalls the portal's own/admin/sweep(auth:X-Admin-Token=WEBHOOK_TOKEN) to reconcile members against the live Open Collective list: provisions missed signups, deactivates lapsed members, syncs Loomio + Matrix. Idempotent; a missed webhook self-heals within 24 hours. - rag_refresh (nightly 3:30am) —
rag_refresh.pypulls this repo's sibling docs from the Gitea raw API (public, no credentials), uploads changed files to Open WebUI (OWUI_API_KEY), attaches them to the shared knowledge collection (DOCS_KNOWLEDGE_ID), reindexes, and re-grants the members group. State persists in/app/data/rag_refresh_state.json.
Verify after any manifest change: the deploy log must show
setupAddons: ... scheduler ... listing both tasks, and each script must
succeed when run manually inside the container.
Description
Membership middleware: syncs Open Collective members with Cloudron users and LiteLLM keys
4.9 MiB
0 Stars
2 Watchers
0 Forks
Languages
Python
99%
Shell
0.7%
Dockerfile
0.3%