All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 43s
Adds a groupId-based pairing system so devices can always see each other regardless of network. Scan a QR code once from the other device, and they're permanently linked via a shared group stored in localStorage. No account, no email — just one-time QR scan like Bluetooth pairing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
175 lines
3.7 KiB
TypeScript
175 lines
3.7 KiB
TypeScript
import type { DeviceType } from "./names.js";
|
|
|
|
// ── Client → Server messages ──
|
|
|
|
export interface HelloMessage {
|
|
type: "hello";
|
|
deviceId: string;
|
|
deviceName: string;
|
|
deviceType: DeviceType;
|
|
avatar?: string;
|
|
localIP?: string; // e.g. "192.168.1.42" — used for LAN detection fallback
|
|
groupId?: string; // permanent device group for cross-network pairing
|
|
joinCode?: string;
|
|
}
|
|
|
|
export interface CreatePublicRoomMessage {
|
|
type: "create-public-room";
|
|
}
|
|
|
|
export interface SignalMessage {
|
|
type: "signal";
|
|
to: string;
|
|
data: unknown;
|
|
}
|
|
|
|
export interface LeaveMessage {
|
|
type: "leave";
|
|
}
|
|
|
|
/** Client sends its push subscription so the server can wake it when offline */
|
|
export interface SubscribePushMessage {
|
|
type: "subscribe-push";
|
|
deviceId: string;
|
|
subscription: PushSubscriptionJSON;
|
|
}
|
|
|
|
/** Client asks the server to send a push notification to wake a specific offline device */
|
|
export interface WakePeerMessage {
|
|
type: "wake-peer";
|
|
deviceId: string;
|
|
}
|
|
|
|
export type ClientMessage =
|
|
| HelloMessage
|
|
| CreatePublicRoomMessage
|
|
| SignalMessage
|
|
| LeaveMessage
|
|
| SubscribePushMessage
|
|
| WakePeerMessage;
|
|
|
|
// ── Server → Client messages ──
|
|
|
|
export interface PeerInfo {
|
|
peerId: string;
|
|
displayName: string;
|
|
deviceType: DeviceType;
|
|
avatar?: string;
|
|
online?: boolean; // default true; false = offline but reachable via push
|
|
deviceId?: string; // for offline peers (used to wake them)
|
|
}
|
|
|
|
export interface WelcomeMessage {
|
|
type: "welcome";
|
|
peerId: string;
|
|
roomId: string;
|
|
peers: PeerInfo[];
|
|
vapidPublicKey?: string;
|
|
}
|
|
|
|
export interface PublicRoomCreatedMessage {
|
|
type: "public-room-created";
|
|
code: string;
|
|
url: string;
|
|
expiresAt: string;
|
|
}
|
|
|
|
export interface PeerJoinedMessage {
|
|
type: "peer-joined";
|
|
peerId: string;
|
|
displayName: string;
|
|
deviceType: DeviceType;
|
|
avatar?: string;
|
|
}
|
|
|
|
export interface PeerLeftMessage {
|
|
type: "peer-left";
|
|
peerId: string;
|
|
}
|
|
|
|
export interface SignalRelayMessage {
|
|
type: "signal";
|
|
from: string;
|
|
data: unknown;
|
|
}
|
|
|
|
export type ErrorCode = "room-not-found" | "room-expired" | "rate-limit";
|
|
|
|
export interface ErrorMessage {
|
|
type: "error";
|
|
code: ErrorCode;
|
|
message: string;
|
|
}
|
|
|
|
export type ServerMessage =
|
|
| WelcomeMessage
|
|
| PublicRoomCreatedMessage
|
|
| PeerJoinedMessage
|
|
| PeerLeftMessage
|
|
| SignalRelayMessage
|
|
| ErrorMessage;
|
|
|
|
// ── Data channel messages (peer-to-peer) ──
|
|
|
|
export interface FileMetaMessage {
|
|
type: "file-meta";
|
|
id: string;
|
|
name: string;
|
|
size: number;
|
|
mime: string;
|
|
}
|
|
|
|
export interface FileEndMessage {
|
|
type: "file-end";
|
|
id: string;
|
|
}
|
|
|
|
export interface TextMessage {
|
|
type: "text";
|
|
id: string;
|
|
content: string;
|
|
}
|
|
|
|
export interface TransferRequestMessage {
|
|
type: "transfer-request";
|
|
files: { id: string; name: string; size: number; mime: string }[];
|
|
text?: string;
|
|
}
|
|
|
|
export interface TransferResponseMessage {
|
|
type: "transfer-response";
|
|
accepted: boolean;
|
|
}
|
|
|
|
export type DataChannelMessage =
|
|
| FileMetaMessage
|
|
| FileEndMessage
|
|
| TextMessage
|
|
| TransferRequestMessage
|
|
| TransferResponseMessage;
|
|
|
|
// ── Push notification payload ──
|
|
|
|
export interface PushPayload {
|
|
type: "peer-nearby";
|
|
displayName: string;
|
|
deviceType: DeviceType;
|
|
}
|
|
|
|
// ── Constants ──
|
|
|
|
export const CHUNK_SIZE = 16 * 1024; // 16 KB
|
|
export const MAX_BUFFER = 1024 * 1024; // 1 MB backpressure threshold
|
|
|
|
export const SHORT_CODE_ALPHABET = "abcdefghjkmnpqrstuvwxyz23456789";
|
|
export const SHORT_CODE_LENGTH = 3;
|
|
export const SHORT_CODE_MAX_LENGTH = 4;
|
|
export const SHORT_CODE_MAX_RETRIES = 5;
|
|
|
|
export const PUBLIC_ROOM_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
|
|
export const ICE_SERVERS: RTCIceServer[] = [
|
|
{ urls: "stun:stun.l.google.com:19302" },
|
|
{ urls: "stun:stun1.l.google.com:19302" },
|
|
];
|