improve websocket typing

This commit is contained in:
Zoe
2025-09-16 15:50:12 +00:00
parent cad5d6d98e
commit 80b83a8e93
14 changed files with 426 additions and 367 deletions

9
.prettierignore Normal file
View File

@@ -0,0 +1,9 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock
bun.lock
bun.lockb
# Miscellaneous
/static/

34
.prettierrc Normal file
View File

@@ -0,0 +1,34 @@
{
"arrowParens": "always",
"bracketSameLine": true,
"objectWrap": "preserve",
"bracketSpacing": true,
"semi": true,
"experimentalOperatorPosition": "end",
"experimentalTernaries": false,
"singleQuote": false,
"jsxSingleQuote": false,
"quoteProps": "as-needed",
"trailingComma": "all",
"singleAttributePerLine": false,
"htmlWhitespaceSensitivity": "css",
"vueIndentScriptAndStyle": true,
"proseWrap": "preserve",
"insertPragma": false,
"printWidth": 100,
"plugins": [
"prettier-plugin-svelte"
],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
],
"requirePragma": false,
"tabWidth": 4,
"useTabs": false,
"embeddedLanguageFormatting": "auto"
}

View File

@@ -23,6 +23,8 @@
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"@tailwindcss/vite": "^4.0.0",
"@types/polka": "^0.5.7",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^4.0.0",
@@ -357,6 +359,10 @@
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
"prettier-plugin-svelte": ["prettier-plugin-svelte@3.4.0", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],

View File

@@ -1,43 +1,47 @@
{
"name": "wormhole",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"@tailwindcss/vite": "^4.0.0",
"@types/polka": "^0.5.7",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^7.0.4"
},
"dependencies": {
"@hpke/chacha20poly1305": "^1.7.1",
"@hpke/core": "^1.7.4",
"@hpke/hybridkem-x-wing": "^0.6.1",
"@noble/ciphers": "^1.3.0",
"@noble/curves": "^1.9.0",
"@sveltejs/adapter-node": "^5.3.1",
"@types/streamsaver": "^2.0.5",
"@types/ws": "^8.18.1",
"polka": "^0.5.2",
"streamsaver": "^2.0.6",
"ts-mls": "^1.1.0",
"ws": "^8.18.3"
},
"trustedDependencies": [
"@tailwindcss/oxide"
]
"name": "wormhole",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
"lint": "prettier --check ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"@tailwindcss/vite": "^4.0.0",
"@types/polka": "^0.5.7",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^7.0.4"
},
"dependencies": {
"@hpke/chacha20poly1305": "^1.7.1",
"@hpke/core": "^1.7.4",
"@hpke/hybridkem-x-wing": "^0.6.1",
"@noble/ciphers": "^1.3.0",
"@noble/curves": "^1.9.0",
"@sveltejs/adapter-node": "^5.3.1",
"@types/streamsaver": "^2.0.5",
"@types/ws": "^8.18.1",
"polka": "^0.5.2",
"streamsaver": "^2.0.6",
"ts-mls": "^1.1.0",
"ws": "^8.18.3"
},
"trustedDependencies": [
"@tailwindcss/oxide"
]
}

View File

@@ -1,22 +1,22 @@
import { ws } from "$stores/websocketStore";
import { WebSocketMessageType } from "$types/websocket";
import { WebSocketRequestType, WebSocketResponseType } from "$types/websocket";
import { solveChallenge } from "./powUtil";
export async function doChallenge(additionalData: string = ""): Promise<{
challenge: string;
target: string;
nonce: string;
} | null> {
let roomChallenge: string | null = null;
let roomChallengeTarget: string | null = null;
let challengePromise = new Promise<string | null>((resolve) => {
let unsubscribe = ws.handleEvent(
WebSocketMessageType.CHALLENGE,
WebSocketResponseType.CHALLENGE_RESPONSE,
async (value) => {
unsubscribe();
roomChallenge = value.challenge;
roomChallengeTarget = value.target;
resolve(
await solveChallenge(
roomChallenge,
roomChallengeTarget,
value.difficulty,
additionalData,
),
@@ -26,7 +26,7 @@ export async function doChallenge(additionalData: string = ""): Promise<{
});
ws.send({
type: WebSocketMessageType.REQUEST_CHALLENGE,
type: WebSocketRequestType.CHALLENGE_REQUEST,
});
let challengeNonce = await challengePromise;
@@ -34,12 +34,12 @@ export async function doChallenge(additionalData: string = ""): Promise<{
throw new Error("Could not solve challenge within max iterations");
}
if (!roomChallenge) {
if (!roomChallengeTarget) {
throw new Error("No room challenge");
}
return {
challenge: roomChallenge,
target: roomChallengeTarget,
nonce: challengeNonce,
};
}

View File

@@ -1,5 +1,5 @@
import { WebSocketServer } from "ws";
import { Socket, WebSocketMessageType, type WebSocketMessage } from "../../types/websocket.ts";
import { RoomStatusType, Socket, WebSocketErrorType, WebSocketRequestType, WebSocketResponseType, WebSocketRoomMessageType, WebSocketWebRtcMessageType, type WebSocketMessage } from "../../types/websocket.ts";
import { LiveMap } from '../liveMap.ts';
import { hashStringSHA256 } from "../powUtil.ts";
@@ -69,7 +69,7 @@ async function createRoom(socket: Socket, roomName?: string): Promise<string> {
let room = rooms.set(roomId, new ServerRoom());
socket.send({ type: WebSocketMessageType.ROOM_CREATED, data: room.key });
socket.send({ type: WebSocketResponseType.ROOM_CREATED, data: room.key });
try {
await joinRoom(room.key, socket, true);
@@ -86,21 +86,21 @@ async function joinRoom(roomId: string, socket: Socket, initial?: boolean): Prom
// should be unreachable
if (!room) {
socket.send({ type: WebSocketMessageType.ERROR, data: errors.ROOM_NOT_FOUND });
socket.send({ type: WebSocketErrorType.ERROR, data: errors.ROOM_NOT_FOUND });
return undefined;
}
if (room.length == 2) {
socket.send({ type: WebSocketMessageType.ERROR, data: errors.ROOM_FULL });
socket.send({ type: WebSocketErrorType.ERROR, data: errors.ROOM_FULL });
return undefined;
}
// notify all clients in the room of the new client, except the client itself
room.notifyAll({ type: WebSocketMessageType.JOIN_ROOM, roomId });
room.notifyAll({ type: WebSocketRoomMessageType.PARTICIPANT_JOINED, roomId, participants: room.length });
room.push(socket);
socket.addEventListener('close', (ev) => {
room.notifyAll({ type: WebSocketMessageType.ROOM_LEFT, roomId });
room.notifyAll({ type: WebSocketRoomMessageType.PARTICIPANT_LEFT, roomId, participants: room.length });
// for some reason, when you filter the array when the length is 1 it stays at 1, but we *know* that if its 1
// then when this client disconnects, the room should be deleted since the room is empty
@@ -118,12 +118,13 @@ async function joinRoom(roomId: string, socket: Socket, initial?: boolean): Prom
room.set(room.filter(client => client.ws !== ev.target));
});
// sending the join message to the client who created the room is fucky
if (!initial) {
socket.send({ type: WebSocketMessageType.ROOM_JOINED, roomId: roomId, participants: room.length });
socket.send({ type: WebSocketResponseType.ROOM_JOINED, roomId: roomId, participants: room.length });
}
// TODO: consider letting rooms get larger than 2 clients
if (room.length == 2) {
room.forEachClient(client => client.send({ type: WebSocketMessageType.ROOM_READY, data: { isInitiator: client !== socket } }));
room.forEachClient(client => client.send({ type: WebSocketRoomMessageType.ROOM_READY, data: { isInitiator: client !== socket, roomId, participants: room.length } }));
}
console.log("Room created:", roomId, room.length);
@@ -148,17 +149,17 @@ function generateChallenge(): string {
return challenge;
}
async function validateChallenge(challenge: string, nonce: string, additionalData: string = ""): Promise<boolean> {
if (!outstandingChallenges.has(challenge)) {
async function validateChallenge(challenge: {target: string, nonce: string}, additionalData: string = ""): Promise<boolean> {
if (!outstandingChallenges.has(challenge.target)) {
return false;
}
let hash = await hashStringSHA256(`${additionalData}${challenge}${nonce}`);
let hash = await hashStringSHA256(`${additionalData}${challenge.target}${challenge.nonce}`);
let result = hash.startsWith('0'.repeat(CHALLENGE_DIFFICULTY));
if (result) {
console.log("Challenge solved:", challenge);
clearTimeout(outstandingChallenges.get(challenge)!);
outstandingChallenges.delete(challenge);
clearTimeout(outstandingChallenges.get(challenge.target)!);
outstandingChallenges.delete(challenge.target);
}
return result;
@@ -170,7 +171,7 @@ function leaveRoom(roomId: string, socket: Socket): ServerRoom | undefined {
// should be unreachable
if (!room) {
socket.send({ type: WebSocketMessageType.ERROR, data: errors.ROOM_NOT_FOUND });
socket.send({ type: WebSocketErrorType.ERROR, data: errors.ROOM_NOT_FOUND });
return undefined;
}
@@ -187,7 +188,7 @@ function leaveRoom(roomId: string, socket: Socket): ServerRoom | undefined {
room.set(room.filter(client => client !== socket));
socket.send({ type: WebSocketMessageType.ROOM_LEFT, roomId });
socket.send({ type: WebSocketResponseType.ROOM_LEFT, roomId });
return room;
}
@@ -217,7 +218,7 @@ export function confgiureWebsocketServer(wss: WebSocketServer) {
if (message === undefined) {
console.log("Received non-JSON message:", event);
// If the message is not JSON, send an error message
socket.send({ type: WebSocketMessageType.ERROR, data: errors.MALFORMED_MESSAGE });
socket.send({ type: WebSocketErrorType.ERROR, data: errors.MALFORMED_MESSAGE });
return;
}
@@ -226,14 +227,14 @@ export function confgiureWebsocketServer(wss: WebSocketServer) {
let room: ServerRoom | undefined = undefined;
switch (message.type) {
case WebSocketMessageType.CREATE_ROOM:
if (!message.nonce || !message.challenge) {
socket.send({ type: WebSocketMessageType.ERROR, data: errors.MISSING_DATA });
case WebSocketRequestType.CREATE_ROOM:
if (!message.challenge || !message.challenge.target || !message.challenge.nonce) {
socket.send({ type: WebSocketErrorType.ERROR, data: errors.MISSING_DATA });
return;
}
if (!await validateChallenge(message.challenge, message.nonce)) {
socket.send({ type: WebSocketMessageType.ERROR, data: errors.INVALID_CHALLENGE });
if (!await validateChallenge(message.challenge)) {
socket.send({ type: WebSocketErrorType.ERROR, data: errors.INVALID_CHALLENGE });
return;
}
@@ -251,23 +252,23 @@ export function confgiureWebsocketServer(wss: WebSocketServer) {
await createRoom(socket, message.roomName);
} catch (e: any) {
socket.send({ type: WebSocketMessageType.ERROR, data: e.message });
socket.send({ type: WebSocketErrorType.ERROR, data: e.message });
throw e;
}
break;
case WebSocketMessageType.JOIN_ROOM:
if (!message.roomId || !message.nonce || !message.challenge) {
socket.send({ type: WebSocketMessageType.ERROR, data: errors.MISSING_DATA });
case WebSocketRequestType.ROOM_JOIN:
if (!message.roomId || !message.challenge || !message.challenge.target || !message.challenge.nonce) {
socket.send({ type: WebSocketErrorType.ERROR, data: errors.MISSING_DATA });
return;
}
if (!await validateChallenge(message.challenge, message.nonce, message.roomId)) {
socket.send({ type: WebSocketMessageType.ERROR, data: errors.INVALID_CHALLENGE });
if (!await validateChallenge(message.challenge, message.roomId)) {
socket.send({ type: WebSocketErrorType.ERROR, data: errors.INVALID_CHALLENGE });
return;
}
if (rooms.get(message.roomId) == undefined) {
socket.send({ type: WebSocketMessageType.ERROR, data: errors.ROOM_NOT_FOUND });
socket.send({ type: WebSocketErrorType.ERROR, data: errors.ROOM_NOT_FOUND });
return;
}
@@ -275,14 +276,14 @@ export function confgiureWebsocketServer(wss: WebSocketServer) {
if (!room) return;
break;
case WebSocketMessageType.LEAVE_ROOM:
case WebSocketRequestType.ROOM_LEAVE:
if (!message.roomId) {
socket.send({ type: WebSocketMessageType.ERROR, data: errors.MALFORMED_MESSAGE });
socket.send({ type: WebSocketErrorType.ERROR, data: errors.MALFORMED_MESSAGE });
return;
}
if (rooms.get(message.roomId) == undefined) {
socket.send({ type: WebSocketMessageType.ERROR, data: errors.ROOM_NOT_FOUND });
socket.send({ type: WebSocketErrorType.ERROR, data: errors.ROOM_NOT_FOUND });
return;
}
@@ -290,27 +291,34 @@ export function confgiureWebsocketServer(wss: WebSocketServer) {
if (!room) return;
break;
case WebSocketMessageType.CHECK_ROOM_EXISTS:
if (!message.roomId || !message.nonce || !message.challenge) {
socket.send({ type: WebSocketMessageType.ERROR, data: errors.MISSING_DATA });
case WebSocketRequestType.ROOM_STATUS:
if (!message.roomId || !message.challenge || !message.challenge.target || !message.challenge.nonce) {
socket.send({ type: WebSocketErrorType.ERROR, data: errors.MISSING_DATA });
return;
}
if (!await validateChallenge(message.challenge, message.nonce, message.roomId)) {
socket.send({ type: WebSocketMessageType.ERROR, data: errors.INVALID_CHALLENGE });
if (!await validateChallenge(message.challenge, message.roomId)) {
socket.send({ type: WebSocketErrorType.ERROR, data: errors.INVALID_CHALLENGE });
return;
}
socket.send({ type: WebSocketMessageType.ROOM_STATUS, roomId: message.roomId, status: rooms.get(message.roomId) ? 'found' : 'not-found' });
break;
case WebSocketMessageType.REQUEST_CHALLENGE:
let challenge = generateChallenge();
let roomStatus = RoomStatusType.OPEN;
if (!rooms.get(message.roomId)) {
roomStatus = RoomStatusType.NOT_FOUND;
} else if (rooms.get(message.roomId)!.length === 2) {
roomStatus = RoomStatusType.OPEN;
}
socket.send({ type: WebSocketMessageType.CHALLENGE, challenge, difficulty: CHALLENGE_DIFFICULTY });
socket.send({ type: WebSocketResponseType.ROOM_STATUS, roomId: message.roomId, status: roomStatus });
break;
case WebSocketMessageType.WEBRTC_OFFER:
case WebSocketMessageType.WERTC_ANSWER:
case WebSocketMessageType.WEBRTC_ICE_CANDIDATE:
case WebSocketRequestType.CHALLENGE_REQUEST:
let target = generateChallenge();
socket.send({ type: WebSocketResponseType.CHALLENGE_RESPONSE, target, difficulty: CHALLENGE_DIFFICULTY });
break;
case WebSocketWebRtcMessageType.OFFER:
case WebSocketWebRtcMessageType.ANSWER:
case WebSocketWebRtcMessageType.ICE_CANDIDATE:
// relay these messages to the other peers in the room
room = rooms.get(message.data.roomId);
@@ -324,7 +332,7 @@ export function confgiureWebsocketServer(wss: WebSocketServer) {
break;
default:
console.warn(`Unknown message type: ${message.type}`);
socket.send({ type: WebSocketMessageType.ERROR, data: errors.UNKNOWN_MESSAGE_TYPE });
socket.send({ type: WebSocketErrorType.ERROR, data: errors.UNKNOWN_MESSAGE_TYPE });
break;
}
});

View File

@@ -1,9 +1,9 @@
import { ws } from '$stores/websocketStore';
import { WebSocketMessageType } from '$types/websocket';
import { WebRTCPacketType, type WebRTCPeerCallbacks } from '$types/webrtc';
import { browser } from '$app/environment';
import { createApplicationMessage, createCommit, createGroup, decodeMlsMessage, defaultCapabilities, defaultLifetime, emptyPskIndex, encodeMlsMessage, generateKeyPackage, getCiphersuiteFromName, getCiphersuiteImpl, joinGroup, processPrivateMessage, type CiphersuiteImpl, type ClientState, type Credential, type KeyPackage, type PrivateKeyPackage, type Proposal } from 'ts-mls';
import { WebSocketWebRtcMessageType } from '$types/websocket';
export class WebRTCPeer {
private peer: RTCPeerConnection | null = null;
@@ -35,7 +35,7 @@ export class WebRTCPeer {
private sendIceCandidate(candidate: RTCIceCandidate) {
ws.send({
type: WebSocketMessageType.WEBRTC_ICE_CANDIDATE,
type: WebSocketWebRtcMessageType.ICE_CANDIDATE,
data: {
roomId: this.roomId,
candidate: candidate,
@@ -261,7 +261,7 @@ export class WebRTCPeer {
await this.peer.setLocalDescription(offer)
ws.send({
type: WebSocketMessageType.WEBRTC_OFFER,
type: WebSocketWebRtcMessageType.OFFER,
data: {
roomId: this.roomId,
sdp: offer,
@@ -295,7 +295,7 @@ export class WebRTCPeer {
console.log("Sending answer", answer);
ws.send({
type: WebSocketMessageType.WERTC_ANSWER,
type: WebSocketWebRtcMessageType.ANSWER,
data: {
roomId: this.roomId,
sdp: answer,
@@ -353,7 +353,7 @@ export class WebRTCPeer {
this.send(keyPackageMessageBuf, WebRTCPacketType.KEY_PACKAGE);
}
public async send(data: ArrayBuffer, type: WebRTCPacketType) {
public async send(data: ArrayBufferLike, type: WebRTCPacketType) {
console.log("Sending message of type", type, "with data", data);
if (!this.dataChannel || this.dataChannel.readyState !== 'open') throw new Error('Data channel not initialized');

View File

@@ -2,10 +2,10 @@ import { writable, get, type Writable } from "svelte/store";
import { WebRTCPeer } from "$lib/webrtc";
import { WebRTCPacketType } from "$types/webrtc";
import { room } from "$stores/roomStore";
import { RoomConnectionState, type Room } from "$types/websocket";
import { RoomConnectionState, WebSocketErrorType, WebSocketResponseType, WebSocketRoomMessageType, WebSocketWebRtcMessageType, type Room } from "$types/websocket";
import { advertisedOffers, fileRequestIds, messages, receivedOffers } from "$stores/messageStore";
import { MessageType, type Message } from "$types/message";
import { WebSocketMessageType, type WebSocketMessage } from "$types/websocket";
import { type WebSocketMessage } from "$types/websocket";
import { WebBuffer } from "./buffer";
import { goto } from "$app/navigation";
@@ -257,30 +257,30 @@ export async function handleMessage(event: MessageEvent) {
const message: WebSocketMessage = JSON.parse(event.data);
switch (message.type) {
case WebSocketMessageType.ROOM_CREATED:
case WebSocketResponseType.ROOM_CREATED:
console.log("Room created:", message.data);
room.set({ id: message.data, host: true, RTCConnectionReady: false, connectionState: RoomConnectionState.CONNECTED, participants: 1 });
goto(`/${message.data}`);
return;
case WebSocketMessageType.JOIN_ROOM:
case WebSocketRoomMessageType.PARTICIPANT_JOINED:
console.log("new client joined room");
room.update((room) => ({ ...room, participants: room.participants + 1 }));
return;
case WebSocketMessageType.ROOM_JOINED:
case WebSocketResponseType.ROOM_JOINED:
// TODO: if a client disconnects, we need to resync the room state
room.set({ host: false, id: message.roomId, RTCConnectionReady: false, connectionState: RoomConnectionState.CONNECTED, participants: message.participants });
console.log("Joined room");
return;
case WebSocketMessageType.ROOM_LEFT:
case WebSocketRoomMessageType.PARTICIPANT_LEFT:
room.update((room) => ({ ...room, participants: room.participants - 1 }));
console.log("Participant left room");
return;
case WebSocketMessageType.ERROR:
case WebSocketErrorType.ERROR:
console.error("Error:", message.data);
error.set(message.data);
return;
case WebSocketMessageType.ROOM_READY:
case WebSocketRoomMessageType.ROOM_READY:
let roomId = get(room).id;
if (roomId === null) {
@@ -307,20 +307,20 @@ export async function handleMessage(event: MessageEvent) {
}
switch (message.type) {
case WebSocketMessageType.WEBRTC_OFFER:
case WebSocketWebRtcMessageType.OFFER:
console.log("Received offer");
await get(peer)?.setRemoteDescription(
new RTCSessionDescription(message.data.sdp),
);
await get(peer)?.createAnswer();
return;
case WebSocketMessageType.WERTC_ANSWER:
case WebSocketWebRtcMessageType.ANSWER:
console.log("Received answer");
await get(peer)?.setRemoteDescription(
new RTCSessionDescription(message.data.sdp),
);
return;
case WebSocketMessageType.WEBRTC_ICE_CANDIDATE:
case WebSocketWebRtcMessageType.ICE_CANDIDATE:
console.log("Received ICE candidate");
await get(peer)?.addIceCandidate(message.data.candidate);
return;

View File

@@ -15,11 +15,7 @@
ws.subscribe((newWs) => {
if (newWs.status === WebsocketConnectionState.CONNECTED) {
console.log(
"Connected to websocket server, room id:",
$room.id,
"reconnecting",
);
console.log("Connected to websocket server, room id:", $room.id, "reconnecting");
}
});
@@ -33,35 +29,29 @@
as="font"
type="font/woff2"
crossorigin="anonymous"
href="/fonts/InstrumentSans-VariableFont_wdth,wght.woff2"
/>
href="/fonts/InstrumentSans-VariableFont_wdth,wght.woff2" />
{#if process.env.NODE_ENV !== "production"}
<!-- Debug console. Particularly useful for debugging on mobile devices -->
<script src="https://cdn.jsdelivr.net/npm/eruda"></script>
<script>
eruda.init();
</script>
{/if}
<script
src="https://cdn.jsdelivr.net/npm/web-streams-polyfill@2.0.2/dist/ponyfill.min.js"
></script>
<script
src="https://cdn.jsdelivr.net/npm/streamsaver@2.0.3/StreamSaver.min.js"
></script>
src="https://cdn.jsdelivr.net/npm/web-streams-polyfill@2.0.2/dist/ponyfill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/streamsaver@2.0.3/StreamSaver.min.js"></script>
</svelte:head>
<header class="p-5">
<div class="flex justify-between items-center max-w-7xl px-5 mx-auto">
<div class="text-2xl font-bold text-white">
<a href="/" class="!text-white !no-underline"
>Noctis<span class="text-accent">.</span></a
>
<a href="/" class="!text-white !no-underline">Noctis<span class="text-accent">.</span>
</a>
</div>
<nav>
<a
href="https://github.com/juls0730/noctis"
target="_blank"
rel="noopener noreferrer">GitHub</a
>
<a href="https://github.com/juls0730/noctis" target="_blank" rel="noopener noreferrer">
GitHub
</a>
</nav>
</div>
</header>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { WebsocketConnectionState, ws } from "$stores/websocketStore";
import { WebSocketMessageType } from "$types/websocket";
import { WebSocketRequestType } from "$types/websocket";
import { writable, type Writable } from "svelte/store";
import LoadingSpinner from "$components/LoadingSpinner.svelte";
import { doChallenge } from "$lib/challenge";
@@ -18,10 +18,12 @@
}
ws.send({
type: WebSocketMessageType.CREATE_ROOM,
type: WebSocketRequestType.CREATE_ROOM,
roomName: roomId,
nonce: challengeResult.nonce,
challenge: challengeResult.challenge,
challenge: {
target: challengeResult.target,
nonce: challengeResult.nonce,
}
});
console.log("Created room:", roomId);
@@ -35,72 +37,63 @@
<div class="max-w-6xl px-5 mx-auto flex flex-col items-center">
<h1 class="font-bold">Your Private, Peer-to-Peer Chat Room</h1>
<p class="max-w-xl mx-8">
End-to-end encrypted. Peer-to-peer. No servers. No sign-ups. Just
chat.
End-to-end encrypted. Peer-to-peer. No servers. No sign-ups. Just chat.
</p>
<div
class="bg-surface p-10 rounded-xl max-w-xl shadow-xl border border-[#21293b] mt-10 mr-auto ml-auto w-full"
>
class="bg-surface p-10 rounded-xl max-w-xl shadow-xl border border-[#21293b] mt-10 mr-auto ml-auto w-full">
<form class="flex flex-col gap-5" id="roomForm">
<button
onclick={createRoom}
disabled={$ws.status !==
WebsocketConnectionState.CONNECTED || $roomLoading}
class="py-4 px-8 text-xl font-semibold bg-accent text-[#121826] disabled:opacity-50 disabled:cursor-not-allowed rounded-lg cursor-pointer transition-[background-color,_translate,_box-shadow] ease-out duration-200 w-full inline-flex justify-center items-center gap-2.5 hover:bg-[#00f0c8] hover:-translate-y-1 hover:shadow-md shadow-accent/20"
>
disabled={$ws.status !== WebsocketConnectionState.CONNECTED || $roomLoading}
class="py-4 px-8 text-xl font-semibold bg-accent text-[#121826] disabled:opacity-50 disabled:cursor-not-allowed rounded-lg cursor-pointer transition-[background-color,_translate,_box-shadow] ease-out duration-200 w-full inline-flex justify-center items-center gap-2.5 hover:bg-[#00f0c8] hover:-translate-y-1 hover:shadow-md shadow-accent/20">
{#if $ws.status !== WebsocketConnectionState.CONNECTED}
<span class="flex items-center"
><LoadingSpinner /> Connecting to server...</span
>
<span class="flex items-center">
<LoadingSpinner /> Connecting to server...
</span>
{:else if $roomLoading}
<span class="flex items-center"
><LoadingSpinner /> Creating Room...</span
>
<span class="flex items-center">
<LoadingSpinner /> Creating Room...
</span>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><path
viewBox="0 0 24 24">
<!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
<path
fill="currentColor"
d="M12 2a5 5 0 0 1 5 5v3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3v-6a3 3 0 0 1 3-3V7a5 5 0 0 1 5-5m0 12a2 2 0 0 0-1.995 1.85L10 16a2 2 0 1 0 2-2m0-10a3 3 0 0 0-3 3v3h6V7a3 3 0 0 0-3-3"
/></svg
>
d="M12 2a5 5 0 0 1 5 5v3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3v-6a3 3 0 0 1 3-3V7a5 5 0 0 1 5-5m0 12a2 2 0 0 0-1.995 1.85L10 16a2 2 0 1 0 2-2m0-10a3 3 0 0 0-3 3v3h6V7a3 3 0 0 0-3-3" />
</svg>
Create Secure Room
{/if}
</button>
<div
class="{$showRoomNameInput
? 'max-h-32'
: 'max-h-0 opacity-0'} overflow-hidden transition-[max-height,_opacity] duration-700"
>
: 'max-h-0 opacity-0'} overflow-hidden transition-[max-height,_opacity] duration-700">
<label
aria-hidden={!$showRoomNameInput}
for="roomNameInput"
class="text-paragraph block text-sm font-medium mb-2 text-left"
>Enter a custom room name</label
>
class="text-paragraph block text-sm font-medium mb-2 text-left">
Enter a custom room name
</label>
<input
type="text"
id="roomNameInput"
bind:value={$roomName}
class="placeholder:text-paragraph-muted w-full py-3 px-4 rounded-lg border border-[#2c3444] bg-[#232b3e] text-paragraph transition-[border-color,_box-shadow] duration-300 ease-in-out focus:outline-none focus:border-accent focus:shadow-sm shadow-accent/20"
placeholder="e.g., private-chat"
/>
placeholder="e.g., private-chat" />
</div>
<span
class="text-paragraph {$showRoomNameInput
? 'hidden'
: '-mt-5'}"
>or <button
<span class="text-paragraph {$showRoomNameInput ? 'hidden' : '-mt-5'}">
or <button
id="showCustomNameLink"
class="cursor-pointer underline hover:no-underline text-accent"
onclick={() => showRoomNameInput.set(true)}
>choose a custom room name</button
></span
>
onclick={() => showRoomNameInput.set(true)}>
choose a custom room name
</button>
</span>
</form>
</div>
</div>
@@ -112,39 +105,35 @@
<div class="mt-10 flex justify-around gap-8 flex-wrap">
<div class="text-center max-w-3xs">
<div
class="text-2xl font-bold w-12 h-12 leading-12 rounded-full bg-primary text-accent mx-auto mb-5"
>
class="text-2xl font-bold w-12 h-12 leading-12 rounded-full bg-primary text-accent mx-auto mb-5">
1
</div>
<h3>Create a Room</h3>
<p>
Click the button above to create a random room instantly, no
personal info required.
Click the button above to create a random room instantly, no personal info
required.
</p>
</div>
<div class="text-center max-w-3xs">
<div
class="text-2xl font-bold w-12 h-12 leading-12 rounded-full bg-primary text-accent mx-auto mb-5"
>
class="text-2xl font-bold w-12 h-12 leading-12 rounded-full bg-primary text-accent mx-auto mb-5">
2
</div>
<h3>Share the Link</h3>
<p>
You'll get a unique link to your private room. Share this
link with anyone you want to chat with securely.
You'll get a unique link to your private room. Share this link with anyone you
want to chat with securely.
</p>
</div>
<div class="text-center max-w-3xs">
<div
class="text-2xl font-bold w-12 h-12 leading-12 rounded-full bg-primary text-accent mx-auto mb-5"
>
class="text-2xl font-bold w-12 h-12 leading-12 rounded-full bg-primary text-accent mx-auto mb-5">
3
</div>
<h3>Chat Privately</h3>
<p>
Once they join, your messages are sent directly between your
devices, encrypted from end to end. Hidden from everyone
else.
Once they join, your messages are sent directly between your devices, encrypted
from end to end. Hidden from everyone else.
</p>
</div>
</div>
@@ -154,93 +143,80 @@
<section class="py-20">
<div class="max-w-6xl px-10 mx-auto">
<h2 class="font-semibold">Security by Design</h2>
<div
class="mt-10 grid grid-cols-[repeat(auto-fit,_minmax(300px,_1fr))] gap-8"
>
<div
class="bg-surface p-8 rounded-xl border border-[#21293b] text-center"
>
<div class="mt-10 grid grid-cols-[repeat(auto-fit,_minmax(300px,_1fr))] gap-8">
<div class="bg-surface p-8 rounded-xl border border-[#21293b] text-center">
<div
class="mb-5 bg-accent/10 w-16 h-16 rounded-full inline-flex justify-center items-center text-paragraph"
>
class="mb-5 bg-accent/10 w-16 h-16 rounded-full inline-flex justify-center items-center text-paragraph">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><g
viewBox="0 0 24 24">
<!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
><path
d="M5 13a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2z"
/><path
d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0-2 0m-3-5V7a4 4 0 1 1 8 0v4"
/></g
></svg
>
stroke-width="2">
<path
d="M5 13a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2z" />
<path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0-2 0m-3-5V7a4 4 0 1 1 8 0v4" />
</g>
</svg>
</div>
<h3 class="font-bold">End-to-End Encrypted</h3>
<p>
Only you and the people in your room can read the messages.
Your data is encrypted before its sent using the Message
Layer Security (MLS) protocol.
Only you and the people in your room can read the messages. Your data is
encrypted before its sent using the Message Layer Security (MLS) protocol.
</p>
</div>
<div
class="bg-surface p-8 rounded-xl border border-[#21293b] text-center"
>
<div class="bg-surface p-8 rounded-xl border border-[#21293b] text-center">
<div
class="mb-5 bg-accent/10 w-16 h-16 rounded-full inline-flex justify-center items-center text-paragraph"
>
class="mb-5 bg-accent/10 w-16 h-16 rounded-full inline-flex justify-center items-center text-paragraph">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><path
viewBox="0 0 24 24">
<!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 11V8a2 2 0 0 0-2-2h-6m0 0l3 3m-3-3l3-3M3 13.013v3a2 2 0 0 0 2 2h6m0 0l-3-3m3 3l-3 3m8-4.511a2 2 0 1 0 4.001-.001a2 2 0 0 0-4.001.001m-12-12a2 2 0 1 0 4.001-.001A2 2 0 0 0 4 4.502m17 16.997a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2m-6-12a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2"
/></svg
>
d="M21 11V8a2 2 0 0 0-2-2h-6m0 0l3 3m-3-3l3-3M3 13.013v3a2 2 0 0 0 2 2h6m0 0l-3-3m3 3l-3 3m8-4.511a2 2 0 1 0 4.001-.001a2 2 0 0 0-4.001.001m-12-12a2 2 0 1 0 4.001-.001A2 2 0 0 0 4 4.502m17 16.997a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2m-6-12a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2" />
</svg>
</div>
<h3 class="font-bold">Truly Peer-to-Peer</h3>
<p>
Your messages are sent directly from your device to the
recipient's. They never pass through a central server.
Your messages are sent directly from your device to the recipient's. They never
pass through a central server.
</p>
</div>
<div
class="bg-surface p-8 rounded-xl border border-[#21293b] text-center"
>
<div class="bg-surface p-8 rounded-xl border border-[#21293b] text-center">
<div
class="mb-5 bg-accent/10 w-16 h-16 rounded-full inline-flex justify-center items-center text-paragraph"
>
class="mb-5 bg-accent/10 w-16 h-16 rounded-full inline-flex justify-center items-center text-paragraph">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><path
viewBox="0 0 24 24">
<!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m3 3l18 18M7 3h7l5 5v7m0 4a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V5"
/></svg
>
d="m3 3l18 18M7 3h7l5 5v7m0 4a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V5" />
</svg>
</div>
<h3 class="font-bold">No Data Stored</h3>
<p>
We don't have accounts, and we don't store your messages.
Once you close the tab, the conversation is gone forever.
We don't have accounts, and we don't store your messages. Once you close the
tab, the conversation is gone forever.
</p>
</div>
</div>
@@ -253,19 +229,19 @@
&copy; {new Date().getFullYear()} Noctis - MIT License
<br />
Made with
<span class="text-accent"
><svg
<span class="text-accent">
<svg
class="inline-block"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><path
viewBox="0 0 24 24">
<!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
<path
fill="currentColor"
d="M6.979 3.074a6 6 0 0 1 4.988 1.425l.037.033l.034-.03a6 6 0 0 1 4.733-1.44l.246.036a6 6 0 0 1 3.364 10.008l-.18.185l-.048.041l-7.45 7.379a1 1 0 0 1-1.313.082l-.094-.082l-7.493-7.422A6 6 0 0 1 6.979 3.074"
/></svg
></span
>
d="M6.979 3.074a6 6 0 0 1 4.988 1.425l.037.033l.034-.03a6 6 0 0 1 4.733-1.44l.246.036a6 6 0 0 1 3.364 10.008l-.18.185l-.048.041l-7.45 7.379a1 1 0 0 1-1.313.082l-.094-.082l-7.493-7.422A6 6 0 0 1 6.979 3.074" />
</svg>
</span>
by
<a href="https://zoeissleeping.com">zoeissleeping</a>
</p>

View File

@@ -2,7 +2,7 @@
import { onMount } from "svelte";
import { room } from "$stores/roomStore";
import { WebsocketConnectionState, ws } from "$stores/websocketStore";
import { WebSocketMessageType } from "$types/websocket";
import { RoomStatusType, WebSocketRequestType, WebSocketResponseType } from "$types/websocket";
import { dataChannelReady, error } from "$lib/webrtcUtil";
import { goto } from "$app/navigation";
import RtcMessage from "$components/RTCMessage.svelte";
@@ -47,10 +47,12 @@
}
ws.send({
type: WebSocketMessageType.JOIN_ROOM,
type: WebSocketRequestType.ROOM_JOIN,
roomId: roomId!,
nonce: challengeResult.nonce,
challenge: challengeResult.challenge,
challenge: {
target: challengeResult.target,
nonce: challengeResult.nonce,
}
});
}
@@ -62,11 +64,7 @@
}
function handleLeave() {
if (
confirm(
"Are you sure you want to leave? The chat history will be deleted.",
)
) {
if (confirm("Are you sure you want to leave? The chat history will be deleted.")) {
// In a real app, this would disconnect the P2P session and redirect.
window.location.href = "/";
}
@@ -85,24 +83,23 @@
let challengeResult = await doChallenge(roomId);
if (challengeResult) {
let unsubscribe = ws.handleEvent(
WebSocketMessageType.ROOM_STATUS,
(value) => {
if (value.status === "found") {
unsubscribe();
roomExists = true;
} else if (value.status === "not-found") {
unsubscribe();
roomExists = false;
}
},
);
let unsubscribe = ws.handleEvent(WebSocketResponseType.ROOM_STATUS, (value) => {
if (value.status === RoomStatusType.OPEN) {
unsubscribe();
roomExists = true;
} else if (value.status === RoomStatusType.NOT_FOUND) {
unsubscribe();
roomExists = false;
}
});
ws.send({
type: WebSocketMessageType.CHECK_ROOM_EXISTS,
type: WebSocketRequestType.ROOM_STATUS,
roomId: roomId,
nonce: challengeResult.nonce,
challenge: challengeResult.challenge,
challenge: {
target: challengeResult.target,
nonce: challengeResult.nonce,
}
});
}
}
@@ -115,32 +112,28 @@
Something went wrong: {$error.toLocaleLowerCase()}
</h2>
<p class="!text-paragraph">
click <a href="/">here</a> to go back to the homepage
click <a href="/">here</a>
to go back to the homepage
</p>
{/if}
{#if !$error}
{#if isHost}
{#if !$room.RTCConnectionReady}
<h2 class="text-3xl font-bold text-white mb-2">
Your secure room is ready.
</h2>
<h2 class="text-3xl font-bold text-white mb-2">Your secure room is ready.</h2>
<p class="text-gray-400 mb-6 text-center">
Share the link below to invite someone to chat directly with
you. Once they join, you will be connected automatically.
Share the link below to invite someone to chat directly with you. Once they
join, you will be connected automatically.
</p>
<div
class="bg-gray-900 rounded-lg p-4 flex items-center justify-between gap-4 border border-gray-600"
>
<span
class="text-accent font-mono text-sm overflow-x-auto whitespace-nowrap"
>{roomLink}</span
>
class="bg-gray-900 rounded-lg p-4 flex items-center justify-between gap-4 border border-gray-600">
<span class="text-accent font-mono text-sm overflow-x-auto whitespace-nowrap">
{roomLink}
</span>
<button
onclick={handleCopyLink}
class="bg-accent hover:bg-accent/80 active:bg-accent/60 cursor-pointer text-gray-900 font-bold py-2 px-4 rounded-md transition-colors whitespace-nowrap"
>
class="bg-accent hover:bg-accent/80 active:bg-accent/60 cursor-pointer text-gray-900 font-bold py-2 px-4 rounded-md transition-colors whitespace-nowrap">
{copyButtonText}
</button>
</div>
@@ -150,35 +143,31 @@
{:else if awaitingJoinConfirmation}
{#if $ws.status !== WebsocketConnectionState.CONNECTED || roomExists === undefined}
<h2 class="text-3xl font-bold text-white mb-2">
<span class="flex items-center"
><LoadingSpinner size="24" /> Connecting to server...</span
>
<span class="flex items-center">
<LoadingSpinner size="24" /> Connecting to server...
</span>
</h2>
<p class="!text-paragraph">
click <a href="/">here</a> to go back to the homepage
click <a href="/">here</a>
to go back to the homepage
</p>
{:else if roomExists === false}
<h2 class="text-3xl font-bold text-white mb-2">
That room does not exist.
</h2>
<h2 class="text-3xl font-bold text-white mb-2">That room does not exist.</h2>
<p class="!text-paragraph">
click <a href="/">here</a> to go back to the homepage
click <a href="/">here</a>
to go back to the homepage
</p>
{:else}
<h2 class="text-3xl font-bold text-white mb-2">
You're invited to chat.
</h2>
<h2 class="text-3xl font-bold text-white mb-2">You're invited to chat.</h2>
<div class="flex flex-row gap-2">
<button
onclick={handleConfirmJoin}
class="bg-accent hover:bg-accent/80 active:bg-accent/60 cursor-pointer text-gray-900 font-bold py-2 px-4 rounded-md transition-colors whitespace-nowrap"
>
class="bg-accent hover:bg-accent/80 active:bg-accent/60 cursor-pointer text-gray-900 font-bold py-2 px-4 rounded-md transition-colors whitespace-nowrap">
Accept
</button>
<button
onclick={handleDeclineJoin}
class="bg-red-400 hover:bg-red-400/80 active:bg-red-400/60 cursor-pointer text-gray-900 font-bold py-2 px-4 rounded-md transition-colors whitespace-nowrap"
>
class="bg-red-400 hover:bg-red-400/80 active:bg-red-400/60 cursor-pointer text-gray-900 font-bold py-2 px-4 rounded-md transition-colors whitespace-nowrap">
Decline
</button>
</div>

View File

@@ -1,6 +1,6 @@
import { get, writable, type Readable, type Writable } from 'svelte/store';
import { browser } from '$app/environment';
import { Socket, WebSocketMessageType, type WebSocketMessage } from '$types/websocket';
import { Socket, type WebSocketMessage, type WebSocketMessageType } from '$types/websocket';
import { handleMessage } from '../lib/webrtcUtil';
export enum WebsocketConnectionState {

View File

@@ -11,143 +11,187 @@ export interface Room {
connectionState: RoomConnectionState;
}
export enum WebSocketMessageType {
// room messages
CREATE_ROOM = "create",
JOIN_ROOM = "join",
LEAVE_ROOM = "leave",
CHECK_ROOM_EXISTS = "check",
REQUEST_CHALLENGE = "request-challenge",
export interface Challenge {
// the answer is the sha256(additionalData + target + nonce)
target: string;
nonce: string;
}
// response messages
ROOM_CREATED = "created",
ROOM_JOINED = "joined",
ROOM_LEFT = "left",
ROOM_READY = "ready",
ROOM_STATUS = "status",
CHALLENGE = "challenge",
export enum WebSocketRequestType {
CREATE_ROOM = "create-room",
ROOM_JOIN = "join-room",
ROOM_LEAVE = "leave-room",
ROOM_STATUS = "get-room-status",
CHALLENGE_REQUEST = "get-challenge",
}
// webrtc messages
WEBRTC_OFFER = "offer",
WERTC_ANSWER = "answer",
WEBRTC_ICE_CANDIDATE = "ice-candidate",
export enum WebSocketResponseType {
ROOM_CREATED = "room-created",
ROOM_JOINED = "room-joined",
ROOM_LEFT = "room-left",
ROOM_STATUS = "room-status",
CHALLENGE_RESPONSE = "challenge",
}
// messages sent to room participants providing information about the room
export enum WebSocketRoomMessageType {
PARTICIPANT_LEFT = "peer-left",
ROOM_READY = "room-ready",
PARTICIPANT_JOINED = "peer-joined",
}
export enum WebSocketWebRtcMessageType {
OFFER = "offer",
ANSWER = "answer",
ICE_CANDIDATE = "ice-candidate",
}
export enum WebSocketErrorType {
ERROR = "error",
}
export type WebSocketMessageType = WebSocketRequestType | WebSocketResponseType | WebSocketRoomMessageType | WebSocketWebRtcMessageType | WebSocketErrorType;
// TODO: name the interfaces better
export type WebSocketMessage =
| CreateRoomMessage
| JoinRoomMessage
| LeaveRoomMessage
| CheckRoomExistsMessage
| RequestChallengeMessage
| RoomCreatedMessage
| RoomJoinedMessage
| RoomLeftMessage
| RoomStatusMessage
// request messages
| CreateRoomRequest
| JoinRoomRequest
| LeaveRoomRequest
| RoomStatusRequest
| ChallengeRequest
// response messages
| RoomCreatedResponse
| RoomJoinedResponse
| RoomLeftResponse
| RoomStatusResponse
| ChallengeResponse
// room messages
| ParticipantJoinedMessage
| ParticipantLeftMessage
| RoomReadyMessage
| ChallengeMessage
| OfferMessage
| AnswerMessage
| IceCandidateMessage
| ErrorMessage;
// webrtc messages
| WebRTCOfferMessage
| WebRTCAnswerMessage
| WebRTCIceCandidateMessage
// errors
| Error;
interface ErrorMessage {
type: WebSocketMessageType.ERROR;
interface Error {
type: WebSocketErrorType.ERROR;
data: string;
}
// ====== Query Messages ======
interface CreateRoomMessage {
type: WebSocketMessageType.CREATE_ROOM;
interface CreateRoomRequest {
type: WebSocketRequestType.CREATE_ROOM;
roomName?: string;
nonce: string;
challenge: string;
challenge: Challenge;
}
// TODO: this is used as a query message, but it's also used as a response message
interface JoinRoomMessage {
type: WebSocketMessageType.JOIN_ROOM;
interface JoinRoomRequest {
type: WebSocketRequestType.ROOM_JOIN;
roomId: string;
nonce?: string;
challenge?: string;
challenge: Challenge;
}
interface LeaveRoomMessage {
type: WebSocketMessageType.LEAVE_ROOM;
interface LeaveRoomRequest {
type: WebSocketRequestType.ROOM_LEAVE;
roomId: string;
}
interface CheckRoomExistsMessage {
type: WebSocketMessageType.CHECK_ROOM_EXISTS;
// if sha256(roomId + challenge + nonce) has a certain number of leading zeros, then we can give the status to the user
interface RoomStatusRequest {
type: WebSocketRequestType.ROOM_STATUS;
roomId: string;
nonce: string;
challenge: string;
challenge: Challenge;
}
interface RequestChallengeMessage {
type: WebSocketMessageType.REQUEST_CHALLENGE;
interface ChallengeRequest {
type: WebSocketRequestType.CHALLENGE_REQUEST;
}
// ====== Response Messages ======
interface RoomCreatedMessage {
type: WebSocketMessageType.ROOM_CREATED;
interface RoomCreatedResponse {
type: WebSocketResponseType.ROOM_CREATED;
data: string;
}
interface RoomJoinedMessage {
type: WebSocketMessageType.ROOM_JOINED;
interface RoomJoinedResponse {
type: WebSocketResponseType.ROOM_JOINED;
roomId: string;
participants: number;
}
interface RoomLeftMessage {
type: WebSocketMessageType.ROOM_LEFT;
interface RoomLeftResponse {
type: WebSocketResponseType.ROOM_LEFT;
roomId: string;
}
interface RoomStatusMessage {
type: WebSocketMessageType.ROOM_STATUS;
interface RoomStatusRequest {
type: WebSocketRequestType.ROOM_STATUS;
roomId: string;
status: 'found' | 'not-found';
}
export enum RoomStatusType {
OPEN = "open",
FULL = "full",
NOT_FOUND = "not-found",
}
interface RoomStatusResponse {
type: WebSocketResponseType.ROOM_STATUS;
roomId: string;
status: RoomStatusType;
}
interface ChallengeResponse {
type: WebSocketResponseType.CHALLENGE_RESPONSE;
target: string;
difficulty: number;
}
// ====== Room messages ======
interface ParticipantJoinedMessage {
type: WebSocketRoomMessageType.PARTICIPANT_JOINED;
roomId: string;
participants: number;
}
interface ParticipantLeftMessage {
type: WebSocketRoomMessageType.PARTICIPANT_LEFT;
roomId: string;
participants: number;
}
interface RoomReadyMessage {
type: WebSocketMessageType.ROOM_READY;
type: WebSocketRoomMessageType.ROOM_READY;
data: {
isInitiator: boolean;
roomId: string;
participants: number;
};
}
interface ChallengeMessage {
type: WebSocketMessageType.CHALLENGE;
challenge: string;
difficulty: number;
}
// ====== WebRTC signaling messages ======
// as the server, we dont do anything with these messages other than relay them to the other peers in the room
interface OfferMessage {
type: WebSocketMessageType.WEBRTC_OFFER;
interface WebRTCOfferMessage {
type: WebSocketWebRtcMessageType.OFFER;
data: {
roomId: string;
sdp: RTCSessionDescriptionInit;
};
}
interface AnswerMessage {
type: WebSocketMessageType.WERTC_ANSWER;
interface WebRTCAnswerMessage {
type: WebSocketWebRtcMessageType.ANSWER;
data: {
roomId: string;
sdp: RTCSessionDescriptionInit;
};
}
interface IceCandidateMessage {
type: WebSocketMessageType.WEBRTC_ICE_CANDIDATE;
interface WebRTCIceCandidateMessage {
type: WebSocketWebRtcMessageType.ICE_CANDIDATE;
data: {
roomId: string;
candidate: RTCIceCandidateInit;

View File

@@ -15,7 +15,6 @@ const config = {
$stores: './src/stores',
$components: './src/components',
$types: './src/types',
'$lib/server': './src/lib/server',
}
},
compilerOptions: {