- tools/scripts/llm-key-hunter: GitHub leak hunting pipeline (hunt_*, pivot miner, two-layer verify/content caches, per-provider verification) - usable_keys: verified key vault across 12 providers (deepseek, minimax, volcanoark, longcat, codingplan, zhipu free-tier, mimo, siliconflow, etc.) - .grok/skills/llm-key-hunter: operator skill for the hunt/verify/vault flow - NewAPI channel import scripts and CDP capture helpers - Result verdict buckets (excluding multi-GB blob caches and dedup dumps)
114 lines
3.5 KiB
Python
114 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Persistent verification cache for key hunters.
|
|
|
|
Avoids re-querying keys whose verdict is already known. TTL by verdict:
|
|
DEAD -> never re-check (401 does not come back)
|
|
NO_BALANCE / NO_ACCESS -> 6h (account may be recharged / plan opened)
|
|
USABLE -> 30m (balance drains; refresh periodically)
|
|
UNKNOWN/network error -> never cached (retry next run)
|
|
|
|
Cache is one JSON file per provider under results/_cache/<provider>.json,
|
|
shape: { key: {"v": verdict, "d": detail, "t": epoch_seconds} }
|
|
|
|
Usage in a hunter:
|
|
|
|
from verify_cache import CachedVerifier
|
|
ver = CachedVerifier("deepseek", verify_key) # verify_key(key)->(verdict,detail)
|
|
verdict, detail = ver(key) # cached or live
|
|
ver.save()
|
|
|
|
Or wrap the whole batch loop:
|
|
with CachedVerifier("deepseek", verify_key) as ver:
|
|
results = list(pool.map(ver, keys))
|
|
"""
|
|
import json
|
|
import os
|
|
import threading
|
|
import time
|
|
from pathlib import Path
|
|
|
|
HERE = Path(__file__).parent
|
|
CACHE_DIR = HERE / "results" / "_cache"
|
|
|
|
# TTL in seconds per verdict. None = never expire.
|
|
TTL = {
|
|
"DEAD": None,
|
|
"NO_BALANCE": 6 * 3600,
|
|
"NO_ACCESS": 6 * 3600,
|
|
"USABLE": 30 * 60,
|
|
}
|
|
# verdicts that are never written to cache (re-run every time)
|
|
NEVER_CACHE = {"UNKNOWN"}
|
|
|
|
|
|
class CachedVerifier:
|
|
def __init__(self, provider, verify_fn, cache_dir=None, force=False, ttl=None):
|
|
self.provider = provider
|
|
self.verify_fn = verify_fn
|
|
self.force = force or os.environ.get("NO_CACHE") == "1"
|
|
self.ttl = {**TTL, **(ttl or {})}
|
|
self.path = Path(cache_dir) if cache_dir else (CACHE_DIR / f"{provider}.json")
|
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
self._lock = threading.Lock()
|
|
self._cache = {}
|
|
self.hits = 0
|
|
self.live = 0
|
|
self._load()
|
|
|
|
def _load(self):
|
|
if self.path.exists():
|
|
try:
|
|
self._cache = json.loads(self.path.read_text())
|
|
except Exception:
|
|
self._cache = {}
|
|
|
|
def save(self):
|
|
tmp = self.path.with_suffix(".tmp")
|
|
with self._lock:
|
|
tmp.write_text(json.dumps(self._cache, ensure_ascii=False))
|
|
os.replace(tmp, self.path)
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *exc):
|
|
self.save()
|
|
|
|
def _fresh(self, entry):
|
|
ttl = self.ttl.get(entry.get("v"))
|
|
if ttl is None:
|
|
return True # never expires (DEAD)
|
|
return (time.time() - entry.get("t", 0)) < ttl
|
|
|
|
def __call__(self, key):
|
|
if not self.force:
|
|
with self._lock:
|
|
ent = self._cache.get(key)
|
|
if ent and self._fresh(ent):
|
|
self.hits += 1
|
|
return ent["v"], ent["d"]
|
|
verdict, detail = self.verify_fn(key)
|
|
self.live += 1
|
|
if verdict not in NEVER_CACHE:
|
|
with self._lock:
|
|
self._cache[key] = {"v": verdict, "d": detail, "t": time.time()}
|
|
return verdict, detail
|
|
|
|
def stats(self):
|
|
return {"hits": self.hits, "live": self.live, "cached": len(self._cache)}
|
|
|
|
|
|
def prune_dead(provider=None):
|
|
"""Utility: print cache size / drop nothing."""
|
|
for p in sorted(CACHE_DIR.glob("*.json")):
|
|
if provider and p.stem != provider:
|
|
continue
|
|
try:
|
|
d = json.loads(p.read_text())
|
|
except Exception:
|
|
continue
|
|
counts = {}
|
|
for v in d.values():
|
|
counts[v.get("v", "?")] = counts.get(v.get("v", "?"), 0) + 1
|
|
print(f"{p.stem:16s} {len(d):6d} entries {counts}")
|