refactor: remove cross-network device pairing
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 48s

Remove dead pair-code flow end-to-end (client, server, protocol) and
the orphaned PublicRoomPanel/DevicePairingPanel/Pair components that
were no longer reachable from the UI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
ordinarthur 2026-04-20 13:18:02 +02:00
parent 54021f88d8
commit 35aca309c9
9 changed files with 6 additions and 472 deletions

View File

@ -139,19 +139,6 @@ function validateAvatar(avatar: unknown): string | undefined {
return avatar; return avatar;
} }
// ── Pair codes (temporary mapping: short code → groupId, 5 min TTL) ──
import { customAlphabet } from "nanoid";
const generatePairCode = customAlphabet("ABCDEFGHJKLMNPQRSTUVWXYZ23456789", 6);
const pairCodes = new Map<string, { groupId: string; expiresAt: number }>();
setInterval(() => {
const now = Date.now();
for (const [code, entry] of pairCodes) {
if (now > entry.expiresAt) pairCodes.delete(code);
}
}, 60_000);
// ── HTTP server (Hono) ── // ── HTTP server (Hono) ──
const honoApp = buildApp(); const honoApp = buildApp();
@ -224,12 +211,6 @@ wss.on("connection", (ws, req) => {
case "wake-peer": case "wake-peer":
handleWakePeer(client, msg); handleWakePeer(client, msg);
break; break;
case "create-pair-code":
handleCreatePairCode(client, msg);
break;
case "resolve-pair-code":
handleResolvePairCode(client, msg);
break;
case "leave": case "leave":
handleLeave(client); handleLeave(client);
break; break;
@ -429,38 +410,6 @@ function handleSignal(client: Client, to: string, data: unknown): void {
} }
} }
function handleCreatePairCode(client: Client, msg: ClientMessage & { type: "create-pair-code" }): void {
if (typeof msg.groupId !== "string" || msg.groupId.length === 0) return;
// Check if this groupId already has an active code
for (const [code, entry] of pairCodes) {
if (entry.groupId === msg.groupId && Date.now() < entry.expiresAt) {
send(client.ws, { type: "pair-code-created", code });
return;
}
}
const code = generatePairCode();
pairCodes.set(code, { groupId: msg.groupId, expiresAt: Date.now() + 5 * 60 * 1000 });
console.log(`[pair] created code ${code} for group ${msg.groupId.slice(0, 8)}...`);
send(client.ws, { type: "pair-code-created", code });
}
function handleResolvePairCode(client: Client, msg: ClientMessage & { type: "resolve-pair-code" }): void {
if (typeof msg.code !== "string" || msg.code.length !== 6) {
send(client.ws, { type: "error", code: "pair-code-not-found", message: "Code d'appairage invalide" });
return;
}
const entry = pairCodes.get(msg.code.toUpperCase());
if (!entry || Date.now() > entry.expiresAt) {
send(client.ws, { type: "error", code: "pair-code-not-found", message: "Code d'appairage invalide ou expiré" });
return;
}
pairCodes.delete(msg.code.toUpperCase());
console.log(`[pair] resolved code ${msg.code} → group ${entry.groupId.slice(0, 8)}...`);
send(client.ws, { type: "pair-code-resolved", groupId: entry.groupId });
}
function handleLeave(client: Client): void { function handleLeave(client: Client): void {
const lanRoom = roomManager.getRoomById(client.lanRoomId); const lanRoom = roomManager.getRoomById(client.lanRoomId);
if (!lanRoom) return; if (!lanRoom) return;

View File

@ -40,25 +40,13 @@ export interface WakePeerMessage {
deviceId: string; deviceId: string;
} }
export interface CreatePairCodeMessage {
type: "create-pair-code";
groupId: string;
}
export interface ResolvePairCodeMessage {
type: "resolve-pair-code";
code: string;
}
export type ClientMessage = export type ClientMessage =
| HelloMessage | HelloMessage
| CreatePublicRoomMessage | CreatePublicRoomMessage
| SignalMessage | SignalMessage
| LeaveMessage | LeaveMessage
| SubscribePushMessage | SubscribePushMessage
| WakePeerMessage | WakePeerMessage;
| CreatePairCodeMessage
| ResolvePairCodeMessage;
// ── Server → Client messages ── // ── Server → Client messages ──
@ -105,17 +93,7 @@ export interface SignalRelayMessage {
data: unknown; data: unknown;
} }
export interface PairCodeCreatedMessage { export type ErrorCode = "room-not-found" | "room-expired" | "rate-limit";
type: "pair-code-created";
code: string;
}
export interface PairCodeResolvedMessage {
type: "pair-code-resolved";
groupId: string;
}
export type ErrorCode = "room-not-found" | "room-expired" | "rate-limit" | "pair-code-not-found";
export interface ErrorMessage { export interface ErrorMessage {
type: "error"; type: "error";
@ -129,8 +107,6 @@ export type ServerMessage =
| PeerJoinedMessage | PeerJoinedMessage
| PeerLeftMessage | PeerLeftMessage
| SignalRelayMessage | SignalRelayMessage
| PairCodeCreatedMessage
| PairCodeResolvedMessage
| ErrorMessage; | ErrorMessage;
// ── Data channel messages (peer-to-peer) ── // ── Data channel messages (peer-to-peer) ──

View File

@ -2,7 +2,6 @@ import { Routes, Route } from "react-router-dom";
import Home from "./pages/Home"; import Home from "./pages/Home";
import JoinRoom from "./pages/JoinRoom"; import JoinRoom from "./pages/JoinRoom";
import Share from "./pages/Share"; import Share from "./pages/Share";
import Pair from "./pages/Pair";
import Settings from "./pages/Settings"; import Settings from "./pages/Settings";
import Receive from "./pages/Receive"; import Receive from "./pages/Receive";
import Inbox from "./pages/Inbox"; import Inbox from "./pages/Inbox";
@ -12,7 +11,6 @@ export default function App() {
<Routes> <Routes>
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
<Route path="/share" element={<Share />} /> <Route path="/share" element={<Share />} />
<Route path="/pair" element={<Pair />} />
<Route path="/settings" element={<Settings />} /> <Route path="/settings" element={<Settings />} />
<Route path="/inbox" element={<Inbox />} /> <Route path="/inbox" element={<Inbox />} />
<Route path="/r/:id" element={<Receive />} /> <Route path="/r/:id" element={<Receive />} />

View File

@ -1,187 +0,0 @@
import { useState, useEffect } from "react";
import { useProfileStore } from "../stores/useProfileStore";
import { useStore } from "../stores/useStore";
interface DevicePairingPanelProps {
onRequestCode: (groupId: string) => void;
onResolveCode: (code: string) => void;
}
export default function DevicePairingPanel({
onRequestCode,
onResolveCode,
}: DevicePairingPanelProps) {
const { groupId, setGroupId } = useProfileStore();
const pairCode = useStore((s) => s.pairCode);
const error = useStore((s) => s.error);
const [showModal, setShowModal] = useState(false);
const [mode, setMode] = useState<"show" | "enter">("show");
const [inputCode, setInputCode] = useState("");
const [pairError, setPairError] = useState<string | null>(null);
useEffect(() => {
if (showModal && mode === "show") {
let gid = groupId;
if (!gid) {
gid = crypto.randomUUID();
setGroupId(gid);
}
useStore.getState().setPairCode(null);
onRequestCode(gid);
}
}, [showModal, mode]);
useEffect(() => {
if (error && error.includes("appairage")) {
setPairError(error);
useStore.getState().setError(null);
}
}, [error]);
const handleClose = () => {
setShowModal(false);
useStore.getState().setPairCode(null);
setPairError(null);
setInputCode("");
};
const handleSubmitCode = () => {
if (inputCode.length >= 6) {
setPairError(null);
onResolveCode(inputCode);
}
};
const currentGroupId = useProfileStore((s) => s.groupId);
useEffect(() => {
if (mode === "enter" && showModal && currentGroupId && currentGroupId !== groupId) {
handleClose();
}
}, [currentGroupId]);
return (
<>
<button
onClick={() => { setMode("show"); setShowModal(true); }}
className="paper-panel px-4 py-4 flex flex-col items-start gap-1
hover:border-ink transition-colors duration-fast ease-crisp
text-left"
>
<span className="font-mono text-[10px] uppercase tracking-[0.22em] text-ink-muted">
Pair device
</span>
<span className="text-sm text-ink">Link your own </span>
</button>
{showModal && (
<div
className="fixed inset-0 z-50 bg-ink/40 flex items-center justify-center p-4"
onClick={handleClose}
>
<div
className="paper-panel shadow-lift rounded-sm p-6 max-w-sm w-full"
onClick={(e) => e.stopPropagation()}
>
<div className="text-xs uppercase tracking-[0.2em] text-ink-muted">
Pair device
</div>
<h3 className="font-display text-2xl text-ink mt-1 mb-5">
Link your devices
</h3>
{/* Tab switcher */}
<div className="flex border border-paper-edge rounded-sm overflow-hidden mb-5">
<button
onClick={() => setMode("show")}
className={`flex-1 py-2 text-xs uppercase tracking-[0.15em] transition-colors
${mode === "show"
? "bg-ink text-paper"
: "bg-paper text-ink-muted hover:text-ink"}`}
>
Show code
</button>
<button
onClick={() => { setMode("enter"); setPairError(null); }}
className={`flex-1 py-2 text-xs uppercase tracking-[0.15em] transition-colors
${mode === "enter"
? "bg-ink text-paper"
: "bg-paper text-ink-muted hover:text-ink"}`}
>
Enter code
</button>
</div>
{mode === "show" ? (
<>
<p className="text-sm text-ink-muted mb-5">
Enter this code on your other device.
</p>
{pairCode ? (
<p className="font-mono text-4xl text-signal tracking-[0.3em] text-center">
{pairCode}
</p>
) : (
<div className="flex justify-center py-3">
<div className="w-5 h-5 border border-signal border-t-transparent rounded-full animate-spin" />
</div>
)}
<p className="text-xs text-ink-muted mt-5 leading-relaxed text-center">
Code expires in 5 minutes. Pairing is permanent.
</p>
</>
) : (
<>
<p className="text-sm text-ink-muted mb-4">
Enter the code shown on the other device.
</p>
<input
type="text"
value={inputCode}
onChange={(e) => {
setInputCode(e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, ""));
setPairError(null);
}}
placeholder="ABC123"
maxLength={6}
autoFocus
className="w-full px-4 py-3 bg-paper border border-paper-edge
rounded-sm text-ink text-2xl text-center font-mono tracking-[0.3em]
placeholder:text-ink-faint focus:outline-none focus:border-ink
transition-colors duration-fast ease-crisp"
onKeyDown={(e) => {
if (e.key === "Enter") handleSubmitCode();
}}
/>
{pairError && (
<p className="text-sm text-fail mt-3">{pairError}</p>
)}
<button
onClick={handleSubmitCode}
disabled={inputCode.length < 6}
className="mt-5 w-full py-2.5 bg-ink text-paper rounded-sm text-sm
font-medium hover:bg-signal transition-colors duration-fast
ease-crisp disabled:opacity-30 disabled:cursor-not-allowed"
>
Pair
</button>
</>
)}
<button
onClick={handleClose}
className="mt-6 w-full py-2.5 border border-paper-edge hover:border-ink
text-sm text-ink rounded-sm transition-colors duration-fast ease-crisp"
>
Close
</button>
</div>
</div>
)}
</>
);
}

View File

@ -1,121 +0,0 @@
import { useState } from "react";
import { QRCodeSVG } from "qrcode.react";
import { useStore } from "../stores/useStore";
interface PublicRoomPanelProps {
onCreateRoom: () => void;
}
export default function PublicRoomPanel({ onCreateRoom }: PublicRoomPanelProps) {
const publicRoomCode = useStore((s) => s.publicRoomCode);
const publicRoomUrl = useStore((s) => s.publicRoomUrl);
const [showModal, setShowModal] = useState(false);
const handleClick = () => {
if (publicRoomCode && publicRoomUrl) {
setShowModal(true);
} else {
onCreateRoom();
setShowModal(true);
}
};
return (
<>
<button
onClick={handleClick}
className="paper-panel px-4 py-4 flex flex-col items-start gap-1
hover:border-ink transition-colors duration-fast ease-crisp
text-left"
>
<span className="font-mono text-[10px] uppercase tracking-[0.22em] text-ink-muted">
Public link
</span>
<span className="text-sm text-ink">Send to anyone </span>
</button>
{showModal && (
<PublicRoomModal
code={publicRoomCode}
url={publicRoomUrl}
onClose={() => setShowModal(false)}
/>
)}
</>
);
}
function PublicRoomModal({
code,
url,
onClose,
}: {
code: string | null;
url: string | null;
onClose: () => void;
}) {
const [copied, setCopied] = useState(false);
const copyToClipboard = () => {
if (!url) return;
navigator.clipboard.writeText(url);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
};
return (
<div
className="fixed inset-0 z-50 bg-ink/40 flex items-center justify-center p-4"
onClick={onClose}
>
<div
className="paper-panel shadow-lift rounded-sm p-6 max-w-sm w-full"
onClick={(e) => e.stopPropagation()}
>
<div className="text-xs uppercase tracking-[0.2em] text-ink-muted">
Public link
</div>
<h3 className="font-display text-2xl text-ink mt-1 mb-5">
Receive from anyone
</h3>
{!code || !url ? (
<p className="text-sm text-ink-muted">Generating</p>
) : (
<>
<div className="flex justify-center mb-5">
<div className="bg-paper p-3 border border-paper-edge rounded-sm">
<QRCodeSVG value={url} size={180} bgColor="#F5F0E6" fgColor="#2563EB" />
</div>
</div>
<div className="text-center space-y-2">
<p className="font-mono text-3xl font-medium text-signal tracking-[0.3em]">
{code.toUpperCase()}
</p>
<button
onClick={copyToClipboard}
className="text-xs text-ink-muted hover:text-ink transition-colors
font-mono break-all"
>
{copied ? "Copied ✓" : url}
</button>
</div>
<p className="text-xs text-ink-muted mt-5 leading-relaxed">
Anyone with this code or link can send you files. Expires in 10 minutes.
</p>
</>
)}
<button
onClick={onClose}
className="mt-6 w-full py-2.5 border border-paper-edge hover:border-ink
text-sm text-ink rounded-sm transition-colors duration-fast ease-crisp"
>
Close
</button>
</div>
</div>
);
}

View File

@ -240,18 +240,6 @@ export function useSignaling(joinCode?: string) {
case "public-room-created": case "public-room-created":
setPublicRoom(msg.code, msg.url, msg.expiresAt); setPublicRoom(msg.code, msg.url, msg.expiresAt);
break; break;
case "pair-code-created":
useStore.getState().setPairCode(msg.code);
break;
case "pair-code-resolved": {
// Store the groupId and reconnect to join the group room
useProfileStore.getState().setGroupId(msg.groupId);
// Update the signaling client's profile so reconnect sends the new groupId
signalingRef.current?.updateProfile({ groupId: msg.groupId });
signalingRef.current?.disconnect();
setTimeout(() => signalingRef.current?.connect(), 500);
break;
}
case "error": case "error":
setError(msg.message); setError(msg.message);
break; break;
@ -556,14 +544,6 @@ export function useSignaling(joinCode?: string) {
signalingRef.current?.send({ type: "wake-peer", deviceId }); signalingRef.current?.send({ type: "wake-peer", deviceId });
}, []); }, []);
const requestPairCode = useCallback((groupId: string) => {
signalingRef.current?.send({ type: "create-pair-code", groupId });
}, []);
const resolvePairCode = useCallback((code: string) => {
signalingRef.current?.send({ type: "resolve-pair-code", code: code.toUpperCase() });
}, []);
return { return {
sendFiles, sendFiles,
sendText, sendText,
@ -571,7 +551,5 @@ export function useSignaling(joinCode?: string) {
rejectTransfer, rejectTransfer,
createPublicRoom, createPublicRoom,
wakePeer, wakePeer,
requestPairCode,
resolvePairCode,
}; };
} }

View File

@ -145,16 +145,11 @@ function HomeConnected() {
</div> </div>
</section> </section>
<footer className="pt-6 mt-10 sm:pt-8 sm:mt-14 rule flex flex-col sm:flex-row sm:items-center gap-3 sm:justify-between text-xs text-ink-muted"> <footer className="pt-6 mt-10 sm:pt-8 sm:mt-14 rule flex items-center justify-between text-xs text-ink-muted">
<span>Nothing transits the server</span> <span>Nothing transits the server</span>
<div className="flex items-center gap-4"> <Link to="/settings" className="text-ink hover:text-signal transition-colors duration-fast">
<Link to="/pair" className="text-ink-muted hover:text-ink transition-colors duration-fast"> Account
Pair across networks </Link>
</Link>
<Link to="/settings" className="text-ink hover:text-signal transition-colors duration-fast">
Account
</Link>
</div>
</footer> </footer>
</div> </div>

View File

@ -1,47 +0,0 @@
import { useEffect } from "react";
import { useSearchParams, useNavigate } from "react-router-dom";
import { useProfileStore } from "../stores/useProfileStore";
export default function Pair() {
const [params] = useSearchParams();
const navigate = useNavigate();
const { setGroupId } = useProfileStore();
const groupId = params.get("g");
useEffect(() => {
if (groupId) {
setGroupId(groupId);
const t = setTimeout(() => navigate("/", { replace: true }), 1500);
return () => clearTimeout(t);
}
}, [groupId, setGroupId, navigate]);
if (!groupId) {
return (
<div className="min-h-screen flex items-center justify-center px-4">
<div className="paper-panel px-6 py-5 text-center">
<div className="text-xs uppercase tracking-[0.22em] text-fail">Invalid</div>
<p className="mt-2 text-sm text-ink">This pairing link is malformed.</p>
</div>
</div>
);
}
return (
<div className="min-h-screen flex items-center justify-center px-4">
<div className="paper-panel px-8 py-8 max-w-sm w-full text-center">
<div className="text-xs uppercase tracking-[0.22em] text-ok">Paired</div>
<h1 className="font-display text-2xl text-ink mt-2 tracking-tight">
Device linked
</h1>
<p className="text-sm text-ink-muted mt-3 leading-relaxed">
Your devices will recognize each other automatically from now on.
</p>
<p className="text-xs text-ink-faint mt-6 font-mono uppercase tracking-widest">
Redirecting
</p>
</div>
</div>
);
}

View File

@ -37,9 +37,6 @@ interface AppState {
transfers: TransferInfo[]; transfers: TransferInfo[];
incomingRequest: IncomingRequest | null; incomingRequest: IncomingRequest | null;
// Pairing
pairCode: string | null;
// UI // UI
showTextModal: boolean; showTextModal: boolean;
selectedPeerId: string | null; selectedPeerId: string | null;
@ -57,7 +54,6 @@ interface AppState {
updateTransfer: (id: string, updates: Partial<TransferInfo>) => void; updateTransfer: (id: string, updates: Partial<TransferInfo>) => void;
removeTransfer: (id: string) => void; removeTransfer: (id: string) => void;
setIncomingRequest: (request: IncomingRequest | null) => void; setIncomingRequest: (request: IncomingRequest | null) => void;
setPairCode: (code: string | null) => void;
setShowTextModal: (show: boolean) => void; setShowTextModal: (show: boolean) => void;
setSelectedPeerId: (peerId: string | null) => void; setSelectedPeerId: (peerId: string | null) => void;
setError: (error: string | null) => void; setError: (error: string | null) => void;
@ -74,7 +70,6 @@ const initialState = {
publicRoomExpiresAt: null, publicRoomExpiresAt: null,
transfers: [], transfers: [],
incomingRequest: null, incomingRequest: null,
pairCode: null,
showTextModal: false, showTextModal: false,
selectedPeerId: null, selectedPeerId: null,
error: null, error: null,
@ -119,8 +114,6 @@ export const useStore = create<AppState>((set) => ({
setIncomingRequest: (request) => set({ incomingRequest: request }), setIncomingRequest: (request) => set({ incomingRequest: request }),
setPairCode: (code) => set({ pairCode: code }),
setShowTextModal: (show) => set({ showTextModal: show }), setShowTextModal: (show) => set({ showTextModal: show }),
setSelectedPeerId: (peerId) => set({ selectedPeerId: peerId }), setSelectedPeerId: (peerId) => set({ selectedPeerId: peerId }),