"""Public read-only view of a shared chat.
A share link is a capability token: the holder reads the session's display
history and nothing else. There is no auth on these endpoints by design — the
recipient is typically someone without a Navi account — so they must stay
strictly read-only and must never expose more than the owner's own chat feed:
- only display history (``get_history_page``) — never the LLM context,
planning logs, todos, terminals or the session's file listing;
- no session id or owner id in the payload (content URLs inside tool results
are the pre-existing artifact links and carry the session id themselves);
- a deleted session or an unknown token answers 404 alike, so a link cannot
be used to probe for sessions.
"""
from typing import Annotated
import structlog
from fastapi import APIRouter, Depends, HTTPException, Query
from navi.api.deps import get_profile_registry, get_session_store
from navi.core import ProfileRegistry, SessionStore
from navi.exceptions import ProfileNotFound
from navi.llm.base import Message
log = structlog.get_logger()
router = APIRouter(prefix="/shared", tags=["shared"])
async def _load(token: str, store: SessionStore):
share = await store.get_share(token)
if share is None:
raise HTTPException(status_code=404, detail="Share not found")
session = await store.get_meta(share["session_id"])
if session is None:
raise HTTPException(status_code=404, detail="Share not found")
return share, session
def _profile_name(profiles: ProfileRegistry, profile_id: str | None) -> str | None:
if not profile_id:
return None
try:
return profiles.get(profile_id).name
except ProfileNotFound:
return None
def _items(messages: list[Message]) -> list[dict]:
return [m.model_dump(mode="json", exclude_none=True) for m in messages]
@router.get("/{token}")
async def get_shared_chat(
token: str,
store: Annotated[SessionStore, Depends(get_session_store)],
profiles: Annotated[ProfileRegistry, Depends(get_profile_registry)],
limit: int = Query(200, ge=1, le=200),
) -> dict:
"""The shared chat's head plus its newest page of messages."""
share, session = await _load(token, store)
msgs, has_more = await store.get_history_page(
share["session_id"], before_seq=None, limit=limit
)
return {
"name": session.name,
"profile_id": session.profile_id,
"profile_name": _profile_name(profiles, session.profile_id),
"created_at": session.created_at.isoformat(),
"last_active": session.last_active.isoformat(),
"messages": _items(msgs),
"has_more": has_more,
"next_before_seq": msgs[0].sequence_number if msgs else None,
}
@router.get("/{token}/messages")
async def get_shared_history(
token: str,
store: Annotated[SessionStore, Depends(get_session_store)],
before_seq: int | None = None,
limit: int = Query(150, ge=1, le=200),
) -> dict:
"""Older page of the shared history, for scroll-up loading."""
share, _ = await _load(token, store)
msgs, has_more = await store.get_history_page(
share["session_id"], before_seq=before_seq, limit=limit
)
return {
"messages": _items(msgs),
"has_more": has_more,
"next_before_seq": msgs[0].sequence_number if msgs else None,
}