OXYGENHUB

Coverage / crates/spectra-mcp/src/api.rs 91.7%

410 of 447 lines run, 66 of 146 functions · run #22 on main · run missed

1
//! The JSON API (spec §18): what spectra-vscode, scripts and SpectraHub
2
//! read and write. Every request is a signed request (spec §8.5); a read
3
//! may instead carry the owner's Radar session. Errors are RFC 7807
4
//! problems; lists page with `?after=`; reads carry an ETag.
5
6
use crate::http::{AppState, PeerInfo};
7
use axum::body::Bytes;
8
use axum::extract::connect_info::ConnectInfo;
9
use axum::extract::{Path, Query, State};
10
use axum::http::{header, HeaderMap, StatusCode};
11
use axum::response::sse::{Event as SseEvent, KeepAlive, Sse};
12
use axum::response::{IntoResponse, Response};
13
use axum::routing::{get, post};
14
use axum::Router;
15
use futures::StreamExt;
16
use serde::Deserialize;
17
use spectra_api as api;
18
use spectra_core::authn::Caller;
19
use spectra_core::conversation::{self as conv, Author, Post, Posted, Signer};
20
use spectra_core::event::{Auth, Method};
21
use spectra_core::ids::now_ms;
22
use spectra_core::principals::Principal;
23
use std::time::Duration;
24
2519×
pub fn router(state: AppState) -> Router {
2619×
    Router::new()
2719×
        .route("/api/v1/health", get(health))
2819×
        .route("/api/v1/version", get(version))
2919×
        .route("/api/v1/brief", get(brief))
3019×
        .route("/api/v1/agents", get(agents))
3119×
        .route("/api/v1/projects", get(projects))
3219×
        .route("/api/v1/channels", get(channels))
3319×
        .route("/api/v1/channels/{name}/messages", get(messages).post(post_message))
3419×
        .route("/api/v1/threads/{id}", get(thread))
3519×
        .route("/api/v1/inbox", get(inbox))
3619×
        .route("/api/v1/inbox/ack", post(ack))
3719×
        .route("/api/v1/search", get(search))
3819×
        .route("/api/v1/events", get(events))
3919×
        .route("/api/v1/admin/verify", get(admin_verify))
4019×
        .with_state(state)
4119×
}
42
43
/// An RFC 7807 problem.
444×
pub fn problem(status: StatusCode, title: &str, detail: Option<String>) -> Response {
454×
    let p = api::Problem { kind: "about:blank".into(), title: title.into(), status: status.as_u16(), detail };
464×
    let mut r = (status, axum::Json(p)).into_response();
474×
    r.headers_mut().insert(header::CONTENT_TYPE, "application/problem+json".parse().unwrap());
484×
    r
494×
}
50
511×
fn bad(e: anyhow::Error) -> Response {
521×
    problem(StatusCode::BAD_REQUEST, "the request cannot be done", Some(format!("{e:#}")))
531×
}
54
55
/// JSON with an ETag; 304 when the client has it already.
5635×
fn json_etag<T: serde::Serialize>(headers: &HeaderMap, v: &T) -> Response {
5735×
    let body = serde_json::to_vec(v).unwrap_or_default();
5835×
    let tag = format!("\"{}\"", &blake3_hex(&body)[..32]);
5935×
    if headers.get(header::IF_NONE_MATCH).and_then(|h| h.to_str().ok()) == Some(tag.as_str()) {
601×
        return StatusCode::NOT_MODIFIED.into_response();
6134×
    }
6234×
    ([(header::CONTENT_TYPE, "application/json".to_string()), (header::ETAG, tag)], body).into_response()
6335×
}
64
6535×
fn blake3_hex(b: &[u8]) -> String {
6635×
    hex::encode(*spectra_core::event::hash_body_plain(b))
6735×
}
68
69
/// Who is asking: a signed request, or for a read the owner's session.
7052×
async fn caller(
7152×
    st: &AppState,
7252×
    peer: PeerInfo,
7352×
    headers: &HeaderMap,
7452×
    method_path: &str,
7552×
    body: &[u8],
7652×
    read: bool,
7752×
) -> Result<Caller, Response> {
7852×
    if read && headers.get("spectra-key").is_none() {
791×
        let cookie = headers.get(header::COOKIE).and_then(|c| c.to_str().ok()).unwrap_or("");
801×
        let id = cookie.split(';').map(str::trim).find_map(|kv| kv.strip_prefix("spectra_session=")).unwrap_or("");
811×
        if let Some(s) = st.sessions.get(id, now_ms()) {
820×
            if !crate::http::origin_ok(headers, &st.origins) {
830×
                return Err(problem(StatusCode::FORBIDDEN, "this origin may not read Spectra", None));
840×
            }
850×
            let key = oxy_core::hash::ObjectRef(s.key);
860×
            let principal = st
870×
                .daemon
880×
                .principals
890×
                .read()
900×
                .await
910×
                .principal(&key, now_ms())
920×
                .map_err(|why| problem(StatusCode::UNAUTHORIZED, "not authenticated", Some(why)))?;
930×
            return Ok(Caller {
940×
                principal,
950×
                auth: Auth { key: Some(s.key), method: Method::Session, assurance: 2, proof: Some(s.proof) },
960×
                learn_uid: None,
970×
            });
981×
        }
9951×
    }
10052×
    let signed = headers.get("spectra-key").is_some();
10152×
    crate::http::authenticate_why(st, peer, headers, method_path, body).await.map_err(|(status, why)| {
1022×
        if status == StatusCode::FORBIDDEN {
1030×
            problem(status, "this origin may not call Spectra", None)
1042×
        } else if signed {
105
            // A signed request is told why its signature was not enough.
1061×
            problem(StatusCode::UNAUTHORIZED, "not authenticated", Some(why))
107
        } else {
1081×
            problem(
109
                StatusCode::UNAUTHORIZED,
1101×
                "not authenticated",
1111×
                Some("sign the request with your key (spectra's client does), or open Radar with `spectra open`".into()),
112
            )
113
        }
1142×
    })
11552×
}
116
11752×
fn path_of(uri: &axum::http::Uri) -> String {
11852×
    uri.path_and_query().map(|p| p.as_str().to_string()).unwrap_or_else(|| uri.path().to_string())
11952×
}
120
121
macro_rules! who {
122
    ($st:expr, $peer:expr, $headers:expr, $uri:expr, $method:literal, $body:expr, $read:expr) => {
123
        match caller(&$st, $peer, &$headers, &format!("{} {}", $method, path_of(&$uri)), $body, $read).await {
124
            Ok(c) => c,
125
            Err(r) => return r,
126
        }
127
    };
128
}
129
130
/// `GET /api/v1/admin/verify`: the owner asks the daemon to verify its
131
/// ledger (spec §8.6): what `spectra admin verify` prints, from afar.
1321×
async fn admin_verify(
1331×
    State(st): State<AppState>,
1341×
    ConnectInfo(peer): ConnectInfo<PeerInfo>,
1351×
    headers: HeaderMap,
1361×
    uri: axum::http::Uri,
1371×
) -> Response {
1381×
    let c = who!(st, peer, headers, uri, "GET", b"", false);
1391×
    if !matches!(c.principal, Principal::Owner { .. }) {
1400×
        return problem(StatusCode::FORBIDDEN, "only the owner verifies the ledger", None);
1411×
    }
1421×
    let ledger = match spectra_core::ledger::Ledger::open_read_only(&st.daemon.data.join("spectra.db")).await {
1431×
        Ok(l) => l,
1440×
        Err(e) => return bad(e),
145
    };
1461×
    match ledger.verify(&mut spectra_core::principals::Principals::default()).await {
1471×
        Ok(v) => axum::Json(v).into_response(),
1480×
        Err(e) => bad(e),
149
    }
1501×
}
151
1521×
async fn health() -> Response {
1531×
    axum::Json(serde_json::json!({ "ok": true })).into_response()
1541×
}
155
1561×
async fn version() -> Response {
157
    axum::Json(api::Version {
1581×
        version: env!("CARGO_PKG_VERSION").into(),
159
        ledger_format: spectra_core::LEDGER_FORMAT,
1604×
        mcp_protocol_versions: crate::PROTOCOL_VERSIONS.iter().map(|v| v.to_string()).collect(),
161
    })
1621×
    .into_response()
1631×
}
164
16511×
async fn agent_list(st: &AppState) -> Vec<api::Agent> {
166
    #[allow(clippy::type_complexity)]
16711×
    let rows: Vec<(String, String, String, String, String, Option<String>, Option<i64>)> = sqlx::query_as(
16811×
        "SELECT a.name, a.owner_name, a.key, a.kind, a.project, p.status_text, a.disabled_ms FROM agents a LEFT JOIN presence p ON p.agent = a.name ORDER BY a.name",
16911×
    )
17011×
    .fetch_all(st.daemon.ledger.pool())
17111×
    .await
17211×
    .unwrap_or_default();
17311×
    let presence = st.daemon.presence.lock().await;
17411×
    let principals = st.daemon.principals.read().await;
17511×
    let now = now_ms();
17611×
    rows.into_iter()
17711×
        .map(|(name, owner, key, kind, project, status, disabled)| {
17811×
            let k = oxy_core::hash::ObjectRef::from_hex(&key);
17911×
            let state = match k.map(|k| principals.principal(&k, now)) {
18010×
                Some(Ok(_)) => "ok".to_string(),
1811×
                Some(Err(why)) => why,
1820×
                None => "a malformed key".into(),
183
            };
18411×
            let delegation_expires = k.and_then(|k| principals.owner().and_then(|o| o.delegate(&k)).and_then(|d| d.expires));
185
            api::Agent {
18611×
                display: format!("{name} · {owner}'s agent"),
18711×
                presence: if disabled.is_some() { "offline".into() } else { presence.state(&name, now).into() },
18811×
                disabled: disabled.is_some(),
18911×
                name,
19011×
                owner,
19111×
                key,
19211×
                kind,
19311×
                project,
19411×
                status,
19511×
                state,
19611×
                delegation_expires,
197
            }
19811×
        })
19911×
        .collect()
20011×
}
201
20210×
async fn agents(
20310×
    State(st): State<AppState>,
20410×
    ConnectInfo(peer): ConnectInfo<PeerInfo>,
20510×
    headers: HeaderMap,
20610×
    uri: axum::http::Uri,
20710×
) -> Response {
20810×
    let _ = who!(st, peer, headers, uri, "GET", b"", true);
2098×
    json_etag(&headers, &agent_list(&st).await)
21010×
}
211
21211×
async fn projects(
21311×
    State(st): State<AppState>,
21411×
    ConnectInfo(peer): ConnectInfo<PeerInfo>,
21511×
    headers: HeaderMap,
21611×
    uri: axum::http::Uri,
21711×
) -> Response {
21811×
    let _ = who!(st, peer, headers, uri, "GET", b"", true);
21911×
    let pool = st.daemon.ledger.pool();
22011×
    let rows: Vec<(String, String, Option<String>)> =
22111×
        sqlx::query_as("SELECT name, path, remote FROM projects ORDER BY name").fetch_all(pool).await.unwrap_or_default();
22211×
    let mut out = Vec::new();
22311×
    for (name, path, remote) in rows {
22411×
        let depends_on = sqlx::query_scalar("SELECT depends_on FROM project_relations WHERE project = ? ORDER BY 1")
22511×
            .bind(&name)
22611×
            .fetch_all(pool)
22711×
            .await
22811×
            .unwrap_or_default();
22911×
        let depended_on_by = sqlx::query_scalar("SELECT project FROM project_relations WHERE depends_on = ? ORDER BY 1")
23011×
            .bind(&name)
23111×
            .fetch_all(pool)
23211×
            .await
23311×
            .unwrap_or_default();
23411×
        let agents = sqlx::query_scalar("SELECT name FROM agents WHERE project = ? AND disabled_ms IS NULL ORDER BY name")
23511×
            .bind(&name)
23611×
            .fetch_all(pool)
23711×
            .await
23811×
            .unwrap_or_default();
23911×
        out.push(api::Project { name, path, remote, depends_on, depended_on_by, agents });
240
    }
24111×
    json_etag(&headers, &out)
24211×
}
243
2441×
async fn channels(
2451×
    State(st): State<AppState>,
2461×
    ConnectInfo(peer): ConnectInfo<PeerInfo>,
2471×
    headers: HeaderMap,
2481×
    uri: axum::http::Uri,
2491×
) -> Response {
2501×
    let c = who!(st, peer, headers, uri, "GET", b"", true);
2511×
    let all = match conv::channels(&st.daemon.ledger).await {
2521×
        Ok(a) => a,
2530×
        Err(e) => return bad(e),
254
    };
2551×
    let mut out = Vec::new();
2564×
    for ch in all.into_iter().filter(|ch| conv::can_read(&c.principal, ch)) {
2574×
        let n: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM messages WHERE channel = ?")
2584×
            .bind(&ch.id)
2594×
            .fetch_one(st.daemon.ledger.pool())
2604×
            .await
2614×
            .unwrap_or(0);
2624×
        out.push(api::Channel { display: ch.display(), id: ch.id, name: ch.name, kind: ch.kind, purpose: ch.purpose, messages: n });
263
    }
2641×
    json_etag(&headers, &out)
2651×
}
266
2674×
async fn to_api(st: &AppState, m: conv::Message, ch: &str) -> api::Message {
268
    api::Message {
2694×
        author_display: conv::display_name(&st.daemon.ledger, &m.author).await,
2704×
        mentions: serde_json::from_str(&m.mentions).unwrap_or_default(),
2714×
        refs: serde_json::from_str(&m.refs).unwrap_or_default(),
2724×
        redacted: m.redacted.is_some(),
2734×
        id: m.id,
2744×
        channel: ch.to_string(),
2754×
        reply_to: m.reply_to,
2764×
        author: m.author,
2774×
        kind: m.kind,
2784×
        body: m.body,
2794×
        at_ms: m.at_ms,
2804×
        replies: m.replies,
281
    }
2824×
}
283
284
#[derive(Deserialize)]
285
struct PageQuery {
286
    after: Option<String>,
287
    limit: Option<u32>,
288
}
289
2901×
async fn messages(
2911×
    State(st): State<AppState>,
2921×
    ConnectInfo(peer): ConnectInfo<PeerInfo>,
2931×
    headers: HeaderMap,
2941×
    uri: axum::http::Uri,
2951×
    Path(name): Path<String>,
2961×
    Query(q): Query<PageQuery>,
2971×
) -> Response {
2981×
    let c = who!(st, peer, headers, uri, "GET", b"", true);
2991×
    let limit = q.limit.unwrap_or(50).clamp(1, 500);
3001×
    match conv::read(&st.daemon.ledger, &c.principal, Some(&name), None, q.after.as_deref(), limit).await {
3011×
        Ok((ch, msgs)) => {
3021×
            let next = (msgs.len() as u32 == limit).then(|| msgs.last().map(|m| m.id.clone())).flatten();
3031×
            let mut items = Vec::new();
3042×
            for m in msgs {
3052×
                items.push(to_api(&st, m, &ch.display()).await);
306
            }
3071×
            json_etag(&headers, &api::Messages { channel: ch.display(), items, next })
308
        }
3090×
        Err(e) => problem(StatusCode::NOT_FOUND, "no such channel, or not yours to read", Some(format!("{e:#}"))),
310
    }
3111×
}
312
3131×
async fn thread(
3141×
    State(st): State<AppState>,
3151×
    ConnectInfo(peer): ConnectInfo<PeerInfo>,
3161×
    headers: HeaderMap,
3171×
    uri: axum::http::Uri,
3181×
    Path(id): Path<String>,
3191×
    Query(q): Query<PageQuery>,
3201×
) -> Response {
3211×
    let c = who!(st, peer, headers, uri, "GET", b"", true);
3221×
    let limit = q.limit.unwrap_or(200).clamp(1, 500);
3231×
    match conv::read(&st.daemon.ledger, &c.principal, None, Some(&id), q.after.as_deref(), limit).await {
3241×
        Ok((ch, msgs)) => {
3251×
            let next = (msgs.len() as u32 == limit).then(|| msgs.last().map(|m| m.id.clone())).flatten();
3261×
            let mut items = Vec::new();
3272×
            for m in msgs {
3282×
                items.push(to_api(&st, m, &ch.display()).await);
329
            }
3301×
            json_etag(&headers, &api::Messages { channel: ch.display(), items, next })
331
        }
3320×
        Err(e) => problem(StatusCode::NOT_FOUND, "no such thread, or not yours to read", Some(format!("{e:#}"))),
333
    }
3341×
}
335
33610×
async fn post_message(
33710×
    State(st): State<AppState>,
33810×
    ConnectInfo(peer): ConnectInfo<PeerInfo>,
33910×
    headers: HeaderMap,
34010×
    uri: axum::http::Uri,
34110×
    Path(name): Path<String>,
34210×
    body: Bytes,
34310×
) -> Response {
34410×
    let c = who!(st, peer, headers, uri, "POST", &body, false);
34510×
    let n: api::NewMessage = match serde_json::from_slice(&body) {
34610×
        Ok(n) => n,
3470×
        Err(e) => return problem(StatusCode::BAD_REQUEST, "not a message", Some(e.to_string())),
348
    };
34910×
    let author = Author { principal: c.principal.clone(), signer: Signer::Request(c.auth.clone()) };
35010×
    let post = Post {
35110×
        channel: if n.reply_to.is_some() { None } else { Some(name) },
35210×
        body: n.body,
35310×
        reply_to: n.reply_to,
35410×
        kind: n.kind,
35510×
        ..Default::default()
356
    };
35710×
    match conv::post(&st.daemon.ledger, &author, post, now_ms()).await {
3589×
        Ok(Posted::Message { id, channel }) => {
3599×
            (StatusCode::CREATED, axum::Json(api::Posted { id, channel, duplicate: false })).into_response()
360
        }
3610×
        Ok(Posted::Duplicate { id }) => axum::Json(api::Posted { id, channel: String::new(), duplicate: true }).into_response(),
3621×
        Err(e) => bad(e),
363
    }
36410×
}
365
3668×
async fn inbox_items(st: &AppState, actor: &str) -> Vec<api::InboxItem> {
3678×
    let mut out = Vec::new();
3688×
    for i in conv::inbox(&st.daemon.ledger, actor, None).await.unwrap_or_default() {
3696×
        let from_display = conv::display_name(&st.daemon.ledger, &i.from).await;
3706×
        out.push(api::InboxItem {
3716×
            id: i.id,
3726×
            kind: i.kind,
3736×
            at_ms: i.at_ms,
3746×
            from: i.from,
3756×
            from_display,
3766×
            channel: i.channel,
3776×
            summary: i.summary,
3786×
        });
379
    }
3808×
    out
3818×
}
382
3835×
async fn inbox(State(st): State<AppState>, ConnectInfo(peer): ConnectInfo<PeerInfo>, headers: HeaderMap, uri: axum::http::Uri) -> Response {
3845×
    let c = who!(st, peer, headers, uri, "GET", b"", true);
3855×
    json_etag(&headers, &inbox_items(&st, &c.principal.actor()).await)
3865×
}
387
3881×
async fn ack(
3891×
    State(st): State<AppState>,
3901×
    ConnectInfo(peer): ConnectInfo<PeerInfo>,
3911×
    headers: HeaderMap,
3921×
    uri: axum::http::Uri,
3931×
    body: Bytes,
3941×
) -> Response {
3951×
    let c = who!(st, peer, headers, uri, "POST", &body, false);
3961×
    let a: api::Ack = match serde_json::from_slice(&body) {
3971×
        Ok(a) => a,
3980×
        Err(e) => return problem(StatusCode::BAD_REQUEST, "not an ack", Some(e.to_string())),
399
    };
4001×
    let author = Author { principal: c.principal, signer: Signer::Request(c.auth) };
4011×
    match conv::ack(&st.daemon.ledger, &author, &a.ids, a.reaction.as_deref()).await {
4021×
        Ok(n) => axum::Json(serde_json::json!({ "read": n })).into_response(),
4030×
        Err(e) => bad(e),
404
    }
4051×
}
406
4073×
async fn brief(State(st): State<AppState>, ConnectInfo(peer): ConnectInfo<PeerInfo>, headers: HeaderMap, uri: axum::http::Uri) -> Response {
4083×
    let c = who!(st, peer, headers, uri, "GET", b"", true);
4093×
    let project = match &c.principal {
4101×
        Principal::Agent { project, .. } => Some(project.clone()),
4112×
        _ => None,
412
    };
4133×
    let rules = spectra_core::rules::compile(&st.daemon.data).map(|r| r.version).unwrap_or_default();
4143×
    let inbox = inbox_items(&st, &c.principal.actor()).await;
4153×
    let b = api::Brief {
4163×
        you: c.principal.display(),
4173×
        actor: c.principal.actor(),
4183×
        project,
4193×
        rules_version: rules,
4203×
        unread: inbox.len() as i64,
4213×
        inbox,
4223×
        agents: agent_list(&st).await,
4233×
        head: st.daemon.ledger.head().await.0,
424
    };
4253×
    json_etag(&headers, &b)
4263×
}
427
428
#[derive(Deserialize)]
429
struct SearchQuery {
430
    q: String,
431
    channel: Option<String>,
432
    author: Option<String>,
433
    since: Option<String>,
434
    limit: Option<u32>,
435
}
436
4373×
async fn search(
4383×
    State(st): State<AppState>,
4393×
    ConnectInfo(peer): ConnectInfo<PeerInfo>,
4403×
    headers: HeaderMap,
4413×
    uri: axum::http::Uri,
4423×
    Query(q): Query<SearchQuery>,
4433×
) -> Response {
4443×
    let c = who!(st, peer, headers, uri, "GET", b"", true);
4453×
    let since = q.since.as_deref().and_then(|s| crate::tools::parse_since(s, now_ms()));
4463×
    match conv::search(&st.daemon.ledger, &c.principal, &q.q, q.channel.as_deref(), q.author.as_deref(), since, q.limit.unwrap_or(20)).await
447
    {
4483×
        Ok(hits) => {
4493×
            let mut out: Vec<api::Hit> = Vec::new();
4505×
            for h in hits {
4515×
                let author_display = conv::display_name(&st.daemon.ledger, &h.author).await;
4525×
                out.push(api::Hit {
4535×
                    id: h.id,
4545×
                    channel: h.channel,
4555×
                    author: h.author,
4565×
                    at_ms: h.at_ms,
4575×
                    snippet: h.snippet,
4585×
                    thread: h.thread,
4595×
                    author_display,
4605×
                });
461
            }
4623×
            json_etag(&headers, &out)
463
        }
4640×
        Err(e) => bad(e),
465
    }
4663×
}
467
468
#[derive(Deserialize)]
469
struct EventsQuery {
470
    after: Option<u64>,
471
    kind: Option<String>,
472
    limit: Option<u32>,
473
    follow: Option<u8>,
474
    /// With `follow`, end the stream after this many events.
475
    max: Option<usize>,
476
}
477
47813×
fn summary(e: &spectra_core::event::Event) -> api::EventSummary {
47913×
    api::EventSummary {
48013×
        seq: e.seq,
48113×
        id: e.id.clone(),
48213×
        at_ms: e.at_ms,
48313×
        kind: e.kind.clone(),
48413×
        actor: e.actor.clone(),
48513×
        subject: e.subject.clone(),
48613×
        method: e.auth.method.name().into(),
48713×
        assurance: e.auth.assurance,
48813×
        hash: hex::encode(e.hash),
48913×
    }
49013×
}
491
492
/// The log as summaries; `follow=1` streams new ones as SSE instead.
4935×
async fn events(
4945×
    State(st): State<AppState>,
4955×
    ConnectInfo(peer): ConnectInfo<PeerInfo>,
4965×
    headers: HeaderMap,
4975×
    uri: axum::http::Uri,
4985×
    Query(q): Query<EventsQuery>,
4995×
) -> Response {
5005×
    let c = who!(st, peer, headers, uri, "GET", b"", true);
501
    // The log is the owner's to read; an agent reads what concerns it
502
    // through the conversation and its inbox.
5035×
    if !matches!(c.principal, Principal::Owner { .. }) {
5041×
        return problem(StatusCode::FORBIDDEN, "the event log is the owner's", None);
5054×
    }
5064×
    if q.follow == Some(1) {
5072×
        let kind = q.kind.clone();
5082×
        let rx = st.daemon.ledger.subscribe();
5092×
        let stream = tokio_stream::wrappers::BroadcastStream::new(rx)
5104×
            .filter_map(move |e| {
5114×
                let kind = kind.clone();
5124×
                async move {
5134×
                    let e = e.ok()?;
5144×
                    if kind.as_ref().is_some_and(|k| *k != e.kind) {
5152×
                        return None;
5162×
                    }
5172×
                    Some(Ok::<_, std::convert::Infallible>(
5182×
                        SseEvent::default().data(serde_json::to_string(&summary(&e)).unwrap_or_default()),
5192×
                    ))
5204×
                }
5214×
            })
5222×
            .take(q.max.unwrap_or(usize::MAX));
5232×
        return Sse::new(stream).keep_alive(KeepAlive::new().interval(Duration::from_secs(15))).into_response();
5242×
    }
5252×
    let limit = q.limit.unwrap_or(100).clamp(1, 1000);
5262×
    let from = q.after.map(|a| a + 1).unwrap_or(1);
5272×
    let evs = match &q.kind {
5281×
        Some(k) => st.daemon.ledger.events_of_kinds(from, &[k.as_str()], limit).await,
5291×
        None => st.daemon.ledger.events(from, limit).await,
530
    };
5312×
    match evs {
5322×
        Ok(evs) => {
5332×
            let next = (evs.len() as u32 == limit).then(|| evs.last().map(|e| e.seq)).flatten();
5342×
            json_etag(&headers, &api::Events { items: evs.iter().map(summary).collect(), next })
535
        }
5360×
        Err(e) => bad(e),
537
    }
5385×
}