Skip to main content

brows3r_lib/commands/
media_cmd.rs

1//! Tauri commands for the loopback media server.
2//!
3//! # Commands
4//!
5//! - [`media_register`] — mint a signed token and return a loopback URL.
6//! - [`media_revoke`]   — immediately revoke a single token.
7//!
8//! # Design
9//!
10//! The media server is an `axum` HTTP server bound to `127.0.0.1:0`.  The
11//! frontend embeds the returned URL directly as a `<video>` or `<audio>` `src`
12//! attribute; the browser's byte-range requests are proxied by the server to S3.
13//!
14//! Token security: tokens are 48 random bytes encoded as URL-safe base64 (64
15//! chars). They are session-scoped — `revoke_session` sweeps all tokens when
16//! the session ends.
17
18use serde::{Deserialize, Serialize};
19use tauri::State;
20
21use crate::{
22    error::AppError,
23    events::{self, EventKind},
24    ids::{BucketId, ProfileId},
25    media_server::MediaServerHandle,
26    profiles::ProfileStoreHandle,
27};
28
29// ---------------------------------------------------------------------------
30// Types
31// ---------------------------------------------------------------------------
32
33/// Response from [`media_register`].
34#[derive(Debug, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase")]
36pub struct MediaRegisterResponse {
37    /// Full loopback URL, e.g. `http://127.0.0.1:49231/m/<token>`.
38    pub url: String,
39    /// Unix epoch seconds at which the token expires.
40    pub expires_at: i64,
41}
42
43// ---------------------------------------------------------------------------
44// media_register
45// ---------------------------------------------------------------------------
46
47/// Mint a signed token for the given S3 object and return a loopback URL.
48///
49/// The returned URL can be set as the `src` of a `<video>` or `<audio>` element.
50/// The server validates the token, enforces expiry, and streams the S3 object
51/// with byte-range support.
52///
53/// # Parameters
54///
55/// - `profile_id` — profile whose credentials service the stream.
56/// - `bucket` / `key` — S3 coordinates.
57/// - `server` — managed [`MediaServerHandle`] (port + registry).
58/// - `store` — profile store used to resolve the bucket region.
59///
60/// # Token TTL
61///
62/// Default: 3 600 seconds (1 hour).  Tokens also expire when the session ends
63/// via `revoke_session`.
64///
65/// # Errors
66///
67/// Returns `AppError::NotFound` when `profile_id` does not exist.
68/// Returns `AppError::Auth` when the profile has not been validated.
69#[tauri::command]
70pub async fn media_register(
71    profile_id: ProfileId,
72    bucket: BucketId,
73    key: String,
74    server: State<'_, MediaServerHandle>,
75    store: State<'_, ProfileStoreHandle>,
76) -> Result<MediaRegisterResponse, AppError> {
77    // Resolve profile to get region and validate the session is authenticated.
78    let profile = {
79        let store_guard = store.inner.lock().await;
80        store_guard
81            .get(&profile_id)
82            .ok_or_else(|| AppError::NotFound {
83                resource: format!("profile:{}", profile_id.as_str()),
84            })?
85    };
86
87    if profile.validated_at.is_none() {
88        return Err(AppError::Auth {
89            reason: "profile_not_validated_in_session".to_string(),
90        });
91    }
92
93    let region = profile
94        .default_region
95        .clone()
96        .unwrap_or_else(|| "us-east-1".to_string());
97
98    let session_id = server.session_id.clone();
99    let (token, expires_at) = server
100        .registry
101        .mint(profile_id, bucket, key, region, 3600, session_id);
102
103    let url = format!("http://127.0.0.1:{}/m/{}", server.port, token);
104
105    Ok(MediaRegisterResponse { url, expires_at })
106}
107
108// ---------------------------------------------------------------------------
109// media_revoke
110// ---------------------------------------------------------------------------
111
112/// Immediately revoke a single media token.
113///
114/// After revocation, any in-flight request using the token will receive a 403
115/// response on the next check (or 404 after GC).  The `media:revoked` event is
116/// emitted so the frontend can react (e.g. show an expired-token message).
117///
118/// # Errors
119///
120/// This command is idempotent — revoking an already-revoked or unknown token
121/// is not an error.
122#[tauri::command]
123pub async fn media_revoke(
124    token: String,
125    server: State<'_, MediaServerHandle>,
126    channel: tauri::AppHandle,
127) -> Result<(), AppError> {
128    server.registry.revoke(&token);
129
130    // Emit media:revoked so the frontend can react.
131    events::emit(
132        &channel,
133        EventKind::MediaRevoked,
134        serde_json::json!({ "token": token }),
135    )?;
136
137    Ok(())
138}