inference-bot 452a27a778 portal: guard against an implausible member list; redact the webhook token from logs
Two fixes, both filed rather than deferred.

1. member-portal#2 — the sweep deactivates every member whose email is missing
   from the Open Collective member list, and the existing fail-safe only covers a
   fetch that RAISES. A fetch that SUCCEEDS while returning nothing or far less
   than usual is unguarded, and is what a narrowed token scope, an expired
   credential with partial access, or a renamed Membership tier produces (the
   fetch filters on the tier's name). That path deactivates the membership
   instead of failing. fetch_members() now checks the result against the local
   active count before any caller can act on it: empty, or at or below half of
   local, raises — so the sweep's fail-safe takes over and members stay active.
   Skipped when we have no active members of our own (fresh install).

   Correction to the issue as filed: listmonk_reconcile reads the local members
   table, not this fetch, so the newsletter path was never exposed to it. The
   sweep is the only destructive consumer.

2. ops/security#2 — OC's generic webhooks cannot send headers, so the webhook
   secret must travel in the URL path, and uvicorn wrote it into the access log
   on every delivery. A logging filter on uvicorn.access and on the root handler
   replaces the token with <redacted>. The token cannot be kept out of the URL;
   it can be kept out of the retention.

Both pieces are unit-tested in isolation before shipping: the guard's decision
function over eight cases, and the filter against a real access-log record, an
unrelated path (no over-redaction), and a missing dependency (must not break
logging).
2026-10-27 05:04:05 +00:00

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

  1. Webhook handler (POST /webhook/opencollective) — listens for Open Collective membership events and activates/deactivates members.
  2. Key injector (/v1/*) — reads the member's email from the X-User-Email header, looks up their LiteLLM key, and forwards the request to the gateway with that key.
  3. 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 tokens
  • member — $15/month
  • supporter — $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), firstPayment flag
  • Verify Cloudron user-creation API payload (role/group assignment) → POST /api/v1/users with {username, email, displayName, role, active} → group assignment via PUT /api/v1/users/:userId/groups with {groupIds: [...]}
  • Add a persistent store (SQLite) for email→key mapping → key/list returns token strings (not objects), so we store email→token locally
  • Verify LiteLLM key/generate + key/list payload shapes against live gateway → key/generate returns {key: "sk-..."}; key/list returns {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.sh calls 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.py pulls 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.

S
Description
Membership middleware: syncs Open Collective members with Cloudron users and LiteLLM keys
Readme
4.9 MiB
0 Stars 2 Watchers 0 Forks
Languages
Python 99%
Shell 0.7%
Dockerfile 0.3%