feat(v1.1.6): realtime channels + v1.1.5 follow-ups + version bumps
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>
This commit is contained in:
70
crates/shared/src/realtime_authority.rs
Normal file
70
crates/shared/src/realtime_authority.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
//! `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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user