Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions apis/link/src/relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ interface Client {
deviceId: string;
role: "desktop" | "mobile";
sessionId: string;
mode: string;
}

const sessions = new Map<string, { desktop: Client | null; mobile: Client | null }>();
Expand Down Expand Up @@ -86,6 +87,12 @@ export function handleRelaySocket(socket: WebSocket, app: FastifyInstance) {
return;
}

if (session.status !== "approved") {
sendJson(socket, { type: "relay.reject", reason: "Session not approved" });
socket.close();
return;
}

// Verify deviceId matches session participants
if (role === "desktop" && session.desktopDeviceId !== deviceId) {
sendJson(socket, { type: "relay.reject", reason: "deviceId mismatch" });
Expand All @@ -100,7 +107,14 @@ export function handleRelaySocket(socket: WebSocket, app: FastifyInstance) {

console.log(`[relay] ${role} authenticated for session ${sessionId}, device ${deviceId}`);

client = { socket, userId: payload.sub, deviceId, role, sessionId };
client = {
socket,
userId: payload.sub,
deviceId,
role,
sessionId,
mode: String(session.mode || ""),
};
authenticated = true;

// Register in session
Expand All @@ -114,7 +128,8 @@ export function handleRelaySocket(socket: WebSocket, app: FastifyInstance) {

// Set presence
const redis = getRedis();
const presenceKey = role === "desktop" ? `presence:desktop:${deviceId}` : `presence:mobile:${deviceId}`;
const presenceKey =
role === "desktop" ? `presence:desktop:${deviceId}` : `presence:mobile:${deviceId}`;
await redis.setex(presenceKey, 60, "online");

sendJson(socket, { type: "relay.ready", sessionId, role });
Expand All @@ -124,6 +139,19 @@ export function handleRelaySocket(socket: WebSocket, app: FastifyInstance) {

// Forward messages to the other peer
if (client) {
if (
client.role === "mobile" &&
msg.type === "relay.message" &&
msg.direction === "input" &&
client.mode !== "full_control"
) {
sendJson(socket, {
type: "relay.reject",
reason: "Terminal input requires full_control mode",
});
return;
}

const sess = sessions.get(client.sessionId);
if (!sess) return;

Expand Down Expand Up @@ -156,7 +184,10 @@ export function handleRelaySocket(socket: WebSocket, app: FastifyInstance) {

// Clear presence
const redis = getRedis();
const presenceKey = client.role === "desktop" ? `presence:desktop:${client.deviceId}` : `presence:mobile:${client.deviceId}`;
const presenceKey =
client.role === "desktop"
? `presence:desktop:${client.deviceId}`
: `presence:mobile:${client.deviceId}`;
await redis.del(presenceKey);

console.log(`[relay] ${client.role} disconnected from session ${client.sessionId}`);
Expand Down
20 changes: 19 additions & 1 deletion crates/orphix-core/src/link/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,16 @@ impl LinkManager {
self.relay_output_tx.clone()
}

fn session_allows_terminal_input(&self, session_id: &str, terminal_id: &str) -> bool {
if self.session.session_id.as_deref() != Some(session_id) {
return false;
}
if self.session.mode.as_deref() != Some("full_control") {
return false;
}
self.relay.get_terminal_for_session(session_id).as_deref() == Some(terminal_id)
}

/// Phase 1 (sync): Validate params and create device identity. No network I/O.
/// Must be called with the parking_lot lock held.
pub fn prepare_enable(&mut self, params: &EnableParams) -> Result<PreparedEnable, String> {
Expand Down Expand Up @@ -495,8 +505,16 @@ impl LinkManager {
self.send_workspace_snapshot();
}

LinkMessage::RelayMessage { terminal_id, data, direction, .. } => {
LinkMessage::RelayMessage { session_id, terminal_id, data, direction } => {
if direction == "input" {
if !self.session_allows_terminal_input(&session_id, &terminal_id) {
eprintln!(
"[link] Denied relay input: session={}, terminal={}, mode={:?}",
session_id, terminal_id, self.session.mode
);
return;
}

// Check if this is an RPC call (JSON with type field)
if data.starts_with('{') {
if let Ok(msg) = serde_json::from_str::<serde_json::Value>(&data) {
Expand Down
Loading