magnetk 2.1.1 → 2.2.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.
Files changed (4) hide show
  1. package/README.md +52 -63
  2. package/client.js +223 -65
  3. package/index.js +1 -0
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -12,62 +12,58 @@ npm install magnetk
12
12
 
13
13
  ## Usage
14
14
 
15
- ### Parsing a Magnetk Link
15
+ ### One-Line Seeding (Zero Config)
16
+ The SDK automatically hashes your file, discovers the relay, and spawns the high-performance Go seeder in the background.
16
17
 
17
18
  ```javascript
18
- import { MagnetkLink } from 'magnetk';
19
+ import { MagnetkClient } from 'magnetk';
19
20
 
20
- const uri = 'magnetk:?xt=urn:sha256:abc123...&dn=myfile.zip&xl=1048576&relay=1.2.3.4';
21
- const link = MagnetkLink.parse(uri);
21
+ const client = new MagnetkClient({
22
+ relayUrl: '69.169.109.243', // Optional: defaults to public relay
23
+ relayPort: 4003
24
+ });
22
25
 
23
- console.log(link.fileName); // myfile.zip
24
- console.log(link.fileHash); // abc123...
26
+ // Seed a file and get a shareable link
27
+ const link = await client.seed('./my-app.zip');
28
+ console.log(`Share this link: ${link}`);
29
+
30
+ // Keep the process alive to serve peers (Ctrl+C to stop)
31
+ await client.keepSeeding();
25
32
  ```
26
33
 
27
- ### Generating a Magnetk Link
34
+ ### One-Line Downloading
35
+ Download files with automatic connection optimization (Direct > Local > Relay).
28
36
 
29
37
  ```javascript
30
- import { MagnetkLink } from 'magnetk';
31
-
32
- const link = new MagnetkLink(
33
- '98df9a59...', // File Hash
34
- 'app.txt', // File Name
35
- 4, // File Size (bytes)
36
- '12D3KooWDmX2...', // Seed Peer ID
37
- '/ip4/1.2.3.4/tcp/4001' // Relay Multiaddr
38
- );
39
-
40
- console.log(link.toString());
41
- // Output: magnetk:?dn=app.txt&relay=%2Fip4%2F1.2.3.4...
38
+ const magnetURI = 'magnetk:?xt=urn:sha256:abc...&relay=...';
39
+
40
+ console.log('Downloading...');
41
+ await client.download(magnetURI, './my-app-downloaded.zip');
42
+ console.log('Done!');
42
43
  ```
43
44
 
44
- ### Downloading a File
45
+ ### Event-Driven Progress tracking
46
+ Track every stage of the transfer with the new Event API.
45
47
 
46
48
  ```javascript
47
- import { MagnetkClient, CONNECTION_TYPE } from 'magnetk';
48
-
49
49
  const client = new MagnetkClient();
50
- const uri = 'magnetk:?xt=urn:sha256:98df9a...&dn=app.txt&relay=/ip4/1.2.3.4/tcp/4001';
51
-
52
- try {
53
- console.log('Starting P2P download...');
54
- await client.download(uri, './downloads/app.txt');
55
-
56
- // Access connection diagnostics
57
- const stats = client.lastDownloadStats;
58
- console.log(`Success! Connection Type: ${stats.connectionType}`);
59
- console.log(`Remote Address: ${stats.remoteAddr}`);
60
- } catch (err) {
61
- console.error('Download failed:', err.message);
62
- }
63
- ```
64
50
 
65
- ### Checking Connection Types
66
-
67
- The SDK automatically tracks the quality of your P2P connection:
68
- - `CONNECTION_TYPE.DIRECT`: High-speed P2P (via STUN discovered public IP).
69
- - `CONNECTION_TYPE.LOCAL`: Super-fast local transfer (same machine/LAN).
70
- - `CONNECTION_TYPE.RELAYED`: Connection bridged via relay (fallback).
51
+ // Seeding Events
52
+ client.on('hashing', (p) => console.log(`Hashing file: ${p.percent}%`));
53
+ client.on('seeding', (info) => console.log(`Seeding ${info.fileName} (${info.fileSize} bytes)`));
54
+ client.on('relay-connected', (r) => console.log(`Live on Relay: ${r.host}`));
55
+
56
+ // Download Events
57
+ client.on('download-start', (info) => console.log(`Starting: ${info.fileName}`));
58
+ client.on('progress', (p) => {
59
+ process.stdout.write(`\rDownload: ${p.percent.toFixed(1)}% | Speed: ${p.speed} KB/s`);
60
+ });
61
+ client.on('connection-type', (c) => console.log(`\nConnection Mode: ${c.type}`)); // DIRECT, RELAYED, LOCAL
62
+ client.on('complete', (info) => console.log(`\nSaved to: ${info.filePath}`));
63
+
64
+ // Error Handling
65
+ client.on('error', (err) => console.error(`Error (${err.code}): ${err.message}`));
66
+ ```
71
67
 
72
68
  ## Advanced Features
73
69
 
@@ -79,37 +75,30 @@ const identity = await client.getPublicIdentity();
79
75
  console.log(`Public Identity: ${identity.ip}:${identity.port}`);
80
76
  ```
81
77
 
82
- ### Seeding (Distributing Files)
83
- To share a file on the Magnetk network, use the high-performance **Go Seeder** included in the toolkit:
84
-
85
- ```bash
86
- # Seed a file via the relay
87
- ./bin/seed.exe -relay "/ip4/relay-ip/tcp/4001" -file "path/to/file.zip"
88
- ```
89
-
90
- ## CLI Interface
91
-
78
+ ### Manual CLI
92
79
  For quick operations, use the global `magnetk` command:
93
80
 
94
81
  ```bash
95
- # Lookup seeds for a hash
96
- magnetk lookup 98df9a59... --relay 1.2.3.4
97
-
98
82
  # Download a file
99
83
  magnetk download "magnetk:?..." --output ./file.zip
100
84
  ```
101
85
 
102
86
  ## API Reference
103
87
 
104
- ### `MagnetkLink`
105
- - `static parse(uri)`: Create a link object from string.
106
- - `toString()`: Serialize back to string.
107
-
108
88
  ### `MagnetkClient`
109
- - `download(uri, outputPath)`: High-level P2P download.
110
- - `getPublicIdentity()`: Resolve public IP via STUN.
111
- - `lastDownloadStats`: Metadata from the previous download.
112
- - `lookupPeer(relayHost, relayPort, fileHash)`: Query relay for seeds.
89
+ - `constructor(config)`: `{ relayUrl, relayPort, seedPort, enableSTUN }`
90
+ - `seed(filePath)`: Returns `Promise<magnetLink>`. Spawns background seeder.
91
+ - `download(uri, outputPath)`: Downloads file. Emits progress events.
92
+ - `stop()`: Kills all background seeder processes.
93
+ - `getRelayIdentity()`: Fetches Relay Peer ID and Multiaddrs.
94
+
95
+ ### Events
96
+ - `hashing`: `{ percent }`
97
+ - `seeding`: `{ fileName, fileSize, hash }`
98
+ - `progress`: `{ percent, bytesDownloaded, totalBytes, speed }`
99
+ - `connection-type`: `{ type }` (DIRECT, RELAYED, LOCAL)
100
+ - `error`: `{ code, message }`
113
101
 
114
102
  ## License
115
103
  MIT
104
+
package/client.js CHANGED
@@ -8,6 +8,10 @@
8
8
  import net from 'net';
9
9
  import fs from 'fs';
10
10
  import dgram from 'dgram';
11
+ import crypto from 'crypto';
12
+ import path from 'path';
13
+ import { spawn } from 'child_process';
14
+ import { EventEmitter } from 'events';
11
15
  import { MagnetkLink } from './magnetk.js';
12
16
 
13
17
  const MSG_TYPE = {
@@ -17,6 +21,8 @@ const MSG_TYPE = {
17
21
  MSG_MANIFEST_RES: 0x11,
18
22
  MSG_CHUNK_REQ: 0x20,
19
23
  MSG_CHUNK_RES: 0x21,
24
+ MSG_GET_IDENTITY: 0x32,
25
+ MSG_IDENTITY_RES: 0x33,
20
26
  MSG_ERROR: 0xFF
21
27
  };
22
28
 
@@ -27,10 +33,17 @@ export const CONNECTION_TYPE = {
27
33
  UNKNOWN: 'UNKNOWN'
28
34
  };
29
35
 
30
- export class MagnetkClient {
36
+ export class MagnetkClient extends EventEmitter {
31
37
  constructor(config = {}) {
32
- this.config = config;
33
- this.connections = new Map();
38
+ super();
39
+ this.config = {
40
+ relayUrl: '69.169.109.243',
41
+ relayPort: 4003,
42
+ seedPort: 4002,
43
+ enableSTUN: true,
44
+ ...config
45
+ };
46
+ this.processes = [];
34
47
  this.lastDownloadStats = {
35
48
  connectionType: CONNECTION_TYPE.UNKNOWN,
36
49
  remoteAddr: null,
@@ -104,6 +117,33 @@ export class MagnetkClient {
104
117
  });
105
118
  }
106
119
 
120
+ /**
121
+ * Retrieves the relay's Peer ID and multiaddrs via TCP.
122
+ */
123
+ async getRelayIdentity() {
124
+ console.log(`[Magnetk-JS] Fetching relay identity from ${this.config.relayUrl}:${this.config.relayPort}...`);
125
+ const socket = await this.connect(this.config.relayUrl, this.config.relayPort);
126
+ const writer = new FrameWriter(socket);
127
+ const reader = new FrameReader(socket);
128
+
129
+ try {
130
+ await writer.writeFrame(MSG_TYPE.MSG_GET_IDENTITY, Buffer.alloc(0)); // Empty payload
131
+ const frame = await reader.readFrame();
132
+
133
+ if (frame.type === MSG_TYPE.MSG_IDENTITY_RES) {
134
+ const result = JSON.parse(frame.payload.toString());
135
+ return result;
136
+ } else if (frame.type === MSG_TYPE.MSG_ERROR) {
137
+ const err = JSON.parse(frame.payload.toString());
138
+ throw new Error(`Relay error: ${err.message}`);
139
+ } else {
140
+ throw new Error(`Unexpected response type: 0x${frame.type.toString(16)}`);
141
+ }
142
+ } finally {
143
+ socket.end();
144
+ }
145
+ }
146
+
107
147
  /**
108
148
  * Look up seeds for a file hash from the relay.
109
149
  * @param {string} relayHost
@@ -135,39 +175,137 @@ export class MagnetkClient {
135
175
  }
136
176
  }
137
177
 
178
+ /**
179
+ * Seeds a file and returns a Magnetk link.
180
+ * @param {string} filePath
181
+ * @returns {Promise<string>}
182
+ */
183
+ async seed(filePath) {
184
+ const absolutePath = path.resolve(filePath);
185
+ if (!fs.existsSync(absolutePath)) {
186
+ throw new Error(`File not found: ${absolutePath}`);
187
+ }
188
+
189
+ this.emit('validating-relay');
190
+ // Simple reachability check (optional but good for UX)
191
+
192
+ const fileStats = fs.statSync(absolutePath);
193
+ this.emit('hashing', { percent: 0 });
194
+
195
+ // Calculate hash
196
+ const hash = await this._calculateHash(absolutePath);
197
+ this.emit('hashing', { percent: 100 });
198
+
199
+ // Fetch Relay Identity to get Peer ID
200
+ let relayPeerId;
201
+ try {
202
+ const identity = await this.getRelayIdentity();
203
+ relayPeerId = identity.peer_id;
204
+ console.log(`[Magnetk-JS] Relay Peer ID: ${relayPeerId}`);
205
+ } catch (e) {
206
+ console.warn(`[Magnetk-JS] Failed to fetch relay identity: ${e.message}. Using default/configured address might fail if Peer ID is missing.`);
207
+ }
208
+
209
+ const binPath = this._resolveBinPath('seed.exe');
210
+ const configPath = this._resolveConfigPath();
211
+
212
+ // Construct full multiaddr if we have the Peer ID
213
+ let relayMultiaddr = `/ip4/${this.config.relayUrl}/tcp/4001`;
214
+ if (relayPeerId) {
215
+ relayMultiaddr += `/p2p/${relayPeerId}`;
216
+ }
217
+
218
+ const args = [
219
+ '-relay', relayMultiaddr,
220
+ '-file', absolutePath,
221
+ '-port', this.config.seedPort.toString(),
222
+ '-config', configPath
223
+ ];
224
+
225
+ console.log(`[Magnetk-JS] Spawning seeder: ${binPath} ${args.join(' ')}`);
226
+
227
+ return new Promise((resolve, reject) => {
228
+ const seeder = spawn(binPath, args);
229
+ this.processes.push(seeder);
230
+
231
+ let linkFound = false;
232
+
233
+ seeder.stdout.on('data', (data) => {
234
+ const output = data.toString();
235
+ if (output.includes('Magnetk Link:')) {
236
+ const link = output.split('Magnetk Link:')[1].trim().split('\n')[0].trim();
237
+ linkFound = true;
238
+ this.emit('seeding', {
239
+ fileName: path.basename(absolutePath),
240
+ fileSize: fileStats.size,
241
+ hash: hash
242
+ });
243
+ resolve(link);
244
+ }
245
+ });
246
+
247
+ seeder.stderr.on('data', (data) => {
248
+ const msg = data.toString();
249
+ console.log(`[Seeder Stderr] ${msg}`); // Uncommented for debug
250
+ if (msg.includes('Connected to relay')) {
251
+ this.emit('relay-connected', { host: this.config.relayUrl, port: 4001 });
252
+ }
253
+ });
254
+
255
+ seeder.on('error', (err) => {
256
+ if (!linkFound) reject(err);
257
+ this.emit('error', { code: 'SEEDER_ERROR', message: err.message });
258
+ });
259
+
260
+ seeder.on('close', (code) => {
261
+ if (!linkFound && code !== 0) {
262
+ reject(new Error(`Seeder exited with code ${code}`));
263
+ }
264
+ });
265
+ });
266
+ }
267
+
268
+ /**
269
+ * Keeps seeding active processes.
270
+ */
271
+ async keepSeeding() {
272
+ console.log('Press Ctrl+C to stop seeding');
273
+ return new Promise(() => { }); // Wait forever
274
+ }
275
+
276
+ /**
277
+ * Stops all active seeding processes.
278
+ */
279
+ stop() {
280
+ this.processes.forEach(p => p.kill());
281
+ this.processes = [];
282
+ }
283
+
138
284
  /**
139
285
  * Downloads a file from a magnetk link.
140
286
  * @param {string} magnetURI
141
287
  * @param {string} outputPath
142
288
  */
143
289
  async download(magnetURI, outputPath) {
144
- const ml = MagnetkLink.parse(magnetURI);
145
- console.log(`[Magnetk-JS] Starting download for: ${ml.fileName}`);
290
+ const ml = typeof magnetURI === 'string' ? MagnetkLink.parse(magnetURI) : magnetURI;
291
+ this.emit('download-start', { fileName: ml.fileName });
146
292
 
147
- // 1. Resolve seed address from the Relay
148
- // We extract the relay host from the Magnetk Link
149
- const relayHost = ml.relayAddr.split('/')[2];
150
- const discoveryPort = 4003;
293
+ // Resolve relay info from link or config
294
+ const relayHost = ml.relayAddr ? ml.relayAddr.split('/')[2] : this.config.relayUrl;
295
+ const discoveryPort = this.config.relayPort;
151
296
 
152
- console.log(`[Magnetk-JS] Querying relay ${relayHost}:${discoveryPort} for seeds...`);
153
297
  let lookupResult;
154
298
  try {
155
299
  lookupResult = await this.lookupPeer(relayHost, discoveryPort, ml.fileHash);
156
300
  } catch (e) {
157
- console.error(`[Magnetk-JS] Initial lookup failed: ${e.message}`);
301
+ this.emit('error', { code: 'LOOKUP_FAILED', message: e.message });
302
+ throw e;
158
303
  }
159
304
 
160
305
  const addressesToTry = [];
161
306
  if (lookupResult && lookupResult.found && lookupResult.seeds.length > 0) {
162
307
  const seed = lookupResult.seeds[0];
163
- console.log(`[Magnetk-JS] Found seed: ${seed.peer_id}`);
164
-
165
- // 1. Add transport_addr if present
166
- if (seed.transport_addr) {
167
- addressesToTry.push(seed.transport_addr);
168
- }
169
-
170
- // 2. Add from multiaddrs (look for /ip4/x.x.x.x/tcp/yyyy)
308
+ if (seed.transport_addr) addressesToTry.push(seed.transport_addr);
171
309
  if (seed.addresses) {
172
310
  seed.addresses.forEach(addr => {
173
311
  const parts = addr.split('/');
@@ -177,14 +315,9 @@ export class MagnetkClient {
177
315
  });
178
316
  }
179
317
  }
180
-
181
- // Add localhost fallback as last resort for dev
182
318
  addressesToTry.push('127.0.0.1:4002');
183
319
 
184
- // Unique addresses only
185
320
  const uniqueAddrs = [...new Set(addressesToTry)];
186
- console.log(`[Magnetk-JS] Candidate seeds: [${uniqueAddrs.join(', ')}]`);
187
-
188
321
  let socket;
189
322
  let connectionType = CONNECTION_TYPE.UNKNOWN;
190
323
  let finalAddr = null;
@@ -196,81 +329,106 @@ export class MagnetkClient {
196
329
  try {
197
330
  socket = await this.connect(host, port);
198
331
  finalAddr = addr;
332
+ if (host === '127.0.0.1' || host === 'localhost') connectionType = CONNECTION_TYPE.LOCAL;
333
+ else if (host === relayHost) connectionType = CONNECTION_TYPE.RELAYED;
334
+ else connectionType = CONNECTION_TYPE.DIRECT;
199
335
 
200
- // Determine connection type
201
- if (host === '127.0.0.1' || host === 'localhost') {
202
- connectionType = CONNECTION_TYPE.LOCAL;
203
- } else if (host === relayHost) {
204
- connectionType = CONNECTION_TYPE.RELAYED;
205
- } else {
206
- connectionType = CONNECTION_TYPE.DIRECT;
207
- }
336
+ this.emit('connection-type', { type: connectionType });
208
337
  break;
209
- } catch (e) {
210
- console.log(`[Magnetk-JS] Failed to connect to ${addr}.`);
211
- }
338
+ } catch (e) { }
212
339
  }
213
340
 
214
- if (!socket) {
215
- throw new Error('All seed connection attempts failed. Possible NAT blocking.');
216
- }
341
+ if (!socket) throw new Error('All seed connection attempts failed.');
217
342
 
218
343
  this.lastDownloadStats.connectionType = connectionType;
219
344
  this.lastDownloadStats.remoteAddr = finalAddr;
220
- console.log(`[Magnetk-JS] » Final Connection: ${finalAddr} (${connectionType})`);
221
345
 
222
346
  const writer = new FrameWriter(socket);
223
347
  const reader = new FrameReader(socket);
224
348
 
225
349
  try {
226
- // STEP 1: Request manifest
227
- console.log(`[Magnetk-JS] Requesting manifest for ${ml.fileHash.substring(0, 10)}...`);
228
350
  await writer.writeJSON(MSG_TYPE.MSG_MANIFEST_REQ, { file_hash: ml.fileHash });
229
-
230
351
  const manifestFrame = await reader.readFrame();
231
- if (manifestFrame.type !== MSG_TYPE.MSG_MANIFEST_RES) {
232
- throw new Error(`Unexpected manifest response type: 0x${manifestFrame.type.toString(16)}`);
233
- }
234
-
235
352
  const manifest = JSON.parse(manifestFrame.payload.toString());
236
- console.log(`[Magnetk-JS] Manifest received: ${manifest.file_name}, ${manifest.total_chunks} chunks, total size: ${manifest.file_size} bytes`);
237
353
 
238
- // STEP 2: Prepare output file
239
354
  const fd = fs.openSync(outputPath, 'w');
355
+ let bytesDownloaded = 0;
356
+ const startTime = Date.now();
240
357
 
241
- // STEP 3: Request and write chunks
242
358
  for (let i = 0; i < manifest.total_chunks; i++) {
243
- process.stdout.write(`\r[Magnetk-JS] Downloading chunk ${i + 1}/${manifest.total_chunks}...`);
244
-
245
359
  await writer.writeJSON(MSG_TYPE.MSG_CHUNK_REQ, {
246
360
  file_hash: ml.fileHash,
247
361
  chunk_indices: [i]
248
362
  });
249
363
 
250
364
  const chunkFrame = await reader.readFrame();
251
- if (chunkFrame.type !== MSG_TYPE.MSG_CHUNK_RES) {
252
- throw new Error(`Unexpected chunk response type: 0x${chunkFrame.type.toString(16)}`);
253
- }
254
-
255
- // Chunk payload format: [4 bytes: index][raw data]
256
- const index = chunkFrame.payload.readUInt32BE(0);
257
365
  const data = chunkFrame.payload.slice(4);
258
366
 
259
- // Write to file at correct offset
260
- const offset = i * manifest.chunk_size;
261
- fs.writeSync(fd, data, 0, data.length, offset);
367
+ fs.writeSync(fd, data, 0, data.length, i * manifest.chunk_size);
368
+
369
+ bytesDownloaded += data.length;
370
+ const percent = (bytesDownloaded / manifest.file_size) * 100;
371
+ const elapsedS = (Date.now() - startTime) / 1000;
372
+ const speed = elapsedS > 0 ? (bytesDownloaded / 1024 / elapsedS).toFixed(1) : 0;
373
+
374
+ this.emit('progress', {
375
+ percent,
376
+ bytesDownloaded,
377
+ totalBytes: manifest.file_size,
378
+ speed
379
+ });
262
380
  }
263
381
 
264
382
  fs.closeSync(fd);
265
- console.log(`\n[Magnetk-JS] Download complete: ${outputPath}`);
266
-
267
- } catch (err) {
268
- console.error(`[Magnetk-JS] Download failed: ${err.message}`);
269
- throw err;
383
+ this.emit('complete', { filePath: outputPath });
270
384
  } finally {
271
385
  socket.end();
272
386
  }
273
387
  }
388
+
389
+ /**
390
+ * Finds available seeders for a hash.
391
+ */
392
+ async lookupSeeds(fileHash) {
393
+ const result = await this.lookupPeer(this.config.relayUrl, this.config.relayPort, fileHash);
394
+ return result.found ? result.seeds : [];
395
+ }
396
+
397
+ _calculateHash(filePath) {
398
+ return new Promise((resolve, reject) => {
399
+ const hash = crypto.createHash('sha256');
400
+ const stream = fs.createReadStream(filePath);
401
+ stream.on('data', data => hash.update(data));
402
+ stream.on('end', () => resolve(hash.digest('hex')));
403
+ stream.on('error', reject);
404
+ });
405
+ }
406
+
407
+ _resolveBinPath(binName) {
408
+ // Try to find bin in likely locations
409
+ const paths = [
410
+ path.join(process.cwd(), 'bin', binName),
411
+ path.join(process.cwd(), '..', '..', '..', 'bin', binName),
412
+ path.join(process.cwd(), '..', '..', 'bin', binName),
413
+ path.join(process.cwd(), '..', 'bin', binName)
414
+ ];
415
+ for (const p of paths) {
416
+ if (fs.existsSync(p)) return p;
417
+ }
418
+ return binName; // Fallback to PATH
419
+ }
420
+
421
+ _resolveConfigPath() {
422
+ const paths = [
423
+ path.join(process.cwd(), 'config', 'settings.conf'),
424
+ path.join(process.cwd(), '..', '..', '..', 'config', 'settings.conf'),
425
+ path.join(process.cwd(), '..', '..', 'config', 'settings.conf')
426
+ ];
427
+ for (const p of paths) {
428
+ if (fs.existsSync(p)) return p;
429
+ }
430
+ return ''; // Let seeder use default
431
+ }
274
432
  }
275
433
 
276
434
  class FrameWriter {
package/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './client.js';
2
2
  export * from './magnetk.js';
3
+ export { CONNECTION_TYPE } from './client.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magnetk",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "JavaScript SDK for Magnetk P2P File Transfer System",
5
5
  "main": "index.js",
6
6
  "bin": {