├── .gitignore
├── client
├── src
│ ├── react-app-env.d.ts
│ ├── components
│ │ ├── routes
│ │ │ ├── Home
│ │ │ │ ├── index.tsx
│ │ │ │ ├── Hero.tsx
│ │ │ │ └── Features.tsx
│ │ │ ├── Room
│ │ │ │ ├── ConnectionStatus.tsx
│ │ │ │ ├── ShareLink
│ │ │ │ │ ├── ShareLinkAlert.tsx
│ │ │ │ │ ├── ShareLinkDialog.tsx
│ │ │ │ │ └── ShareLinkToolbar.tsx
│ │ │ │ ├── NetworkGraph
│ │ │ │ │ ├── FloatingEdge.tsx
│ │ │ │ │ ├── UserNode.tsx
│ │ │ │ │ ├── utils.ts
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── RoomInfo.tsx
│ │ │ │ ├── TransferSpeed.tsx
│ │ │ │ ├── QRCodeDialog.tsx
│ │ │ │ ├── DownloadDialog.tsx
│ │ │ │ ├── UsernameTakenDialog.tsx
│ │ │ │ ├── SendFileButton.tsx
│ │ │ │ └── index.tsx
│ │ │ └── Settings
│ │ │ │ └── index.tsx
│ │ └── ui
│ │ │ ├── footer.tsx
│ │ │ ├── header.tsx
│ │ │ ├── toaster.tsx
│ │ │ ├── input.tsx
│ │ │ ├── tooltip.tsx
│ │ │ ├── alert.tsx
│ │ │ ├── button.tsx
│ │ │ ├── card.tsx
│ │ │ ├── dialog.tsx
│ │ │ ├── use-toast.ts
│ │ │ ├── alert-dialog.tsx
│ │ │ └── toast.tsx
│ ├── index.tsx
│ ├── lib
│ │ ├── constants.ts
│ │ └── utils.ts
│ ├── App.tsx
│ ├── index.css
│ └── hooks
│ │ ├── useLocalStorage.ts
│ │ └── useFileSharing.ts
├── public
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── robots.txt
│ ├── manifest.json
│ └── index.html
├── .gitignore
├── config-overrides.js
├── components.json
├── tsconfig.json
├── package.json
└── tailwind.config.js
├── package.json
├── server
├── package.json
├── index.js
└── package-lock.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | .env
4 |
--------------------------------------------------------------------------------
/client/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Swanand01/Pulse/HEAD/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Swanand01/Pulse/HEAD/client/public/logo192.png
--------------------------------------------------------------------------------
/client/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Swanand01/Pulse/HEAD/client/public/logo512.png
--------------------------------------------------------------------------------
/client/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | /node_modules
3 |
4 |
5 | # testing
6 | /coverage
7 |
8 | # production
9 | /build
10 |
11 | # misc
12 | .DS_Store
13 | .env.local
14 | .env.development.local
15 | .env.test.local
16 | .env.production.local
17 |
18 | npm-debug.log*
19 |
--------------------------------------------------------------------------------
/client/src/components/routes/Home/index.tsx:
--------------------------------------------------------------------------------
1 | import Features from "./Features";
2 | import Hero from "./Hero";
3 |
4 | export default function Home() {
5 | return (
6 |
7 |
8 |
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/client/config-overrides.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require("path");
2 |
3 | module.exports = function override(config) {
4 | config.resolve = {
5 | ...config.resolve,
6 | alias: {
7 | "@/components": resolve(__dirname, "src/components/"),
8 | "@/lib": resolve(__dirname, "src/lib/"),
9 | "@/hooks": resolve(__dirname, "src/hooks/"),
10 | },
11 | };
12 |
13 | return config;
14 | };
15 |
--------------------------------------------------------------------------------
/client/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "src/index.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/client/src/components/ui/footer.tsx:
--------------------------------------------------------------------------------
1 | export default function Footer() {
2 | return (
3 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/client/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import { BrowserRouter } from "react-router-dom";
4 | import "./index.css";
5 | import App from "./App";
6 |
7 | const root = ReactDOM.createRoot(
8 | document.getElementById("root") as HTMLElement
9 | );
10 |
11 | root.render(
12 |
13 |
14 |
15 |
16 |
17 | );
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pulse",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "install": "npm install --prefix client && npm install --prefix server",
7 | "dev": "concurrently \"npm start --prefix client\" \"npm run dev --prefix server\"",
8 | "build": "npm run build --prefix client",
9 | "start": "node server/index.js"
10 | },
11 | "devDependencies": {
12 | "@types/d3": "^7.4.3",
13 | "concurrently": "^8.2.2"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/client/src/components/routes/Room/ConnectionStatus.tsx:
--------------------------------------------------------------------------------
1 | import { Zap } from "lucide-react";
2 |
3 | interface ConnectionStatusProps {
4 | connectionStatus: string;
5 | }
6 |
7 | export default function ConnectionStatus({
8 | connectionStatus,
9 | }: ConnectionStatusProps) {
10 | return (
11 |
12 |
13 |
{connectionStatus}
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pulse-server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "type": "module",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "start": "node index.js",
10 | "dev": "nodemon index.js"
11 | },
12 | "author": "",
13 | "license": "MIT",
14 | "dependencies": {
15 | "ejs": "^3.1.10",
16 | "express": "^4.19.2",
17 | "socket.io": "^4.7.5"
18 | },
19 | "devDependencies": {
20 | "nodemon": "^3.1.4"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Pulse",
3 | "name": "Pulse",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/client/src/lib/constants.ts:
--------------------------------------------------------------------------------
1 | import { isChrome } from "./utils";
2 |
3 | const TRACKERS = [
4 | "wss://tracker.btorrent.xyz",
5 | "wss://tracker.openwebtorrent.com",
6 | "wss://tracker.webtorrent.dev",
7 | ];
8 |
9 | const ICE_SERVERS = [
10 | isChrome()
11 | ? { url: "stun:68.183.83.122:3478" }
12 | : { urls: "stun:68.183.83.122:3478" },
13 | {
14 | urls: "turn:68.183.83.122:3478",
15 | username: "pulse",
16 | credential: "pulsepassword",
17 | },
18 | ];
19 |
20 | export const WEBTORRENT_CONFIG = {
21 | tracker: {
22 | announce: TRACKERS,
23 | rtcConfig: {
24 | iceServers: ICE_SERVERS,
25 | },
26 | },
27 | };
28 |
--------------------------------------------------------------------------------
/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "noFallthroughCasesInSwitch": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx",
18 | "baseUrl": ".",
19 | "paths": {
20 | "@/*": ["./src/*"]
21 | }
22 | },
23 | "include": ["src"]
24 | }
25 |
--------------------------------------------------------------------------------
/client/src/components/routes/Room/ShareLink/ShareLinkAlert.tsx:
--------------------------------------------------------------------------------
1 | import { Alert, AlertDescription } from "@/components/ui/alert";
2 | import { cn } from "@/lib/utils";
3 | import ShareLinkToolbar from "./ShareLinkToolbar";
4 |
5 | interface ShareLinkAlertProps {
6 | className?: string;
7 | }
8 |
9 | export default function ShareLinkAlert({ className }: ShareLinkAlertProps) {
10 | return (
11 |
12 |
13 |
14 | Share this link to devices you want to share files with
15 |
16 |
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/client/src/components/ui/header.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 | import { Settings } from "lucide-react";
3 | import { cn } from "@/lib/utils";
4 |
5 | interface HeaderProps {
6 | className?: string;
7 | }
8 |
9 | export default function Header({ className }: HeaderProps) {
10 | return (
11 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/client/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { Routes, Route } from "react-router-dom";
2 | import Header from "@/components/ui/header";
3 | import Footer from "@/components/ui/footer";
4 | import Home from "./components/routes/Home";
5 | import Room from "./components/routes/Room";
6 | import Settings from "./components/routes/Settings";
7 | import { Toaster } from "./components/ui/toaster";
8 |
9 | function App() {
10 | return (
11 |
12 |
13 |
14 | } />
15 | } />
16 | } />
17 |
18 |
19 |
20 |
21 | );
22 | }
23 |
24 | export default App;
25 |
--------------------------------------------------------------------------------
/client/src/components/routes/Room/NetworkGraph/FloatingEdge.tsx:
--------------------------------------------------------------------------------
1 | import { Edge, useInternalNode } from "@xyflow/react";
2 | import { getEdgeParams } from "./utils";
3 |
4 | function FloatingEdge({ id, source, target, markerEnd, style }: Edge) {
5 | const sourceNode = useInternalNode(source);
6 | const targetNode = useInternalNode(target);
7 |
8 | if (!sourceNode || !targetNode) {
9 | return null;
10 | }
11 |
12 | const { sx, sy, tx, ty } = getEdgeParams(sourceNode, targetNode);
13 |
14 | const edgePath = `M ${sx} ${sy} L ${tx} ${ty}`;
15 |
16 | return (
17 |
25 | );
26 | }
27 |
28 | export default FloatingEdge;
29 |
--------------------------------------------------------------------------------
/client/src/components/routes/Room/RoomInfo.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import ConnectionStatus from "./ConnectionStatus";
3 | import TransferSpeed from "./TransferSpeed";
4 |
5 | interface RoomInfoProps {
6 | connectionStatus: string;
7 | transferSpeed: string;
8 | className?: string;
9 | }
10 |
11 | export default function RoomInfo({
12 | connectionStatus,
13 | transferSpeed,
14 | className,
15 | }: RoomInfoProps) {
16 | return (
17 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 | Pulse
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/client/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Toast,
3 | ToastClose,
4 | ToastDescription,
5 | ToastProvider,
6 | ToastTitle,
7 | ToastViewport,
8 | } from "@/components/ui/toast"
9 | import { useToast } from "@/components/ui/use-toast"
10 |
11 | export function Toaster() {
12 | const { toasts } = useToast()
13 |
14 | return (
15 |
16 | {toasts.map(function ({ id, title, description, action, ...props }) {
17 | return (
18 |
19 |
20 | {title && {title}}
21 | {description && (
22 | {description}
23 | )}
24 |
25 | {action}
26 |
27 |
28 | )
29 | })}
30 |
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/client/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
8 | export function download(blob: Blob, fileName: string) {
9 | const url = window.URL.createObjectURL(blob);
10 | const a = document.createElement("a");
11 | a.style.display = "none";
12 | a.href = url;
13 | a.download = fileName;
14 | document.body.appendChild(a);
15 | a.click();
16 | window.URL.revokeObjectURL(url);
17 | }
18 |
19 | export function formatBytes(bytes: number) {
20 | if (bytes === 0) return "0 Bytes";
21 | const k = 1024;
22 | const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
23 | const i = Math.floor(Math.log(bytes) / Math.log(k));
24 | return parseFloat((bytes / Math.pow(k, i)).toFixed()) + " " + sizes[i];
25 | }
26 |
27 | export function isChrome() {
28 | return /chrome|chromium|crios/i.test(navigator.userAgent);
29 | }
30 |
--------------------------------------------------------------------------------
/client/src/components/routes/Home/Hero.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 | import { v4 as uuidV4 } from "uuid";
3 | import { Button } from "@/components/ui/button";
4 | import useLocalStorage from "@/hooks/useLocalStorage";
5 |
6 | export default function Hero() {
7 | const [username] = useLocalStorage("username", null);
8 | const roomId = uuidV4();
9 |
10 | let btnLink = `/${roomId}`;
11 | if (!username) {
12 | btnLink = `/settings?roomId=${roomId}`;
13 | }
14 |
15 | return (
16 |
17 |
Pulse: Next-Gen File Sharing, Fast and Free.
18 |
19 | Instantly share files with anyone, anywhere, directly through your
20 | browser.
21 |
22 |
23 |
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/client/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | );
21 | },
22 | );
23 | Input.displayName = "Input";
24 |
25 | export { Input };
26 |
--------------------------------------------------------------------------------
/client/src/components/routes/Room/TransferSpeed.tsx:
--------------------------------------------------------------------------------
1 | import { Gauge } from "lucide-react";
2 | import { Button } from "@/components/ui/button";
3 | import {
4 | Tooltip,
5 | TooltipContent,
6 | TooltipProvider,
7 | TooltipTrigger,
8 | } from "@/components/ui/tooltip";
9 |
10 | interface TransferSpeedProps {
11 | transferSpeed: string;
12 | }
13 |
14 | export default function TransferSpeed({ transferSpeed }: TransferSpeedProps) {
15 | return (
16 |
17 |
18 |
19 |
26 |
27 |
28 | Transfer speed
29 |
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 214 17% 8%;
8 | --foreground: 218 7% 78%;
9 | --muted: 214 12% 15%;
10 | --muted-foreground: 214 12% 65%;
11 | --popover: 214 17% 5%;
12 | --popover-foreground: 218 7% 88%;
13 | --card: 214 17% 6%;
14 | --card-foreground: 218 7% 83%;
15 | --border: 214 7% 13%;
16 | --input: 214 7% 16%;
17 | --primary: 176 56% 50%;
18 | --primary-foreground: 176 56% 10%;
19 | --secondary: 176 30% 25%;
20 | --secondary-foreground: 176 30% 85%;
21 | --accent: 214 17% 23%;
22 | --accent-foreground: 214 17% 83%;
23 | --destructive: 4 93% 52%;
24 | --destructive-foreground: 0 0% 100%;
25 | --ring: 176 56% 50%;
26 | --radius: 0.5rem;
27 | }
28 |
29 | * {
30 | @apply border-border;
31 | }
32 | body {
33 | @apply bg-background text-foreground;
34 | }
35 | }
36 |
37 | .react-flow__attribution {
38 | display: none;
39 | }
40 |
--------------------------------------------------------------------------------
/client/src/hooks/useLocalStorage.ts:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | export default function useLocalStorage(
4 | key: string,
5 | defaultValue: T,
6 | ): [T, (valueOrFn: T | ((val: T) => T)) => void] {
7 | function getLocalStorageValue(): T {
8 | try {
9 | const value = localStorage.getItem(key);
10 | if (value) {
11 | return JSON.parse(value) as T;
12 | }
13 | } catch (error) {
14 | console.error(error);
15 | }
16 |
17 | localStorage.setItem(key, JSON.stringify(defaultValue));
18 | return defaultValue;
19 | }
20 |
21 | const [localStorageValue, setLocalStorageValue] = useState(() => {
22 | return getLocalStorageValue();
23 | });
24 |
25 | function setLocalStorageStateValue(valueOrFn: T | ((val: T) => T)): void {
26 | const newValue =
27 | typeof valueOrFn === "function"
28 | ? (valueOrFn as (val: T) => T)(localStorageValue)
29 | : valueOrFn;
30 |
31 | localStorage.setItem(key, JSON.stringify(newValue));
32 | setLocalStorageValue(newValue);
33 | }
34 |
35 | return [localStorageValue, setLocalStorageStateValue];
36 | }
37 |
--------------------------------------------------------------------------------
/client/src/components/routes/Room/QRCodeDialog.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import {
3 | Dialog,
4 | DialogContent,
5 | DialogDescription,
6 | DialogHeader,
7 | DialogTitle,
8 | DialogTrigger,
9 | } from "@/components/ui/dialog";
10 | import { QrCode } from "lucide-react";
11 | import QRCode from "react-qr-code";
12 |
13 | interface QRCodeDialogProps {
14 | link: string;
15 | }
16 |
17 | export default function QRCodeDialog({ link }: QRCodeDialogProps) {
18 | return (
19 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/client/src/components/routes/Room/ShareLink/ShareLinkDialog.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Dialog,
3 | DialogContent,
4 | DialogDescription,
5 | DialogHeader,
6 | DialogTitle,
7 | DialogTrigger,
8 | } from "@/components/ui/dialog";
9 | import { Button } from "@/components/ui/button";
10 | import { Share2 } from "lucide-react";
11 | import ShareLinkToolbar from "./ShareLinkToolbar";
12 |
13 | export function ShareLinkDialog() {
14 | return (
15 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/client/src/components/routes/Room/ShareLink/ShareLinkToolbar.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Button } from "@/components/ui/button";
3 | import { Input } from "@/components/ui/input";
4 | import { Check, Copy } from "lucide-react";
5 | import QRCodeDialog from "../QRCodeDialog";
6 |
7 | export default function ShareLinkToolbar() {
8 | const [copied, setCopied] = useState(false);
9 | const currentLink = window.location.href;
10 |
11 | const copyToClipboard = async () => {
12 | try {
13 | await navigator.clipboard.writeText(currentLink);
14 | setCopied(true);
15 | setTimeout(() => setCopied(false), 2000);
16 | } catch (err) {
17 | console.error("Failed to copy text: ", err);
18 | }
19 | };
20 |
21 | return (
22 |
23 |
24 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/client/src/components/routes/Home/Features.tsx:
--------------------------------------------------------------------------------
1 | import { CheckCircle, Globe, Lock } from "lucide-react";
2 |
3 | const FEATURES = [
4 | {
5 | icon: ,
6 | heading: "Easy to Use",
7 | text: "No login or sign-up needed. Open the web app and start sharing instantly!",
8 | },
9 | {
10 | icon: ,
11 | heading: "Secure",
12 | text: "Your files are transferred browser to browser, and never stored on the server.",
13 | },
14 | {
15 | icon: ,
16 | heading: "Anywhere",
17 | text: "Effortlessly share files across any device, no matter where you are in the world.",
18 | },
19 | ];
20 |
21 | export default function Features() {
22 | return (
23 |
24 | {FEATURES.map(({ icon, heading, text }) => (
25 |
26 |
27 | {icon}
28 |
{heading}
29 |
30 |
{text}
31 |
32 | ))}
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/client/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const TooltipProvider = TooltipPrimitive.Provider
7 |
8 | const Tooltip = TooltipPrimitive.Root
9 |
10 | const TooltipTrigger = TooltipPrimitive.Trigger
11 |
12 | const TooltipContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, sideOffset = 4, ...props }, ref) => (
16 |
25 | ))
26 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
27 |
28 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
29 |
--------------------------------------------------------------------------------
/client/src/components/routes/Room/DownloadDialog.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AlertDialog,
3 | AlertDialogAction,
4 | AlertDialogCancel,
5 | AlertDialogContent,
6 | AlertDialogDescription,
7 | AlertDialogFooter,
8 | AlertDialogHeader,
9 | AlertDialogTitle,
10 | } from "@/components/ui/alert-dialog";
11 |
12 | interface DownloadDialogProps {
13 | open: boolean;
14 | setOpen: React.Dispatch>;
15 | filename: string;
16 | onClickDownload: () => void;
17 | }
18 |
19 | export default function DownloadDialog({
20 | open,
21 | setOpen,
22 | filename,
23 | onClickDownload,
24 | }: DownloadDialogProps) {
25 | return (
26 |
27 |
28 |
29 | Download file
30 |
31 | Do you want to download {filename}?
32 |
33 |
34 |
35 | setOpen(false)}>
36 | Cancel
37 |
38 |
39 | Continue
40 |
41 |
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/client/src/components/routes/Room/NetworkGraph/UserNode.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { Handle, Position } from "@xyflow/react";
3 |
4 | interface UserNodeProps {
5 | data: {
6 | label: string;
7 | };
8 | }
9 |
10 | export default function UserNode({ data }: UserNodeProps) {
11 | const textLength = data.label.length;
12 | let sizeClass = "w-16 h-16";
13 |
14 | if (textLength > 6 && textLength <= 16) {
15 | sizeClass = "w-24 h-24";
16 | } else if (textLength > 16) {
17 | sizeClass = "w-28 h-28";
18 | }
19 |
20 | return (
21 |
27 |
{data.label}
28 |
34 |
40 |
46 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/client/src/components/routes/Room/UsernameTakenDialog.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AlertDialog,
3 | AlertDialogAction,
4 | AlertDialogCancel,
5 | AlertDialogContent,
6 | AlertDialogDescription,
7 | AlertDialogFooter,
8 | AlertDialogHeader,
9 | AlertDialogTitle,
10 | } from "@/components/ui/alert-dialog";
11 | import { useNavigate } from "react-router-dom";
12 |
13 | interface DownloadDialogProps {
14 | open: boolean;
15 | username: string;
16 | roomId: string;
17 | }
18 |
19 | export default function UsernameTakenDialog({
20 | open,
21 | username,
22 | roomId,
23 | }: DownloadDialogProps) {
24 | const navigate = useNavigate();
25 |
26 | return (
27 |
28 |
29 |
30 | Username Taken
31 |
32 | Someone with the username {username} is already present in the room.
33 | Do you want to change your username?
34 |
35 |
36 |
37 | navigate("/")}>
38 | Cancel
39 |
40 | navigate(`/settings?roomId=${roomId}`)}
42 | >
43 | Continue
44 |
45 |
46 |
47 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/client/src/components/routes/Room/SendFileButton.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { Input } from "@/components/ui/input";
3 | import { cn } from "@/lib/utils";
4 | import { Plus } from "lucide-react";
5 | import { useRef } from "react";
6 |
7 | interface SendFileButtonProps {
8 | sendFile: (file: File) => void;
9 | disabled: boolean;
10 | className?: string;
11 | }
12 |
13 | export default function SendFileButton({
14 | sendFile,
15 | disabled,
16 | className,
17 | }: SendFileButtonProps) {
18 | const fileInputRef = useRef(null);
19 |
20 | const handleFileChange = (event: React.ChangeEvent) => {
21 | const file = event.target.files?.[0];
22 | if (file) {
23 | sendFile(file);
24 | }
25 | };
26 |
27 | const handleButtonClick = () => {
28 | fileInputRef.current?.click();
29 | };
30 |
31 | return (
32 | <>
33 |
39 |
54 | >
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pulse
2 |
3 | Pulse is a file sharing web app that allows users to transfer files between multiple devices. It supports instant file sharing with multiple devices at once.
4 |
5 | Pulse primarily uses WebTorrent to transfer files between multiple devices, and WebSockets to create temporary rooms. Files shared via WebTorrent are peer-to-peer(as they use WebRTC internally) which means there is direct transfer between the sender and receiver without any intermediate server. Do note that tracker servers in WebTorrent are used which carry metadata and facilitate the file transfer but do not get the complete file in any form.
6 |
7 |
8 | ## Features
9 |
10 | **Easy to Use**: No login or sign-up needed. Open the web app and start sharing instantly!
11 |
12 | **Secure**: Your files are transferred browser to browser and never stored on the server.
13 |
14 | **Anywhere**: Effortlessly share files across any device, no matter where you are in the world.
15 |
16 |
17 | ## Try it!
18 |
19 | - Head over to the Pulse website [here.](https://pulse-zmn77.ondigitalocean.app/)
20 | - Share the link of the page to the other peer.
21 | - A connection will be established once the other peer opens the link.
22 | - Start sharing files!
23 |
24 | ## Run locally
25 |
26 | ```bash
27 | npm install
28 | npm run dev
29 | ```
30 |
31 | ## Tech Stack
32 |
33 | **Server:** Node, Express, SocketIO, WebTorrent
34 |
35 | **Client:** React, Tailwind
36 |
37 | ## Screenshots
38 |
39 |
40 |
41 | ## License
42 |
43 | [MIT](https://choosealicense.com/licenses/mit/)
44 |
--------------------------------------------------------------------------------
/client/src/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cva, type VariantProps } from "class-variance-authority";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const alertVariants = cva(
7 | "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-background text-foreground",
12 | destructive:
13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | },
20 | );
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
32 | ));
33 | Alert.displayName = "Alert";
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes
38 | >(({ className, children, ...props }, ref) => (
39 |
44 | {children}
45 |
46 | ));
47 | AlertTitle.displayName = "AlertTitle";
48 |
49 | const AlertDescription = React.forwardRef<
50 | HTMLParagraphElement,
51 | React.HTMLAttributes
52 | >(({ className, ...props }, ref) => (
53 |
58 | ));
59 | AlertDescription.displayName = "AlertDescription";
60 |
61 | export { Alert, AlertTitle, AlertDescription };
62 |
--------------------------------------------------------------------------------
/client/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pulse-client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@hookform/resolvers": "^3.9.0",
7 | "@radix-ui/react-alert-dialog": "^1.1.1",
8 | "@radix-ui/react-dialog": "^1.1.1",
9 | "@radix-ui/react-slot": "^1.1.0",
10 | "@radix-ui/react-toast": "^1.2.1",
11 | "@radix-ui/react-tooltip": "^1.1.2",
12 | "@testing-library/jest-dom": "^5.17.0",
13 | "@testing-library/react": "^13.4.0",
14 | "@testing-library/user-event": "^13.5.0",
15 | "@types/jest": "^27.5.2",
16 | "@types/node": "^16.18.102",
17 | "@types/react": "^18.3.3",
18 | "@types/react-dom": "^18.3.0",
19 | "@types/webtorrent": "^2.0.0",
20 | "@xyflow/react": "^12.0.4",
21 | "class-variance-authority": "^0.7.0",
22 | "clsx": "^2.1.1",
23 | "lucide-react": "^0.414.0",
24 | "react": "^18.3.1",
25 | "react-app-rewired": "^2.2.1",
26 | "react-dom": "^18.3.1",
27 | "react-qr-code": "^2.0.15",
28 | "react-router-dom": "^6.25.0",
29 | "react-scripts": "5.0.1",
30 | "socket.io-client": "^4.7.5",
31 | "tailwind-merge": "^2.4.0",
32 | "tailwindcss-animate": "^1.0.7",
33 | "typescript": "^4.9.5",
34 | "uuid": "^10.0.0",
35 | "web-vitals": "^2.1.4",
36 | "webtorrent": "^2.4.11"
37 | },
38 | "scripts": {
39 | "start": "react-app-rewired start",
40 | "build": "react-app-rewired build",
41 | "test": "react-app-rewired test",
42 | "eject": "react-app-rewired eject"
43 | },
44 | "eslintConfig": {
45 | "extends": [
46 | "react-app",
47 | "react-app/jest"
48 | ]
49 | },
50 | "browserslist": {
51 | "production": [
52 | ">0.2%",
53 | "not dead",
54 | "not op_mini all"
55 | ],
56 | "development": [
57 | "last 1 chrome version",
58 | "last 1 firefox version",
59 | "last 1 safari version"
60 | ]
61 | },
62 | "devDependencies": {
63 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
64 | "@tailwindcss/typography": "^0.5.13",
65 | "@types/uuid": "^10.0.0",
66 | "tailwindcss": "^3.4.6"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import http from "http";
3 | import express from "express";
4 | import { Server } from "socket.io";
5 | import { fileURLToPath } from "url";
6 |
7 | const __filename = fileURLToPath(import.meta.url);
8 | const __dirname = path.dirname(__filename);
9 |
10 | const app = express();
11 | const server = http.Server(app);
12 | const io = new Server(server, {
13 | cors: {
14 | origin: process.env.WEB_APP_URL || "http://localhost:3000",
15 | methods: ["GET", "POST"],
16 | },
17 | });
18 |
19 | const PORT = process.env.PORT || 3001;
20 |
21 | app.set("view engine", "ejs");
22 |
23 | const rooms = {};
24 | io.on("connection", (socket) => {
25 | socket.on("join-room", (roomId, username, userId) => {
26 | if (rooms[roomId] && rooms[roomId].includes(username)) {
27 | io.to(userId).emit("username-taken");
28 | return;
29 | }
30 |
31 | socket.join(roomId);
32 | rooms[roomId] = [...(rooms[roomId] || []), username];
33 | socket.broadcast.to(roomId).emit("user-connected", username);
34 |
35 | socket.on("disconnect", () => {
36 | const users = rooms[roomId] || [];
37 | const index = users.indexOf(username);
38 | if (users.indexOf(username) > -1) {
39 | users.splice(index, 1);
40 | }
41 | rooms[roomId] = users;
42 | socket.broadcast.to(roomId).emit("user-disconnected", username);
43 | });
44 |
45 | socket.on("file-link", (fileLink, senderId) => {
46 | socket.broadcast.to(roomId).emit("file-link", fileLink, senderId);
47 | });
48 |
49 | socket.on("done-downloading", (senderId) => {
50 | io.to(senderId).emit("done-downloading");
51 | });
52 |
53 | socket.on("connection-established", () => {
54 | socket.broadcast.to(roomId).emit("connection-established", username);
55 | });
56 | });
57 | });
58 |
59 | app.use(express.static(path.join(__dirname, "../client/build")));
60 |
61 | app.get("*", (req, res) => {
62 | res.sendFile(path.join(__dirname, "../client/build", "index.html"));
63 | });
64 |
65 | server.listen(PORT, () => {
66 | console.log(`Server listening on port ${PORT}`);
67 | });
68 |
--------------------------------------------------------------------------------
/client/src/components/routes/Settings/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import {
3 | Card,
4 | CardContent,
5 | CardDescription,
6 | CardHeader,
7 | CardTitle,
8 | } from "@/components/ui/card";
9 | import { Input } from "@/components/ui/input";
10 | import { useToast } from "@/components/ui/use-toast";
11 | import useLocalStorage from "@/hooks/useLocalStorage";
12 | import { useSearchParams, useNavigate } from "react-router-dom";
13 |
14 | export default function Settings() {
15 | const [username, setUsername] = useLocalStorage("username", "");
16 | const [searchParams] = useSearchParams();
17 | const roomId = searchParams.get("roomId");
18 | const navigate = useNavigate();
19 | const { toast } = useToast();
20 |
21 | function handleSubmit(event: React.FormEvent) {
22 | event.preventDefault();
23 |
24 | const form = event.currentTarget;
25 | const formElements = form.elements as typeof form.elements & {
26 | usernameInput: HTMLInputElement;
27 | };
28 | const usernameInputValue = formElements.usernameInput.value;
29 |
30 | setUsername(usernameInputValue);
31 |
32 | if (roomId) {
33 | navigate(`/${roomId}`, { replace: true });
34 | } else {
35 | toast({ title: "Settings saved." });
36 | }
37 | }
38 |
39 | return (
40 |
41 |
42 |
43 | Choose a Nickname
44 |
45 | Nicknames are used to identify devices in a room.
46 |
47 |
48 |
49 |
61 |
62 |
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/client/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ));
18 | Card.displayName = "Card";
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ));
30 | CardHeader.displayName = "CardHeader";
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, children, ...props }, ref) => (
36 |
41 | {children}
42 |
43 | ));
44 | CardTitle.displayName = "CardTitle";
45 |
46 | const CardDescription = React.forwardRef<
47 | HTMLParagraphElement,
48 | React.HTMLAttributes
49 | >(({ className, ...props }, ref) => (
50 |
55 | ));
56 | CardDescription.displayName = "CardDescription";
57 |
58 | const CardContent = React.forwardRef<
59 | HTMLDivElement,
60 | React.HTMLAttributes
61 | >(({ className, ...props }, ref) => (
62 |
63 | ));
64 | CardContent.displayName = "CardContent";
65 |
66 | const CardFooter = React.forwardRef<
67 | HTMLDivElement,
68 | React.HTMLAttributes
69 | >(({ className, ...props }, ref) => (
70 |
75 | ));
76 | CardFooter.displayName = "CardFooter";
77 |
78 | export {
79 | Card,
80 | CardHeader,
81 | CardFooter,
82 | CardTitle,
83 | CardDescription,
84 | CardContent,
85 | };
86 |
--------------------------------------------------------------------------------
/client/src/components/routes/Room/index.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useParams, useNavigate } from "react-router-dom";
3 | import { useFileSharing } from "@/hooks/useFileSharing";
4 | import ShareLinkAlert from "./ShareLink/ShareLinkAlert";
5 | import DownloadDialog from "./DownloadDialog";
6 | import SendFileButton from "./SendFileButton";
7 | import RoomInfo from "./RoomInfo";
8 | import useLocalStorage from "@/hooks/useLocalStorage";
9 | import UsernameTakenDialog from "./UsernameTakenDialog";
10 | import NetworkGraph from "./NetworkGraph";
11 | import { ShareLinkDialog } from "./ShareLink/ShareLinkDialog";
12 |
13 | export default function Room() {
14 | const { roomId } = useParams<{ roomId: string }>();
15 | const [username] = useLocalStorage("username", "");
16 | const navigate = useNavigate();
17 |
18 | const {
19 | connectionStatus,
20 | transferSpeed,
21 | sendFile,
22 | showDownloadDialog,
23 | setShowDownloadDialog,
24 | showUsernameTakenDialog,
25 | downloadData,
26 | handleDownload,
27 | peers,
28 | } = useFileSharing({ roomId: roomId || "", username });
29 |
30 | useEffect(() => {
31 | if (!username) {
32 | navigate(`/settings?roomId=${roomId}`);
33 | }
34 | }, [navigate, roomId, username]);
35 |
36 | return (
37 |
38 |
39 |
44 | {connectionStatus === "" &&
}
45 | {connectionStatus !== "" && (
46 |
47 |
48 |
49 | )}
50 |
51 |
55 |
56 | {showDownloadDialog && (
57 |
63 | )}
64 | {showUsernameTakenDialog && (
65 |
70 | )}
71 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/client/src/components/routes/Room/NetworkGraph/utils.ts:
--------------------------------------------------------------------------------
1 | import { Position, type Node } from "@xyflow/react";
2 |
3 | // returns the position (top,right,bottom or right) passed node compared to
4 | function getParams(nodeA: Node, nodeB: Node) {
5 | const centerA = getNodeCenter(nodeA);
6 | const centerB = getNodeCenter(nodeB);
7 |
8 | const horizontalDiff = Math.abs(centerA.x - centerB.x);
9 | const verticalDiff = Math.abs(centerA.y - centerB.y);
10 |
11 | let position;
12 |
13 | // when the horizontal difference between the nodes is bigger, we use Position.Left or Position.Right for the handle
14 | if (horizontalDiff > verticalDiff) {
15 | position = centerA.x > centerB.x ? Position.Left : Position.Right;
16 | } else {
17 | // here the vertical difference between the nodes is bigger, so we use Position.Top or Position.Bottom for the handle
18 | position = centerA.y > centerB.y ? Position.Top : Position.Bottom;
19 | }
20 |
21 | const [x, y] = getHandleCoordsByPosition(nodeA, position);
22 | return [x, y, position];
23 | }
24 |
25 | function getHandleCoordsByPosition(node: any, handlePosition: Position) {
26 | // all handles are from type source, that's why we use handleBounds.source here
27 | const handle = node.internals.handleBounds.source.find(
28 | (h: any) => h.position === handlePosition,
29 | );
30 |
31 | let offsetX = handle.width / 2;
32 | let offsetY = handle.height / 2;
33 |
34 | // this is a tiny detail to make the markerEnd of an edge visible.
35 | // The handle position that gets calculated has the origin top-left, so depending which side we are using, we add a little offset
36 | // when the handlePosition is Position.Right for example, we need to add an offset as big as the handle itself in order to get the correct position
37 | switch (handlePosition) {
38 | case Position.Left:
39 | offsetX = 0;
40 | break;
41 | case Position.Right:
42 | offsetX = handle.width;
43 | break;
44 | case Position.Top:
45 | offsetY = 0;
46 | break;
47 | case Position.Bottom:
48 | offsetY = handle.height;
49 | break;
50 | }
51 |
52 | const x = node.internals.positionAbsolute.x + handle.x + offsetX;
53 | const y = node.internals.positionAbsolute.y + handle.y + offsetY;
54 |
55 | return [x, y];
56 | }
57 |
58 | function getNodeCenter(node: any) {
59 | return {
60 | x: node.internals.positionAbsolute.x + node.measured.width / 2,
61 | y: node.internals.positionAbsolute.y + node.measured.height / 2,
62 | };
63 | }
64 |
65 | // returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge
66 | export function getEdgeParams(source: Node, target: Node) {
67 | const [sx, sy, sourcePos] = getParams(source, target);
68 | const [tx, ty, targetPos] = getParams(target, source);
69 |
70 | return {
71 | sx,
72 | sy,
73 | tx,
74 | ty,
75 | sourcePos,
76 | targetPos,
77 | };
78 | }
79 |
--------------------------------------------------------------------------------
/client/src/components/routes/Room/NetworkGraph/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useCallback } from "react";
2 | import {
3 | ReactFlow,
4 | useNodesState,
5 | useEdgesState,
6 | ConnectionMode,
7 | type Node,
8 | type Edge,
9 | ReactFlowInstance,
10 | } from "@xyflow/react";
11 | import "@xyflow/react/dist/style.css";
12 | import SimpleFloatingEdge from "./FloatingEdge";
13 | import UserNode from "./UserNode";
14 |
15 | interface NetworkGraphProps {
16 | users: string[];
17 | }
18 |
19 | const nodeTypes = {
20 | custom: UserNode,
21 | };
22 |
23 | const edgeTypes = {
24 | floating: SimpleFloatingEdge,
25 | };
26 |
27 | const createNodesAndEdges = (
28 | users: string[],
29 | ): { nodes: Node[]; edges: Edge[] } => {
30 | const nodes: Node[] = [];
31 | const edges: Edge[] = [];
32 | const centerX = 250;
33 | const centerY = 250;
34 | const radius = 100;
35 |
36 | users.forEach((user, i) => {
37 | const angle = (i / users.length) * 2 * Math.PI;
38 | const x = centerX + radius * Math.cos(angle);
39 | const y = centerY + radius * Math.sin(angle);
40 | nodes.push({
41 | id: `${user}`,
42 | position: { x, y },
43 | data: { label: user },
44 | type: "custom",
45 | });
46 | });
47 |
48 | users.forEach((sourceUser, i) => {
49 | users.slice(i + 1).forEach((targetUser) => {
50 | edges.push({
51 | id: `${sourceUser}-${targetUser}`,
52 | source: `${sourceUser}`,
53 | target: `${targetUser}`,
54 | sourceHandle: `a`,
55 | targetHandle: `b`,
56 | type: "floating",
57 | animated: true,
58 | selectable: false,
59 | focusable: false,
60 | });
61 | });
62 | });
63 |
64 | return { nodes, edges };
65 | };
66 |
67 | const styles = {
68 | background: "#121417",
69 | };
70 |
71 | const NetworkGraph: React.FC = ({ users }) => {
72 | const [nodes, setNodes, onNodesChange] = useNodesState([]);
73 | const [edges, setEdges, onEdgesChange] = useEdgesState([]);
74 | const [reactFlowInstance, setReactFlowInstance] =
75 | useState(null);
76 |
77 | const onInit = useCallback((rf: ReactFlowInstance) => {
78 | setReactFlowInstance(rf);
79 | }, []);
80 |
81 | useEffect(() => {
82 | const { nodes: newNodes, edges: newEdges } = createNodesAndEdges(users);
83 | setNodes(newNodes);
84 | setEdges(newEdges);
85 | }, [users, setNodes, setEdges]);
86 |
87 | useEffect(() => {
88 | if (reactFlowInstance) {
89 | reactFlowInstance.fitView();
90 | }
91 | }, [reactFlowInstance, nodes]);
92 |
93 | return (
94 |
95 |
115 |
116 | );
117 | };
118 |
119 | export default NetworkGraph;
120 |
--------------------------------------------------------------------------------
/client/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const plugin = require("tailwindcss/plugin");
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | module.exports = {
5 | darkMode: ["class"],
6 | content: [
7 | "./pages/**/*.{ts,tsx}",
8 | "./components/**/*.{ts,tsx}",
9 | "./app/**/*.{ts,tsx}",
10 | "./src/**/*.{ts,tsx}",
11 | ],
12 | prefix: "",
13 | theme: {
14 | container: {
15 | center: true,
16 | padding: "2rem",
17 | screens: {
18 | "2xl": "1400px",
19 | },
20 | },
21 | extend: {
22 | colors: {
23 | border: "hsl(var(--border))",
24 | input: "hsl(var(--input))",
25 | ring: "hsl(var(--ring))",
26 | background: "hsl(var(--background))",
27 | foreground: "hsl(var(--foreground))",
28 | primary: {
29 | DEFAULT: "hsl(var(--primary))",
30 | foreground: "hsl(var(--primary-foreground))",
31 | },
32 | secondary: {
33 | DEFAULT: "hsl(var(--secondary))",
34 | foreground: "hsl(var(--secondary-foreground))",
35 | },
36 | destructive: {
37 | DEFAULT: "hsl(var(--destructive))",
38 | foreground: "hsl(var(--destructive-foreground))",
39 | },
40 | muted: {
41 | DEFAULT: "hsl(var(--muted))",
42 | foreground: "hsl(var(--muted-foreground))",
43 | },
44 | accent: {
45 | DEFAULT: "hsl(var(--accent))",
46 | foreground: "hsl(var(--accent-foreground))",
47 | },
48 | popover: {
49 | DEFAULT: "hsl(var(--popover))",
50 | foreground: "hsl(var(--popover-foreground))",
51 | },
52 | card: {
53 | DEFAULT: "hsl(var(--card))",
54 | foreground: "hsl(var(--card-foreground))",
55 | },
56 | },
57 | borderRadius: {
58 | lg: "var(--radius)",
59 | md: "calc(var(--radius) - 2px)",
60 | sm: "calc(var(--radius) - 4px)",
61 | },
62 | keyframes: {
63 | "accordion-down": {
64 | from: { height: "0" },
65 | to: { height: "var(--radix-accordion-content-height)" },
66 | },
67 | "accordion-up": {
68 | from: { height: "var(--radix-accordion-content-height)" },
69 | to: { height: "0" },
70 | },
71 | },
72 | animation: {
73 | "accordion-down": "accordion-down 0.2s ease-out",
74 | "accordion-up": "accordion-up 0.2s ease-out",
75 | },
76 | fontSize: {
77 | xs: ["0.75rem", { lineHeight: "1rem" }],
78 | sm: ["0.875rem", { lineHeight: "1.25rem" }],
79 | base: ["1rem", { lineHeight: "1.5rem" }],
80 | lg: ["1.125rem", { lineHeight: "1.75rem" }],
81 | xl: ["1.25rem", { lineHeight: "1.75rem" }],
82 | "2xl": ["1.5rem", { lineHeight: "2rem" }],
83 | "3xl": ["1.875rem", { lineHeight: "2.25rem" }],
84 | "4xl": ["2.25rem", { lineHeight: "2.5rem" }],
85 | "5xl": ["3rem", { lineHeight: "1" }],
86 | "6xl": ["3.75rem", { lineHeight: "1" }],
87 | "7xl": ["4.5rem", { lineHeight: "1" }],
88 | "8xl": ["6rem", { lineHeight: "1" }],
89 | "9xl": ["8rem", { lineHeight: "1" }],
90 | },
91 | typography: {
92 | DEFAULT: {
93 | css: {
94 | maxWidth: "100%",
95 | },
96 | },
97 | },
98 | },
99 | },
100 | plugins: [
101 | plugin(function ({ addBase }) {
102 | addBase({
103 | html: { fontSize: "15px" },
104 | "@screen sm": { html: { fontSize: "15px" } },
105 | "@screen md": { html: { fontSize: "16px" } },
106 | "@screen lg": { html: { fontSize: "17px" } },
107 | "@screen xl": { html: { fontSize: "18px" } },
108 | "@screen 2xl": { html: { fontSize: "19px" } },
109 | });
110 | }),
111 | require("tailwindcss-animate"),
112 | require("@tailwindcss/typography"),
113 | ],
114 | };
115 |
--------------------------------------------------------------------------------
/client/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as DialogPrimitive from "@radix-ui/react-dialog"
3 | import { X } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Dialog = DialogPrimitive.Root
8 |
9 | const DialogTrigger = DialogPrimitive.Trigger
10 |
11 | const DialogPortal = DialogPrimitive.Portal
12 |
13 | const DialogClose = DialogPrimitive.Close
14 |
15 | const DialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
29 |
30 | const DialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, children, ...props }, ref) => (
34 |
35 |
36 |
44 | {children}
45 |
46 |
47 | Close
48 |
49 |
50 |
51 | ))
52 | DialogContent.displayName = DialogPrimitive.Content.displayName
53 |
54 | const DialogHeader = ({
55 | className,
56 | ...props
57 | }: React.HTMLAttributes) => (
58 |
65 | )
66 | DialogHeader.displayName = "DialogHeader"
67 |
68 | const DialogFooter = ({
69 | className,
70 | ...props
71 | }: React.HTMLAttributes) => (
72 |
79 | )
80 | DialogFooter.displayName = "DialogFooter"
81 |
82 | const DialogTitle = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef
85 | >(({ className, ...props }, ref) => (
86 |
94 | ))
95 | DialogTitle.displayName = DialogPrimitive.Title.displayName
96 |
97 | const DialogDescription = React.forwardRef<
98 | React.ElementRef,
99 | React.ComponentPropsWithoutRef
100 | >(({ className, ...props }, ref) => (
101 |
106 | ))
107 | DialogDescription.displayName = DialogPrimitive.Description.displayName
108 |
109 | export {
110 | Dialog,
111 | DialogPortal,
112 | DialogOverlay,
113 | DialogClose,
114 | DialogTrigger,
115 | DialogContent,
116 | DialogHeader,
117 | DialogFooter,
118 | DialogTitle,
119 | DialogDescription,
120 | }
121 |
--------------------------------------------------------------------------------
/client/src/components/ui/use-toast.ts:
--------------------------------------------------------------------------------
1 | // Inspired by react-hot-toast library
2 | import * as React from "react"
3 |
4 | import type {
5 | ToastActionElement,
6 | ToastProps,
7 | } from "@/components/ui/toast"
8 |
9 | const TOAST_LIMIT = 1
10 | const TOAST_REMOVE_DELAY = 1000000
11 |
12 | type ToasterToast = ToastProps & {
13 | id: string
14 | title?: React.ReactNode
15 | description?: React.ReactNode
16 | action?: ToastActionElement
17 | }
18 |
19 | const actionTypes = {
20 | ADD_TOAST: "ADD_TOAST",
21 | UPDATE_TOAST: "UPDATE_TOAST",
22 | DISMISS_TOAST: "DISMISS_TOAST",
23 | REMOVE_TOAST: "REMOVE_TOAST",
24 | } as const
25 |
26 | let count = 0
27 |
28 | function genId() {
29 | count = (count + 1) % Number.MAX_SAFE_INTEGER
30 | return count.toString()
31 | }
32 |
33 | type ActionType = typeof actionTypes
34 |
35 | type Action =
36 | | {
37 | type: ActionType["ADD_TOAST"]
38 | toast: ToasterToast
39 | }
40 | | {
41 | type: ActionType["UPDATE_TOAST"]
42 | toast: Partial
43 | }
44 | | {
45 | type: ActionType["DISMISS_TOAST"]
46 | toastId?: ToasterToast["id"]
47 | }
48 | | {
49 | type: ActionType["REMOVE_TOAST"]
50 | toastId?: ToasterToast["id"]
51 | }
52 |
53 | interface State {
54 | toasts: ToasterToast[]
55 | }
56 |
57 | const toastTimeouts = new Map>()
58 |
59 | const addToRemoveQueue = (toastId: string) => {
60 | if (toastTimeouts.has(toastId)) {
61 | return
62 | }
63 |
64 | const timeout = setTimeout(() => {
65 | toastTimeouts.delete(toastId)
66 | dispatch({
67 | type: "REMOVE_TOAST",
68 | toastId: toastId,
69 | })
70 | }, TOAST_REMOVE_DELAY)
71 |
72 | toastTimeouts.set(toastId, timeout)
73 | }
74 |
75 | export const reducer = (state: State, action: Action): State => {
76 | switch (action.type) {
77 | case "ADD_TOAST":
78 | return {
79 | ...state,
80 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
81 | }
82 |
83 | case "UPDATE_TOAST":
84 | return {
85 | ...state,
86 | toasts: state.toasts.map((t) =>
87 | t.id === action.toast.id ? { ...t, ...action.toast } : t
88 | ),
89 | }
90 |
91 | case "DISMISS_TOAST": {
92 | const { toastId } = action
93 |
94 | // ! Side effects ! - This could be extracted into a dismissToast() action,
95 | // but I'll keep it here for simplicity
96 | if (toastId) {
97 | addToRemoveQueue(toastId)
98 | } else {
99 | state.toasts.forEach((toast) => {
100 | addToRemoveQueue(toast.id)
101 | })
102 | }
103 |
104 | return {
105 | ...state,
106 | toasts: state.toasts.map((t) =>
107 | t.id === toastId || toastId === undefined
108 | ? {
109 | ...t,
110 | open: false,
111 | }
112 | : t
113 | ),
114 | }
115 | }
116 | case "REMOVE_TOAST":
117 | if (action.toastId === undefined) {
118 | return {
119 | ...state,
120 | toasts: [],
121 | }
122 | }
123 | return {
124 | ...state,
125 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
126 | }
127 | }
128 | }
129 |
130 | const listeners: Array<(state: State) => void> = []
131 |
132 | let memoryState: State = { toasts: [] }
133 |
134 | function dispatch(action: Action) {
135 | memoryState = reducer(memoryState, action)
136 | listeners.forEach((listener) => {
137 | listener(memoryState)
138 | })
139 | }
140 |
141 | type Toast = Omit
142 |
143 | function toast({ ...props }: Toast) {
144 | const id = genId()
145 |
146 | const update = (props: ToasterToast) =>
147 | dispatch({
148 | type: "UPDATE_TOAST",
149 | toast: { ...props, id },
150 | })
151 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
152 |
153 | dispatch({
154 | type: "ADD_TOAST",
155 | toast: {
156 | ...props,
157 | id,
158 | open: true,
159 | onOpenChange: (open) => {
160 | if (!open) dismiss()
161 | },
162 | },
163 | })
164 |
165 | return {
166 | id: id,
167 | dismiss,
168 | update,
169 | }
170 | }
171 |
172 | function useToast() {
173 | const [state, setState] = React.useState(memoryState)
174 |
175 | React.useEffect(() => {
176 | listeners.push(setState)
177 | return () => {
178 | const index = listeners.indexOf(setState)
179 | if (index > -1) {
180 | listeners.splice(index, 1)
181 | }
182 | }
183 | }, [state])
184 |
185 | return {
186 | ...state,
187 | toast,
188 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
189 | }
190 | }
191 |
192 | export { useToast, toast }
193 |
--------------------------------------------------------------------------------
/client/src/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
3 |
4 | import { cn } from "@/lib/utils"
5 | import { buttonVariants } from "@/components/ui/button"
6 |
7 | const AlertDialog = AlertDialogPrimitive.Root
8 |
9 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger
10 |
11 | const AlertDialogPortal = AlertDialogPrimitive.Portal
12 |
13 | const AlertDialogOverlay = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef
16 | >(({ className, ...props }, ref) => (
17 |
25 | ))
26 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
27 |
28 | const AlertDialogContent = React.forwardRef<
29 | React.ElementRef,
30 | React.ComponentPropsWithoutRef
31 | >(({ className, ...props }, ref) => (
32 |
33 |
34 |
42 |
43 | ))
44 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
45 |
46 | const AlertDialogHeader = ({
47 | className,
48 | ...props
49 | }: React.HTMLAttributes) => (
50 |
57 | )
58 | AlertDialogHeader.displayName = "AlertDialogHeader"
59 |
60 | const AlertDialogFooter = ({
61 | className,
62 | ...props
63 | }: React.HTMLAttributes) => (
64 |
71 | )
72 | AlertDialogFooter.displayName = "AlertDialogFooter"
73 |
74 | const AlertDialogTitle = React.forwardRef<
75 | React.ElementRef,
76 | React.ComponentPropsWithoutRef
77 | >(({ className, ...props }, ref) => (
78 |
83 | ))
84 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
85 |
86 | const AlertDialogDescription = React.forwardRef<
87 | React.ElementRef,
88 | React.ComponentPropsWithoutRef
89 | >(({ className, ...props }, ref) => (
90 |
95 | ))
96 | AlertDialogDescription.displayName =
97 | AlertDialogPrimitive.Description.displayName
98 |
99 | const AlertDialogAction = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
110 |
111 | const AlertDialogCancel = React.forwardRef<
112 | React.ElementRef,
113 | React.ComponentPropsWithoutRef
114 | >(({ className, ...props }, ref) => (
115 |
124 | ))
125 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
126 |
127 | export {
128 | AlertDialog,
129 | AlertDialogPortal,
130 | AlertDialogOverlay,
131 | AlertDialogTrigger,
132 | AlertDialogContent,
133 | AlertDialogHeader,
134 | AlertDialogFooter,
135 | AlertDialogTitle,
136 | AlertDialogDescription,
137 | AlertDialogAction,
138 | AlertDialogCancel,
139 | }
140 |
--------------------------------------------------------------------------------
/client/src/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ToastPrimitives from "@radix-ui/react-toast"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 | import { X } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ToastProvider = ToastPrimitives.Provider
9 |
10 | const ToastViewport = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
24 |
25 | const toastVariants = cva(
26 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
27 | {
28 | variants: {
29 | variant: {
30 | default: "border bg-background text-foreground",
31 | destructive:
32 | "destructive group border-destructive bg-destructive text-destructive-foreground",
33 | },
34 | },
35 | defaultVariants: {
36 | variant: "default",
37 | },
38 | }
39 | )
40 |
41 | const Toast = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef &
44 | VariantProps
45 | >(({ className, variant, ...props }, ref) => {
46 | return (
47 |
52 | )
53 | })
54 | Toast.displayName = ToastPrimitives.Root.displayName
55 |
56 | const ToastAction = React.forwardRef<
57 | React.ElementRef,
58 | React.ComponentPropsWithoutRef
59 | >(({ className, ...props }, ref) => (
60 |
68 | ))
69 | ToastAction.displayName = ToastPrimitives.Action.displayName
70 |
71 | const ToastClose = React.forwardRef<
72 | React.ElementRef,
73 | React.ComponentPropsWithoutRef
74 | >(({ className, ...props }, ref) => (
75 |
84 |
85 |
86 | ))
87 | ToastClose.displayName = ToastPrimitives.Close.displayName
88 |
89 | const ToastTitle = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => (
93 |
98 | ))
99 | ToastTitle.displayName = ToastPrimitives.Title.displayName
100 |
101 | const ToastDescription = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | ToastDescription.displayName = ToastPrimitives.Description.displayName
112 |
113 | type ToastProps = React.ComponentPropsWithoutRef
114 |
115 | type ToastActionElement = React.ReactElement
116 |
117 | export {
118 | type ToastProps,
119 | type ToastActionElement,
120 | ToastProvider,
121 | ToastViewport,
122 | Toast,
123 | ToastTitle,
124 | ToastDescription,
125 | ToastClose,
126 | ToastAction,
127 | }
128 |
--------------------------------------------------------------------------------
/client/src/hooks/useFileSharing.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useCallback } from "react";
2 | import { io, Socket } from "socket.io-client";
3 | // @ts-ignore
4 | import WebTorrent from "webtorrent/dist/webtorrent.min.js";
5 | import { download, formatBytes } from "../lib/utils";
6 | import { WEBTORRENT_CONFIG } from "@/lib/constants";
7 |
8 | interface UseFileSharingProps {
9 | roomId: string;
10 | username: string;
11 | }
12 |
13 | interface UseFileSharingReturn {
14 | connectionStatus: string;
15 | transferSpeed: string;
16 | sendFile: (file: File) => void;
17 | showDownloadDialog: boolean;
18 | setShowDownloadDialog: React.Dispatch>;
19 | showUsernameTakenDialog: boolean;
20 | downloadData: { file: WebTorrent.File } | null;
21 | handleDownload: () => void;
22 | peers: string[];
23 | }
24 |
25 | export function useFileSharing({
26 | roomId,
27 | username,
28 | }: UseFileSharingProps): UseFileSharingReturn {
29 | const [socket, setSocket] = useState(null);
30 | const [webtorrent] = useState(() => new WebTorrent(WEBTORRENT_CONFIG));
31 | const [connectionStatus, setConnectionStatus] = useState("");
32 | const [torrentBeingSent, setTorrentBeingSent] =
33 | useState(null);
34 | const [transferSpeed, setTransferSpeed] = useState("0 kB/s");
35 | const [showDownloadDialog, setShowDownloadDialog] = useState(false);
36 | const [showUsernameTakenDialog, setShowUsernameTakenDialog] = useState(false);
37 | const [downloadData, setDownloadData] = useState<{
38 | file: WebTorrent.File;
39 | } | null>(null);
40 | const [peers, setPeers] = useState([]);
41 |
42 | const onUserConnected = (username: string) => {
43 | setPeers((peers) => Array.from(new Set([...peers, username])));
44 | setConnectionStatus("Connection established.");
45 | };
46 |
47 | const onFileUploadComplete = () => {
48 | setConnectionStatus("File sent.");
49 | setTransferSpeed("0 kB/s");
50 | };
51 |
52 | const onFileDownloadComplete = () => {
53 | setConnectionStatus("File received! Generating your download.");
54 | setTransferSpeed("0 kB/s");
55 | };
56 |
57 | const onUploadingFile = (
58 | filename: string,
59 | progress: number,
60 | uploadSpeed: string,
61 | ) => {
62 | setConnectionStatus(`Sending ${filename}: ${progress}%`);
63 | setTransferSpeed(uploadSpeed);
64 | };
65 |
66 | const onDownloadingFile = (progress: number, downloadSpeed: string) => {
67 | setConnectionStatus(`Receiving file: ${progress}%`);
68 | setTransferSpeed(downloadSpeed);
69 | };
70 |
71 | const reset = useCallback(() => {
72 | if (torrentBeingSent) {
73 | torrentBeingSent.destroy();
74 | setTorrentBeingSent(null);
75 | }
76 | setConnectionStatus("");
77 | setTransferSpeed("0 kB/s");
78 | }, [torrentBeingSent]);
79 |
80 | const sendFile = useCallback(
81 | (fileToSend: File) => {
82 | if (!fileToSend || !socket) return;
83 | setConnectionStatus("Preparing to send.");
84 |
85 | webtorrent.seed(fileToSend, (torrent: WebTorrent.Torrent) => {
86 | setTorrentBeingSent(torrent);
87 |
88 | socket.emit("file-link", torrent.magnetURI, socket.id);
89 |
90 | torrent.on("upload", () => {
91 | const progress = Math.round(
92 | (torrent.uploaded / torrent.length) * 100,
93 | );
94 | const uploadSpeed = `${formatBytes(torrent.uploadSpeed)}/s`;
95 |
96 | if (progress >= 100) {
97 | onFileUploadComplete();
98 | return;
99 | }
100 |
101 | onUploadingFile(fileToSend.name, progress, uploadSpeed);
102 | });
103 |
104 | torrent.on("error", (error: Error) => {
105 | console.error("WebTorrent error:", error);
106 | });
107 | });
108 | },
109 | [socket, webtorrent],
110 | );
111 |
112 | const handleDownload = useCallback(async () => {
113 | if (downloadData && downloadData.file) {
114 | const file = downloadData.file;
115 | const blob = await file.blob();
116 | download(blob, file.name);
117 | setShowDownloadDialog(false);
118 | }
119 | }, [downloadData]);
120 |
121 | useEffect(() => {
122 | const SOCKET_URL =
123 | process.env.NODE_ENV === "production"
124 | ? window.location.origin
125 | : "http://localhost:3001";
126 |
127 | const socket = io(SOCKET_URL);
128 | setSocket(socket);
129 |
130 | return () => {
131 | socket.close();
132 | };
133 | }, []);
134 |
135 | useEffect(() => {
136 | webtorrent.on("error", (error: any) => {
137 | console.error("WebTorrent client error:", error);
138 | });
139 | }, [webtorrent]);
140 |
141 | useEffect(() => {
142 | if (!socket || !roomId || !username) return;
143 |
144 | const handleConnect = () => {
145 | socket.emit("join-room", roomId, username, socket.id);
146 | };
147 |
148 | const handleUserConnected = (username: string) => {
149 | onUserConnected(username);
150 | socket.emit("connection-established", username);
151 | };
152 |
153 | const handleConnectionEstablished = (username: string) => {
154 | onUserConnected(username);
155 | };
156 |
157 | const handleFileLink = async (fileLink: string, senderId: string) => {
158 | setConnectionStatus("Received magnet link.");
159 |
160 | const torrentHash = await webtorrent.get(fileLink);
161 | if (torrentHash) {
162 | webtorrent.remove(torrentHash);
163 | }
164 |
165 | webtorrent.add(fileLink, (torrent: WebTorrent.Torrent) => {
166 | setTorrentBeingSent(torrent);
167 |
168 | torrent.on("download", () => {
169 | const progress = Math.round(torrent.progress * 100);
170 | const downloadSpeed = `${formatBytes(torrent.downloadSpeed)}/s`;
171 |
172 | if (progress >= 100) {
173 | onFileDownloadComplete();
174 | return;
175 | }
176 |
177 | onDownloadingFile(progress, downloadSpeed);
178 | });
179 |
180 | torrent.on("done", async () => {
181 | onFileDownloadComplete();
182 |
183 | try {
184 | const file = torrent.files[0];
185 | setDownloadData({ file });
186 | setShowDownloadDialog(true);
187 | } catch (err) {
188 | console.error("Error generating download:", err);
189 | }
190 |
191 | socket.emit("done-downloading", senderId);
192 | });
193 |
194 | torrent.on("error", (err: Error) => {
195 | console.error("Torrent error:", err);
196 | });
197 | });
198 | };
199 |
200 | const handleDoneDownloading = () => {
201 | onFileUploadComplete();
202 | };
203 |
204 | const handleUserDisconnected = (username: string) => {
205 | const index = peers.indexOf(username);
206 | if (peers.indexOf(username) > -1) {
207 | peers.splice(index, 1);
208 | setPeers([...peers]);
209 | }
210 | reset();
211 | };
212 |
213 | const handleUsernameTaken = () => {
214 | setShowUsernameTakenDialog(true);
215 | };
216 |
217 | socket.on("connect", handleConnect);
218 | socket.on("username-taken", handleUsernameTaken);
219 | socket.on("user-connected", handleUserConnected);
220 | socket.on("connection-established", handleConnectionEstablished);
221 | socket.on("file-link", handleFileLink);
222 | socket.on("done-downloading", handleDoneDownloading);
223 | socket.on("user-disconnected", handleUserDisconnected);
224 |
225 | return () => {
226 | socket.off("connect", handleConnect);
227 | socket.off("username-taken", handleUsernameTaken);
228 | socket.off("user-connected", handleUserConnected);
229 | socket.off("connection-established", handleConnectionEstablished);
230 | socket.off("file-link", handleFileLink);
231 | socket.off("done-downloading", handleDoneDownloading);
232 | socket.off("user-disconnected", handleUserDisconnected);
233 | };
234 | }, [socket, roomId, webtorrent, reset, username, peers]);
235 |
236 | return {
237 | connectionStatus,
238 | transferSpeed,
239 | sendFile,
240 | showDownloadDialog,
241 | setShowDownloadDialog,
242 | showUsernameTakenDialog,
243 | downloadData,
244 | handleDownload,
245 | peers,
246 | };
247 | }
248 |
--------------------------------------------------------------------------------
/server/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pulse-server",
3 | "version": "1.0.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "pulse-server",
9 | "version": "1.0.0",
10 | "license": "MIT",
11 | "dependencies": {
12 | "ejs": "^3.1.10",
13 | "express": "^4.19.2",
14 | "socket.io": "^4.7.5"
15 | },
16 | "devDependencies": {
17 | "nodemon": "^3.1.4"
18 | }
19 | },
20 | "node_modules/@socket.io/component-emitter": {
21 | "version": "3.1.2",
22 | "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
23 | "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
24 | },
25 | "node_modules/@types/cookie": {
26 | "version": "0.4.1",
27 | "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
28 | "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
29 | },
30 | "node_modules/@types/cors": {
31 | "version": "2.8.17",
32 | "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
33 | "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
34 | "dependencies": {
35 | "@types/node": "*"
36 | }
37 | },
38 | "node_modules/@types/node": {
39 | "version": "20.14.12",
40 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz",
41 | "integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==",
42 | "dependencies": {
43 | "undici-types": "~5.26.4"
44 | }
45 | },
46 | "node_modules/accepts": {
47 | "version": "1.3.8",
48 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
49 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
50 | "dependencies": {
51 | "mime-types": "~2.1.34",
52 | "negotiator": "0.6.3"
53 | },
54 | "engines": {
55 | "node": ">= 0.6"
56 | }
57 | },
58 | "node_modules/ansi-styles": {
59 | "version": "4.3.0",
60 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
61 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
62 | "dependencies": {
63 | "color-convert": "^2.0.1"
64 | },
65 | "engines": {
66 | "node": ">=8"
67 | },
68 | "funding": {
69 | "url": "https://github.com/chalk/ansi-styles?sponsor=1"
70 | }
71 | },
72 | "node_modules/anymatch": {
73 | "version": "3.1.3",
74 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
75 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
76 | "dev": true,
77 | "dependencies": {
78 | "normalize-path": "^3.0.0",
79 | "picomatch": "^2.0.4"
80 | },
81 | "engines": {
82 | "node": ">= 8"
83 | }
84 | },
85 | "node_modules/array-flatten": {
86 | "version": "1.1.1",
87 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
88 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
89 | },
90 | "node_modules/async": {
91 | "version": "3.2.5",
92 | "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
93 | "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
94 | },
95 | "node_modules/balanced-match": {
96 | "version": "1.0.2",
97 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
98 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
99 | },
100 | "node_modules/base64id": {
101 | "version": "2.0.0",
102 | "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
103 | "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
104 | "engines": {
105 | "node": "^4.5.0 || >= 5.9"
106 | }
107 | },
108 | "node_modules/binary-extensions": {
109 | "version": "2.3.0",
110 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
111 | "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
112 | "dev": true,
113 | "engines": {
114 | "node": ">=8"
115 | },
116 | "funding": {
117 | "url": "https://github.com/sponsors/sindresorhus"
118 | }
119 | },
120 | "node_modules/body-parser": {
121 | "version": "1.20.2",
122 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
123 | "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
124 | "dependencies": {
125 | "bytes": "3.1.2",
126 | "content-type": "~1.0.5",
127 | "debug": "2.6.9",
128 | "depd": "2.0.0",
129 | "destroy": "1.2.0",
130 | "http-errors": "2.0.0",
131 | "iconv-lite": "0.4.24",
132 | "on-finished": "2.4.1",
133 | "qs": "6.11.0",
134 | "raw-body": "2.5.2",
135 | "type-is": "~1.6.18",
136 | "unpipe": "1.0.0"
137 | },
138 | "engines": {
139 | "node": ">= 0.8",
140 | "npm": "1.2.8000 || >= 1.4.16"
141 | }
142 | },
143 | "node_modules/brace-expansion": {
144 | "version": "1.1.11",
145 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
146 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
147 | "dependencies": {
148 | "balanced-match": "^1.0.0",
149 | "concat-map": "0.0.1"
150 | }
151 | },
152 | "node_modules/braces": {
153 | "version": "3.0.3",
154 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
155 | "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
156 | "dev": true,
157 | "dependencies": {
158 | "fill-range": "^7.1.1"
159 | },
160 | "engines": {
161 | "node": ">=8"
162 | }
163 | },
164 | "node_modules/bytes": {
165 | "version": "3.1.2",
166 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
167 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
168 | "engines": {
169 | "node": ">= 0.8"
170 | }
171 | },
172 | "node_modules/call-bind": {
173 | "version": "1.0.7",
174 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
175 | "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
176 | "dependencies": {
177 | "es-define-property": "^1.0.0",
178 | "es-errors": "^1.3.0",
179 | "function-bind": "^1.1.2",
180 | "get-intrinsic": "^1.2.4",
181 | "set-function-length": "^1.2.1"
182 | },
183 | "engines": {
184 | "node": ">= 0.4"
185 | },
186 | "funding": {
187 | "url": "https://github.com/sponsors/ljharb"
188 | }
189 | },
190 | "node_modules/chalk": {
191 | "version": "4.1.2",
192 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
193 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
194 | "dependencies": {
195 | "ansi-styles": "^4.1.0",
196 | "supports-color": "^7.1.0"
197 | },
198 | "engines": {
199 | "node": ">=10"
200 | },
201 | "funding": {
202 | "url": "https://github.com/chalk/chalk?sponsor=1"
203 | }
204 | },
205 | "node_modules/chokidar": {
206 | "version": "3.6.0",
207 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
208 | "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
209 | "dev": true,
210 | "dependencies": {
211 | "anymatch": "~3.1.2",
212 | "braces": "~3.0.2",
213 | "glob-parent": "~5.1.2",
214 | "is-binary-path": "~2.1.0",
215 | "is-glob": "~4.0.1",
216 | "normalize-path": "~3.0.0",
217 | "readdirp": "~3.6.0"
218 | },
219 | "engines": {
220 | "node": ">= 8.10.0"
221 | },
222 | "funding": {
223 | "url": "https://paulmillr.com/funding/"
224 | },
225 | "optionalDependencies": {
226 | "fsevents": "~2.3.2"
227 | }
228 | },
229 | "node_modules/color-convert": {
230 | "version": "2.0.1",
231 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
232 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
233 | "dependencies": {
234 | "color-name": "~1.1.4"
235 | },
236 | "engines": {
237 | "node": ">=7.0.0"
238 | }
239 | },
240 | "node_modules/color-name": {
241 | "version": "1.1.4",
242 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
243 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
244 | },
245 | "node_modules/concat-map": {
246 | "version": "0.0.1",
247 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
248 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
249 | },
250 | "node_modules/content-disposition": {
251 | "version": "0.5.4",
252 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
253 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
254 | "dependencies": {
255 | "safe-buffer": "5.2.1"
256 | },
257 | "engines": {
258 | "node": ">= 0.6"
259 | }
260 | },
261 | "node_modules/content-type": {
262 | "version": "1.0.5",
263 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
264 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
265 | "engines": {
266 | "node": ">= 0.6"
267 | }
268 | },
269 | "node_modules/cookie": {
270 | "version": "0.6.0",
271 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
272 | "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
273 | "engines": {
274 | "node": ">= 0.6"
275 | }
276 | },
277 | "node_modules/cookie-signature": {
278 | "version": "1.0.6",
279 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
280 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
281 | },
282 | "node_modules/cors": {
283 | "version": "2.8.5",
284 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
285 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
286 | "dependencies": {
287 | "object-assign": "^4",
288 | "vary": "^1"
289 | },
290 | "engines": {
291 | "node": ">= 0.10"
292 | }
293 | },
294 | "node_modules/debug": {
295 | "version": "2.6.9",
296 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
297 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
298 | "dependencies": {
299 | "ms": "2.0.0"
300 | }
301 | },
302 | "node_modules/define-data-property": {
303 | "version": "1.1.4",
304 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
305 | "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
306 | "dependencies": {
307 | "es-define-property": "^1.0.0",
308 | "es-errors": "^1.3.0",
309 | "gopd": "^1.0.1"
310 | },
311 | "engines": {
312 | "node": ">= 0.4"
313 | },
314 | "funding": {
315 | "url": "https://github.com/sponsors/ljharb"
316 | }
317 | },
318 | "node_modules/depd": {
319 | "version": "2.0.0",
320 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
321 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
322 | "engines": {
323 | "node": ">= 0.8"
324 | }
325 | },
326 | "node_modules/destroy": {
327 | "version": "1.2.0",
328 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
329 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
330 | "engines": {
331 | "node": ">= 0.8",
332 | "npm": "1.2.8000 || >= 1.4.16"
333 | }
334 | },
335 | "node_modules/ee-first": {
336 | "version": "1.1.1",
337 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
338 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
339 | },
340 | "node_modules/ejs": {
341 | "version": "3.1.10",
342 | "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
343 | "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
344 | "dependencies": {
345 | "jake": "^10.8.5"
346 | },
347 | "bin": {
348 | "ejs": "bin/cli.js"
349 | },
350 | "engines": {
351 | "node": ">=0.10.0"
352 | }
353 | },
354 | "node_modules/encodeurl": {
355 | "version": "1.0.2",
356 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
357 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
358 | "engines": {
359 | "node": ">= 0.8"
360 | }
361 | },
362 | "node_modules/engine.io": {
363 | "version": "6.5.5",
364 | "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz",
365 | "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==",
366 | "dependencies": {
367 | "@types/cookie": "^0.4.1",
368 | "@types/cors": "^2.8.12",
369 | "@types/node": ">=10.0.0",
370 | "accepts": "~1.3.4",
371 | "base64id": "2.0.0",
372 | "cookie": "~0.4.1",
373 | "cors": "~2.8.5",
374 | "debug": "~4.3.1",
375 | "engine.io-parser": "~5.2.1",
376 | "ws": "~8.17.1"
377 | },
378 | "engines": {
379 | "node": ">=10.2.0"
380 | }
381 | },
382 | "node_modules/engine.io-parser": {
383 | "version": "5.2.3",
384 | "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
385 | "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
386 | "engines": {
387 | "node": ">=10.0.0"
388 | }
389 | },
390 | "node_modules/engine.io/node_modules/cookie": {
391 | "version": "0.4.2",
392 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
393 | "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
394 | "engines": {
395 | "node": ">= 0.6"
396 | }
397 | },
398 | "node_modules/engine.io/node_modules/debug": {
399 | "version": "4.3.6",
400 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
401 | "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
402 | "dependencies": {
403 | "ms": "2.1.2"
404 | },
405 | "engines": {
406 | "node": ">=6.0"
407 | },
408 | "peerDependenciesMeta": {
409 | "supports-color": {
410 | "optional": true
411 | }
412 | }
413 | },
414 | "node_modules/engine.io/node_modules/ms": {
415 | "version": "2.1.2",
416 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
417 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
418 | },
419 | "node_modules/es-define-property": {
420 | "version": "1.0.0",
421 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
422 | "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
423 | "dependencies": {
424 | "get-intrinsic": "^1.2.4"
425 | },
426 | "engines": {
427 | "node": ">= 0.4"
428 | }
429 | },
430 | "node_modules/es-errors": {
431 | "version": "1.3.0",
432 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
433 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
434 | "engines": {
435 | "node": ">= 0.4"
436 | }
437 | },
438 | "node_modules/escape-html": {
439 | "version": "1.0.3",
440 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
441 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
442 | },
443 | "node_modules/etag": {
444 | "version": "1.8.1",
445 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
446 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
447 | "engines": {
448 | "node": ">= 0.6"
449 | }
450 | },
451 | "node_modules/express": {
452 | "version": "4.19.2",
453 | "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
454 | "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
455 | "dependencies": {
456 | "accepts": "~1.3.8",
457 | "array-flatten": "1.1.1",
458 | "body-parser": "1.20.2",
459 | "content-disposition": "0.5.4",
460 | "content-type": "~1.0.4",
461 | "cookie": "0.6.0",
462 | "cookie-signature": "1.0.6",
463 | "debug": "2.6.9",
464 | "depd": "2.0.0",
465 | "encodeurl": "~1.0.2",
466 | "escape-html": "~1.0.3",
467 | "etag": "~1.8.1",
468 | "finalhandler": "1.2.0",
469 | "fresh": "0.5.2",
470 | "http-errors": "2.0.0",
471 | "merge-descriptors": "1.0.1",
472 | "methods": "~1.1.2",
473 | "on-finished": "2.4.1",
474 | "parseurl": "~1.3.3",
475 | "path-to-regexp": "0.1.7",
476 | "proxy-addr": "~2.0.7",
477 | "qs": "6.11.0",
478 | "range-parser": "~1.2.1",
479 | "safe-buffer": "5.2.1",
480 | "send": "0.18.0",
481 | "serve-static": "1.15.0",
482 | "setprototypeof": "1.2.0",
483 | "statuses": "2.0.1",
484 | "type-is": "~1.6.18",
485 | "utils-merge": "1.0.1",
486 | "vary": "~1.1.2"
487 | },
488 | "engines": {
489 | "node": ">= 0.10.0"
490 | }
491 | },
492 | "node_modules/filelist": {
493 | "version": "1.0.4",
494 | "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
495 | "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
496 | "dependencies": {
497 | "minimatch": "^5.0.1"
498 | }
499 | },
500 | "node_modules/filelist/node_modules/brace-expansion": {
501 | "version": "2.0.1",
502 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
503 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
504 | "dependencies": {
505 | "balanced-match": "^1.0.0"
506 | }
507 | },
508 | "node_modules/filelist/node_modules/minimatch": {
509 | "version": "5.1.6",
510 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
511 | "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
512 | "dependencies": {
513 | "brace-expansion": "^2.0.1"
514 | },
515 | "engines": {
516 | "node": ">=10"
517 | }
518 | },
519 | "node_modules/fill-range": {
520 | "version": "7.1.1",
521 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
522 | "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
523 | "dev": true,
524 | "dependencies": {
525 | "to-regex-range": "^5.0.1"
526 | },
527 | "engines": {
528 | "node": ">=8"
529 | }
530 | },
531 | "node_modules/finalhandler": {
532 | "version": "1.2.0",
533 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
534 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
535 | "dependencies": {
536 | "debug": "2.6.9",
537 | "encodeurl": "~1.0.2",
538 | "escape-html": "~1.0.3",
539 | "on-finished": "2.4.1",
540 | "parseurl": "~1.3.3",
541 | "statuses": "2.0.1",
542 | "unpipe": "~1.0.0"
543 | },
544 | "engines": {
545 | "node": ">= 0.8"
546 | }
547 | },
548 | "node_modules/forwarded": {
549 | "version": "0.2.0",
550 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
551 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
552 | "engines": {
553 | "node": ">= 0.6"
554 | }
555 | },
556 | "node_modules/fresh": {
557 | "version": "0.5.2",
558 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
559 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
560 | "engines": {
561 | "node": ">= 0.6"
562 | }
563 | },
564 | "node_modules/fsevents": {
565 | "version": "2.3.3",
566 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
567 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
568 | "dev": true,
569 | "hasInstallScript": true,
570 | "optional": true,
571 | "os": [
572 | "darwin"
573 | ],
574 | "engines": {
575 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
576 | }
577 | },
578 | "node_modules/function-bind": {
579 | "version": "1.1.2",
580 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
581 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
582 | "funding": {
583 | "url": "https://github.com/sponsors/ljharb"
584 | }
585 | },
586 | "node_modules/get-intrinsic": {
587 | "version": "1.2.4",
588 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
589 | "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
590 | "dependencies": {
591 | "es-errors": "^1.3.0",
592 | "function-bind": "^1.1.2",
593 | "has-proto": "^1.0.1",
594 | "has-symbols": "^1.0.3",
595 | "hasown": "^2.0.0"
596 | },
597 | "engines": {
598 | "node": ">= 0.4"
599 | },
600 | "funding": {
601 | "url": "https://github.com/sponsors/ljharb"
602 | }
603 | },
604 | "node_modules/glob-parent": {
605 | "version": "5.1.2",
606 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
607 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
608 | "dev": true,
609 | "dependencies": {
610 | "is-glob": "^4.0.1"
611 | },
612 | "engines": {
613 | "node": ">= 6"
614 | }
615 | },
616 | "node_modules/gopd": {
617 | "version": "1.0.1",
618 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
619 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
620 | "dependencies": {
621 | "get-intrinsic": "^1.1.3"
622 | },
623 | "funding": {
624 | "url": "https://github.com/sponsors/ljharb"
625 | }
626 | },
627 | "node_modules/has-flag": {
628 | "version": "4.0.0",
629 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
630 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
631 | "engines": {
632 | "node": ">=8"
633 | }
634 | },
635 | "node_modules/has-property-descriptors": {
636 | "version": "1.0.2",
637 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
638 | "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
639 | "dependencies": {
640 | "es-define-property": "^1.0.0"
641 | },
642 | "funding": {
643 | "url": "https://github.com/sponsors/ljharb"
644 | }
645 | },
646 | "node_modules/has-proto": {
647 | "version": "1.0.3",
648 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
649 | "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
650 | "engines": {
651 | "node": ">= 0.4"
652 | },
653 | "funding": {
654 | "url": "https://github.com/sponsors/ljharb"
655 | }
656 | },
657 | "node_modules/has-symbols": {
658 | "version": "1.0.3",
659 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
660 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
661 | "engines": {
662 | "node": ">= 0.4"
663 | },
664 | "funding": {
665 | "url": "https://github.com/sponsors/ljharb"
666 | }
667 | },
668 | "node_modules/hasown": {
669 | "version": "2.0.2",
670 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
671 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
672 | "dependencies": {
673 | "function-bind": "^1.1.2"
674 | },
675 | "engines": {
676 | "node": ">= 0.4"
677 | }
678 | },
679 | "node_modules/http-errors": {
680 | "version": "2.0.0",
681 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
682 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
683 | "dependencies": {
684 | "depd": "2.0.0",
685 | "inherits": "2.0.4",
686 | "setprototypeof": "1.2.0",
687 | "statuses": "2.0.1",
688 | "toidentifier": "1.0.1"
689 | },
690 | "engines": {
691 | "node": ">= 0.8"
692 | }
693 | },
694 | "node_modules/iconv-lite": {
695 | "version": "0.4.24",
696 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
697 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
698 | "dependencies": {
699 | "safer-buffer": ">= 2.1.2 < 3"
700 | },
701 | "engines": {
702 | "node": ">=0.10.0"
703 | }
704 | },
705 | "node_modules/ignore-by-default": {
706 | "version": "1.0.1",
707 | "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
708 | "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
709 | "dev": true
710 | },
711 | "node_modules/inherits": {
712 | "version": "2.0.4",
713 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
714 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
715 | },
716 | "node_modules/ipaddr.js": {
717 | "version": "1.9.1",
718 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
719 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
720 | "engines": {
721 | "node": ">= 0.10"
722 | }
723 | },
724 | "node_modules/is-binary-path": {
725 | "version": "2.1.0",
726 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
727 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
728 | "dev": true,
729 | "dependencies": {
730 | "binary-extensions": "^2.0.0"
731 | },
732 | "engines": {
733 | "node": ">=8"
734 | }
735 | },
736 | "node_modules/is-extglob": {
737 | "version": "2.1.1",
738 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
739 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
740 | "dev": true,
741 | "engines": {
742 | "node": ">=0.10.0"
743 | }
744 | },
745 | "node_modules/is-glob": {
746 | "version": "4.0.3",
747 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
748 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
749 | "dev": true,
750 | "dependencies": {
751 | "is-extglob": "^2.1.1"
752 | },
753 | "engines": {
754 | "node": ">=0.10.0"
755 | }
756 | },
757 | "node_modules/is-number": {
758 | "version": "7.0.0",
759 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
760 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
761 | "dev": true,
762 | "engines": {
763 | "node": ">=0.12.0"
764 | }
765 | },
766 | "node_modules/jake": {
767 | "version": "10.9.2",
768 | "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz",
769 | "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==",
770 | "dependencies": {
771 | "async": "^3.2.3",
772 | "chalk": "^4.0.2",
773 | "filelist": "^1.0.4",
774 | "minimatch": "^3.1.2"
775 | },
776 | "bin": {
777 | "jake": "bin/cli.js"
778 | },
779 | "engines": {
780 | "node": ">=10"
781 | }
782 | },
783 | "node_modules/media-typer": {
784 | "version": "0.3.0",
785 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
786 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
787 | "engines": {
788 | "node": ">= 0.6"
789 | }
790 | },
791 | "node_modules/merge-descriptors": {
792 | "version": "1.0.1",
793 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
794 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
795 | },
796 | "node_modules/methods": {
797 | "version": "1.1.2",
798 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
799 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
800 | "engines": {
801 | "node": ">= 0.6"
802 | }
803 | },
804 | "node_modules/mime": {
805 | "version": "1.6.0",
806 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
807 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
808 | "bin": {
809 | "mime": "cli.js"
810 | },
811 | "engines": {
812 | "node": ">=4"
813 | }
814 | },
815 | "node_modules/mime-db": {
816 | "version": "1.52.0",
817 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
818 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
819 | "engines": {
820 | "node": ">= 0.6"
821 | }
822 | },
823 | "node_modules/mime-types": {
824 | "version": "2.1.35",
825 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
826 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
827 | "dependencies": {
828 | "mime-db": "1.52.0"
829 | },
830 | "engines": {
831 | "node": ">= 0.6"
832 | }
833 | },
834 | "node_modules/minimatch": {
835 | "version": "3.1.2",
836 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
837 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
838 | "dependencies": {
839 | "brace-expansion": "^1.1.7"
840 | },
841 | "engines": {
842 | "node": "*"
843 | }
844 | },
845 | "node_modules/ms": {
846 | "version": "2.0.0",
847 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
848 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
849 | },
850 | "node_modules/negotiator": {
851 | "version": "0.6.3",
852 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
853 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
854 | "engines": {
855 | "node": ">= 0.6"
856 | }
857 | },
858 | "node_modules/nodemon": {
859 | "version": "3.1.4",
860 | "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.4.tgz",
861 | "integrity": "sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==",
862 | "dev": true,
863 | "dependencies": {
864 | "chokidar": "^3.5.2",
865 | "debug": "^4",
866 | "ignore-by-default": "^1.0.1",
867 | "minimatch": "^3.1.2",
868 | "pstree.remy": "^1.1.8",
869 | "semver": "^7.5.3",
870 | "simple-update-notifier": "^2.0.0",
871 | "supports-color": "^5.5.0",
872 | "touch": "^3.1.0",
873 | "undefsafe": "^2.0.5"
874 | },
875 | "bin": {
876 | "nodemon": "bin/nodemon.js"
877 | },
878 | "engines": {
879 | "node": ">=10"
880 | },
881 | "funding": {
882 | "type": "opencollective",
883 | "url": "https://opencollective.com/nodemon"
884 | }
885 | },
886 | "node_modules/nodemon/node_modules/debug": {
887 | "version": "4.3.6",
888 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
889 | "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
890 | "dev": true,
891 | "dependencies": {
892 | "ms": "2.1.2"
893 | },
894 | "engines": {
895 | "node": ">=6.0"
896 | },
897 | "peerDependenciesMeta": {
898 | "supports-color": {
899 | "optional": true
900 | }
901 | }
902 | },
903 | "node_modules/nodemon/node_modules/has-flag": {
904 | "version": "3.0.0",
905 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
906 | "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
907 | "dev": true,
908 | "engines": {
909 | "node": ">=4"
910 | }
911 | },
912 | "node_modules/nodemon/node_modules/ms": {
913 | "version": "2.1.2",
914 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
915 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
916 | "dev": true
917 | },
918 | "node_modules/nodemon/node_modules/supports-color": {
919 | "version": "5.5.0",
920 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
921 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
922 | "dev": true,
923 | "dependencies": {
924 | "has-flag": "^3.0.0"
925 | },
926 | "engines": {
927 | "node": ">=4"
928 | }
929 | },
930 | "node_modules/normalize-path": {
931 | "version": "3.0.0",
932 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
933 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
934 | "dev": true,
935 | "engines": {
936 | "node": ">=0.10.0"
937 | }
938 | },
939 | "node_modules/object-assign": {
940 | "version": "4.1.1",
941 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
942 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
943 | "engines": {
944 | "node": ">=0.10.0"
945 | }
946 | },
947 | "node_modules/object-inspect": {
948 | "version": "1.13.2",
949 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
950 | "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
951 | "engines": {
952 | "node": ">= 0.4"
953 | },
954 | "funding": {
955 | "url": "https://github.com/sponsors/ljharb"
956 | }
957 | },
958 | "node_modules/on-finished": {
959 | "version": "2.4.1",
960 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
961 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
962 | "dependencies": {
963 | "ee-first": "1.1.1"
964 | },
965 | "engines": {
966 | "node": ">= 0.8"
967 | }
968 | },
969 | "node_modules/parseurl": {
970 | "version": "1.3.3",
971 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
972 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
973 | "engines": {
974 | "node": ">= 0.8"
975 | }
976 | },
977 | "node_modules/path-to-regexp": {
978 | "version": "0.1.7",
979 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
980 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
981 | },
982 | "node_modules/picomatch": {
983 | "version": "2.3.1",
984 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
985 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
986 | "dev": true,
987 | "engines": {
988 | "node": ">=8.6"
989 | },
990 | "funding": {
991 | "url": "https://github.com/sponsors/jonschlinkert"
992 | }
993 | },
994 | "node_modules/proxy-addr": {
995 | "version": "2.0.7",
996 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
997 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
998 | "dependencies": {
999 | "forwarded": "0.2.0",
1000 | "ipaddr.js": "1.9.1"
1001 | },
1002 | "engines": {
1003 | "node": ">= 0.10"
1004 | }
1005 | },
1006 | "node_modules/pstree.remy": {
1007 | "version": "1.1.8",
1008 | "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
1009 | "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
1010 | "dev": true
1011 | },
1012 | "node_modules/qs": {
1013 | "version": "6.11.0",
1014 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
1015 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
1016 | "dependencies": {
1017 | "side-channel": "^1.0.4"
1018 | },
1019 | "engines": {
1020 | "node": ">=0.6"
1021 | },
1022 | "funding": {
1023 | "url": "https://github.com/sponsors/ljharb"
1024 | }
1025 | },
1026 | "node_modules/range-parser": {
1027 | "version": "1.2.1",
1028 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
1029 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
1030 | "engines": {
1031 | "node": ">= 0.6"
1032 | }
1033 | },
1034 | "node_modules/raw-body": {
1035 | "version": "2.5.2",
1036 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
1037 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
1038 | "dependencies": {
1039 | "bytes": "3.1.2",
1040 | "http-errors": "2.0.0",
1041 | "iconv-lite": "0.4.24",
1042 | "unpipe": "1.0.0"
1043 | },
1044 | "engines": {
1045 | "node": ">= 0.8"
1046 | }
1047 | },
1048 | "node_modules/readdirp": {
1049 | "version": "3.6.0",
1050 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
1051 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
1052 | "dev": true,
1053 | "dependencies": {
1054 | "picomatch": "^2.2.1"
1055 | },
1056 | "engines": {
1057 | "node": ">=8.10.0"
1058 | }
1059 | },
1060 | "node_modules/safe-buffer": {
1061 | "version": "5.2.1",
1062 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1063 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1064 | "funding": [
1065 | {
1066 | "type": "github",
1067 | "url": "https://github.com/sponsors/feross"
1068 | },
1069 | {
1070 | "type": "patreon",
1071 | "url": "https://www.patreon.com/feross"
1072 | },
1073 | {
1074 | "type": "consulting",
1075 | "url": "https://feross.org/support"
1076 | }
1077 | ]
1078 | },
1079 | "node_modules/safer-buffer": {
1080 | "version": "2.1.2",
1081 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1082 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
1083 | },
1084 | "node_modules/semver": {
1085 | "version": "7.6.3",
1086 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
1087 | "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
1088 | "dev": true,
1089 | "bin": {
1090 | "semver": "bin/semver.js"
1091 | },
1092 | "engines": {
1093 | "node": ">=10"
1094 | }
1095 | },
1096 | "node_modules/send": {
1097 | "version": "0.18.0",
1098 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
1099 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
1100 | "dependencies": {
1101 | "debug": "2.6.9",
1102 | "depd": "2.0.0",
1103 | "destroy": "1.2.0",
1104 | "encodeurl": "~1.0.2",
1105 | "escape-html": "~1.0.3",
1106 | "etag": "~1.8.1",
1107 | "fresh": "0.5.2",
1108 | "http-errors": "2.0.0",
1109 | "mime": "1.6.0",
1110 | "ms": "2.1.3",
1111 | "on-finished": "2.4.1",
1112 | "range-parser": "~1.2.1",
1113 | "statuses": "2.0.1"
1114 | },
1115 | "engines": {
1116 | "node": ">= 0.8.0"
1117 | }
1118 | },
1119 | "node_modules/send/node_modules/ms": {
1120 | "version": "2.1.3",
1121 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1122 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
1123 | },
1124 | "node_modules/serve-static": {
1125 | "version": "1.15.0",
1126 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
1127 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
1128 | "dependencies": {
1129 | "encodeurl": "~1.0.2",
1130 | "escape-html": "~1.0.3",
1131 | "parseurl": "~1.3.3",
1132 | "send": "0.18.0"
1133 | },
1134 | "engines": {
1135 | "node": ">= 0.8.0"
1136 | }
1137 | },
1138 | "node_modules/set-function-length": {
1139 | "version": "1.2.2",
1140 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
1141 | "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
1142 | "dependencies": {
1143 | "define-data-property": "^1.1.4",
1144 | "es-errors": "^1.3.0",
1145 | "function-bind": "^1.1.2",
1146 | "get-intrinsic": "^1.2.4",
1147 | "gopd": "^1.0.1",
1148 | "has-property-descriptors": "^1.0.2"
1149 | },
1150 | "engines": {
1151 | "node": ">= 0.4"
1152 | }
1153 | },
1154 | "node_modules/setprototypeof": {
1155 | "version": "1.2.0",
1156 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
1157 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
1158 | },
1159 | "node_modules/side-channel": {
1160 | "version": "1.0.6",
1161 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
1162 | "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
1163 | "dependencies": {
1164 | "call-bind": "^1.0.7",
1165 | "es-errors": "^1.3.0",
1166 | "get-intrinsic": "^1.2.4",
1167 | "object-inspect": "^1.13.1"
1168 | },
1169 | "engines": {
1170 | "node": ">= 0.4"
1171 | },
1172 | "funding": {
1173 | "url": "https://github.com/sponsors/ljharb"
1174 | }
1175 | },
1176 | "node_modules/simple-update-notifier": {
1177 | "version": "2.0.0",
1178 | "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
1179 | "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
1180 | "dev": true,
1181 | "dependencies": {
1182 | "semver": "^7.5.3"
1183 | },
1184 | "engines": {
1185 | "node": ">=10"
1186 | }
1187 | },
1188 | "node_modules/socket.io": {
1189 | "version": "4.7.5",
1190 | "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz",
1191 | "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==",
1192 | "dependencies": {
1193 | "accepts": "~1.3.4",
1194 | "base64id": "~2.0.0",
1195 | "cors": "~2.8.5",
1196 | "debug": "~4.3.2",
1197 | "engine.io": "~6.5.2",
1198 | "socket.io-adapter": "~2.5.2",
1199 | "socket.io-parser": "~4.2.4"
1200 | },
1201 | "engines": {
1202 | "node": ">=10.2.0"
1203 | }
1204 | },
1205 | "node_modules/socket.io-adapter": {
1206 | "version": "2.5.5",
1207 | "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
1208 | "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
1209 | "dependencies": {
1210 | "debug": "~4.3.4",
1211 | "ws": "~8.17.1"
1212 | }
1213 | },
1214 | "node_modules/socket.io-adapter/node_modules/debug": {
1215 | "version": "4.3.6",
1216 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
1217 | "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
1218 | "dependencies": {
1219 | "ms": "2.1.2"
1220 | },
1221 | "engines": {
1222 | "node": ">=6.0"
1223 | },
1224 | "peerDependenciesMeta": {
1225 | "supports-color": {
1226 | "optional": true
1227 | }
1228 | }
1229 | },
1230 | "node_modules/socket.io-adapter/node_modules/ms": {
1231 | "version": "2.1.2",
1232 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
1233 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
1234 | },
1235 | "node_modules/socket.io-parser": {
1236 | "version": "4.2.4",
1237 | "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
1238 | "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
1239 | "dependencies": {
1240 | "@socket.io/component-emitter": "~3.1.0",
1241 | "debug": "~4.3.1"
1242 | },
1243 | "engines": {
1244 | "node": ">=10.0.0"
1245 | }
1246 | },
1247 | "node_modules/socket.io-parser/node_modules/debug": {
1248 | "version": "4.3.6",
1249 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
1250 | "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
1251 | "dependencies": {
1252 | "ms": "2.1.2"
1253 | },
1254 | "engines": {
1255 | "node": ">=6.0"
1256 | },
1257 | "peerDependenciesMeta": {
1258 | "supports-color": {
1259 | "optional": true
1260 | }
1261 | }
1262 | },
1263 | "node_modules/socket.io-parser/node_modules/ms": {
1264 | "version": "2.1.2",
1265 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
1266 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
1267 | },
1268 | "node_modules/socket.io/node_modules/debug": {
1269 | "version": "4.3.6",
1270 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
1271 | "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
1272 | "dependencies": {
1273 | "ms": "2.1.2"
1274 | },
1275 | "engines": {
1276 | "node": ">=6.0"
1277 | },
1278 | "peerDependenciesMeta": {
1279 | "supports-color": {
1280 | "optional": true
1281 | }
1282 | }
1283 | },
1284 | "node_modules/socket.io/node_modules/ms": {
1285 | "version": "2.1.2",
1286 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
1287 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
1288 | },
1289 | "node_modules/statuses": {
1290 | "version": "2.0.1",
1291 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
1292 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
1293 | "engines": {
1294 | "node": ">= 0.8"
1295 | }
1296 | },
1297 | "node_modules/supports-color": {
1298 | "version": "7.2.0",
1299 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
1300 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
1301 | "dependencies": {
1302 | "has-flag": "^4.0.0"
1303 | },
1304 | "engines": {
1305 | "node": ">=8"
1306 | }
1307 | },
1308 | "node_modules/to-regex-range": {
1309 | "version": "5.0.1",
1310 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
1311 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
1312 | "dev": true,
1313 | "dependencies": {
1314 | "is-number": "^7.0.0"
1315 | },
1316 | "engines": {
1317 | "node": ">=8.0"
1318 | }
1319 | },
1320 | "node_modules/toidentifier": {
1321 | "version": "1.0.1",
1322 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1323 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1324 | "engines": {
1325 | "node": ">=0.6"
1326 | }
1327 | },
1328 | "node_modules/touch": {
1329 | "version": "3.1.1",
1330 | "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
1331 | "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
1332 | "dev": true,
1333 | "bin": {
1334 | "nodetouch": "bin/nodetouch.js"
1335 | }
1336 | },
1337 | "node_modules/type-is": {
1338 | "version": "1.6.18",
1339 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
1340 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
1341 | "dependencies": {
1342 | "media-typer": "0.3.0",
1343 | "mime-types": "~2.1.24"
1344 | },
1345 | "engines": {
1346 | "node": ">= 0.6"
1347 | }
1348 | },
1349 | "node_modules/undefsafe": {
1350 | "version": "2.0.5",
1351 | "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
1352 | "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
1353 | "dev": true
1354 | },
1355 | "node_modules/undici-types": {
1356 | "version": "5.26.5",
1357 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
1358 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
1359 | },
1360 | "node_modules/unpipe": {
1361 | "version": "1.0.0",
1362 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
1363 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
1364 | "engines": {
1365 | "node": ">= 0.8"
1366 | }
1367 | },
1368 | "node_modules/utils-merge": {
1369 | "version": "1.0.1",
1370 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
1371 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
1372 | "engines": {
1373 | "node": ">= 0.4.0"
1374 | }
1375 | },
1376 | "node_modules/vary": {
1377 | "version": "1.1.2",
1378 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1379 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
1380 | "engines": {
1381 | "node": ">= 0.8"
1382 | }
1383 | },
1384 | "node_modules/ws": {
1385 | "version": "8.17.1",
1386 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
1387 | "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
1388 | "engines": {
1389 | "node": ">=10.0.0"
1390 | },
1391 | "peerDependencies": {
1392 | "bufferutil": "^4.0.1",
1393 | "utf-8-validate": ">=5.0.2"
1394 | },
1395 | "peerDependenciesMeta": {
1396 | "bufferutil": {
1397 | "optional": true
1398 | },
1399 | "utf-8-validate": {
1400 | "optional": true
1401 | }
1402 | }
1403 | }
1404 | }
1405 | }
1406 |
--------------------------------------------------------------------------------