Server-side realtime SSE on per-app pub/sub topics, plus the three
v1.1.5 follow-ups and the version bumps.
Realtime:
- topics registry (0021) + admin endpoints + Capability::AppTopicManage
(-> app:admin; no new scope).
- GET /realtime/topics/{topic} SSE endpoint (orchestrator-core data
plane): Host -> app, RealtimeAuthority gate (404 missing/internal,
401 bad/absent token), broadcast::Receiver stream + heartbeat.
- RealtimeBroadcaster / RealtimeEvent / RealtimeAuthority traits
(picloud-shared); InProcessBroadcaster + GC (orchestrator-core);
DB-backed RealtimeAuthorityImpl (manager-core). Publish path fans out
to in-process subscribers after the durable outbox commit (best-effort,
panic-isolated).
- HMAC subscriber tokens (subscriber_token.rs) + app_secrets table (0022)
+ pubsub::subscriber_token SDK (schema 1.6 -> 1.7). TTL clamp + env
overrides.
- Dashboard Topics tab (register/list/edit/delete, prominent external
badge, flip confirmation).
v1.1.5 follow-ups:
- Empty blobs accepted (NewFile/FileUpdate::validate) + round-trip test.
- Orphan *.tmp.* sweeper (spawn_files_orphan_sweep).
- Dispatcher e2e tests, one per trigger kind (DATABASE_URL-gated).
Versions: workspace 1.1.6, SDK 1.7, dashboard 0.12.0. Schema-snapshot
golden re-blessed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
71 lines
2.6 KiB
Rust
71 lines
2.6 KiB
Rust
//! `RealtimeAuthority` — the SSE subscribe authorization seam (v1.1.6).
|
|
//!
|
|
//! The SSE endpoint (`GET /realtime/topics/{topic}`) is a data-plane
|
|
//! surface in orchestrator-core, but deciding whether a subscribe is
|
|
//! allowed needs a `topics` table read plus (for token-gated topics) an
|
|
//! HMAC verify against the app's signing key — both of which require DB
|
|
//! access and the signing-key material that must NOT leak into the
|
|
//! data-plane crate. This trait keeps all of that inside the manager-core
|
|
//! impl: orchestrator-core only ever sees the three-way verdict below.
|
|
//!
|
|
//! `NotFound` is deliberately returned for *both* "no such topic" and
|
|
//! "topic exists but isn't externally subscribable" so the endpoint
|
|
//! can't be used to probe which internal topics exist (design notes §5).
|
|
|
|
use async_trait::async_trait;
|
|
|
|
use crate::AppId;
|
|
|
|
/// Why a subscribe attempt was refused. The SSE handler maps these to
|
|
/// HTTP status codes.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum SubscribeDenied {
|
|
/// No externally-subscribable topic by that name in this app → 404.
|
|
/// Used for genuinely-missing topics AND internal-only ones, so the
|
|
/// endpoint doesn't leak which internal topics exist.
|
|
NotFound,
|
|
/// The topic is token-gated and the presented token was missing,
|
|
/// malformed, badly signed, expired, or not scoped to this topic →
|
|
/// 401 (generic; never says which check failed).
|
|
Unauthorized,
|
|
/// Backend failure (DB unavailable, etc.) → 500.
|
|
Backend(String),
|
|
}
|
|
|
|
#[async_trait]
|
|
pub trait RealtimeAuthority: Send + Sync {
|
|
/// Decide whether an external client may subscribe to
|
|
/// `(app_id, topic)`. `token` is the bearer/query token if the
|
|
/// client presented one (`None` otherwise).
|
|
///
|
|
/// Returns `Ok(())` when the subscribe is permitted (public topic,
|
|
/// or token-gated topic with a valid token scoped to it).
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// [`SubscribeDenied`] — see the variants for the status mapping.
|
|
async fn authorize_subscribe(
|
|
&self,
|
|
app_id: AppId,
|
|
topic: &str,
|
|
token: Option<&str>,
|
|
) -> Result<(), SubscribeDenied>;
|
|
}
|
|
|
|
/// Bootstrap impl: denies everything as `NotFound`. Replaced in
|
|
/// `build_app` with the manager-core DB-backed authority.
|
|
#[derive(Debug, Default, Clone, Copy)]
|
|
pub struct DenyAllRealtimeAuthority;
|
|
|
|
#[async_trait]
|
|
impl RealtimeAuthority for DenyAllRealtimeAuthority {
|
|
async fn authorize_subscribe(
|
|
&self,
|
|
_app_id: AppId,
|
|
_topic: &str,
|
|
_token: Option<&str>,
|
|
) -> Result<(), SubscribeDenied> {
|
|
Err(SubscribeDenied::NotFound)
|
|
}
|
|
}
|