stage-exp-b explore: public routes + CORS on existing public endpoints

This commit is contained in:
m17hr1l
2026-06-07 01:12:25 +02:00
parent 56466c334d
commit a10203d8f1

View File

@@ -34,6 +34,20 @@ _PUBLIC_PEERS_TTL = 60.0
_PUBLIC_NETWORK_CACHE: Dict[str, Any] = {"ts": 0.0, "payload": None} _PUBLIC_NETWORK_CACHE: Dict[str, Any] = {"ts": 0.0, "payload": None}
_PUBLIC_NETWORK_TTL = 60.0 _PUBLIC_NETWORK_TTL = 60.0
# Explore-view cache. The builder fans out to trusted peers' explore feeds
# for the distance-2 snapshot, so a polled hit must NEVER trigger that walk.
_EXPLORE_CACHE: Dict[str, Any] = {"ts": 0.0, "payload": None, "domain": None}
_EXPLORE_TTL = 60.0
# Headers we slap on every public endpoint so other psyc nodes' explore
# pages can fetch them cross-origin from the browser.
_CORS_HEADERS: Dict[str, str] = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
}
def _admin_ok(request: Request) -> bool: def _admin_ok(request: Request) -> bool:
return bool(request.session.get("admin_ok")) return bool(request.session.get("admin_ok"))
@@ -63,6 +77,30 @@ def _cached_public_network() -> Dict[str, Any]:
return _PUBLIC_NETWORK_CACHE["payload"] return _PUBLIC_NETWORK_CACHE["payload"]
def _cached_explore(domain: Optional[str]) -> Dict[str, Any]:
"""Cached explore payload. Re-uses the cache when the host domain matches.
Domain is recorded into the payload's `node.domain` field, so a fresh
cache slot per host avoids serving the wrong reflected name.
"""
now = time.time()
cached_domain = _EXPLORE_CACHE.get("domain")
if (
_EXPLORE_CACHE["payload"] is None
or (now - _EXPLORE_CACHE["ts"]) > _EXPLORE_TTL
or cached_domain != domain
):
_EXPLORE_CACHE["payload"] = network_view.build_explore_view(node_domain=domain)
_EXPLORE_CACHE["ts"] = now
_EXPLORE_CACHE["domain"] = domain
return _EXPLORE_CACHE["payload"]
def _public_json(payload: Any) -> JSONResponse:
"""JSONResponse with the public-CORS header set."""
return JSONResponse(payload, headers=_CORS_HEADERS)
def register(app: FastAPI, TEMPLATES: Jinja2Templates) -> None: def register(app: FastAPI, TEMPLATES: Jinja2Templates) -> None:
"""Mount all federation routes onto `app`.""" """Mount all federation routes onto `app`."""
@@ -180,20 +218,25 @@ def register(app: FastAPI, TEMPLATES: Jinja2Templates) -> None:
@app.get("/federation/info") @app.get("/federation/info")
def federation_info() -> JSONResponse: def federation_info() -> JSONResponse:
return JSONResponse({ return _public_json({
"fingerprint": federation.node_fingerprint(), "fingerprint": federation.node_fingerprint(),
"version": federation.FEED_VERSION, "version": federation.FEED_VERSION,
"feed": federation.FEED_PATH, "feed": federation.FEED_PATH,
"key": "/federation/key", "key": "/federation/key",
"explore": "/federation/explore",
}) })
@app.get("/federation/key", response_class=PlainTextResponse) @app.get("/federation/key", response_class=PlainTextResponse)
def federation_key() -> PlainTextResponse: def federation_key() -> PlainTextResponse:
return PlainTextResponse(federation.public_key_pem(), media_type="text/plain") return PlainTextResponse(
federation.public_key_pem(),
media_type="text/plain",
headers=_CORS_HEADERS,
)
@app.get("/federation/feed") @app.get("/federation/feed")
def federation_feed() -> JSONResponse: def federation_feed() -> JSONResponse:
return JSONResponse(_cached_feed()) return _public_json(_cached_feed())
@app.get("/federation/peers/public") @app.get("/federation/peers/public")
def federation_peers_public() -> JSONResponse: def federation_peers_public() -> JSONResponse:
@@ -202,7 +245,7 @@ def register(app: FastAPI, TEMPLATES: Jinja2Templates) -> None:
Only trusted peers leak; unknown + blocked are internal state and must Only trusted peers leak; unknown + blocked are internal state and must
never appear here. never appear here.
""" """
return JSONResponse(_cached_public_peers()) return _public_json(_cached_public_peers())
@app.get("/federation/network") @app.get("/federation/network")
def federation_network_public() -> JSONResponse: def federation_network_public() -> JSONResponse:
@@ -212,14 +255,14 @@ def register(app: FastAPI, TEMPLATES: Jinja2Templates) -> None:
so a fetcher can reconstruct the local web-of-trust shape. Trusted peers so a fetcher can reconstruct the local web-of-trust shape. Trusted peers
only — never unknown or blocked. Signal hashes are deliberately omitted. only — never unknown or blocked. Signal hashes are deliberately omitted.
""" """
return JSONResponse(_cached_public_network()) return _public_json(_cached_public_network())
# ---------- public vouches + transparency log -------------------- # ---------- public vouches + transparency log --------------------
@app.get("/federation/vouches") @app.get("/federation/vouches")
def federation_vouches() -> JSONResponse: def federation_vouches() -> JSONResponse:
"""Vouches WE have issued. Peers fetch this to learn who we trust.""" """Vouches WE have issued. Peers fetch this to learn who we trust."""
return JSONResponse({ return _public_json({
"fingerprint": federation.node_fingerprint(), "fingerprint": federation.node_fingerprint(),
"vouches": [v.model_dump(mode="json") for v in federation.our_vouches()], "vouches": [v.model_dump(mode="json") for v in federation.our_vouches()],
}) })
@@ -228,7 +271,7 @@ def register(app: FastAPI, TEMPLATES: Jinja2Templates) -> None:
def federation_log() -> JSONResponse: def federation_log() -> JSONResponse:
"""Last 100 transparency-log entries, newest first.""" """Last 100 transparency-log entries, newest first."""
entries = translog.recent(limit=100) entries = translog.recent(limit=100)
return JSONResponse({ return _public_json({
"count": len(entries), "count": len(entries),
"entries": [e.model_dump(mode="json") for e in entries], "entries": [e.model_dump(mode="json") for e in entries],
}) })
@@ -240,8 +283,41 @@ def register(app: FastAPI, TEMPLATES: Jinja2Templates) -> None:
head = translog.head() head = translog.head()
head_hash = head.entry_hash if head else None head_hash = head.entry_hash if head else None
if isinstance(result, Err): if isinstance(result, Err):
return JSONResponse({"error": result.reason, "head_hash": head_hash}, status_code=409) return JSONResponse(
return JSONResponse({"verified": result.value, "head_hash": head_hash}) {"error": result.reason, "head_hash": head_hash},
status_code=409,
headers=_CORS_HEADERS,
)
return _public_json({"verified": result.value, "head_hash": head_hash})
# ---------- public explore page + data ---------------------------
@app.get("/federation/explore", response_class=HTMLResponse)
def federation_explore_page(request: Request) -> HTMLResponse:
"""Public transparency view — anyone can verify this network."""
host = request.url.hostname or ""
peer_param = request.query_params.get("peer", "").strip()
return TEMPLATES.TemplateResponse(
request,
"federation_explore.html",
{
"fingerprint": federation.node_fingerprint(),
"domain": host,
"peer": peer_param,
},
)
@app.get("/federation/explore/data")
def federation_explore_data(request: Request) -> JSONResponse:
"""Signed public explorer payload — peer counts, vouches, transitives.
Public, no auth, CORS-enabled. Cached so a polled hit never triggers
the distance-2 fan-out. See `network_view.build_explore_view` for the
no-leak contract.
"""
host = request.url.hostname or None
payload = _cached_explore(host)
return _public_json(payload)
# ---------- admin: vouches page --------------------------------- # ---------- admin: vouches page ---------------------------------