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,325 @@
1
+ import crypto from 'crypto';
2
+ import config from '../utils/config.js';
3
+ import { signConfigKey, verifyConfigKey } from '../utils/configIntegrity.js';
4
+ import { getFriends, getActivePublicKey, getActiveUser } from './users.js';
5
+ import keytar from 'keytar';
6
+ import path from 'path';
7
+ import fs from 'fs';
8
+ import { hooks } from './extensions.js';
9
+
10
+
11
+ const SHARE_KEY_SERVICE = 'pal-share-keys';
12
+
13
+ const generateId = () => crypto.randomBytes(8).toString('hex');
14
+
15
+ // Valid visibility levels (expanded from global/private)
16
+ export const VISIBILITY_LEVELS = ['public', 'global', 'private', 'group', 'network', 'link-only'];
17
+
18
+ export const SHARE_CATEGORIES = ['documents', 'media', 'projects', 'games', 'backups', 'other'];
19
+
20
+ export function addShare(filePath, type = 'file', visibility = 'global', permissions = { read: true, write: false }, recipients = [], { recursive = true, expiresIn = 0, maxDownloads = 0, password = null, category = null, description = null, color = null, icon = null } = {}) {
21
+ const shares = config.get('shares') || [];
22
+ const absolutePath = path.resolve(filePath);
23
+
24
+ const exists = shares.find(s => s.path === absolutePath);
25
+ if (exists) {
26
+ throw new Error(`Resource already shared: ${absolutePath}`);
27
+ }
28
+
29
+ const share = {
30
+ id: generateId(),
31
+ path: absolutePath,
32
+ name: path.basename(absolutePath),
33
+ type,
34
+ visibility,
35
+ permissions,
36
+ recipients,
37
+ recursive,
38
+ createdAt: new Date().toISOString(),
39
+ createdBy: getActivePublicKey(),
40
+ status: 'active',
41
+ downloads: 0,
42
+ };
43
+ if (password) share.password = password;
44
+ if (category) share.category = category;
45
+ if (description) share.description = description;
46
+ if (color) share.color = color;
47
+ if (icon) share.icon = icon;
48
+ if (expiresIn > 0) share.expiresAt = new Date(Date.now() + expiresIn).toISOString();
49
+ if (maxDownloads > 0) share.maxDownloads = maxDownloads;
50
+
51
+ shares.push(share);
52
+ config.set('shares', shares); signConfigKey('shares').catch(() => {});
53
+
54
+ // Fire extension hooks (non-blocking)
55
+ hooks.emit('after:share:create', {
56
+ path: absolutePath, name: path.basename(absolutePath), shareId: share.id,
57
+ }).catch(() => {});
58
+
59
+ return share;
60
+ }
61
+
62
+ // Async version with before-hook support (used by GUI/extensions)
63
+ export async function addShareChecked(filePath, type = 'file', visibility = 'global', permissions = { read: true, write: false }, recipients = [], opts = {}) {
64
+ const absolutePath = path.resolve(filePath);
65
+ const hookResult = await hooks.emit('before:share:create', {
66
+ path: absolutePath, name: path.basename(absolutePath), visibility, recipients,
67
+ });
68
+ if (hookResult.blocked) {
69
+ throw new Error(`Blocked by extension: ${hookResult.reason}`);
70
+ }
71
+ return addShare(filePath, type, visibility, permissions, recipients, opts);
72
+ }
73
+
74
+ export function isShareExpired(share) {
75
+ if (share.expiresAt && new Date(share.expiresAt) < new Date()) return true;
76
+ if (share.maxDownloads > 0 && (share.downloads || 0) >= share.maxDownloads) return true;
77
+ return false;
78
+ }
79
+
80
+ export function incrementShareDownloads(shareId) {
81
+ const shares = config.get('shares') || [];
82
+ const share = shares.find(s => s.id === shareId);
83
+ if (!share) return;
84
+ share.downloads = (share.downloads || 0) + 1;
85
+ if (share.maxDownloads > 0 && share.downloads >= share.maxDownloads) {
86
+ share.status = 'expired';
87
+ }
88
+ config.set('shares', shares);
89
+ }
90
+
91
+ export function expireOldShares() {
92
+ const shares = config.get('shares') || [];
93
+ let changed = false;
94
+ for (const share of shares) {
95
+ if (share.status === 'active' && isShareExpired(share)) {
96
+ share.status = 'expired';
97
+ changed = true;
98
+ }
99
+ }
100
+ if (changed) {
101
+ config.set('shares', shares);
102
+ signConfigKey('shares').catch(() => {});
103
+ }
104
+ return changed;
105
+ }
106
+
107
+ export function listShares({ allUsers = false } = {}) {
108
+ let shares = config.get('shares') || [];
109
+ if (!allUsers) {
110
+ const activeUser = getActiveUser();
111
+ if (activeUser && activeUser.role !== 'owner') {
112
+ const pk = activeUser.publicKey;
113
+ shares = shares.filter(s => s.createdBy === pk);
114
+ }
115
+ }
116
+ return shares.map(s => ({
117
+ ...s,
118
+ name: s.name || path.basename(s.path),
119
+ expired: isShareExpired(s),
120
+ }));
121
+ }
122
+
123
+ export function removeShare(idOrPath) {
124
+ const shares = config.get('shares') || [];
125
+ const initialLength = shares.length;
126
+ const resolved = path.resolve(idOrPath);
127
+ const target = shares.find(s => s.id === idOrPath || s.path === resolved);
128
+
129
+ if (!target) {
130
+ throw new Error('Share not found.');
131
+ }
132
+
133
+ // Ownership check: non-owners can only revoke their own shares
134
+ // Shares without createdBy are legacy (pre-multiuser) and belong to the owner
135
+ const activeUser = getActiveUser();
136
+ if (activeUser && activeUser.role !== 'owner') {
137
+ if (!target.createdBy || target.createdBy !== activeUser.publicKey) {
138
+ throw new Error('You can only revoke your own shares.');
139
+ }
140
+ }
141
+
142
+ const newShares = shares.filter(s => s.id !== idOrPath && s.path !== resolved);
143
+
144
+ if (newShares.length === initialLength) {
145
+ throw new Error('Share not found.');
146
+ }
147
+
148
+ config.set('shares', newShares); signConfigKey('shares').catch(() => {});
149
+
150
+ // Fire extension hooks (non-blocking)
151
+ if (target) {
152
+ hooks.emit('after:share:revoke', { path: target.path, shareId: target.id }).catch(() => {});
153
+ }
154
+
155
+ return true;
156
+ }
157
+
158
+ // Async version with before-hook support (used by GUI/extensions)
159
+ export async function removeShareChecked(idOrPath) {
160
+ const shares = config.get('shares') || [];
161
+ const resolved = path.resolve(idOrPath);
162
+ const target = shares.find(s => s.id === idOrPath || s.path === resolved);
163
+ if (target) {
164
+ const hookResult = await hooks.emit('before:share:revoke', {
165
+ path: target.path, shareId: target.id,
166
+ });
167
+ if (hookResult.blocked) {
168
+ throw new Error(`Blocked by extension: ${hookResult.reason}`);
169
+ }
170
+ }
171
+ return removeShare(idOrPath);
172
+ }
173
+
174
+ export function updateShare(idOrPath, updates) {
175
+ const shares = config.get('shares') || [];
176
+ const resolved = path.resolve(idOrPath);
177
+ const share = shares.find(s => s.id === idOrPath || s.path === resolved);
178
+ if (!share) throw new Error('Share not found.');
179
+
180
+ const allowed = ['visibility', 'recipients', 'sharedWithGroups', 'sharedWithNetworks',
181
+ 'streamable', 'recursive', 'name', 'permissions'];
182
+ for (const key of allowed) {
183
+ if (updates[key] !== undefined) share[key] = updates[key];
184
+ }
185
+ share.updatedAt = new Date().toISOString();
186
+ config.set('shares', shares); signConfigKey('shares').catch(() => {});
187
+ return share;
188
+ }
189
+
190
+ export function getShareSummary() {
191
+ const shares = config.get('shares') || [];
192
+ return shares.map(s => ({
193
+ id: s.id,
194
+ name: s.name || path.basename(s.path),
195
+ path: s.path,
196
+ type: s.type,
197
+ visibility: s.visibility || 'global',
198
+ streamable: s.streamable || false,
199
+ recipientCount: (s.recipients || []).length,
200
+ recipients: (s.recipients || []).map(r => r.name || r.handle || r.id),
201
+ groups: s.sharedWithGroups || [],
202
+ networks: s.sharedWithNetworks || [],
203
+ hasMagnet: !!s.magnet,
204
+ hasPassword: !!s.password,
205
+ recursive: s.recursive !== false,
206
+ createdAt: s.createdAt,
207
+ updatedAt: s.updatedAt,
208
+ }));
209
+ }
210
+
211
+ export function getSharesByDrive() {
212
+ const shares = listShares();
213
+ const byDrive = {};
214
+
215
+ shares.forEach(share => {
216
+ const drive = path.parse(share.path).root;
217
+ if (!byDrive[drive]) byDrive[drive] = [];
218
+ byDrive[drive].push(share);
219
+ });
220
+
221
+ return byDrive;
222
+ }
223
+
224
+ export async function storeShareKey(shareId, shareKeyHex) {
225
+ try {
226
+ await keytar.setPassword(SHARE_KEY_SERVICE, shareId, shareKeyHex);
227
+ } catch (err) {
228
+ throw new Error(`Failed to store share key securely: ${err.message}`);
229
+ }
230
+ }
231
+
232
+ export async function getShareKey(shareId) {
233
+ try {
234
+ return await keytar.getPassword(SHARE_KEY_SERVICE, shareId);
235
+ } catch (err) {
236
+ console.error(`Failed to retrieve share key: ${err.message}`);
237
+ return null;
238
+ }
239
+ }
240
+
241
+ export async function rotateShareKey(shareId) {
242
+ const { generateShareKey, encryptShareKeyForRecipient } = await import('../crypto/shareEncryption.js');
243
+ const { encryptForSeed, getEncryptedSeedDir } = await import('../crypto/streamEncryption.js');
244
+
245
+ const shares = config.get('shares') || [];
246
+ const share = shares.find(s => s.id === shareId);
247
+ if (!share) throw new Error('Share not found');
248
+
249
+ const newShareKey = generateShareKey();
250
+
251
+ // Re-encrypt the source files with the new key
252
+ const encDir = getEncryptedSeedDir(shareId + '-' + Date.now().toString(36));
253
+ if (fs.existsSync(share.path)) {
254
+ encryptForSeed(share.path, encDir, newShareKey);
255
+ }
256
+
257
+ // Clean up old encrypted dir
258
+ if (share.encryptedPath && fs.existsSync(share.encryptedPath)) {
259
+ try { fs.rmSync(share.encryptedPath, { recursive: true, force: true }); } catch {}
260
+ }
261
+
262
+ // Re-wrap key for remaining recipients
263
+ const friends = getFriends();
264
+ const newEncryptedShareKeys = {};
265
+ for (const recipient of (share.recipients || [])) {
266
+ const pal = friends.find(f => f.id === recipient.id);
267
+ if (pal?.id && pal.id !== 'unknown') {
268
+ newEncryptedShareKeys[pal.id] = encryptShareKeyForRecipient(newShareKey, pal.id);
269
+ }
270
+ }
271
+
272
+ // Store new key securely
273
+ await keytar.setPassword(SHARE_KEY_SERVICE, shareId, newShareKey.toString('hex'));
274
+
275
+ // Update share config
276
+ share.encryptedPath = encDir;
277
+ share.encryptedShareKeys = newEncryptedShareKeys;
278
+ share.magnet = null; // Will be regenerated on next seed
279
+ share.rotatedAt = new Date().toISOString();
280
+ config.set('shares', shares); signConfigKey('shares').catch(() => {});
281
+
282
+ return { share, newEncryptedShareKeys };
283
+ }
284
+
285
+ export function removeRecipientFromShare(shareId, palId) {
286
+ const shares = config.get('shares') || [];
287
+ const share = shares.find(s => s.id === shareId);
288
+ if (!share) return null;
289
+
290
+ share.recipients = (share.recipients || []).filter(r => r.id !== palId);
291
+ if (share.encryptedShareKeys) {
292
+ delete share.encryptedShareKeys[palId];
293
+ }
294
+ config.set('shares', shares); signConfigKey('shares').catch(() => {});
295
+ return share;
296
+ }
297
+
298
+ export function getSharesForPal(palId) {
299
+ const shares = config.get('shares') || [];
300
+ return shares.filter(s =>
301
+ (s.recipients || []).some(r => r.id === palId)
302
+ );
303
+ }
304
+
305
+ export async function migrateShareKeys() {
306
+ const shares = config.get('shares') || [];
307
+ if (!shares.some(s => s.shareKeyHex)) return;
308
+
309
+ let migrated = 0;
310
+ await Promise.all(shares.map(async (share) => {
311
+ if (!share.shareKeyHex) return;
312
+ try {
313
+ await keytar.setPassword(SHARE_KEY_SERVICE, share.id, share.shareKeyHex);
314
+ delete share.shareKeyHex;
315
+ migrated++;
316
+ } catch (err) {
317
+ console.error(`Failed to migrate share key for ${share.id}: ${err.message}`);
318
+ }
319
+ }));
320
+
321
+ if (migrated > 0) {
322
+ config.set('shares', shares); signConfigKey('shares').catch(() => {});
323
+ console.log(`Migrated ${migrated} share key(s) to credential store.`);
324
+ }
325
+ }