Files
PiCloud/crates/shared/src/realtime_authority.rs
MechaCat02 fcbcc576a2 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>
2026-06-04 20:18:50 +02:00

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)
}
}