termhub-p2p 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,44 @@
1
+ import WebTorrent from 'webtorrent';
2
+ import { getRepositories, initStorage } from '../utils/storage.js';
3
+ import path from 'path';
4
+ import fs from 'fs-extra';
5
+ import os from 'os';
6
+ const PID_FILE = path.join(os.homedir(), '.termhub', 'daemon.pid');
7
+ async function runDaemon() {
8
+ await initStorage();
9
+ const client = new WebTorrent();
10
+ const activeHashes = new Set();
11
+ console.log('TermHub Daemon started...');
12
+ // Function to seed all repositories from storage
13
+ async function syncRepositories() {
14
+ const repos = await getRepositories();
15
+ for (const repo of repos) {
16
+ if (!activeHashes.has(repo.infoHash)) {
17
+ if (repo.path && await fs.pathExists(repo.path)) {
18
+ console.log(`Daemon: Seeding ${repo.name}...`);
19
+ client.seed(repo.path, { name: repo.name }, (torrent) => {
20
+ activeHashes.add(torrent.infoHash);
21
+ console.log(`Daemon: ${repo.name} is now online.`);
22
+ });
23
+ }
24
+ }
25
+ }
26
+ }
27
+ // Initial sync
28
+ await syncRepositories();
29
+ // Watch for changes in the repositories file
30
+ const reposFilePath = path.join(os.homedir(), '.termhub', 'repositories.json');
31
+ fs.watchFile(reposFilePath, async () => {
32
+ console.log('Daemon: Config changed, re-syncing...');
33
+ await syncRepositories();
34
+ });
35
+ // Keep process alive
36
+ setInterval(() => {
37
+ const totalUpload = client.ratio;
38
+ console.log(`Daemon Heartbeat: Seeding ${client.torrents.length} repos. Ratio: ${totalUpload}`);
39
+ }, 60000);
40
+ }
41
+ runDaemon().catch(err => {
42
+ console.error('Daemon Error:', err);
43
+ process.exit(1);
44
+ });
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import chalk from 'chalk';
4
+ import { publish } from './commands/publish.js';
5
+ import { push } from './commands/push.js';
6
+ import { setUsername } from './commands/name.js';
7
+ import { download } from './commands/download.js';
8
+ import { list } from './commands/list.js';
9
+ import { search } from './commands/search.js';
10
+ import { explore } from './commands/explore.js';
11
+ import { ui } from './commands/ui.js';
12
+ import { status } from './commands/status.js';
13
+ import { daemonManager } from './commands/daemon.js';
14
+ import { star } from './commands/star.js';
15
+ const program = new Command();
16
+ console.log(chalk.bold.cyan(`
17
+ _____ _ _ _
18
+ |_ _|__ _ __ _ __ ___ | | | |_ _| |__
19
+ | |/ _ \\ '__| '_ \` _ \\| |_| | | | | '_ \\
20
+ | | __/ | | | | | | | _ | |_| | |_) |
21
+ |_|\\___|_| |_| |_| |_|_| |_|\\__,_|_.__/
22
+
23
+ ${chalk.italic('The Decentralized Terminal Hub')}
24
+ `));
25
+ program
26
+ .name('termhub')
27
+ .description('GitHub for the terminal - P2P File Sharing')
28
+ .version('1.0.0');
29
+ program
30
+ .command('publish')
31
+ .description('Publish a file or directory to TermHub')
32
+ .argument('<path>', 'path to the file or directory')
33
+ .action(publish);
34
+ program
35
+ .command('push')
36
+ .description('Publish the current directory respecting .gitignore')
37
+ .action(push);
38
+ program
39
+ .command('name')
40
+ .description('Set your decentralized handle')
41
+ .argument('<username>', 'desired username')
42
+ .action(setUsername);
43
+ program
44
+ .command('download')
45
+ .description('Download a repository from TermHub')
46
+ .argument('<magnet>', 'magnet link or info hash')
47
+ .option('-o, --output <path>', 'output directory', '.')
48
+ .action(download);
49
+ program
50
+ .command('list')
51
+ .description('List your published repositories')
52
+ .action(list);
53
+ program
54
+ .command('search')
55
+ .description('Search for repositories in the global registry')
56
+ .argument('<query>', 'search query')
57
+ .action(search);
58
+ program
59
+ .command('explore')
60
+ .description('Explore the global registry interactively')
61
+ .action(explore);
62
+ program
63
+ .command('ui')
64
+ .description('Open the TermHub Web Dashboard')
65
+ .action(ui);
66
+ program
67
+ .command('daemon')
68
+ .description('Manage the background seeding daemon')
69
+ .argument('<action>', 'start, stop, or status')
70
+ .action(daemonManager);
71
+ program
72
+ .command('status')
73
+ .description('Show local node status and metrics')
74
+ .action(status);
75
+ program
76
+ .command('star')
77
+ .description('Star a repository or list your stars')
78
+ .argument('[name]', 'repository name')
79
+ .argument('[magnet]', 'magnet link')
80
+ .action(star);
81
+ program.parse();
@@ -0,0 +1,14 @@
1
+ export interface Repository {
2
+ name: string;
3
+ infoHash: string;
4
+ magnetURI: string;
5
+ path?: string;
6
+ description?: string;
7
+ author?: string;
8
+ timestamp?: string;
9
+ }
10
+ export interface Star {
11
+ name: string;
12
+ magnet: string;
13
+ timestamp: string;
14
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,255 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>/th/ - TermHub Registry</title>
6
+ <style>
7
+ body {
8
+ background-color: #ffffee;
9
+ color: #800000;
10
+ font-family: arial,helvetica,sans-serif;
11
+ font-size: 10pt;
12
+ margin: 0;
13
+ padding: 10px;
14
+ }
15
+ header {
16
+ display: flex;
17
+ justify-content: space-between;
18
+ align-items: flex-end;
19
+ padding: 10px 20px;
20
+ border-bottom: 2px solid #d9bfb7;
21
+ margin-bottom: 20px;
22
+ }
23
+ .header-left {
24
+ text-align: left;
25
+ }
26
+ h1 {
27
+ font-family: "Trebuchet MS",tahoma,arial,helvetica,sans-serif;
28
+ font-size: 24pt;
29
+ letter-spacing: -2px;
30
+ margin: 0;
31
+ line-height: 1;
32
+ }
33
+ .banner {
34
+ color: #af0a0f;
35
+ font-weight: bold;
36
+ font-size: 10pt;
37
+ margin-top: 5px;
38
+ }
39
+ .post {
40
+ background-color: #f0e0d6;
41
+ border: 1px solid #d9bfb7;
42
+ display: table;
43
+ padding: 5px;
44
+ margin-bottom: 10px;
45
+ width: 100%;
46
+ max-width: 800px;
47
+ margin-left: auto;
48
+ margin-right: auto;
49
+ }
50
+ .post-header {
51
+ font-weight: bold;
52
+ margin-bottom: 5px;
53
+ border-bottom: 1px solid #d9bfb7;
54
+ }
55
+ .post-id {
56
+ color: #117743;
57
+ font-weight: normal;
58
+ }
59
+ .name {
60
+ color: #117743;
61
+ font-weight: bold;
62
+ }
63
+ .subject {
64
+ color: #0f0c5d;
65
+ font-weight: bold;
66
+ }
67
+ .magnet-btn {
68
+ color: #0000ee;
69
+ text-decoration: underline;
70
+ cursor: pointer;
71
+ background: none;
72
+ border: none;
73
+ padding: 0;
74
+ font-size: 9pt;
75
+ }
76
+ .magnet-btn:hover { color: #ff0000; }
77
+
78
+ .section-title {
79
+ background-color: #e04000;
80
+ color: white;
81
+ padding: 2px 10px;
82
+ font-weight: bold;
83
+ text-align: center;
84
+ margin: 20px auto;
85
+ width: fit-content;
86
+ }
87
+
88
+ .nav {
89
+ font-size: 9pt;
90
+ text-align: center;
91
+ margin-bottom: 10px;
92
+ }
93
+ .nav a { color: #34345c; text-decoration: none; }
94
+ .nav a:hover { color: #ff0000; }
95
+
96
+ .footer {
97
+ font-size: 8pt;
98
+ color: #706b5e;
99
+ text-align: center;
100
+ margin-top: 30px;
101
+ }
102
+ </style>
103
+ </head>
104
+ <body>
105
+ <header>
106
+ <div class="header-left">
107
+ <h1>TermHub</h1>
108
+ </div>
109
+ <div class="nav">
110
+ [<a href="#" onclick="switchView('my')">My Files</a>]
111
+ [<a href="#" onclick="switchView('explore')">Explore</a>]
112
+ [<a href="#" onclick="switchView('status')">Status</a>]
113
+ </div>
114
+ </header>
115
+
116
+ <div class="section-title">Public Repositories</div>
117
+ <div id="content-area">
118
+ <!-- Posts will appear here -->
119
+ <div class="post">
120
+ <div class="post-header">System Notification</div>
121
+ Connecting to Gun.js nodes...
122
+ </div>
123
+ </div>
124
+
125
+ <div class="footer">
126
+ - TermHub v1.0.0 -
127
+ <br>
128
+ All files are P2P. No centralized hosting.
129
+ </div>
130
+
131
+ <script>
132
+ async function loadStatus() {
133
+ const res = await fetch('/api/status');
134
+ const data = await res.json();
135
+ const area = document.getElementById('content-area');
136
+
137
+ // Stats summary post
138
+ const statsPost = `
139
+ <div class="post">
140
+ <div class="post-header">Node Information</div>
141
+ <span class="subject">Current Handle:</span> <span class="name" style="font-size: 1.2rem;">${data.username}</span><br>
142
+ <span class="subject">Status:</span> Connected to Mesh<br>
143
+ <span class="subject">Local Repos:</span> ${data.publishedCount}<br>
144
+ <hr style="border: 0; border-top: 1px solid #d9bfb7; margin: 10px 0;">
145
+ <span class="subject">Change Handle:</span>
146
+ <input type="text" id="new-name" placeholder="new_handle" style="font-size: 8pt;">
147
+ [<a href="#" onclick="updateName()">Update</a>]
148
+ </div>
149
+ `;
150
+
151
+ // Only update if we are in "Status" view or it's the first load
152
+ if (activeView === 'status') {
153
+ area.innerHTML = statsPost;
154
+ }
155
+ }
156
+
157
+ let activeView = 'explore';
158
+
159
+ async function updateName() {
160
+ const name = document.getElementById('new-name').value;
161
+ if (!name) return;
162
+
163
+ const res = await fetch('/api/config', {
164
+ method: 'POST',
165
+ headers: { 'Content-Type': 'application/json' },
166
+ body: JSON.stringify({ username: name })
167
+ });
168
+
169
+ if (res.ok) {
170
+ alert('Identity updated to ' + name);
171
+ loadStatus();
172
+ } else {
173
+ const err = await res.json();
174
+ alert('Error: ' + err.error);
175
+ }
176
+ }
177
+
178
+ async function loadMyRepos() {
179
+ activeView = 'my';
180
+ const res = await fetch('/api/repositories');
181
+ const data = await res.json();
182
+ const area = document.getElementById('content-area');
183
+ area.innerHTML = '';
184
+
185
+ if (data.length === 0) area.innerHTML = '<div class="post">No files published yet.</div>';
186
+
187
+ data.reverse().forEach(repo => {
188
+ const post = document.createElement('div');
189
+ post.className = 'post';
190
+ post.innerHTML = `
191
+ <div class="post-header">
192
+ <span class="subject">${repo.name}</span>
193
+ <span class="name">${repo.author || 'Anonymous'}</span>
194
+ <span class="post-id">No.${repo.infoHash.substring(0,8)}</span>
195
+ </div>
196
+ <div class="body">
197
+ File Hash: ${repo.infoHash}<br>
198
+ Location: ${repo.path}<br>
199
+ <button class="magnet-btn" onclick="copy('${repo.magnetURI}')">Copy Magnet</button>
200
+ </div>
201
+ `;
202
+ area.appendChild(post);
203
+ });
204
+ }
205
+
206
+ async function loadExplore() {
207
+ activeView = 'explore';
208
+ const area = document.getElementById('content-area');
209
+ area.innerHTML = '<div class="post">Searching P2P network...</div>';
210
+
211
+ try {
212
+ const res = await fetch('/api/explore');
213
+ const data = await res.json();
214
+ area.innerHTML = '';
215
+
216
+ if (data.length === 0) area.innerHTML = '<div class="post">No files found on this relay. Try refreshing in a few seconds.</div>';
217
+
218
+ data.forEach(repo => {
219
+ const post = document.createElement('div');
220
+ post.className = 'post';
221
+ post.innerHTML = `
222
+ <div class="post-header">
223
+ <span class="subject">${repo.name}</span>
224
+ <span class="name">${repo.author || 'Anonymous'}</span>
225
+ <span class="post-id">No.${repo.infoHash.substring(0,8)}</span>
226
+ </div>
227
+ <div class="body">
228
+ Published: ${new Date(repo.timestamp).toLocaleString()}<br><br>
229
+ <button class="magnet-btn" onclick="copy('${repo.magnetURI}')">Copy Magnet</button>
230
+ </div>
231
+ `;
232
+ area.appendChild(post);
233
+ });
234
+ } catch (err) {
235
+ area.innerHTML = '<div class="post">Error connecting to network.</div>';
236
+ }
237
+ }
238
+
239
+ function switchView(view) {
240
+ activeView = view;
241
+ if (view === 'status') loadStatus();
242
+ if (view === 'my') loadMyRepos();
243
+ if (view === 'explore') loadExplore();
244
+ }
245
+
246
+ function copy(m) {
247
+ navigator.clipboard.writeText(m);
248
+ alert('Magnet copied to clipboard!');
249
+ }
250
+
251
+ // Default view
252
+ loadExplore();
253
+ </script>
254
+ </body>
255
+ </html>
@@ -0,0 +1,9 @@
1
+ import { Repository } from '../types/index.js';
2
+ export declare function addToRegistry(repoData: Repository): Promise<void>;
3
+ export declare function searchInRegistry(query: string): Promise<Repository[]>;
4
+ export declare function getAllFromRegistry(): Promise<Repository[]>;
5
+ export declare function checkNameAvailability(name: string, myPublicKey: string): Promise<{
6
+ available: boolean;
7
+ owner: boolean;
8
+ }>;
9
+ export declare function claimUsername(name: string, token: string): Promise<void>;
@@ -0,0 +1,130 @@
1
+ import { finalizeEvent, Relay } from 'nostr-tools';
2
+ import { getConfig } from './storage.js';
3
+ import WebSocket from 'ws';
4
+ // @ts-ignore
5
+ global.WebSocket = WebSocket;
6
+ const RELAYS = [
7
+ 'wss://relay.damus.io',
8
+ 'wss://nos.lol',
9
+ 'wss://relay.nostr.band',
10
+ 'wss://offchain.pub'
11
+ ];
12
+ function hexToBytes(hex) {
13
+ const bytes = new Uint8Array(hex.length / 2);
14
+ for (let i = 0; i < bytes.length; i++) {
15
+ bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
16
+ }
17
+ return bytes;
18
+ }
19
+ export async function addToRegistry(repoData) {
20
+ const { privateKey, username } = await getConfig();
21
+ const event = finalizeEvent({
22
+ kind: 1,
23
+ created_at: Math.floor(Date.now() / 1000),
24
+ tags: [
25
+ ['t', 'termhub-repo'],
26
+ ['h', repoData.infoHash],
27
+ ['m', repoData.magnetURI],
28
+ ['n', repoData.name],
29
+ ['a', username]
30
+ ],
31
+ content: `Published TermHub Repository: ${repoData.name}\nMagnet: ${repoData.magnetURI}`,
32
+ }, hexToBytes(privateKey));
33
+ await Promise.allSettled(RELAYS.map(async (url) => {
34
+ try {
35
+ const relay = await Relay.connect(url);
36
+ await relay.publish(event);
37
+ relay.close();
38
+ }
39
+ catch (e) { }
40
+ }));
41
+ }
42
+ export async function searchInRegistry(query) {
43
+ const results = [];
44
+ const seenHashes = new Set();
45
+ const filters = [{
46
+ kinds: [1],
47
+ '#t': ['termhub-repo'],
48
+ limit: 50
49
+ }];
50
+ for (const url of RELAYS) {
51
+ try {
52
+ const relay = await Relay.connect(url);
53
+ await new Promise((resolve) => {
54
+ const sub = relay.subscribe(filters, {
55
+ onevent(event) {
56
+ const infoHash = event.tags.find((t) => t[0] === 'h')?.[1];
57
+ const magnetURI = event.tags.find((t) => t[0] === 'm')?.[1];
58
+ const name = event.tags.find((t) => t[0] === 'n')?.[1];
59
+ const author = event.tags.find((t) => t[0] === 'a')?.[1];
60
+ if (infoHash && magnetURI && name && !seenHashes.has(infoHash)) {
61
+ if (name.toLowerCase().includes(query.toLowerCase())) {
62
+ seenHashes.add(infoHash);
63
+ results.push({
64
+ name,
65
+ infoHash,
66
+ magnetURI,
67
+ author: author || 'Anonymous',
68
+ timestamp: (event.created_at * 1000).toString()
69
+ });
70
+ }
71
+ }
72
+ },
73
+ oneose() {
74
+ sub.close();
75
+ resolve();
76
+ }
77
+ });
78
+ // Safety timeout
79
+ setTimeout(() => {
80
+ sub.close();
81
+ resolve();
82
+ }, 3000);
83
+ });
84
+ relay.close();
85
+ }
86
+ catch (err) { }
87
+ }
88
+ return results;
89
+ }
90
+ export async function getAllFromRegistry() {
91
+ return searchInRegistry('');
92
+ }
93
+ export async function checkNameAvailability(name, myPublicKey) {
94
+ const filters = [{
95
+ kinds: [1],
96
+ '#t': ['termhub-repo'],
97
+ '#a': [name],
98
+ limit: 10
99
+ }];
100
+ for (const url of RELAYS) {
101
+ try {
102
+ const relay = await Relay.connect(url);
103
+ const isAvailable = await new Promise((resolve) => {
104
+ let found = false;
105
+ let owner = false;
106
+ const sub = relay.subscribe(filters, {
107
+ onevent(event) {
108
+ found = true;
109
+ if (event.pubkey === myPublicKey)
110
+ owner = true;
111
+ },
112
+ oneose() {
113
+ sub.close();
114
+ resolve(!found || owner);
115
+ }
116
+ });
117
+ setTimeout(() => {
118
+ sub.close();
119
+ resolve(true);
120
+ }, 2000);
121
+ });
122
+ relay.close();
123
+ if (!isAvailable)
124
+ return { available: false, owner: false };
125
+ }
126
+ catch (err) { }
127
+ }
128
+ return { available: true, owner: false };
129
+ }
130
+ export async function claimUsername(name, token) { }
@@ -0,0 +1,4 @@
1
+ import { Star } from '../types/index.js';
2
+ export declare function initStars(): Promise<void>;
3
+ export declare function starRepository(name: string, magnet: string): Promise<boolean>;
4
+ export declare function getStars(): Promise<Star[]>;
@@ -0,0 +1,23 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ const CONFIG_DIR = path.join(os.homedir(), '.termhub');
5
+ const STARS_FILE = path.join(CONFIG_DIR, 'stars.json');
6
+ export async function initStars() {
7
+ await fs.ensureDir(CONFIG_DIR);
8
+ if (!(await fs.pathExists(STARS_FILE))) {
9
+ await fs.writeJson(STARS_FILE, []);
10
+ }
11
+ }
12
+ export async function starRepository(name, magnet) {
13
+ const stars = await fs.readJson(STARS_FILE);
14
+ if (!stars.find(s => s.magnet === magnet)) {
15
+ stars.push({ name, magnet, timestamp: new Date().toISOString() });
16
+ await fs.writeJson(STARS_FILE, stars);
17
+ return true;
18
+ }
19
+ return false;
20
+ }
21
+ export async function getStars() {
22
+ return await fs.readJson(STARS_FILE);
23
+ }
@@ -0,0 +1,14 @@
1
+ import { Repository } from '../types/index.js';
2
+ export declare function initStorage(): Promise<void>;
3
+ export declare function getConfig(): Promise<{
4
+ username: string;
5
+ privateKey: string;
6
+ publicKey: string;
7
+ }>;
8
+ export declare function setConfig(config: {
9
+ username: string;
10
+ privateKey: string;
11
+ publicKey: string;
12
+ }): Promise<void>;
13
+ export declare function saveRepository(repo: Repository): Promise<void>;
14
+ export declare function getRepositories(): Promise<Repository[]>;
@@ -0,0 +1,42 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { generateSecretKey, getPublicKey } from 'nostr-tools/pure';
5
+ const CONFIG_DIR = path.join(os.homedir(), '.termhub');
6
+ const REPOS_FILE = path.join(CONFIG_DIR, 'repositories.json');
7
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
8
+ // Helper to convert Uint8Array to Hex string
9
+ function bytesToHex(bytes) {
10
+ return Array.from(bytes)
11
+ .map(b => b.toString(16).padStart(2, '0'))
12
+ .join('');
13
+ }
14
+ export async function initStorage() {
15
+ await fs.ensureDir(CONFIG_DIR);
16
+ if (!(await fs.pathExists(REPOS_FILE))) {
17
+ await fs.writeJson(REPOS_FILE, []);
18
+ }
19
+ if (!(await fs.pathExists(CONFIG_FILE))) {
20
+ const sk = generateSecretKey();
21
+ const pk = getPublicKey(sk);
22
+ await fs.writeJson(CONFIG_FILE, {
23
+ username: 'Anonymous',
24
+ privateKey: bytesToHex(sk),
25
+ publicKey: pk
26
+ });
27
+ }
28
+ }
29
+ export async function getConfig() {
30
+ return await fs.readJson(CONFIG_FILE);
31
+ }
32
+ export async function setConfig(config) {
33
+ await fs.writeJson(CONFIG_FILE, config);
34
+ }
35
+ export async function saveRepository(repo) {
36
+ const repos = await getRepositories();
37
+ repos.push(repo);
38
+ await fs.writeJson(REPOS_FILE, repos);
39
+ }
40
+ export async function getRepositories() {
41
+ return await fs.readJson(REPOS_FILE);
42
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "termhub-p2p",
3
+ "version": "1.0.0",
4
+ "description": "Decentralized Terminal Hub - P2P file sharing and registry.",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "termhub": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc && node -e \"require('fs-extra').copySync('src/ui/public', 'dist/ui/public')\"",
15
+ "dev": "tsx src/index.ts",
16
+ "prepublishOnly": "npm run build",
17
+ "test": "echo \"Error: no test specified\" && exit 1"
18
+ },
19
+ "keywords": [
20
+ "p2p",
21
+ "nostr",
22
+ "webtorrent",
23
+ "cli",
24
+ "share"
25
+ ],
26
+ "author": "",
27
+ "license": "ISC",
28
+ "dependencies": {
29
+ "chalk": "^5.6.2",
30
+ "commander": "^14.0.3",
31
+ "enquirer": "^2.4.1",
32
+ "express": "^5.2.1",
33
+ "fs-extra": "^11.3.4",
34
+ "globby": "^16.2.0",
35
+ "ignore": "^7.0.5",
36
+ "nostr-tools": "^2.23.3",
37
+ "open": "^11.0.0",
38
+ "ora": "^9.3.0",
39
+ "webtorrent": "^2.8.5",
40
+ "ws": "^8.20.0"
41
+ },
42
+ "devDependencies": {
43
+ "@noble/hashes": "^2.2.0",
44
+ "@types/express": "^5.0.6",
45
+ "@types/fs-extra": "^11.0.4",
46
+ "@types/node": "^25.6.0",
47
+ "@types/webtorrent": "^0.110.1",
48
+ "@types/ws": "^8.18.1",
49
+ "tsx": "^4.21.0",
50
+ "typescript": "^6.0.2"
51
+ }
52
+ }