pal-explorer-cli 0.4.7 → 0.4.9
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.
- package/bin/pal.js +231 -231
- package/extensions/@palexplorer/analytics/extension.json +1 -1
- package/extensions/@palexplorer/discovery/extension.json +1 -0
- package/extensions/@palexplorer/explorer-integration/extension.json +1 -1
- package/extensions/@palexplorer/networks/extension.json +1 -1
- package/extensions/@palexplorer/vfs/extension.json +1 -1
- package/lib/commands/sync.js +541 -389
- package/lib/core/extensions.js +16 -22
- package/lib/core/syncEngine.js +276 -3
- package/lib/core/syncOptions.js +80 -0
- package/lib/core/syncProtocolHandlers.js +87 -0
- package/lib/core/syncTransport.js +203 -0
- package/package.json +68 -68
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import zlib from 'zlib';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { sendAuthenticatedRequest } from './signalingServer.js';
|
|
4
|
+
import { selectBestTransport, TRANSPORT } from './streamTransport.js';
|
|
5
|
+
import { getNearbyPeers } from './mdnsService.js';
|
|
6
|
+
import { getPrimaryServer } from './discoveryClient.js';
|
|
7
|
+
import config from '../utils/config.js';
|
|
8
|
+
import logger from '../utils/logger.js';
|
|
9
|
+
|
|
10
|
+
const gzip = promisify(zlib.gzip);
|
|
11
|
+
const gunzip = promisify(zlib.gunzip);
|
|
12
|
+
|
|
13
|
+
// Base transport class — all sync transports implement send() and sendFile()
|
|
14
|
+
class SyncTransportBase {
|
|
15
|
+
constructor(type) {
|
|
16
|
+
this.type = type;
|
|
17
|
+
}
|
|
18
|
+
async send(envelope) { throw new Error('Not implemented'); }
|
|
19
|
+
async sendFile(filePath, relativePath, opts) { throw new Error('Not implemented'); }
|
|
20
|
+
async sendChunk(data) { throw new Error('Not implemented'); }
|
|
21
|
+
close() {}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// LAN transport — send PAL/1.0 envelopes via signaling server TCP (port 7474)
|
|
25
|
+
export class LanTransport extends SyncTransportBase {
|
|
26
|
+
constructor(peerAddress) {
|
|
27
|
+
super('lan');
|
|
28
|
+
const [host, port] = peerAddress.split(':');
|
|
29
|
+
this.host = host;
|
|
30
|
+
this.port = parseInt(port, 10) || 7474;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async send(envelope) {
|
|
34
|
+
const identity = config.get('identity');
|
|
35
|
+
if (!identity?.privateKey || !identity?.publicKey) {
|
|
36
|
+
throw new Error('Identity not configured');
|
|
37
|
+
}
|
|
38
|
+
const result = await sendAuthenticatedRequest(
|
|
39
|
+
this.host, this.port,
|
|
40
|
+
{ type: 'message', envelope },
|
|
41
|
+
identity.privateKey, identity.publicKey, 10000
|
|
42
|
+
);
|
|
43
|
+
if (result.error) throw new Error(result.error);
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async sendFile(readStream, relativePath, { onChunk } = {}) {
|
|
48
|
+
// File data is sent as sync.request response chunks via the same TCP protocol
|
|
49
|
+
// For LAN, we embed file data in the envelope payload (chunked for large files)
|
|
50
|
+
const chunks = [];
|
|
51
|
+
for await (const chunk of readStream) {
|
|
52
|
+
chunks.push(chunk);
|
|
53
|
+
if (onChunk) onChunk(chunk.length);
|
|
54
|
+
}
|
|
55
|
+
return Buffer.concat(chunks);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// WebRTC transport — send via WebRTC data channel (for internet peers)
|
|
60
|
+
export class WebRTCTransport extends SyncTransportBase {
|
|
61
|
+
constructor(iceServers, localPK, remotePK) {
|
|
62
|
+
super('webrtc');
|
|
63
|
+
this.iceServers = iceServers;
|
|
64
|
+
this.localPK = localPK;
|
|
65
|
+
this.remotePK = remotePK;
|
|
66
|
+
this.dataChannel = null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async send(envelope) {
|
|
70
|
+
if (!this.dataChannel) {
|
|
71
|
+
throw new Error('WebRTC data channel not established');
|
|
72
|
+
}
|
|
73
|
+
const data = JSON.stringify(envelope);
|
|
74
|
+
this.dataChannel.send(data);
|
|
75
|
+
return { received: true };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async sendChunk(data) {
|
|
79
|
+
if (!this.dataChannel) {
|
|
80
|
+
throw new Error('WebRTC data channel not established');
|
|
81
|
+
}
|
|
82
|
+
// 16KB chunks with backpressure
|
|
83
|
+
const CHUNK_SIZE = 16384;
|
|
84
|
+
for (let offset = 0; offset < data.length; offset += CHUNK_SIZE) {
|
|
85
|
+
const chunk = data.subarray(offset, offset + CHUNK_SIZE);
|
|
86
|
+
while (this.dataChannel.bufferedAmount > 1024 * 1024) {
|
|
87
|
+
await new Promise(r => setTimeout(r, 10));
|
|
88
|
+
}
|
|
89
|
+
this.dataChannel.send(chunk);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
setDataChannel(dc) {
|
|
94
|
+
this.dataChannel = dc;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
close() {
|
|
98
|
+
if (this.dataChannel) {
|
|
99
|
+
try { this.dataChannel.close(); } catch {}
|
|
100
|
+
this.dataChannel = null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Discovery transport — fallback to HTTP via discovery server (extension-only)
|
|
106
|
+
export class DiscoveryTransport extends SyncTransportBase {
|
|
107
|
+
constructor(toHandle, fromHandle) {
|
|
108
|
+
super('discovery');
|
|
109
|
+
this.toHandle = toHandle;
|
|
110
|
+
this.fromHandle = fromHandle;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async send(envelope) {
|
|
114
|
+
const serverUrl = getPrimaryServer();
|
|
115
|
+
const res = await fetch(`${serverUrl}/messages`, {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: { 'Content-Type': 'application/json' },
|
|
118
|
+
body: JSON.stringify({
|
|
119
|
+
toHandle: this.toHandle,
|
|
120
|
+
fromHandle: this.fromHandle,
|
|
121
|
+
payload: envelope,
|
|
122
|
+
}),
|
|
123
|
+
signal: AbortSignal.timeout(15000),
|
|
124
|
+
});
|
|
125
|
+
if (!res.ok) {
|
|
126
|
+
const err = await res.json().catch(() => ({}));
|
|
127
|
+
throw new Error(`Discovery server error: ${res.status} ${err.error || res.statusText}`);
|
|
128
|
+
}
|
|
129
|
+
return { received: true };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Select the best available transport for a given peer
|
|
134
|
+
export async function selectSyncTransport(peerPK, { peerHandle } = {}) {
|
|
135
|
+
const identity = config.get('identity');
|
|
136
|
+
|
|
137
|
+
// Try P2P transports first (LAN, then WebRTC)
|
|
138
|
+
try {
|
|
139
|
+
const best = await selectBestTransport(peerPK);
|
|
140
|
+
if (best) {
|
|
141
|
+
if (best.transport === TRANSPORT.LAN) {
|
|
142
|
+
return new LanTransport(best.address);
|
|
143
|
+
}
|
|
144
|
+
if (best.transport === TRANSPORT.P2P) {
|
|
145
|
+
return new WebRTCTransport(best.iceServers, identity?.publicKey, peerPK);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch (err) {
|
|
149
|
+
logger.debug(`P2P transport selection failed: ${err.message}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Fallback to discovery server if peer has a handle
|
|
153
|
+
if (peerHandle && identity?.handle) {
|
|
154
|
+
return new DiscoveryTransport(peerHandle, identity.handle);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
throw new Error('No available transport to reach peer. Ensure both peers are on the same LAN or have Pro for internet sync.');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Find a peer's LAN address by public key (used for direct connection)
|
|
161
|
+
export function findLanPeer(peerPK) {
|
|
162
|
+
const nearby = getNearbyPeers();
|
|
163
|
+
return nearby.find(p => p.publicKey === peerPK) || null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// --- Compression helpers ---
|
|
167
|
+
|
|
168
|
+
export async function compressData(data) {
|
|
169
|
+
return gzip(data);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function decompressData(data) {
|
|
173
|
+
return gunzip(data);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// --- Bandwidth throttle (token bucket) ---
|
|
177
|
+
|
|
178
|
+
export class BandwidthThrottle {
|
|
179
|
+
constructor(kbps) {
|
|
180
|
+
this.bytesPerMs = (kbps * 1024) / 1000;
|
|
181
|
+
this.tokens = kbps * 1024; // start with 1 second of tokens
|
|
182
|
+
this.lastRefill = Date.now();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async throttle(bytes) {
|
|
186
|
+
if (this.bytesPerMs <= 0) return; // no limit
|
|
187
|
+
|
|
188
|
+
// Refill tokens
|
|
189
|
+
const now = Date.now();
|
|
190
|
+
const elapsed = now - this.lastRefill;
|
|
191
|
+
this.tokens = Math.min(this.bytesPerMs * 1000, this.tokens + elapsed * this.bytesPerMs);
|
|
192
|
+
this.lastRefill = now;
|
|
193
|
+
|
|
194
|
+
// Wait if we don't have enough tokens
|
|
195
|
+
if (this.tokens < bytes) {
|
|
196
|
+
const waitMs = Math.ceil((bytes - this.tokens) / this.bytesPerMs);
|
|
197
|
+
await new Promise(r => setTimeout(r, waitMs));
|
|
198
|
+
this.tokens = 0;
|
|
199
|
+
} else {
|
|
200
|
+
this.tokens -= bytes;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
package/package.json
CHANGED
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "pal-explorer-cli",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "P2P encrypted file sharing CLI — share files directly with friends, not with the cloud",
|
|
5
|
-
"main": "bin/pal.js",
|
|
6
|
-
"bin": {
|
|
7
|
-
"pe": "./bin/pal.js"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"bin/",
|
|
11
|
-
"lib/",
|
|
12
|
-
"extensions/@palexplorer/*/extension.json",
|
|
13
|
-
"extensions/@palexplorer/*/index.js",
|
|
14
|
-
"LICENSE.md"
|
|
15
|
-
],
|
|
16
|
-
"scripts": {
|
|
17
|
-
"start": "node bin/pal.js",
|
|
18
|
-
"test": "node --experimental-test-module-mocks --test \"test/commands/*.test.js\" \"test/core/*.test.js\" \"test/crypto/*.test.js\" \"test/gui/*.test.js\" \"test/stress/*.test.js\" \"test/utils/*.test.js\" \"test/*.test.js\"",
|
|
19
|
-
"test:e2e": "npx playwright test",
|
|
20
|
-
"build": "node scripts/build.js",
|
|
21
|
-
"build:win": "node scripts/build.js --win",
|
|
22
|
-
"build:mac": "node scripts/build.js --mac",
|
|
23
|
-
"build:linux": "node scripts/build.js --linux",
|
|
24
|
-
"postinstall": "node -e \"console.log('\\n Run: pe init to get started\\n')\""
|
|
25
|
-
},
|
|
26
|
-
"engines": {
|
|
27
|
-
"node": ">=20.0.0"
|
|
28
|
-
},
|
|
29
|
-
"keywords": [
|
|
30
|
-
"p2p",
|
|
31
|
-
"file-sharing",
|
|
32
|
-
"encrypted",
|
|
33
|
-
"e2e",
|
|
34
|
-
"peer-to-peer",
|
|
35
|
-
"webtorrent",
|
|
36
|
-
"cli",
|
|
37
|
-
"privacy"
|
|
38
|
-
],
|
|
39
|
-
"dependencies": {
|
|
40
|
-
"acorn": "^8.16.0",
|
|
41
|
-
"acorn-walk": "^8.3.5",
|
|
42
|
-
"bip39": "^3.1.0",
|
|
43
|
-
"bittorrent-dht": "^11.0.11",
|
|
44
|
-
"bonjour-service": "^1.3.0",
|
|
45
|
-
"chalk": "^5.3.0",
|
|
46
|
-
"commander": "^11.1.0",
|
|
47
|
-
"conf": "^12.0.0",
|
|
48
|
-
"express": "^5.2.1",
|
|
49
|
-
"inquirer": "^9.2.12",
|
|
50
|
-
"keytar": "^7.9.0",
|
|
51
|
-
"ora": "^8.0.1",
|
|
52
|
-
"posthog-node": "^5.28.4",
|
|
53
|
-
"qrcode-terminal": "^0.12.0",
|
|
54
|
-
"regedit": "^5.1.4",
|
|
55
|
-
"sodium-native": "^5.0.10",
|
|
56
|
-
"v8-compile-cache-lib": "^3.0.1",
|
|
57
|
-
"webdav-server": "^2.6.2",
|
|
58
|
-
"webtorrent": "^2.1.33",
|
|
59
|
-
"ws": "^8.19.0"
|
|
60
|
-
},
|
|
61
|
-
"author": "Palexplorer",
|
|
62
|
-
"license": "SEE LICENSE IN LICENSE.md",
|
|
63
|
-
"homepage": "https://palexplorer.com",
|
|
64
|
-
"type": "module",
|
|
65
|
-
"devDependencies": {
|
|
66
|
-
"@playwright/test": "^1.58.2"
|
|
67
|
-
}
|
|
68
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "pal-explorer-cli",
|
|
3
|
+
"version": "0.4.9",
|
|
4
|
+
"description": "P2P encrypted file sharing CLI — share files directly with friends, not with the cloud",
|
|
5
|
+
"main": "bin/pal.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pe": "./bin/pal.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"lib/",
|
|
12
|
+
"extensions/@palexplorer/*/extension.json",
|
|
13
|
+
"extensions/@palexplorer/*/index.js",
|
|
14
|
+
"LICENSE.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "node bin/pal.js",
|
|
18
|
+
"test": "node --experimental-test-module-mocks --test \"test/commands/*.test.js\" \"test/core/*.test.js\" \"test/crypto/*.test.js\" \"test/gui/*.test.js\" \"test/stress/*.test.js\" \"test/utils/*.test.js\" \"test/*.test.js\"",
|
|
19
|
+
"test:e2e": "npx playwright test",
|
|
20
|
+
"build": "node scripts/build.js",
|
|
21
|
+
"build:win": "node scripts/build.js --win",
|
|
22
|
+
"build:mac": "node scripts/build.js --mac",
|
|
23
|
+
"build:linux": "node scripts/build.js --linux",
|
|
24
|
+
"postinstall": "node -e \"console.log('\\n Run: pe init to get started\\n')\""
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20.0.0"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"p2p",
|
|
31
|
+
"file-sharing",
|
|
32
|
+
"encrypted",
|
|
33
|
+
"e2e",
|
|
34
|
+
"peer-to-peer",
|
|
35
|
+
"webtorrent",
|
|
36
|
+
"cli",
|
|
37
|
+
"privacy"
|
|
38
|
+
],
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"acorn": "^8.16.0",
|
|
41
|
+
"acorn-walk": "^8.3.5",
|
|
42
|
+
"bip39": "^3.1.0",
|
|
43
|
+
"bittorrent-dht": "^11.0.11",
|
|
44
|
+
"bonjour-service": "^1.3.0",
|
|
45
|
+
"chalk": "^5.3.0",
|
|
46
|
+
"commander": "^11.1.0",
|
|
47
|
+
"conf": "^12.0.0",
|
|
48
|
+
"express": "^5.2.1",
|
|
49
|
+
"inquirer": "^9.2.12",
|
|
50
|
+
"keytar": "^7.9.0",
|
|
51
|
+
"ora": "^8.0.1",
|
|
52
|
+
"posthog-node": "^5.28.4",
|
|
53
|
+
"qrcode-terminal": "^0.12.0",
|
|
54
|
+
"regedit": "^5.1.4",
|
|
55
|
+
"sodium-native": "^5.0.10",
|
|
56
|
+
"v8-compile-cache-lib": "^3.0.1",
|
|
57
|
+
"webdav-server": "^2.6.2",
|
|
58
|
+
"webtorrent": "^2.1.33",
|
|
59
|
+
"ws": "^8.19.0"
|
|
60
|
+
},
|
|
61
|
+
"author": "Palexplorer",
|
|
62
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
63
|
+
"homepage": "https://palexplorer.com",
|
|
64
|
+
"type": "module",
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@playwright/test": "^1.58.2"
|
|
67
|
+
}
|
|
68
|
+
}
|