pal-explorer-cli 0.4.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 (156) hide show
  1. package/LICENSE.md +18 -0
  2. package/README.md +314 -0
  3. package/bin/pal.js +230 -0
  4. package/extensions/@palexplorer/analytics/README.md +45 -0
  5. package/extensions/@palexplorer/analytics/docs/MONETIZATION.md +14 -0
  6. package/extensions/@palexplorer/analytics/docs/PLAN.md +23 -0
  7. package/extensions/@palexplorer/analytics/docs/PRIVACY.md +38 -0
  8. package/extensions/@palexplorer/analytics/extension.json +27 -0
  9. package/extensions/@palexplorer/analytics/index.js +186 -0
  10. package/extensions/@palexplorer/analytics/test/analytics.test.js +82 -0
  11. package/extensions/@palexplorer/audit/extension.json +17 -0
  12. package/extensions/@palexplorer/audit/index.js +2 -0
  13. package/extensions/@palexplorer/auth-email/extension.json +17 -0
  14. package/extensions/@palexplorer/auth-email/index.js +102 -0
  15. package/extensions/@palexplorer/auth-oauth/extension.json +16 -0
  16. package/extensions/@palexplorer/auth-oauth/index.js +199 -0
  17. package/extensions/@palexplorer/chat/extension.json +17 -0
  18. package/extensions/@palexplorer/chat/index.js +2 -0
  19. package/extensions/@palexplorer/discovery/extension.json +16 -0
  20. package/extensions/@palexplorer/discovery/index.js +111 -0
  21. package/extensions/@palexplorer/email-notifications/extension.json +23 -0
  22. package/extensions/@palexplorer/email-notifications/index.js +242 -0
  23. package/extensions/@palexplorer/explorer-integration/extension.json +13 -0
  24. package/extensions/@palexplorer/explorer-integration/index.js +122 -0
  25. package/extensions/@palexplorer/groups/extension.json +17 -0
  26. package/extensions/@palexplorer/groups/index.js +2 -0
  27. package/extensions/@palexplorer/networks/extension.json +17 -0
  28. package/extensions/@palexplorer/networks/index.js +2 -0
  29. package/extensions/@palexplorer/share-links/extension.json +17 -0
  30. package/extensions/@palexplorer/share-links/index.js +2 -0
  31. package/extensions/@palexplorer/sync/extension.json +17 -0
  32. package/extensions/@palexplorer/sync/index.js +2 -0
  33. package/extensions/@palexplorer/user-mgmt/extension.json +17 -0
  34. package/extensions/@palexplorer/user-mgmt/index.js +2 -0
  35. package/extensions/@palexplorer/vfs/extension.json +17 -0
  36. package/extensions/@palexplorer/vfs/index.js +167 -0
  37. package/lib/capabilities.js +263 -0
  38. package/lib/commands/analytics.js +175 -0
  39. package/lib/commands/api-keys.js +131 -0
  40. package/lib/commands/audit.js +235 -0
  41. package/lib/commands/auth.js +137 -0
  42. package/lib/commands/backup.js +76 -0
  43. package/lib/commands/billing.js +148 -0
  44. package/lib/commands/chat.js +217 -0
  45. package/lib/commands/cloud-backup.js +231 -0
  46. package/lib/commands/comment.js +99 -0
  47. package/lib/commands/completion.js +203 -0
  48. package/lib/commands/compliance.js +218 -0
  49. package/lib/commands/config.js +136 -0
  50. package/lib/commands/connect.js +44 -0
  51. package/lib/commands/dept.js +294 -0
  52. package/lib/commands/device.js +146 -0
  53. package/lib/commands/download.js +226 -0
  54. package/lib/commands/explorer.js +178 -0
  55. package/lib/commands/extension.js +970 -0
  56. package/lib/commands/favorite.js +90 -0
  57. package/lib/commands/federation.js +270 -0
  58. package/lib/commands/file.js +533 -0
  59. package/lib/commands/group.js +271 -0
  60. package/lib/commands/gui-share.js +29 -0
  61. package/lib/commands/init.js +61 -0
  62. package/lib/commands/invite.js +59 -0
  63. package/lib/commands/list.js +59 -0
  64. package/lib/commands/log.js +116 -0
  65. package/lib/commands/nearby.js +108 -0
  66. package/lib/commands/network.js +251 -0
  67. package/lib/commands/notify.js +198 -0
  68. package/lib/commands/org.js +273 -0
  69. package/lib/commands/pal.js +180 -0
  70. package/lib/commands/permissions.js +216 -0
  71. package/lib/commands/pin.js +97 -0
  72. package/lib/commands/protocol.js +357 -0
  73. package/lib/commands/rbac.js +147 -0
  74. package/lib/commands/recover.js +36 -0
  75. package/lib/commands/register.js +171 -0
  76. package/lib/commands/relay.js +131 -0
  77. package/lib/commands/remote.js +368 -0
  78. package/lib/commands/revoke.js +50 -0
  79. package/lib/commands/scanner.js +280 -0
  80. package/lib/commands/schedule.js +344 -0
  81. package/lib/commands/scim.js +203 -0
  82. package/lib/commands/search.js +181 -0
  83. package/lib/commands/serve.js +438 -0
  84. package/lib/commands/server.js +350 -0
  85. package/lib/commands/share-link.js +199 -0
  86. package/lib/commands/share.js +323 -0
  87. package/lib/commands/sso.js +200 -0
  88. package/lib/commands/status.js +136 -0
  89. package/lib/commands/stream.js +562 -0
  90. package/lib/commands/su.js +187 -0
  91. package/lib/commands/sync.js +827 -0
  92. package/lib/commands/transfers.js +152 -0
  93. package/lib/commands/uninstall.js +188 -0
  94. package/lib/commands/update.js +204 -0
  95. package/lib/commands/user.js +276 -0
  96. package/lib/commands/vfs.js +84 -0
  97. package/lib/commands/web.js +52 -0
  98. package/lib/commands/webhook.js +180 -0
  99. package/lib/commands/whoami.js +59 -0
  100. package/lib/commands/workspace.js +121 -0
  101. package/lib/core/accessLog.js +54 -0
  102. package/lib/core/analytics.js +99 -0
  103. package/lib/core/backup.js +84 -0
  104. package/lib/core/billing.js +336 -0
  105. package/lib/core/bitfieldStore.js +53 -0
  106. package/lib/core/connectionManager.js +182 -0
  107. package/lib/core/dhtDiscovery.js +148 -0
  108. package/lib/core/discoveryClient.js +408 -0
  109. package/lib/core/extensionAnalyzer.js +357 -0
  110. package/lib/core/extensionSandbox.js +250 -0
  111. package/lib/core/extensionWorkerHost.js +166 -0
  112. package/lib/core/extensions.js +1082 -0
  113. package/lib/core/fileDiff.js +69 -0
  114. package/lib/core/groups.js +119 -0
  115. package/lib/core/identity.js +340 -0
  116. package/lib/core/mdnsService.js +126 -0
  117. package/lib/core/networks.js +81 -0
  118. package/lib/core/permissions.js +109 -0
  119. package/lib/core/pro.js +27 -0
  120. package/lib/core/resolver.js +74 -0
  121. package/lib/core/serverList.js +224 -0
  122. package/lib/core/sharePolicy.js +69 -0
  123. package/lib/core/shares.js +325 -0
  124. package/lib/core/signalingServer.js +441 -0
  125. package/lib/core/streamTransport.js +106 -0
  126. package/lib/core/su.js +55 -0
  127. package/lib/core/syncEngine.js +264 -0
  128. package/lib/core/syncState.js +159 -0
  129. package/lib/core/transfers.js +259 -0
  130. package/lib/core/users.js +225 -0
  131. package/lib/core/vfs.js +216 -0
  132. package/lib/core/webServer.js +702 -0
  133. package/lib/core/webrtcStream.js +396 -0
  134. package/lib/crypto/chatEncryption.js +57 -0
  135. package/lib/crypto/shareEncryption.js +195 -0
  136. package/lib/crypto/sharePassword.js +35 -0
  137. package/lib/crypto/streamEncryption.js +189 -0
  138. package/lib/package.json +1 -0
  139. package/lib/protocol/envelope.js +271 -0
  140. package/lib/protocol/handler.js +191 -0
  141. package/lib/protocol/index.js +27 -0
  142. package/lib/protocol/messages.js +247 -0
  143. package/lib/protocol/negotiation.js +127 -0
  144. package/lib/protocol/policy.js +142 -0
  145. package/lib/protocol/router.js +86 -0
  146. package/lib/protocol/sync.js +122 -0
  147. package/lib/utils/cli.js +15 -0
  148. package/lib/utils/config.js +123 -0
  149. package/lib/utils/configIntegrity.js +87 -0
  150. package/lib/utils/downloadDir.js +9 -0
  151. package/lib/utils/explorer.js +83 -0
  152. package/lib/utils/format.js +12 -0
  153. package/lib/utils/help.js +357 -0
  154. package/lib/utils/logger.js +103 -0
  155. package/lib/utils/torrent.js +203 -0
  156. package/package.json +71 -0
@@ -0,0 +1,53 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync, readdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+
5
+ const STORE_DIR = join(homedir(), '.palexplorer', 'bitfields');
6
+
7
+ function ensureDir() {
8
+ if (!existsSync(STORE_DIR)) mkdirSync(STORE_DIR, { recursive: true });
9
+ }
10
+
11
+ function keyFor(infoHash) {
12
+ return join(STORE_DIR, `${infoHash}.bf`);
13
+ }
14
+
15
+ export function saveBitfield(infoHash, bitfield) {
16
+ ensureDir();
17
+ const data = Buffer.isBuffer(bitfield) ? bitfield : Buffer.from(bitfield);
18
+ const header = Buffer.alloc(8);
19
+ header.writeUInt32LE(1, 0); // version
20
+ header.writeUInt32LE(data.length, 4);
21
+ writeFileSync(keyFor(infoHash), Buffer.concat([header, data]));
22
+ }
23
+
24
+ export function loadBitfield(infoHash) {
25
+ const path = keyFor(infoHash);
26
+ if (!existsSync(path)) return null;
27
+ try {
28
+ const raw = readFileSync(path);
29
+ if (raw.length < 8) return null;
30
+ const version = raw.readUInt32LE(0);
31
+ const len = raw.readUInt32LE(4);
32
+ if (version !== 1 || raw.length < 8 + len) return null;
33
+ return raw.subarray(8, 8 + len);
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ export function removeBitfield(infoHash) {
40
+ const path = keyFor(infoHash);
41
+ if (existsSync(path)) {
42
+ try { unlinkSync(path); } catch {}
43
+ }
44
+ }
45
+
46
+ export function clearAllBitfields() {
47
+ ensureDir();
48
+ for (const file of readdirSync(STORE_DIR)) {
49
+ if (file.endsWith('.bf')) {
50
+ try { unlinkSync(join(STORE_DIR, file)); } catch {}
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,182 @@
1
+ import config from '../utils/config.js';
2
+ import { getServers, getHealthyPrimary, checkServer, replayOfflineQueue } from './discoveryClient.js';
3
+ import { refreshServerList, healthCheckServers, setHealthSortedServers } from './serverList.js';
4
+ import { PROTOCOL_NAME, PROTOCOL_VERSION } from '../protocol/index.js';
5
+
6
+ let connectionState = {
7
+ status: 'disconnected', // 'connecting' | 'connected' | 'disconnected'
8
+ servers: [],
9
+ connectedCount: 0,
10
+ heartbeatTimer: null,
11
+ healthTimer: null,
12
+ consecutiveFailures: 0,
13
+ };
14
+
15
+ const primaryChangedListeners = [];
16
+
17
+ export function onPrimaryChanged(callback) {
18
+ primaryChangedListeners.push(callback);
19
+ return () => { const i = primaryChangedListeners.indexOf(callback); if (i !== -1) primaryChangedListeners.splice(i, 1); };
20
+ }
21
+
22
+ function emitPrimaryChanged(oldServer, newServer) {
23
+ for (const cb of primaryChangedListeners) {
24
+ try { cb({ oldServer, newServer }); } catch {}
25
+ }
26
+ }
27
+
28
+ async function updateHealthSorted() {
29
+ const servers = getServers();
30
+ const checks = await healthCheckServers(servers);
31
+ const reachable = checks.filter(c => c.reachable);
32
+ reachable.sort((a, b) => a.latencyMs - b.latencyMs);
33
+ const sorted = reachable.map(c => c.url);
34
+ setHealthSortedServers(sorted);
35
+ connectionState.servers = checks;
36
+ connectionState.connectedCount = reachable.length;
37
+ return { checks, sorted };
38
+ }
39
+
40
+ export function getConnectionState() {
41
+ return {
42
+ status: connectionState.status,
43
+ servers: connectionState.servers,
44
+ connectedCount: connectionState.connectedCount,
45
+ protocol: connectionState.protocol || null,
46
+ protocolVersion: connectionState.protocolVersion || null,
47
+ };
48
+ }
49
+
50
+ export async function connect({ onProgress } = {}) {
51
+ if (connectionState.status === 'connected') return getConnectionState();
52
+ connectionState.status = 'connecting';
53
+
54
+ // Step 1: Fetch server list
55
+ onProgress?.('Fetching server list...', 10);
56
+ try {
57
+ await refreshServerList((step) => onProgress?.(step, 15));
58
+ } catch {}
59
+
60
+ // Step 2: Health check servers and sort by latency
61
+ onProgress?.('Checking server health...', 20);
62
+ const { checks, sorted } = await updateHealthSorted();
63
+ const reachable = checks.filter(c => c.reachable);
64
+
65
+ if (reachable.length === 0) {
66
+ onProgress?.('No servers reachable', 25);
67
+ } else {
68
+ onProgress?.(`Connected to ${reachable.length} server(s)`, 30);
69
+ }
70
+
71
+ // Step 3: Start heartbeat
72
+ onProgress?.('Starting heartbeat...', 40);
73
+ const identity = config.get('identity');
74
+ const device = config.get('device');
75
+ if (identity?.handle && device?.id) {
76
+ startHeartbeat(identity.handle, device.id);
77
+ }
78
+
79
+ // Step 4: Start DHT
80
+ onProgress?.('Announcing presence...', 55);
81
+ try {
82
+ if (identity?.handle && identity?.publicKey) {
83
+ const { DHTDiscovery } = await import('./dhtDiscovery.js');
84
+ const dht = new DHTDiscovery();
85
+ await dht.publish(identity.handle, identity.publicKey, identity.privateKey || '');
86
+ dht.destroy();
87
+ }
88
+ } catch {}
89
+
90
+ // Step 5: Replay offline queue
91
+ onProgress?.('Replaying offline queue...', 65);
92
+ try {
93
+ await replayOfflineQueue();
94
+ } catch {}
95
+
96
+ // Step 6: Start periodic health monitoring (every 5 minutes)
97
+ startHealthMonitor();
98
+
99
+ connectionState.status = 'connected';
100
+ connectionState.protocol = PROTOCOL_NAME;
101
+ connectionState.protocolVersion = PROTOCOL_VERSION;
102
+ onProgress?.('Connected', 70);
103
+ return getConnectionState();
104
+ }
105
+
106
+ export async function disconnect() {
107
+ if (connectionState.heartbeatTimer) {
108
+ clearInterval(connectionState.heartbeatTimer);
109
+ connectionState.heartbeatTimer = null;
110
+ }
111
+ if (connectionState.healthTimer) {
112
+ clearInterval(connectionState.healthTimer);
113
+ connectionState.healthTimer = null;
114
+ }
115
+
116
+ // Unannounce from discovery
117
+ const identity = config.get('identity');
118
+ if (identity?.handle) {
119
+ const server = getHealthyPrimary();
120
+ try {
121
+ await fetch(`${server}/api/v1/unannounce`, {
122
+ method: 'POST',
123
+ headers: { 'Content-Type': 'application/json' },
124
+ body: JSON.stringify({ handle: identity.handle }),
125
+ signal: AbortSignal.timeout(3000),
126
+ });
127
+ } catch {}
128
+ }
129
+
130
+ connectionState.status = 'disconnected';
131
+ connectionState.servers = [];
132
+ connectionState.connectedCount = 0;
133
+ connectionState.consecutiveFailures = 0;
134
+ setHealthSortedServers(null);
135
+ return getConnectionState();
136
+ }
137
+
138
+ function startHeartbeat(handle, deviceId) {
139
+ if (connectionState.heartbeatTimer) clearInterval(connectionState.heartbeatTimer);
140
+ const beat = async () => {
141
+ const server = getHealthyPrimary();
142
+ try {
143
+ const res = await fetch(`${server}/api/v1/heartbeat`, {
144
+ method: 'POST',
145
+ headers: { 'Content-Type': 'application/json' },
146
+ body: JSON.stringify({ handle, deviceId }),
147
+ signal: AbortSignal.timeout(5000),
148
+ });
149
+ if (res.ok) {
150
+ connectionState.consecutiveFailures = 0;
151
+ } else {
152
+ connectionState.consecutiveFailures++;
153
+ }
154
+ } catch {
155
+ connectionState.consecutiveFailures++;
156
+ }
157
+ // After 3 consecutive failures, re-check health and failover
158
+ if (connectionState.consecutiveFailures >= 3) {
159
+ const oldPrimary = server;
160
+ await updateHealthSorted();
161
+ const newPrimary = getHealthyPrimary();
162
+ connectionState.consecutiveFailures = 0;
163
+ if (newPrimary !== oldPrimary) {
164
+ emitPrimaryChanged(oldPrimary, newPrimary);
165
+ }
166
+ }
167
+ };
168
+ beat();
169
+ connectionState.heartbeatTimer = setInterval(beat, 60000);
170
+ }
171
+
172
+ function startHealthMonitor() {
173
+ if (connectionState.healthTimer) clearInterval(connectionState.healthTimer);
174
+ connectionState.healthTimer = setInterval(async () => {
175
+ const oldPrimary = getHealthyPrimary();
176
+ await updateHealthSorted();
177
+ const newPrimary = getHealthyPrimary();
178
+ if (newPrimary !== oldPrimary) {
179
+ emitPrimaryChanged(oldPrimary, newPrimary);
180
+ }
181
+ }, 5 * 60 * 1000);
182
+ }
@@ -0,0 +1,148 @@
1
+ import DHT from 'bittorrent-dht';
2
+ import crypto from 'crypto';
3
+ import sodium from 'sodium-native';
4
+ import config from '../utils/config.js';
5
+ import { fetchTurnCredentials } from './discoveryClient.js';
6
+
7
+ const DEFAULT_BOOTSTRAP = [
8
+ 'router.bittorrent.com:6881',
9
+ 'dht.transmissionbt.com:6881',
10
+ 'router.utorrent.com:6881',
11
+ ];
12
+
13
+ function getRelayConfig() {
14
+ const settings = config.get('settings') || {};
15
+ return {
16
+ bootstrap: (settings.bootstrapNodes && settings.bootstrapNodes.length > 0)
17
+ ? settings.bootstrapNodes
18
+ : (settings.dht_bootstrap || DEFAULT_BOOTSTRAP),
19
+ stunServers: settings.stunServers || settings.stun_servers || ['stun:stun.l.google.com:19302'],
20
+ turnServers: settings.turn_servers || [],
21
+ };
22
+ }
23
+
24
+ // BEP 44 mutable puts use Ed25519 signatures at the DHT protocol level.
25
+ // The DHT key is SHA-1(publicKey + salt), but the DATA is signed —
26
+ // an attacker cannot replace values without the owner's private key.
27
+
28
+ function sodiumVerify(signature, value, publicKey) {
29
+ try {
30
+ if (publicKey.length !== sodium.crypto_sign_PUBLICKEYBYTES) return false;
31
+ if (signature.length !== sodium.crypto_sign_BYTES) return false;
32
+ return sodium.crypto_sign_verify_detached(signature, value, publicKey);
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+
38
+ // Convert sodium Ed25519 secret key (64 bytes) to the 32-byte seed that
39
+ // bittorrent-dht's ed25519-supercop sign() expects, plus extract the 32-byte pk.
40
+ function extractKeyPair(secretKeyHex) {
41
+ const sk = Buffer.from(secretKeyHex, 'hex');
42
+ if (sk.length !== sodium.crypto_sign_SECRETKEYBYTES) {
43
+ throw new Error(`Invalid secret key length: expected ${sodium.crypto_sign_SECRETKEYBYTES}, got ${sk.length}`);
44
+ }
45
+ // sodium Ed25519 sk = seed(32) || pk(32)
46
+ const pk = sk.subarray(32, 64);
47
+ return { pk, sk };
48
+ }
49
+
50
+ export class DHTDiscovery {
51
+ constructor(opts = {}) {
52
+ const relay = getRelayConfig();
53
+ const dhtOpts = { verify: sodiumVerify, ...opts };
54
+ if (!dhtOpts.bootstrap && relay.bootstrap.length) {
55
+ dhtOpts.bootstrap = relay.bootstrap;
56
+ }
57
+ this.relayConfig = relay;
58
+ this.dht = new DHT(dhtOpts);
59
+ this.ready = new Promise((resolve, reject) => {
60
+ this.dht.once('ready', () => {
61
+ this.dht.on('error', (err) => {
62
+ console.error('DHT error:', err.message);
63
+ });
64
+ resolve();
65
+ });
66
+ this.dht.once('error', reject);
67
+ });
68
+ }
69
+
70
+ async publish(handle, publicKey, privateKey) {
71
+ await this.ready;
72
+ const { pk, sk } = extractKeyPair(privateKey);
73
+ const value = Buffer.from(JSON.stringify({ handle, publicKey, timestamp: Date.now() }));
74
+
75
+ if (value.length > 1000) {
76
+ throw new Error('DHT payload too large (max 1000 bytes for BEP 44)');
77
+ }
78
+
79
+ const salt = Buffer.from(`pal:${handle}`);
80
+
81
+ return new Promise((resolve, reject) => {
82
+ this.dht.put({
83
+ k: pk,
84
+ v: value,
85
+ seq: Math.floor(Date.now() / 1000),
86
+ salt,
87
+ sign: (buf) => {
88
+ const sig = Buffer.alloc(sodium.crypto_sign_BYTES);
89
+ sodium.crypto_sign_detached(sig, buf, sk);
90
+ return sig;
91
+ },
92
+ }, (err, hash) => {
93
+ if (err) return reject(err);
94
+ resolve(hash.toString('hex'));
95
+ });
96
+ });
97
+ }
98
+
99
+ async resolve(handle, knownPublicKeyHex) {
100
+ await this.ready;
101
+
102
+ // For mutable lookups, we need the target hash = SHA-1(pk + salt)
103
+ // If we know the public key, we can compute the target directly
104
+ if (!knownPublicKeyHex) return null;
105
+
106
+ const pk = Buffer.from(knownPublicKeyHex, 'hex');
107
+ if (pk.length !== sodium.crypto_sign_PUBLICKEYBYTES) return null;
108
+
109
+ const salt = Buffer.from(`pal:${handle}`);
110
+ const target = crypto.createHash('sha1').update(Buffer.concat([pk, salt])).digest();
111
+
112
+ return new Promise((resolve) => {
113
+ this.dht.get(target, (err, result) => {
114
+ if (err || !result || !result.v) return resolve(null);
115
+ try {
116
+ const data = JSON.parse(result.v.toString());
117
+ if (data.handle !== handle) return resolve(null);
118
+ // Signature already verified by DHT layer (verify callback)
119
+ resolve(data);
120
+ } catch { resolve(null); }
121
+ });
122
+ });
123
+ }
124
+
125
+ getRelayServers() {
126
+ return {
127
+ stunServers: this.relayConfig.stunServers,
128
+ turnServers: this.relayConfig.turnServers,
129
+ };
130
+ }
131
+
132
+ async getRelayServersWithTurn() {
133
+ const base = this.getRelayServers();
134
+ const creds = await fetchTurnCredentials();
135
+ if (creds) {
136
+ base.turnServers = creds.urls.map(url => ({
137
+ urls: url,
138
+ username: creds.username,
139
+ credential: creds.credential,
140
+ }));
141
+ }
142
+ return base;
143
+ }
144
+
145
+ destroy() {
146
+ this.dht.destroy();
147
+ }
148
+ }