refactor: remove Turnstile + disable local file writes (HF ephemeral runtime)
Browse files- Remove Cloudflare Turnstile verification from /v1/keys/me endpoint
- Make dynamic providers storage ephemeral (no disk writes)
- HF Space runtime is ephemeral, local writes are lost on restart
- API keys already use GitHub Gist for persistence
- .dockerignore +45 -0
- .opencode/config.json +41 -0
- app/config.py +0 -3
- app/index.py +11 -38
- app/templates/landing.html +6 -10
.dockerignore
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# HuggingFace Space Docker build - keep image lean
|
| 2 |
+
|
| 3 |
+
# Git / opencode meta
|
| 4 |
+
.git/
|
| 5 |
+
.gitignore
|
| 6 |
+
.dockerignore
|
| 7 |
+
.opencode/
|
| 8 |
+
thoughts/
|
| 9 |
+
*.md
|
| 10 |
+
!README.md
|
| 11 |
+
|
| 12 |
+
# Python artifacts
|
| 13 |
+
__pycache__/
|
| 14 |
+
*.pyc
|
| 15 |
+
*.pyo
|
| 16 |
+
*.pyd
|
| 17 |
+
*.egg-info/
|
| 18 |
+
.Python
|
| 19 |
+
.pytest_cache/
|
| 20 |
+
.mypy_cache/
|
| 21 |
+
.ruff_cache/
|
| 22 |
+
htmlcov/
|
| 23 |
+
.coverage
|
| 24 |
+
|
| 25 |
+
# Virtual envs
|
| 26 |
+
venv/
|
| 27 |
+
.venv/
|
| 28 |
+
env/
|
| 29 |
+
.env
|
| 30 |
+
.env.*
|
| 31 |
+
|
| 32 |
+
# Local-only runtime data (HF Space provides its own persistence)
|
| 33 |
+
app/data/
|
| 34 |
+
app/__pycache__/
|
| 35 |
+
|
| 36 |
+
# IDE / editor
|
| 37 |
+
.vscode/
|
| 38 |
+
.idea/
|
| 39 |
+
*.swp
|
| 40 |
+
*.swo
|
| 41 |
+
*~
|
| 42 |
+
|
| 43 |
+
# Tests & dev utilities
|
| 44 |
+
test_gist.py
|
| 45 |
+
encrypt_config.py
|
.opencode/config.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://opencode.ai/config.json",
|
| 3 |
+
"agent": {
|
| 4 |
+
"commander": {
|
| 5 |
+
"model": "qwenim/qwen3.7-max"
|
| 6 |
+
},
|
| 7 |
+
"brainstormer": {
|
| 8 |
+
"model": "qwenim/qwen3.7-max"
|
| 9 |
+
},
|
| 10 |
+
"planner": {
|
| 11 |
+
"model": "qwenim/qwen3.7-max"
|
| 12 |
+
},
|
| 13 |
+
"executor": {
|
| 14 |
+
"model": "qwenim/qwen3.7-max"
|
| 15 |
+
},
|
| 16 |
+
"implementer": {
|
| 17 |
+
"model": "qwenim/qwen3.7-max"
|
| 18 |
+
},
|
| 19 |
+
"reviewer": {
|
| 20 |
+
"model": "qwenim/qwen3.7-max"
|
| 21 |
+
},
|
| 22 |
+
"codebase-locator": {
|
| 23 |
+
"model": "qwenim/qwen3.7-max"
|
| 24 |
+
},
|
| 25 |
+
"codebase-analyzer": {
|
| 26 |
+
"model": "qwenim/qwen3.7-max"
|
| 27 |
+
},
|
| 28 |
+
"pattern-finder": {
|
| 29 |
+
"model": "qwenim/qwen3.7-max"
|
| 30 |
+
},
|
| 31 |
+
"ledger-creator": {
|
| 32 |
+
"model": "qwenim/qwen3.7-max"
|
| 33 |
+
},
|
| 34 |
+
"artifact-searcher": {
|
| 35 |
+
"model": "qwenim/qwen3.7-max"
|
| 36 |
+
},
|
| 37 |
+
"mm-orchestrator": {
|
| 38 |
+
"model": "qwenim/qwen3.7-max"
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
}
|
app/config.py
CHANGED
|
@@ -216,9 +216,6 @@ UPSTREAM_API_KEYS = {
|
|
| 216 |
|
| 217 |
# ββ GitHub Gist Storage βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 218 |
|
| 219 |
-
TURNSTILE_SITE_KEY = _decrypt("enc:v1:Q1ElsRjDMKIbb-7l:Q2ewoEx10wQFL-BXxTp8Xapb815hL5Iy1_H0Yz9zhQX6DTX2dTgiAA==")
|
| 220 |
-
TURNSTILE_SECRET_KEY = _decrypt("enc:v1:5AVf8i4aUlDhzWIW:_yLBFEmdzSl3FRbaNI1t0FiKA7gXWNPlsBYLCBwMh6yvKCv3wGW_Uf6ArVs9wpzUqfvt")
|
| 221 |
-
|
| 222 |
HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
| 223 |
HF_DATASET = "rnilkyway/apiarium-logs"
|
| 224 |
|
|
|
|
| 216 |
|
| 217 |
# ββ GitHub Gist Storage βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 218 |
|
|
|
|
|
|
|
|
|
|
| 219 |
HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
| 220 |
HF_DATASET = "rnilkyway/apiarium-logs"
|
| 221 |
|
app/index.py
CHANGED
|
@@ -19,7 +19,6 @@ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
| 19 |
from config import (
|
| 20 |
DEFAULT_UPSTREAM_URL, UPSTREAMS, UPSTREAM_API_KEYS,
|
| 21 |
GITHUB_TOKEN, GIST_ID,
|
| 22 |
-
TURNSTILE_SITE_KEY, TURNSTILE_SECRET_KEY,
|
| 23 |
HF_TOKEN, HF_DATASET,
|
| 24 |
MASTER_KEY,
|
| 25 |
AI_GATEWAY_URL, UPSTREAM_14448_URL,
|
|
@@ -46,7 +45,6 @@ MAX_CONTINUATIONS = 0
|
|
| 46 |
|
| 47 |
RPM_LIMIT = 4
|
| 48 |
KEYS_FILE = os.path.join(os.path.dirname(__file__), "data", "api_keys.json")
|
| 49 |
-
PROVIDERS_FILE = os.path.join(os.path.dirname(__file__), "data", "providers.json")
|
| 50 |
|
| 51 |
_rate_limits: dict = defaultdict(list)
|
| 52 |
|
|
@@ -1142,27 +1140,20 @@ class ProviderImportRequest(BaseModel):
|
|
| 1142 |
|
| 1143 |
|
| 1144 |
def _load_providers():
|
| 1145 |
-
"""
|
| 1146 |
global _dynamic_providers
|
| 1147 |
-
|
| 1148 |
-
if os.path.exists(PROVIDERS_FILE):
|
| 1149 |
-
with open(PROVIDERS_FILE, "r") as f:
|
| 1150 |
-
_dynamic_providers = json.load(f) or {}
|
| 1151 |
-
for pid, prov in _dynamic_providers.items():
|
| 1152 |
-
_register_provider_in_upstreams(prov)
|
| 1153 |
-
logger.info("Loaded %d dynamic providers", len(_dynamic_providers))
|
| 1154 |
-
except Exception as e:
|
| 1155 |
-
logger.warning("Failed to load providers: %s", e)
|
| 1156 |
-
_dynamic_providers = {}
|
| 1157 |
|
| 1158 |
|
| 1159 |
def _save_providers():
|
| 1160 |
-
"""
|
| 1161 |
-
|
| 1162 |
-
|
| 1163 |
-
|
| 1164 |
-
|
| 1165 |
-
|
|
|
|
|
|
|
| 1166 |
logger.warning("Failed to save providers: %s", e)
|
| 1167 |
|
| 1168 |
|
|
@@ -1436,25 +1427,7 @@ async def get_my_key(request: Request, cf_token: str = None):
|
|
| 1436 |
detail={"error": {"message": "VPN/Proxy detected. Please turn off VPN and try again.", "type": "verification_error", "code": "vpn_blocked", "provider": vpn_result["provider"]}}
|
| 1437 |
)
|
| 1438 |
|
| 1439 |
-
#
|
| 1440 |
-
if TURNSTILE_SECRET_KEY:
|
| 1441 |
-
if not cf_token:
|
| 1442 |
-
raise HTTPException(status_code=403, detail={"error": {"message": "Verification required", "type": "verification_error", "code": "missing_cf_token"}})
|
| 1443 |
-
try:
|
| 1444 |
-
verify_resp = await make_client().post(
|
| 1445 |
-
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
|
| 1446 |
-
data={"response": cf_token, "secret": TURNSTILE_SECRET_KEY},
|
| 1447 |
-
)
|
| 1448 |
-
verify_data = verify_resp.json()
|
| 1449 |
-
if not verify_data.get("success"):
|
| 1450 |
-
raise HTTPException(status_code=403, detail={"error": {"message": "Verification failed", "type": "verification_error", "code": "invalid_cf_token"}})
|
| 1451 |
-
except HTTPException:
|
| 1452 |
-
raise
|
| 1453 |
-
except Exception as e:
|
| 1454 |
-
logger.warning("Turnstile verification error: %s", e)
|
| 1455 |
-
raise HTTPException(status_code=500, detail={"error": {"message": "Verification service error", "type": "server_error", "code": "cf_verification_error"}})
|
| 1456 |
-
else:
|
| 1457 |
-
logger.info("Turnstile disabled (no TURNSTILE_SECRET_KEY), allowing key generation for IP %s", client_ip)
|
| 1458 |
|
| 1459 |
# ββ Step 3: Auto-generate key for verified user ββ
|
| 1460 |
new_key = generate_api_key()
|
|
|
|
| 19 |
from config import (
|
| 20 |
DEFAULT_UPSTREAM_URL, UPSTREAMS, UPSTREAM_API_KEYS,
|
| 21 |
GITHUB_TOKEN, GIST_ID,
|
|
|
|
| 22 |
HF_TOKEN, HF_DATASET,
|
| 23 |
MASTER_KEY,
|
| 24 |
AI_GATEWAY_URL, UPSTREAM_14448_URL,
|
|
|
|
| 45 |
|
| 46 |
RPM_LIMIT = 4
|
| 47 |
KEYS_FILE = os.path.join(os.path.dirname(__file__), "data", "api_keys.json")
|
|
|
|
| 48 |
|
| 49 |
_rate_limits: dict = defaultdict(list)
|
| 50 |
|
|
|
|
| 1140 |
|
| 1141 |
|
| 1142 |
def _load_providers():
|
| 1143 |
+
"""Dynamic providers are ephemeral in HF Space runtime - no persistence."""
|
| 1144 |
global _dynamic_providers
|
| 1145 |
+
_dynamic_providers = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1146 |
|
| 1147 |
|
| 1148 |
def _save_providers():
|
| 1149 |
+
"""No-op: HF Space runtime is ephemeral, local writes are lost on restart."""
|
| 1150 |
+
pass
|
| 1151 |
+
# Original code that wrote to PROVIDERS_FILE:
|
| 1152 |
+
# try:
|
| 1153 |
+
# os.makedirs(os.path.dirname(PROVIDERS_FILE), exist_ok=True)
|
| 1154 |
+
# with open(PROVIDERS_FILE, "w") as f:
|
| 1155 |
+
# json.dump(_dynamic_providers, f, indent=2)
|
| 1156 |
+
# except Exception as e:
|
| 1157 |
logger.warning("Failed to save providers: %s", e)
|
| 1158 |
|
| 1159 |
|
|
|
|
| 1427 |
detail={"error": {"message": "VPN/Proxy detected. Please turn off VPN and try again.", "type": "verification_error", "code": "vpn_blocked", "provider": vpn_result["provider"]}}
|
| 1428 |
)
|
| 1429 |
|
| 1430 |
+
# Turnstile removed - no longer needed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1431 |
|
| 1432 |
# ββ Step 3: Auto-generate key for verified user ββ
|
| 1433 |
new_key = generate_api_key()
|
app/templates/landing.html
CHANGED
|
@@ -135,10 +135,7 @@
|
|
| 135 |
</div>
|
| 136 |
</div>
|
| 137 |
|
| 138 |
-
|
| 139 |
-
<div id="turnstile-container" class="hidden w-full flex justify-center py-4">
|
| 140 |
-
<div class="cf-turnstile" data-sitekey="0x4AAAAAADTUsX1T7VmpXhDi" data-callback="onTurnstileSuccess"></div>
|
| 141 |
-
</div>
|
| 142 |
|
| 143 |
<!-- Key Display Area -->
|
| 144 |
<div class="relative group cursor-pointer" onclick="copyKey()">
|
|
@@ -274,12 +271,11 @@
|
|
| 274 |
Copied to clipboard
|
| 275 |
</div>
|
| 276 |
|
| 277 |
-
|
| 278 |
-
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
| 279 |
|
| 280 |
<script>
|
| 281 |
const myKey = { key: null, ip: null };
|
| 282 |
-
|
| 283 |
const keyBadge = document.getElementById('keyBadge');
|
| 284 |
const ykStatus = document.getElementById('ykStatus');
|
| 285 |
const ykHint = document.getElementById('ykHint');
|
|
@@ -315,7 +311,7 @@
|
|
| 315 |
ykIp.textContent = data.ip;
|
| 316 |
ykKey.textContent = data.key;
|
| 317 |
|
| 318 |
-
|
| 319 |
|
| 320 |
keyBadge.textContent = data.status === 'new' ? 'NEW' : 'AUTHORIZED';
|
| 321 |
keyBadge.className = "px-2 py-1 text-[10px] font-mono uppercase tracking-wider rounded border border-emerald-500/30 bg-emerald-500/10 text-emerald-400";
|
|
@@ -328,7 +324,7 @@
|
|
| 328 |
if (err.code === 'vpn_blocked') {
|
| 329 |
ykIp.textContent = 'RESTRICTED';
|
| 330 |
ykKey.textContent = 'β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’';
|
| 331 |
-
|
| 332 |
|
| 333 |
keyBadge.textContent = 'BLOCKED';
|
| 334 |
keyBadge.className = "px-2 py-1 text-[10px] font-mono uppercase tracking-wider rounded border border-red-500/30 bg-red-500/10 text-red-400";
|
|
@@ -338,7 +334,7 @@
|
|
| 338 |
} else {
|
| 339 |
ykIp.textContent = 'Pending verification...';
|
| 340 |
ykKey.textContent = 'β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’';
|
| 341 |
-
|
| 342 |
|
| 343 |
keyBadge.textContent = 'CHALLENGE';
|
| 344 |
keyBadge.className = "px-2 py-1 text-[10px] font-mono uppercase tracking-wider rounded border border-blue-500/30 bg-blue-500/10 text-blue-400";
|
|
|
|
| 135 |
</div>
|
| 136 |
</div>
|
| 137 |
|
| 138 |
+
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
<!-- Key Display Area -->
|
| 141 |
<div class="relative group cursor-pointer" onclick="copyKey()">
|
|
|
|
| 271 |
Copied to clipboard
|
| 272 |
</div>
|
| 273 |
|
| 274 |
+
|
|
|
|
| 275 |
|
| 276 |
<script>
|
| 277 |
const myKey = { key: null, ip: null };
|
| 278 |
+
// Turnstile removed
|
| 279 |
const keyBadge = document.getElementById('keyBadge');
|
| 280 |
const ykStatus = document.getElementById('ykStatus');
|
| 281 |
const ykHint = document.getElementById('ykHint');
|
|
|
|
| 311 |
ykIp.textContent = data.ip;
|
| 312 |
ykKey.textContent = data.key;
|
| 313 |
|
| 314 |
+
// turnstileContainer.classList.add('hidden');
|
| 315 |
|
| 316 |
keyBadge.textContent = data.status === 'new' ? 'NEW' : 'AUTHORIZED';
|
| 317 |
keyBadge.className = "px-2 py-1 text-[10px] font-mono uppercase tracking-wider rounded border border-emerald-500/30 bg-emerald-500/10 text-emerald-400";
|
|
|
|
| 324 |
if (err.code === 'vpn_blocked') {
|
| 325 |
ykIp.textContent = 'RESTRICTED';
|
| 326 |
ykKey.textContent = 'β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’';
|
| 327 |
+
// turnstileContainer.classList.add('hidden');
|
| 328 |
|
| 329 |
keyBadge.textContent = 'BLOCKED';
|
| 330 |
keyBadge.className = "px-2 py-1 text-[10px] font-mono uppercase tracking-wider rounded border border-red-500/30 bg-red-500/10 text-red-400";
|
|
|
|
| 334 |
} else {
|
| 335 |
ykIp.textContent = 'Pending verification...';
|
| 336 |
ykKey.textContent = 'β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’';
|
| 337 |
+
// turnstileContainer.classList.remove('hidden');
|
| 338 |
|
| 339 |
keyBadge.textContent = 'CHALLENGE';
|
| 340 |
keyBadge.className = "px-2 py-1 text-[10px] font-mono uppercase tracking-wider rounded border border-blue-500/30 bg-blue-500/10 text-blue-400";
|