pal-explorer-cli 0.4.11 → 0.4.13

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 (99) hide show
  1. package/README.md +149 -149
  2. package/bin/pal.js +63 -2
  3. package/extensions/@palexplorer/analytics/extension.json +20 -1
  4. package/extensions/@palexplorer/analytics/index.js +19 -9
  5. package/extensions/@palexplorer/audit/extension.json +14 -0
  6. package/extensions/@palexplorer/auth-email/extension.json +15 -0
  7. package/extensions/@palexplorer/auth-oauth/extension.json +15 -0
  8. package/extensions/@palexplorer/chat/extension.json +14 -0
  9. package/extensions/@palexplorer/discovery/extension.json +17 -0
  10. package/extensions/@palexplorer/discovery/index.js +1 -1
  11. package/extensions/@palexplorer/email-notifications/extension.json +23 -0
  12. package/extensions/@palexplorer/groups/extension.json +15 -0
  13. package/extensions/@palexplorer/share-links/extension.json +15 -0
  14. package/extensions/@palexplorer/sync/extension.json +16 -0
  15. package/extensions/@palexplorer/user-mgmt/extension.json +15 -0
  16. package/lib/capabilities.js +24 -24
  17. package/lib/commands/analytics.js +175 -175
  18. package/lib/commands/api-keys.js +131 -131
  19. package/lib/commands/audit.js +235 -235
  20. package/lib/commands/auth.js +137 -137
  21. package/lib/commands/backup.js +76 -76
  22. package/lib/commands/billing.js +148 -148
  23. package/lib/commands/chat.js +217 -217
  24. package/lib/commands/cloud-backup.js +231 -231
  25. package/lib/commands/comment.js +99 -99
  26. package/lib/commands/completion.js +203 -203
  27. package/lib/commands/compliance.js +218 -218
  28. package/lib/commands/config.js +136 -136
  29. package/lib/commands/connect.js +44 -44
  30. package/lib/commands/dept.js +294 -294
  31. package/lib/commands/device.js +146 -146
  32. package/lib/commands/download.js +240 -226
  33. package/lib/commands/explorer.js +178 -178
  34. package/lib/commands/extension.js +1060 -970
  35. package/lib/commands/favorite.js +90 -90
  36. package/lib/commands/federation.js +270 -270
  37. package/lib/commands/file.js +533 -533
  38. package/lib/commands/group.js +271 -271
  39. package/lib/commands/gui-share.js +29 -29
  40. package/lib/commands/init.js +61 -61
  41. package/lib/commands/invite.js +59 -59
  42. package/lib/commands/list.js +58 -58
  43. package/lib/commands/log.js +116 -116
  44. package/lib/commands/nearby.js +108 -108
  45. package/lib/commands/network.js +251 -251
  46. package/lib/commands/notify.js +198 -198
  47. package/lib/commands/org.js +273 -273
  48. package/lib/commands/pal.js +403 -180
  49. package/lib/commands/permissions.js +216 -216
  50. package/lib/commands/pin.js +97 -97
  51. package/lib/commands/protocol.js +357 -357
  52. package/lib/commands/rbac.js +147 -147
  53. package/lib/commands/recover.js +36 -36
  54. package/lib/commands/register.js +171 -171
  55. package/lib/commands/relay.js +131 -131
  56. package/lib/commands/remote.js +368 -368
  57. package/lib/commands/revoke.js +50 -50
  58. package/lib/commands/scanner.js +280 -280
  59. package/lib/commands/schedule.js +344 -344
  60. package/lib/commands/scim.js +203 -203
  61. package/lib/commands/search.js +181 -181
  62. package/lib/commands/serve.js +438 -438
  63. package/lib/commands/server.js +350 -350
  64. package/lib/commands/share-link.js +199 -199
  65. package/lib/commands/share.js +336 -323
  66. package/lib/commands/sso.js +200 -200
  67. package/lib/commands/status.js +145 -145
  68. package/lib/commands/stream.js +562 -562
  69. package/lib/commands/su.js +187 -187
  70. package/lib/commands/sync.js +979 -979
  71. package/lib/commands/transfers.js +152 -152
  72. package/lib/commands/uninstall.js +188 -188
  73. package/lib/commands/update.js +204 -204
  74. package/lib/commands/user.js +276 -276
  75. package/lib/commands/vfs.js +84 -84
  76. package/lib/commands/web-login.js +79 -79
  77. package/lib/commands/web.js +52 -52
  78. package/lib/commands/webhook.js +180 -180
  79. package/lib/commands/whoami.js +59 -59
  80. package/lib/commands/workspace.js +121 -121
  81. package/lib/core/billing.js +16 -5
  82. package/lib/core/dhtDiscovery.js +9 -2
  83. package/lib/core/discoveryClient.js +13 -7
  84. package/lib/core/extensions.js +142 -1
  85. package/lib/core/identity.js +33 -2
  86. package/lib/core/imageProcessor.js +109 -0
  87. package/lib/core/imageTorrent.js +167 -0
  88. package/lib/core/permissions.js +1 -1
  89. package/lib/core/pro.js +11 -4
  90. package/lib/core/serverList.js +4 -1
  91. package/lib/core/shares.js +12 -1
  92. package/lib/core/signalingServer.js +14 -2
  93. package/lib/core/su.js +1 -1
  94. package/lib/core/users.js +1 -1
  95. package/lib/protocol/messages.js +12 -3
  96. package/lib/utils/explorer.js +1 -1
  97. package/lib/utils/help.js +357 -357
  98. package/lib/utils/torrent.js +1 -0
  99. package/package.json +4 -3
@@ -0,0 +1,109 @@
1
+ import sharp from 'sharp';
2
+ import crypto from 'crypto';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import os from 'os';
6
+
7
+ const IMAGES_DIR = path.join(os.homedir(), '.palexplorer', 'images');
8
+ const ALLOWED_FORMATS = new Set(['jpeg', 'png', 'webp']);
9
+ const MAX_AVATAR_INPUT = 256 * 1024; // 256KB
10
+ const MAX_SHARE_INPUT = 512 * 1024; // 512KB
11
+ const AVATAR_SIZE = 256;
12
+ const SHARE_IMAGE_SIZE = 512;
13
+
14
+ function ensureImagesDir() {
15
+ if (!fs.existsSync(IMAGES_DIR)) {
16
+ fs.mkdirSync(IMAGES_DIR, { recursive: true });
17
+ }
18
+ }
19
+
20
+ function contentHash(buffer) {
21
+ return crypto.createHash('sha256').update(buffer).digest('hex');
22
+ }
23
+
24
+ async function validateAndProcess(buffer, { maxInput, size }) {
25
+ if (!Buffer.isBuffer(buffer)) throw new Error('Input must be a Buffer');
26
+ if (buffer.length === 0) throw new Error('Empty image buffer');
27
+ if (buffer.length > maxInput) throw new Error(`Image too large (${buffer.length} bytes, max ${maxInput})`);
28
+
29
+ let metadata;
30
+ try {
31
+ metadata = await sharp(buffer).metadata();
32
+ } catch {
33
+ throw new Error('Invalid or corrupted image file');
34
+ }
35
+
36
+ if (!metadata.format || !ALLOWED_FORMATS.has(metadata.format)) {
37
+ throw new Error(`Unsupported format: ${metadata.format || 'unknown'}. Allowed: ${[...ALLOWED_FORMATS].join(', ')}`);
38
+ }
39
+
40
+ const output = await sharp(buffer)
41
+ .resize(size, size, { fit: 'cover', withoutEnlargement: false })
42
+ .rotate() // auto-rotate based on EXIF, then EXIF is stripped
43
+ .webp({ quality: 80 })
44
+ .toBuffer();
45
+
46
+ const hash = contentHash(output);
47
+ return { buffer: output, hash, format: 'webp', width: size, height: size };
48
+ }
49
+
50
+ export async function processAvatar(buffer) {
51
+ return validateAndProcess(buffer, { maxInput: MAX_AVATAR_INPUT, size: AVATAR_SIZE });
52
+ }
53
+
54
+ export async function processShareImage(buffer) {
55
+ return validateAndProcess(buffer, { maxInput: MAX_SHARE_INPUT, size: SHARE_IMAGE_SIZE });
56
+ }
57
+
58
+ export async function sanitizeReceived(buffer) {
59
+ if (!Buffer.isBuffer(buffer) || buffer.length === 0) throw new Error('Invalid image buffer');
60
+ if (buffer.length > MAX_SHARE_INPUT) throw new Error('Received image too large');
61
+
62
+ let metadata;
63
+ try {
64
+ metadata = await sharp(buffer).metadata();
65
+ } catch {
66
+ throw new Error('Invalid received image');
67
+ }
68
+
69
+ if (!metadata.format || !ALLOWED_FORMATS.has(metadata.format)) {
70
+ throw new Error(`Rejected format: ${metadata.format || 'unknown'}`);
71
+ }
72
+
73
+ const output = await sharp(buffer)
74
+ .resize(SHARE_IMAGE_SIZE, SHARE_IMAGE_SIZE, { fit: 'inside', withoutEnlargement: true })
75
+ .rotate()
76
+ .webp({ quality: 80 })
77
+ .toBuffer();
78
+
79
+ const hash = contentHash(output);
80
+ return { buffer: output, hash, format: 'webp' };
81
+ }
82
+
83
+ export function saveImage(hash, buffer) {
84
+ ensureImagesDir();
85
+ const filePath = path.join(IMAGES_DIR, `${hash}.webp`);
86
+ if (!fs.existsSync(filePath)) {
87
+ fs.writeFileSync(filePath, buffer);
88
+ }
89
+ return filePath;
90
+ }
91
+
92
+ export function getImagePath(hash) {
93
+ return path.join(IMAGES_DIR, `${hash}.webp`);
94
+ }
95
+
96
+ export function hasImage(hash) {
97
+ return fs.existsSync(getImagePath(hash));
98
+ }
99
+
100
+ export function loadImage(hash) {
101
+ const p = getImagePath(hash);
102
+ if (!fs.existsSync(p)) return null;
103
+ return fs.readFileSync(p);
104
+ }
105
+
106
+ export function getImagesDir() {
107
+ ensureImagesDir();
108
+ return IMAGES_DIR;
109
+ }
@@ -0,0 +1,167 @@
1
+ import WebTorrent from 'webtorrent';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { processAvatar, processShareImage, sanitizeReceived, saveImage, hasImage, loadImage, getImagesDir } from './imageProcessor.js';
5
+ import { DEFAULT_TRACKERS } from '../utils/torrent.js';
6
+
7
+ let seedClient = null;
8
+ const seededImages = new Map(); // hash → torrent
9
+ const fetchCache = new Map(); // hash → Promise<Buffer>
10
+
11
+ const FETCH_TIMEOUT = 30_000;
12
+ const MAX_CONCURRENT_FETCHES = 5;
13
+ let activeFetches = 0;
14
+
15
+ // Rate limiting: max 1 avatar update per hour per identity
16
+ const lastAvatarUpdate = new Map(); // publicKey → timestamp
17
+ const AVATAR_RATE_LIMIT_MS = 60 * 60 * 1000;
18
+
19
+ function getSeedClient() {
20
+ if (!seedClient) {
21
+ seedClient = new WebTorrent();
22
+ seedClient.on('error', () => {}); // prevent unhandled errors
23
+ }
24
+ return seedClient;
25
+ }
26
+
27
+ export function checkAvatarRateLimit(publicKey) {
28
+ const last = lastAvatarUpdate.get(publicKey);
29
+ if (last && Date.now() - last < AVATAR_RATE_LIMIT_MS) {
30
+ const waitMin = Math.ceil((AVATAR_RATE_LIMIT_MS - (Date.now() - last)) / 60000);
31
+ throw new Error(`Avatar can only be changed once per hour. Try again in ${waitMin} minute(s).`);
32
+ }
33
+ }
34
+
35
+ export function recordAvatarUpdate(publicKey) {
36
+ lastAvatarUpdate.set(publicKey, Date.now());
37
+ }
38
+
39
+ export async function seedImage(hash) {
40
+ if (seededImages.has(hash)) return seededImages.get(hash);
41
+
42
+ const buffer = loadImage(hash);
43
+ if (!buffer) throw new Error(`Image not found in cache: ${hash}`);
44
+
45
+ const client = getSeedClient();
46
+ const imagePath = path.join(getImagesDir(), `${hash}.webp`);
47
+
48
+ return new Promise((resolve, reject) => {
49
+ client.seed(imagePath, {
50
+ name: `${hash}.webp`,
51
+ announce: DEFAULT_TRACKERS,
52
+ }, (torrent) => {
53
+ seededImages.set(hash, {
54
+ magnetURI: torrent.magnetURI,
55
+ infoHash: torrent.infoHash,
56
+ });
57
+ resolve(seededImages.get(hash));
58
+ });
59
+
60
+ setTimeout(() => reject(new Error('Seed timeout')), 10_000);
61
+ });
62
+ }
63
+
64
+ export async function fetchImage(magnetOrInfoHash, expectedHash) {
65
+ if (expectedHash && hasImage(expectedHash)) {
66
+ return loadImage(expectedHash);
67
+ }
68
+
69
+ if (expectedHash && fetchCache.has(expectedHash)) {
70
+ return fetchCache.get(expectedHash);
71
+ }
72
+
73
+ if (activeFetches >= MAX_CONCURRENT_FETCHES) {
74
+ throw new Error('Too many concurrent image fetches');
75
+ }
76
+
77
+ const promise = _doFetch(magnetOrInfoHash, expectedHash);
78
+ if (expectedHash) fetchCache.set(expectedHash, promise);
79
+ return promise;
80
+ }
81
+
82
+ async function _doFetch(magnetOrInfoHash, expectedHash) {
83
+ activeFetches++;
84
+ const client = new WebTorrent();
85
+
86
+ try {
87
+ const buffer = await new Promise((resolve, reject) => {
88
+ const timer = setTimeout(() => {
89
+ client.destroy();
90
+ reject(new Error('Image fetch timeout'));
91
+ }, FETCH_TIMEOUT);
92
+
93
+ client.add(magnetOrInfoHash, { announce: DEFAULT_TRACKERS, path: getImagesDir() }, (torrent) => {
94
+ const file = torrent.files[0];
95
+ if (!file) {
96
+ clearTimeout(timer);
97
+ client.destroy();
98
+ reject(new Error('No file in torrent'));
99
+ return;
100
+ }
101
+
102
+ if (file.length > 512 * 1024) {
103
+ clearTimeout(timer);
104
+ client.destroy();
105
+ reject(new Error('Image torrent file too large'));
106
+ return;
107
+ }
108
+
109
+ file.getBuffer((err, buf) => {
110
+ clearTimeout(timer);
111
+ client.destroy();
112
+ if (err) return reject(err);
113
+ resolve(buf);
114
+ });
115
+ });
116
+ });
117
+
118
+ // Defense in depth: re-encode received image
119
+ const sanitized = await sanitizeReceived(buffer);
120
+
121
+ if (expectedHash && sanitized.hash !== expectedHash) {
122
+ throw new Error('Image hash mismatch — possible tampering');
123
+ }
124
+
125
+ saveImage(sanitized.hash, sanitized.buffer);
126
+ return sanitized.buffer;
127
+ } finally {
128
+ activeFetches--;
129
+ if (expectedHash) fetchCache.delete(expectedHash);
130
+ }
131
+ }
132
+
133
+ export async function setAvatarImage(inputBuffer, publicKey) {
134
+ checkAvatarRateLimit(publicKey);
135
+
136
+ const processed = await processAvatar(inputBuffer);
137
+ saveImage(processed.hash, processed.buffer);
138
+
139
+ const torrentInfo = await seedImage(processed.hash);
140
+ recordAvatarUpdate(publicKey);
141
+
142
+ return {
143
+ avatarHash: processed.hash,
144
+ avatarMagnet: torrentInfo.magnetURI,
145
+ };
146
+ }
147
+
148
+ export async function setShareCoverImage(inputBuffer) {
149
+ const processed = await processShareImage(inputBuffer);
150
+ saveImage(processed.hash, processed.buffer);
151
+
152
+ const torrentInfo = await seedImage(processed.hash);
153
+
154
+ return {
155
+ coverImageHash: processed.hash,
156
+ coverImageMagnet: torrentInfo.magnetURI,
157
+ };
158
+ }
159
+
160
+ export function destroySeedClient() {
161
+ if (seedClient) {
162
+ seedClient.destroy();
163
+ seedClient = null;
164
+ }
165
+ seededImages.clear();
166
+ fetchCache.clear();
167
+ }
@@ -9,7 +9,7 @@ const AUDIT_MAX = 500;
9
9
  export function requireIdentity() {
10
10
  const identity = config.get('identity');
11
11
  if (!identity || !identity.publicKey) {
12
- throw new Error('No identity found. Run `pe init` first.');
12
+ throw new Error('No identity found. Run `pal init` first.');
13
13
  }
14
14
  return identity;
15
15
  }
package/lib/core/pro.js CHANGED
@@ -5,17 +5,24 @@ export const FREE_LIMITS = {
5
5
  maxShareRecipients: PLANS.free.limits.maxRecipients,
6
6
  };
7
7
 
8
- // BETA: all features unlocked re-enable billing when ready
8
+ // Check if user has a Pro or Enterprise plan
9
9
  export function isPro() {
10
- return true;
10
+ const plan = billingGetActivePlan();
11
+ return !!(plan && plan.id !== 'free');
11
12
  }
12
13
 
13
14
  export function isEnterprise() {
14
- return true;
15
+ const plan = billingGetActivePlan();
16
+ return plan && plan.id === 'enterprise';
15
17
  }
16
18
 
17
19
  export function checkLimit(feature, currentCount) {
18
- return;
20
+ if (isPro()) return;
21
+
22
+ const limit = FREE_LIMITS[feature];
23
+ if (limit !== undefined && currentCount > limit) {
24
+ throw new Error(`Free tier limit reached for ${feature} (max ${limit})`);
25
+ }
19
26
  }
20
27
 
21
28
  export { getFeature, checkFeature, getPlanLimits };
@@ -7,7 +7,10 @@ const WRITE_TRUST_THRESHOLD = 3; // only bootstrap and user-trusted servers can
7
7
 
8
8
  // Bootstrap servers are injected by extensions (e.g. discovery).
9
9
  // Core is pure P2P — no hardcoded server dependencies.
10
- const BOOTSTRAP_SERVERS = [];
10
+ const BOOTSTRAP_SERVERS = [
11
+ 'http://138.2.142.188',
12
+ 'stun:stun.l.google.com:19302'
13
+ ];
11
14
 
12
15
  let cachedServers = null;
13
16
  let healthSortedServers = null; // sorted by health + latency, updated by health checks
@@ -178,10 +178,15 @@ export function updateShare(idOrPath, updates) {
178
178
  if (!share) throw new Error('Share not found.');
179
179
 
180
180
  const allowed = ['visibility', 'recipients', 'sharedWithGroups', 'sharedWithNetworks',
181
- 'streamable', 'recursive', 'name', 'permissions'];
181
+ 'streamable', 'recursive', 'name', 'permissions',
182
+ 'category', 'description', 'color', 'icon', 'tags',
183
+ 'coverImageHash', 'coverImageMagnet'];
182
184
  for (const key of allowed) {
183
185
  if (updates[key] !== undefined) share[key] = updates[key];
184
186
  }
187
+ if (updates.tags) {
188
+ share.tags = Array.isArray(updates.tags) ? updates.tags.slice(0, 20).map(t => String(t).slice(0, 30)) : [];
189
+ }
185
190
  share.updatedAt = new Date().toISOString();
186
191
  config.set('shares', shares); signConfigKey('shares').catch(() => {});
187
192
  return share;
@@ -203,6 +208,12 @@ export function getShareSummary() {
203
208
  hasMagnet: !!s.magnet,
204
209
  hasPassword: !!s.password,
205
210
  recursive: s.recursive !== false,
211
+ category: s.category || null,
212
+ description: s.description || null,
213
+ color: s.color || null,
214
+ icon: s.icon || null,
215
+ tags: s.tags || [],
216
+ coverImageHash: s.coverImageHash || null,
206
217
  createdAt: s.createdAt,
207
218
  updatedAt: s.updatedAt,
208
219
  }));
@@ -383,17 +383,22 @@ export function getPort() {
383
383
  // Client helper — send a request to a peer's signaling port and get a response
384
384
  export function sendRequest(host, port, request, timeout = 3000) {
385
385
  return new Promise((resolve, reject) => {
386
+ let closing = false;
386
387
  const socket = net.createConnection({ host, port }, () => {
388
+ if (closing) return;
387
389
  socket.write(frameMessage(request));
388
390
  });
389
391
 
390
392
  let buffer = Buffer.alloc(0);
391
393
  const timer = setTimeout(() => {
394
+ if (closing) return;
395
+ closing = true;
392
396
  socket.destroy();
393
397
  reject(new Error('Timeout'));
394
398
  }, timeout);
395
399
 
396
400
  socket.on('data', (chunk) => {
401
+ if (closing) return;
397
402
  buffer = Buffer.concat([buffer, chunk]);
398
403
  if (buffer.length >= 4) {
399
404
  const msgLen = buffer.readUInt32BE(0);
@@ -405,18 +410,25 @@ export function sendRequest(host, port, request, timeout = 3000) {
405
410
  } catch (e) {
406
411
  reject(e);
407
412
  }
408
- socket.end();
413
+ if (!closing) {
414
+ closing = true;
415
+ socket.end();
416
+ }
409
417
  }
410
418
  }
411
419
  });
412
420
 
413
421
  socket.on('error', (err) => {
414
422
  clearTimeout(timer);
415
- reject(err);
423
+ if (!closing) {
424
+ closing = true;
425
+ reject(err);
426
+ }
416
427
  });
417
428
 
418
429
  socket.on('close', () => {
419
430
  clearTimeout(timer);
431
+ closing = true;
420
432
  });
421
433
  });
422
434
  }
package/lib/core/su.js CHANGED
@@ -45,7 +45,7 @@ export function isAuthenticated() {
45
45
 
46
46
  export function requireSu() {
47
47
  if (isAuthenticated()) return true;
48
- throw new Error('This command requires super user access. Run "pe su auth" first or set PAL_SU_TOKEN.');
48
+ throw new Error('This command requires super user access. Run "pal su auth" first or set PAL_SU_TOKEN.');
49
49
  }
50
50
 
51
51
  export function removeSuToken() {
package/lib/core/users.js CHANGED
@@ -133,7 +133,7 @@ export function isOwner(publicKey) {
133
133
 
134
134
  export function requireRole(minRole) {
135
135
  const active = getActiveUser();
136
- if (!active) throw new Error('No active user. Run `pe login` first.');
136
+ if (!active) throw new Error('No active user. Run `pal login` first.');
137
137
 
138
138
  const roleLevel = { owner: 2, user: 1, guest: 0 };
139
139
  if ((roleLevel[active.role] || 0) < (roleLevel[minRole] || 0)) {
@@ -4,7 +4,7 @@ import { create, createEncrypted } from './envelope.js';
4
4
 
5
5
  // ── Identity & Discovery ──
6
6
 
7
- export function identityAnnounce(keyPair, { handle, deviceId, deviceName, version, lanAddresses, capabilities }) {
7
+ export function identityAnnounce(keyPair, { handle, deviceId, deviceName, version, lanAddresses, capabilities, profile }) {
8
8
  return create('identity.announce', keyPair, null, {
9
9
  handle,
10
10
  deviceId,
@@ -15,6 +15,7 @@ export function identityAnnounce(keyPair, { handle, deviceId, deviceName, versio
15
15
  lan: lanAddresses || [],
16
16
  public: null,
17
17
  },
18
+ profile: profile || null,
18
19
  });
19
20
  }
20
21
 
@@ -22,7 +23,7 @@ export function identityResolve(keyPair, handle) {
22
23
  return create('identity.resolve', keyPair, null, { handle });
23
24
  }
24
25
 
25
- export function identityResponse(keyPair, requesterPK, { handle, publicKey, deviceId, capabilities, lastSeen, source }) {
26
+ export function identityResponse(keyPair, requesterPK, { handle, publicKey, deviceId, capabilities, lastSeen, source, profile }) {
26
27
  return create('identity.response', keyPair, requesterPK, {
27
28
  handle,
28
29
  publicKey,
@@ -30,6 +31,7 @@ export function identityResponse(keyPair, requesterPK, { handle, publicKey, devi
30
31
  capabilities: capabilities || [],
31
32
  lastSeen: lastSeen || new Date().toISOString(),
32
33
  source: source || 'server',
34
+ profile: profile || null,
33
35
  });
34
36
  }
35
37
 
@@ -39,7 +41,7 @@ export function heartbeat(keyPair, { handle, deviceId }) {
39
41
 
40
42
  // ── Share Negotiation ──
41
43
 
42
- export function shareOffer(keyPair, recipientPK, { shareId, name, type, totalSize, fileCount, policy, preview }) {
44
+ export function shareOffer(keyPair, recipientPK, { shareId, name, type, totalSize, fileCount, policy, preview, category, description, tags, color, icon, coverImageHash, coverImageMagnet }) {
43
45
  return createEncrypted('share.offer', keyPair, recipientPK, {
44
46
  shareId,
45
47
  name,
@@ -48,6 +50,13 @@ export function shareOffer(keyPair, recipientPK, { shareId, name, type, totalSiz
48
50
  fileCount: fileCount || 0,
49
51
  preview: preview || null,
50
52
  policy: policy || {},
53
+ category: category || null,
54
+ description: description || null,
55
+ tags: tags || null,
56
+ color: color || null,
57
+ icon: icon || null,
58
+ coverImageHash: coverImageHash || null,
59
+ coverImageMagnet: coverImageMagnet || null,
51
60
  encryption: {
52
61
  algorithm: 'xchacha20-poly1305',
53
62
  chunkSize: 65536,
@@ -16,7 +16,7 @@ export async function installExplorerContextMenu() {
16
16
 
17
17
  const exePath = process.execPath;
18
18
  const scriptPath = path.resolve(__dirname, '../../bin/pal.js');
19
- // Command to run: pe share "filepath"
19
+ // Command to run: pal share "filepath"
20
20
  // For the GUI, we might want a specific handler that opens the GUI
21
21
  const command = `"${exePath}" "${scriptPath}" gui-share "%1"`;
22
22