improve websocket typing
This commit is contained in:
9
.prettierignore
Normal file
9
.prettierignore
Normal 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
34
.prettierrc
Normal 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"
|
||||
}
|
||||
6
bun.lock
6
bun.lock
@@ -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=="],
|
||||
|
||||
86
package.json
86
package.json
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 @@
|
||||
© {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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -15,7 +15,6 @@ const config = {
|
||||
$stores: './src/stores',
|
||||
$components: './src/components',
|
||||
$types: './src/types',
|
||||
'$lib/server': './src/lib/server',
|
||||
}
|
||||
},
|
||||
compilerOptions: {
|
||||
|
||||
Reference in New Issue
Block a user