Implement replay-safe browser synchronization

This commit is contained in:
ms2k 2026-07-18 00:16:21 +09:00
commit 459ffa04c6

View file

@ -1,4 +1,4 @@
use std::time::Duration;
use std::{collections::HashSet, time::Duration};
use actix_web::{
HttpRequest, HttpResponse, ResponseError,
@ -51,6 +51,8 @@ pub struct PublicApiConfig {
pub event_retention: Duration,
pub event_poll_interval: Duration,
pub event_heartbeat_interval: Duration,
pub event_ticket_ttl: Duration,
pub event_allowed_origins: Vec<String>,
}
#[derive(Clone)]
@ -68,6 +70,8 @@ pub struct PublicApiState {
event_retention: Duration,
event_poll_interval: Duration,
event_heartbeat_interval: Duration,
event_ticket_ttl: Duration,
event_allowed_origins: HashSet<String>,
}
pub struct PublicApiServices {
@ -93,9 +97,28 @@ impl PublicApiState {
if config.event_retention.is_zero()
|| config.event_poll_interval.is_zero()
|| config.event_heartbeat_interval.is_zero()
|| config.event_ticket_ttl.is_zero()
{
return Err(DurableError::Config(
"event retention and intervals must be positive".into(),
"event retention, intervals, and ticket expiry must be positive".into(),
));
}
if config.event_ticket_ttl > Duration::from_secs(60) {
return Err(DurableError::Config(
"event ticket expiry must not exceed 60 seconds".into(),
));
}
let event_allowed_origins = config
.event_allowed_origins
.into_iter()
.collect::<HashSet<_>>();
if event_allowed_origins.is_empty()
|| event_allowed_origins
.iter()
.any(|origin| origin.is_empty() || origin.trim() != origin)
{
return Err(DurableError::Config(
"at least one exact event origin is required".into(),
));
}
Ok(Self {
@ -112,6 +135,8 @@ impl PublicApiState {
event_retention: config.event_retention,
event_poll_interval: config.event_poll_interval,
event_heartbeat_interval: config.event_heartbeat_interval,
event_ticket_ttl: config.event_ticket_ttl,
event_allowed_origins,
})
}
@ -193,6 +218,7 @@ pub fn configure_public_api(config: &mut web::ServiceConfig) {
"/deletions/{operation_id}",
web::get().to(get_deletion_operation),
)
.route("/event-tickets", web::post().to(events::issue_ticket))
.route("/events", web::get().to(events::connect)),
);
}