stage-exp-b explore: public routes + CORS on existing public endpoints
This commit is contained in:
@@ -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 ---------------------------------
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user