diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..c2af7f9 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,24 @@ +# Kiwi gitleaks config — extends base CircuitForge config with local rules + +[extend] +path = "/Library/Development/CircuitForge/circuitforge-hooks/gitleaks.toml" + +# ── Test fixture allowlists ─────────────────────────────────────────────────── + +[[rules]] +id = "cf-generic-env-token" +description = "Generic KEY= in env-style assignment — catches FORGEJO_API_TOKEN=hex etc." +regex = '''(?i)(token|secret|key|password|passwd|pwd|api_key)\s*[=:]\s*['"]?[A-Za-z0-9\-_]{20,}['"]?''' +[rules.allowlist] +paths = [ + '.*test.*', +] +regexes = [ + 'api_key:\s*ollama', + 'api_key:\s*any', + 'your-[a-z\-]+-here', + 'replace-with-', + 'xxxx', + 'test-fixture-', + 'CFG-KIWI-TEST-', +] diff --git a/app/api/endpoints/orch_usage.py b/app/api/endpoints/orch_usage.py new file mode 100644 index 0000000..1d4f7b3 --- /dev/null +++ b/app/api/endpoints/orch_usage.py @@ -0,0 +1,27 @@ +"""Proxy endpoint: exposes cf-orch call budget to the Kiwi frontend. + +Only lifetime/founders users have a license_key — subscription and free +users receive null (no budget UI shown). +""" +from __future__ import annotations + +from fastapi import APIRouter, Depends + +from app.cloud_session import CloudUser, get_session +from app.services.heimdall_orch import get_orch_usage + +router = APIRouter() + + +@router.get("") +async def orch_usage_endpoint( + session: CloudUser = Depends(get_session), +) -> dict | None: + """Return the current period's orch usage for the authenticated user. + + Returns null if the user has no lifetime/founders license key (i.e. they + are on a subscription or free plan — no budget cap applies to them). + """ + if session.license_key is None: + return None + return get_orch_usage(session.license_key, "kiwi") diff --git a/app/api/routes.py b/app/api/routes.py index 69b5190..e5fd5a0 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -1,5 +1,5 @@ from fastapi import APIRouter -from app.api.endpoints import health, receipts, export, inventory, ocr, recipes, settings, staples, feedback, household, saved_recipes, imitate, meal_plans +from app.api.endpoints import health, receipts, export, inventory, ocr, recipes, settings, staples, feedback, household, saved_recipes, imitate, meal_plans, orch_usage from app.api.endpoints.community import router as community_router api_router = APIRouter() @@ -17,4 +17,5 @@ api_router.include_router(feedback.router, prefix="/feedback", tags= api_router.include_router(household.router, prefix="/household", tags=["household"]) api_router.include_router(imitate.router, prefix="/imitate", tags=["imitate"]) api_router.include_router(meal_plans.router, prefix="/meal-plans", tags=["meal-plans"]) +api_router.include_router(orch_usage.router, prefix="/orch-usage", tags=["orch-usage"]) api_router.include_router(community_router) diff --git a/tests/api/test_orch_usage.py b/tests/api/test_orch_usage.py new file mode 100644 index 0000000..e75ee5e --- /dev/null +++ b/tests/api/test_orch_usage.py @@ -0,0 +1,57 @@ +"""Tests for the /orch-usage proxy endpoint.""" +from __future__ import annotations + +from pathlib import Path +from unittest.mock import patch + +import pytest +from fastapi.testclient import TestClient + +from app.cloud_session import CloudUser, get_session +from app.main import app + + +def _make_session(license_key=None, tier="paid"): + return CloudUser( + user_id="user-1", + tier=tier, + db=Path("/tmp/kiwi_test.db"), + has_byok=False, + license_key=license_key, + ) + + +def test_orch_usage_returns_data_for_lifetime_user(): + """GET /orch-usage with a lifetime key returns usage data.""" + app.dependency_overrides[get_session] = lambda: _make_session( + license_key="CFG-KIWI-TEST-0000-0000" + ) + client = TestClient(app) + + with patch("app.api.endpoints.orch_usage.get_orch_usage") as mock_usage: + mock_usage.return_value = { + "calls_used": 10, + "topup_calls": 0, + "calls_total": 60, + "period_start": "2026-04-14", + "resets_on": "2026-05-14", + } + resp = client.get("/api/v1/orch-usage") + + app.dependency_overrides.clear() + assert resp.status_code == 200 + data = resp.json() + assert data["calls_used"] == 10 + assert data["calls_total"] == 60 + + +def test_orch_usage_returns_null_for_subscription_user(): + """GET /orch-usage with no license_key returns null.""" + app.dependency_overrides[get_session] = lambda: _make_session( + license_key=None, tier="paid" + ) + client = TestClient(app) + resp = client.get("/api/v1/orch-usage") + app.dependency_overrides.clear() + assert resp.status_code == 200 + assert resp.json() is None