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}