use sqlx::PgPool;
use uuid::Uuid;
use crate::{
auth::{
AuthCtx, MAX_SESSION_TTL_SECS, SESSION_TTL_SECS, generate_session_token, hash_token,
normalize_session, token_prefix,
},
error::{BusError, BusResult},
model::{SessionCredential, SessionIdentity},
};
pub struct Issued {
pub id: Uuid,
pub token: String,
pub label: String,
pub epoch: i64,
pub expires_at: chrono::DateTime<chrono::Utc>,
}
fn ttl_of(requested: Option<i64>) -> i64 {
requested
.unwrap_or(SESSION_TTL_SECS)
.clamp(60, MAX_SESSION_TTL_SECS)
}
pub async fn register(
pool: &PgPool,
auth: &AuthCtx,
parent_token: Uuid,
label: &str,
ttl_seconds: Option<i64>,
) -> BusResult<Issued> {
if auth.session_is_authenticated() {
return Err(BusError::Forbidden(
"a session credential cannot register another session. Register with the agent \
token that this window's credential was derived from."
.to_owned(),
));
}
let label = normalize_session(label)
.map_err(|why| BusError::invalid(format!("the session label {why}")))?;
if label.is_empty() {
return Err(BusError::invalid(
"a session label is required: it is the address teammates use to reach this \
window (agent/session). Use the id your host gives the conversation.",
));
}
let ttl = ttl_of(ttl_seconds);
let raw = generate_session_token();
let prefix = token_prefix(&raw);
let mut tx = pool.begin().await?;
sqlx::query("SELECT id FROM agents WHERE id = $1 FOR NO KEY UPDATE")
.bind(auth.agent_id)
.fetch_one(&mut *tx)
.await?;
let parent_live: Option<(bool,)> =
sqlx::query_as("SELECT revoked_at IS NULL FROM api_tokens WHERE id = $1 AND agent_id = $2")
.bind(parent_token)
.bind(auth.agent_id)
.fetch_optional(&mut *tx)
.await?;
if !matches!(parent_live, Some((true,))) {
return Err(BusError::Unauthenticated(
"the token that made this request was revoked while it was in flight; nothing \
was registered. Issue a new token and register again."
.to_owned(),
));
}
let row: Option<(Uuid, i64, chrono::DateTime<chrono::Utc>)> = sqlx::query_as(
r#"
INSERT INTO agent_sessions
(agent_id, parent_token, label, token_hash, prefix, expires_at)
VALUES ($1, $2, $3, $4, $5, now() + make_interval(secs => $6))
ON CONFLICT (agent_id, label) DO UPDATE SET
parent_token = EXCLUDED.parent_token,
token_hash = EXCLUDED.token_hash,
prefix = EXCLUDED.prefix,
epoch = agent_sessions.epoch + 1,
expires_at = EXCLUDED.expires_at,
revoked_at = NULL,
last_used_at = NULL
WHERE agent_sessions.revoked_at IS NOT NULL
OR agent_sessions.expires_at <= now()
-- A window whose parent token was revoked cannot answer with its
-- credential any more, whatever its own row says: the label is
-- free. Revocation also marks the row, so this is the seam belt.
OR EXISTS (SELECT 1 FROM api_tokens t
WHERE t.id = agent_sessions.parent_token
AND t.revoked_at IS NOT NULL)
RETURNING id, epoch, expires_at
"#,
)
.bind(auth.agent_id)
.bind(parent_token)
.bind(&label)
.bind(hash_token(&raw))
.bind(&prefix)
.bind(ttl as f64)
.fetch_optional(&mut *tx)
.await?;
tx.commit().await?;
let Some((id, epoch, expires_at)) = row else {
return Err(BusError::conflict(format!(
"session '{label}' is already registered and still live. Holding the agent \
token does not make you that window: reconnect it with resume_session, using \
the session credential the process that owns it holds, or close it first \
with revoke_session if it is gone for good."
)));
};
Ok(Issued {
id,
token: raw,
label,
epoch,
expires_at,
})
}
pub async fn resume(pool: &PgPool, auth: &AuthCtx, ttl_seconds: Option<i64>) -> BusResult<Issued> {
let Some(session_id) = auth.session_id else {
return Err(BusError::Forbidden(
"resume_session needs the session credential of the window being resumed. \
With an agent token, register_session opens a new window and refuses a live \
one."
.to_owned(),
));
};
let ttl = ttl_of(ttl_seconds);
let raw = generate_session_token();
let prefix = token_prefix(&raw);
let mut tx = pool.begin().await?;
sqlx::query("SELECT id FROM agents WHERE id = $1 FOR NO KEY UPDATE")
.bind(auth.agent_id)
.fetch_one(&mut *tx)
.await?;
let row: Option<(i64, chrono::DateTime<chrono::Utc>, String)> = sqlx::query_as(
"UPDATE agent_sessions s
SET token_hash = $2,
prefix = $3,
epoch = s.epoch + 1,
expires_at = now() + make_interval(secs => $4),
last_used_at = NULL
WHERE s.id = $1 AND s.revoked_at IS NULL AND s.epoch = $5
AND NOT EXISTS (SELECT 1 FROM api_tokens t
WHERE t.id = s.parent_token AND t.revoked_at IS NOT NULL)
RETURNING s.epoch, s.expires_at, s.label",
)
.bind(session_id)
.bind(hash_token(&raw))
.bind(&prefix)
.bind(ttl as f64)
.bind(auth.session_epoch.unwrap_or(0))
.fetch_optional(&mut *tx)
.await?;
let Some((epoch, expires_at, label)) = row else {
return Err(BusError::Unauthenticated(
"this session credential is no longer the window's: it was revoked, or the \
window was resumed or re-registered after this connection. Nothing was \
rotated; register a new session with your agent token."
.to_owned(),
));
};
tx.commit().await?;
Ok(Issued {
id: session_id,
token: raw,
label,
epoch,
expires_at,
})
}
pub async fn renew(pool: &PgPool, auth: &AuthCtx, ttl_seconds: Option<i64>) -> BusResult<Issued> {
let Some(session_id) = auth.session_id else {
return Err(BusError::Forbidden(
"renew_session needs a session credential: it extends the credential that made \
the call. Register one first with register_session."
.to_owned(),
));
};
let ttl = ttl_of(ttl_seconds);
let row: Option<(i64, chrono::DateTime<chrono::Utc>, String)> = sqlx::query_as(
"UPDATE agent_sessions
SET expires_at = now() + make_interval(secs => $2)
WHERE id = $1 AND revoked_at IS NULL AND epoch = $3
RETURNING epoch, expires_at, label",
)
.bind(session_id)
.bind(ttl as f64)
.bind(auth.session_epoch.unwrap_or(0))
.fetch_optional(pool)
.await?;
let Some((epoch, expires_at, label)) = row else {
return Err(BusError::not_found(
"this session has been revoked; register a new one with your agent token",
));
};
Ok(Issued {
id: session_id,
token: String::new(),
label,
epoch,
expires_at,
})
}
pub async fn revoke(pool: &PgPool, auth: &AuthCtx, label: Option<&str>) -> BusResult<String> {
let target = match label.map(str::trim).filter(|l| !l.is_empty()) {
Some(l) => normalize_session(l)
.map_err(|why| BusError::invalid(format!("the session label {why}")))?,
None => {
if !auth.session_is_authenticated() {
return Err(BusError::invalid(
"say which session to revoke: this call was made with an agent token, \
which is not itself a session",
));
}
auth.session.clone()
}
};
let mut tx = pool.begin().await?;
sqlx::query("SELECT id FROM agents WHERE id = $1 FOR NO KEY UPDATE")
.bind(auth.agent_id)
.fetch_one(&mut *tx)
.await?;
guard(&mut tx, auth).await?;
let row: Option<(Uuid,)> = sqlx::query_as(
"UPDATE agent_sessions SET revoked_at = now()
WHERE agent_id = $1 AND label = $2 AND revoked_at IS NULL
RETURNING id",
)
.bind(auth.agent_id)
.bind(&target)
.fetch_optional(&mut *tx)
.await?;
if row.is_none() {
let exists: Option<(Uuid,)> =
sqlx::query_as("SELECT id FROM agent_sessions WHERE agent_id = $1 AND label = $2")
.bind(auth.agent_id)
.bind(&target)
.fetch_optional(&mut *tx)
.await?;
if exists.is_none() {
return Err(BusError::not_found(format!(
"no session '{target}' of yours"
)));
}
}
tx.commit().await?;
Ok(target)
}
pub async fn guard(tx: &mut sqlx::PgConnection, auth: &AuthCtx) -> BusResult<()> {
let (Some(session_id), Some(epoch)) = (auth.session_id, auth.session_epoch) else {
return Ok(());
};
let row: Option<(i64, bool, bool)> = sqlx::query_as(
"SELECT epoch, (revoked_at IS NOT NULL), (expires_at <= now())
FROM agent_sessions WHERE id = $1 FOR SHARE",
)
.bind(session_id)
.fetch_optional(&mut *tx)
.await?;
let Some((current, revoked, expired)) = row else {
return Err(BusError::Unauthenticated(
"this session no longer exists; register a new one with your agent token".to_owned(),
));
};
if revoked || expired {
return Err(BusError::Unauthenticated(
"this session credential is no longer valid; register a new one with your agent \
token"
.to_owned(),
));
}
if current != epoch {
return Err(BusError::conflict(format!(
"this connection is stale: it carries epoch {epoch} and the session is at \
{current}, so another process resumed this window after you. Nothing was \
written. Resume the session to take over, or exit."
)));
}
Ok(())
}
pub async fn require_window(pool: &PgPool, auth: &AuthCtx) -> BusResult<()> {
if auth.session.is_empty() || auth.session_id.is_some() {
return Ok(());
}
let (registered,): (i64,) =
sqlx::query_as("SELECT count(*) FROM agent_sessions WHERE agent_id = $1 AND label = $2")
.bind(auth.agent_id)
.bind(&auth.session)
.fetch_one(pool)
.await?;
if registered == 0 {
return Ok(());
}
Err(BusError::Forbidden(format!(
"'{}' is a registered window and this call carries an agent token, not that \
window's session credential. Revoking or expiring it does not hand the label \
back: ask that window to make the call, register it again, or use \
recover_conversation_history, which is the audited way for an agent to reach \
its own windows' threads.",
auth.session
)))
}
pub async fn identity(pool: &PgPool, auth: &AuthCtx) -> BusResult<Option<SessionIdentity>> {
let Some(session_id) = auth.session_id else {
return Ok(None);
};
let row: Option<(
i64,
chrono::DateTime<chrono::Utc>,
chrono::DateTime<chrono::Utc>,
)> = sqlx::query_as("SELECT epoch, created_at, expires_at FROM agent_sessions WHERE id = $1")
.bind(session_id)
.fetch_optional(pool)
.await?;
Ok(row.map(|(epoch, created_at, expires_at)| SessionIdentity {
session_id: session_id.to_string(),
epoch,
registered_at: crate::model::ts(created_at),
expires_at: crate::model::ts(expires_at),
expires_in_seconds: (expires_at - chrono::Utc::now()).num_seconds().max(0),
}))
}
pub fn credential_of(issued: Issued, agent: &str) -> SessionCredential {
SessionCredential {
session_token: (!issued.token.is_empty()).then(|| issued.token.clone()),
session_id: issued.id.to_string(),
session: issued.label.clone(),
address: format!("{agent}/{}", issued.label),
epoch: issued.epoch,
expires_at: crate::model::ts(issued.expires_at),
expires_in_seconds: (issued.expires_at - chrono::Utc::now())
.num_seconds()
.max(0),
}
}