Add AP inbox endpoint + device auth signaling
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m2s

- POST /inbox with HTTP Signature verification
- Device ID cookie tracking + adoption from account
- Silent auth checks local Redis for did_auth signals
- Replaces shared-Redis coupling with AP activity delivery

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-24 00:41:33 +00:00
parent ab3b6b672d
commit b294fd0695
6 changed files with 276 additions and 4 deletions

View File

@@ -4,6 +4,7 @@ Art-DAG L1 Server Application Factory.
Creates and configures the FastAPI application with all routers and middleware.
"""
import secrets
import time
from pathlib import Path
from urllib.parse import quote
@@ -18,8 +19,10 @@ from artdag_common.middleware.auth import get_user_from_cookie
from .config import settings
# Paths that should never trigger a silent auth check
_SKIP_PREFIXES = ("/auth/", "/static/", "/api/", "/ipfs/", "/download/")
_SKIP_PREFIXES = ("/auth/", "/static/", "/api/", "/ipfs/", "/download/", "/inbox")
_SILENT_CHECK_COOLDOWN = 300 # 5 minutes
_DEVICE_COOKIE = "artdag_did"
_DEVICE_COOKIE_MAX_AGE = 30 * 24 * 3600 # 30 days
def create_app() -> FastAPI:
@@ -46,6 +49,30 @@ def create_app() -> FastAPI:
async def shutdown():
await close_db()
# Device ID middleware — track browser identity across domains
@app.middleware("http")
async def device_id_middleware(request: Request, call_next):
did = request.cookies.get(_DEVICE_COOKIE)
if did:
request.state.device_id = did
request.state._new_device_id = False
else:
request.state.device_id = secrets.token_urlsafe(32)
request.state._new_device_id = True
response = await call_next(request)
if getattr(request.state, "_new_device_id", False):
response.set_cookie(
key=_DEVICE_COOKIE,
value=request.state.device_id,
max_age=_DEVICE_COOKIE_MAX_AGE,
httponly=True,
samesite="lax",
secure=True,
)
return response
# Silent auth check — auto-login via prompt=none OAuth
@app.middleware("http")
async def silent_auth_check(request: Request, call_next):
@@ -65,7 +92,24 @@ def create_app() -> FastAPI:
pnone_at = request.cookies.get("pnone_at")
if pnone_at:
try:
if (time.time() - float(pnone_at)) < _SILENT_CHECK_COOLDOWN:
pnone_ts = float(pnone_at)
if (time.time() - pnone_ts) < _SILENT_CHECK_COOLDOWN:
# But first check if account signalled a login via inbox delivery
device_id = getattr(request.state, "device_id", None)
if device_id:
try:
from .dependencies import get_redis_client
r = get_redis_client()
auth_ts = r.get(f"did_auth:{device_id}")
if auth_ts and float(auth_ts) > pnone_ts:
# Login happened since our last check — retry
current_url = str(request.url)
return RedirectResponse(
url=f"/auth/login?prompt=none&next={quote(current_url, safe='')}",
status_code=302,
)
except Exception:
pass
return await call_next(request)
except (ValueError, TypeError):
pass
@@ -94,11 +138,12 @@ def create_app() -> FastAPI:
return JSONResponse({"detail": "Not found"}, status_code=404)
# Include routers
from .routers import auth, storage, api, recipes, cache, runs, home, effects
from .routers import auth, storage, api, recipes, cache, runs, home, effects, inbox
# Home and auth routers (root level)
app.include_router(home.router, tags=["home"])
app.include_router(auth.router, prefix="/auth", tags=["auth"])
app.include_router(inbox.router, tags=["inbox"])
# Feature routers
app.include_router(storage.router, prefix="/storage", tags=["storage"])