pal-explorer-cli 0.4.12 → 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.
- package/README.md +149 -149
- package/bin/pal.js +63 -2
- package/extensions/@palexplorer/analytics/extension.json +20 -1
- package/extensions/@palexplorer/analytics/index.js +19 -9
- package/extensions/@palexplorer/audit/extension.json +14 -0
- package/extensions/@palexplorer/auth-email/extension.json +15 -0
- package/extensions/@palexplorer/auth-oauth/extension.json +15 -0
- package/extensions/@palexplorer/chat/extension.json +14 -0
- package/extensions/@palexplorer/discovery/extension.json +17 -0
- package/extensions/@palexplorer/discovery/index.js +1 -1
- package/extensions/@palexplorer/email-notifications/extension.json +23 -0
- package/extensions/@palexplorer/groups/extension.json +15 -0
- package/extensions/@palexplorer/share-links/extension.json +15 -0
- package/extensions/@palexplorer/sync/extension.json +16 -0
- package/extensions/@palexplorer/user-mgmt/extension.json +15 -0
- package/lib/capabilities.js +24 -24
- package/lib/commands/analytics.js +175 -175
- package/lib/commands/api-keys.js +131 -131
- package/lib/commands/audit.js +235 -235
- package/lib/commands/auth.js +137 -137
- package/lib/commands/backup.js +76 -76
- package/lib/commands/billing.js +148 -148
- package/lib/commands/chat.js +217 -217
- package/lib/commands/cloud-backup.js +231 -231
- package/lib/commands/comment.js +99 -99
- package/lib/commands/completion.js +203 -203
- package/lib/commands/compliance.js +218 -218
- package/lib/commands/config.js +136 -136
- package/lib/commands/connect.js +44 -44
- package/lib/commands/dept.js +294 -294
- package/lib/commands/device.js +146 -146
- package/lib/commands/download.js +240 -226
- package/lib/commands/explorer.js +178 -178
- package/lib/commands/extension.js +1060 -970
- package/lib/commands/favorite.js +90 -90
- package/lib/commands/federation.js +270 -270
- package/lib/commands/file.js +533 -533
- package/lib/commands/group.js +271 -271
- package/lib/commands/gui-share.js +29 -29
- package/lib/commands/init.js +61 -61
- package/lib/commands/invite.js +59 -59
- package/lib/commands/list.js +58 -58
- package/lib/commands/log.js +116 -116
- package/lib/commands/nearby.js +108 -108
- package/lib/commands/network.js +251 -251
- package/lib/commands/notify.js +198 -198
- package/lib/commands/org.js +273 -273
- package/lib/commands/pal.js +403 -180
- package/lib/commands/permissions.js +216 -216
- package/lib/commands/pin.js +97 -97
- package/lib/commands/protocol.js +357 -357
- package/lib/commands/rbac.js +147 -147
- package/lib/commands/recover.js +36 -36
- package/lib/commands/register.js +171 -171
- package/lib/commands/relay.js +131 -131
- package/lib/commands/remote.js +368 -368
- package/lib/commands/revoke.js +50 -50
- package/lib/commands/scanner.js +280 -280
- package/lib/commands/schedule.js +344 -344
- package/lib/commands/scim.js +203 -203
- package/lib/commands/search.js +181 -181
- package/lib/commands/serve.js +438 -438
- package/lib/commands/server.js +350 -350
- package/lib/commands/share-link.js +199 -199
- package/lib/commands/share.js +336 -323
- package/lib/commands/sso.js +200 -200
- package/lib/commands/status.js +145 -145
- package/lib/commands/stream.js +562 -562
- package/lib/commands/su.js +187 -187
- package/lib/commands/sync.js +979 -979
- package/lib/commands/transfers.js +152 -152
- package/lib/commands/uninstall.js +188 -188
- package/lib/commands/update.js +204 -204
- package/lib/commands/user.js +276 -276
- package/lib/commands/vfs.js +84 -84
- package/lib/commands/web-login.js +79 -79
- package/lib/commands/web.js +52 -52
- package/lib/commands/webhook.js +180 -180
- package/lib/commands/whoami.js +59 -59
- package/lib/commands/workspace.js +121 -121
- package/lib/core/billing.js +16 -5
- package/lib/core/dhtDiscovery.js +9 -2
- package/lib/core/discoveryClient.js +13 -7
- package/lib/core/extensions.js +142 -1
- package/lib/core/identity.js +33 -2
- package/lib/core/imageProcessor.js +109 -0
- package/lib/core/imageTorrent.js +167 -0
- package/lib/core/permissions.js +1 -1
- package/lib/core/pro.js +11 -4
- package/lib/core/serverList.js +4 -1
- package/lib/core/shares.js +12 -1
- package/lib/core/signalingServer.js +14 -2
- package/lib/core/su.js +1 -1
- package/lib/core/users.js +1 -1
- package/lib/protocol/messages.js +12 -3
- package/lib/utils/explorer.js +1 -1
- package/lib/utils/help.js +357 -357
- package/lib/utils/torrent.js +1 -0
- 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
|
+
}
|
package/lib/core/permissions.js
CHANGED
|
@@ -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 `
|
|
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
|
-
//
|
|
8
|
+
// Check if user has a Pro or Enterprise plan
|
|
9
9
|
export function isPro() {
|
|
10
|
-
|
|
10
|
+
const plan = billingGetActivePlan();
|
|
11
|
+
return !!(plan && plan.id !== 'free');
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export function isEnterprise() {
|
|
14
|
-
|
|
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 };
|
package/lib/core/serverList.js
CHANGED
|
@@ -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
|
package/lib/core/shares.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 "
|
|
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 `
|
|
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)) {
|
package/lib/protocol/messages.js
CHANGED
|
@@ -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,
|
package/lib/utils/explorer.js
CHANGED
|
@@ -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:
|
|
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
|
|