├── .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 |
4 |

5 | Designed & Developed by  6 | Swanand.   7 | Open source software. 8 |

9 |
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 |
23 | 24 |
25 | 26 |
27 |
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 | 20 | 21 | 24 | 25 | 26 | 27 | Room QR code 28 | 29 | Scan the QR code to join this room. 30 | 31 | 32 |
33 | 34 |
35 |
36 |
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 | 16 | 17 | 24 | 25 | 26 | 27 | Share Room Link 28 | 29 | Share this link to devices you want to share files with 30 | 31 | 32 | 33 | 34 | 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 | image 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 |
50 | 57 | 60 |
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 | --------------------------------------------------------------------------------