diff --git a/app/main.py b/app/main.py index 46514ea..7391625 100644 --- a/app/main.py +++ b/app/main.py @@ -3047,6 +3047,46 @@ async def admin_sweep(request: Request): return result +def _verify_sync_token(request: Request) -> None: + """Authenticate the LibreChat key-sync feed via X-Sync-Token. + + Dedicated machine secret (least privilege): it only ever exposes the + members' own chat keys, never the admin API. The same secret is set on + the LibreChat sandbox app (LIBRECHAT_SYNC_TOKEN in /app/data/env). + """ + expected = os.environ.get("LIBRECHAT_SYNC_TOKEN", "") + if not expected: + raise HTTPException(503, "Sync token not configured") + provided = request.headers.get("x-sync-token", "") + if not secrets.compare_digest(provided, expected): + raise HTTPException(401, "Invalid sync token") + + +@app.get("/admin/librechat/keys") +@limiter.limit("30/minute") +async def admin_librechat_keys(request: Request): + """Key feed for the LibreChat sandbox's per-member key auto-load. + + Returns every ACTIVE member's portal-provisioned chat key so the sandbox + can seed LibreChat's user-key store and members never see an API-key + prompt (chat bills to the member's own LiteLLM team/budget). Auth is the + dedicated X-Sync-Token header — NOT the admin token. + """ + _verify_sync_token(request) + + conn = get_db() + rows = conn.execute( + "SELECT email, key_token FROM members WHERE active = 1 AND key_token != ''" + ).fetchall() + conn.close() + return { + "members": [ + {"email": email, "key": key_token} + for email, key_token in rows + ] + } + + @app.get("/") async def index(): return {"service": "Inference Cooperative Member Portal", "version": "0.1.0"}