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
|
@@ -1,121 +1,121 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import config from '../utils/config.js';
|
|
3
|
-
|
|
4
|
-
function genId() {
|
|
5
|
-
return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export default function workspaceCommand(program) {
|
|
9
|
-
const cmd = program
|
|
10
|
-
.command('workspace')
|
|
11
|
-
.description('manage workspaces (collections of shares)')
|
|
12
|
-
.addHelpText('after', `
|
|
13
|
-
Examples:
|
|
14
|
-
$
|
|
15
|
-
$
|
|
16
|
-
$
|
|
17
|
-
$
|
|
18
|
-
$
|
|
19
|
-
$
|
|
20
|
-
`)
|
|
21
|
-
.action(() => {
|
|
22
|
-
printWorkspaces();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
cmd
|
|
26
|
-
.command('list')
|
|
27
|
-
.description('list all workspaces')
|
|
28
|
-
.action(() => {
|
|
29
|
-
printWorkspaces();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
cmd
|
|
33
|
-
.command('create <name>')
|
|
34
|
-
.description('create a new workspace')
|
|
35
|
-
.option('--description <desc>', 'Workspace description')
|
|
36
|
-
.action((name, opts) => {
|
|
37
|
-
const workspaces = config.get('workspaces') || [];
|
|
38
|
-
const ws = {
|
|
39
|
-
id: genId(),
|
|
40
|
-
name,
|
|
41
|
-
description: opts.description || '',
|
|
42
|
-
shares: [],
|
|
43
|
-
paths: [],
|
|
44
|
-
createdAt: new Date().toISOString(),
|
|
45
|
-
};
|
|
46
|
-
workspaces.push(ws);
|
|
47
|
-
config.set('workspaces', workspaces);
|
|
48
|
-
console.log(chalk.green(`\u2714 Workspace created: ${ws.name}`));
|
|
49
|
-
console.log(` ID: ${chalk.white(ws.id)}`);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
cmd
|
|
53
|
-
.command('delete <id>')
|
|
54
|
-
.description('delete a workspace')
|
|
55
|
-
.action((id) => {
|
|
56
|
-
const workspaces = config.get('workspaces') || [];
|
|
57
|
-
const ws = workspaces.find(w => w.id === id);
|
|
58
|
-
if (!ws) {
|
|
59
|
-
console.log(chalk.red('Workspace not found.'));
|
|
60
|
-
process.exitCode = 1;
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
config.set('workspaces', workspaces.filter(w => w.id !== id));
|
|
64
|
-
console.log(chalk.green(`\u2714 Workspace "${ws.name}" deleted.`));
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
cmd
|
|
68
|
-
.command('add <workspaceId> <shareId>')
|
|
69
|
-
.description('add a share to a workspace')
|
|
70
|
-
.action((workspaceId, shareId) => {
|
|
71
|
-
const workspaces = config.get('workspaces') || [];
|
|
72
|
-
const ws = workspaces.find(w => w.id === workspaceId);
|
|
73
|
-
if (!ws) {
|
|
74
|
-
console.log(chalk.red('Workspace not found.'));
|
|
75
|
-
process.exitCode = 1;
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
if (ws.shares.includes(shareId)) {
|
|
79
|
-
console.log(chalk.yellow('Share already in workspace.'));
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
ws.shares.push(shareId);
|
|
83
|
-
config.set('workspaces', workspaces);
|
|
84
|
-
console.log(chalk.green(`\u2714 Share ${shareId} added to "${ws.name}".`));
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
cmd
|
|
88
|
-
.command('remove <workspaceId> <shareId>')
|
|
89
|
-
.description('remove a share from a workspace')
|
|
90
|
-
.action((workspaceId, shareId) => {
|
|
91
|
-
const workspaces = config.get('workspaces') || [];
|
|
92
|
-
const ws = workspaces.find(w => w.id === workspaceId);
|
|
93
|
-
if (!ws) {
|
|
94
|
-
console.log(chalk.red('Workspace not found.'));
|
|
95
|
-
process.exitCode = 1;
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
if (!ws.shares.includes(shareId)) {
|
|
99
|
-
console.log(chalk.yellow('Share not in workspace.'));
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
ws.shares = ws.shares.filter(id => id !== shareId);
|
|
103
|
-
config.set('workspaces', workspaces);
|
|
104
|
-
console.log(chalk.green(`\u2714 Share ${shareId} removed from "${ws.name}".`));
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function printWorkspaces() {
|
|
109
|
-
const workspaces = config.get('workspaces') || [];
|
|
110
|
-
if (workspaces.length === 0) {
|
|
111
|
-
console.log(chalk.gray('No workspaces. Use `
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
console.log('');
|
|
115
|
-
console.log(chalk.cyan('Workspaces:'));
|
|
116
|
-
for (const ws of workspaces) {
|
|
117
|
-
console.log(` ${chalk.white(ws.id)} ${chalk.yellow(ws.name)}`);
|
|
118
|
-
if (ws.description) console.log(` ${chalk.gray(ws.description)}`);
|
|
119
|
-
console.log(` Shares: ${chalk.white(ws.shares.length)} Created: ${chalk.gray(ws.createdAt)}`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import config from '../utils/config.js';
|
|
3
|
+
|
|
4
|
+
function genId() {
|
|
5
|
+
return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function workspaceCommand(program) {
|
|
9
|
+
const cmd = program
|
|
10
|
+
.command('workspace')
|
|
11
|
+
.description('manage workspaces (collections of shares)')
|
|
12
|
+
.addHelpText('after', `
|
|
13
|
+
Examples:
|
|
14
|
+
$ pal workspace List all workspaces
|
|
15
|
+
$ pal workspace list List all workspaces
|
|
16
|
+
$ pal workspace create "My Project" Create a workspace
|
|
17
|
+
$ pal workspace delete <id> Delete a workspace
|
|
18
|
+
$ pal workspace add <wsId> <shareId> Add share to workspace
|
|
19
|
+
$ pal workspace remove <wsId> <shareId> Remove share from workspace
|
|
20
|
+
`)
|
|
21
|
+
.action(() => {
|
|
22
|
+
printWorkspaces();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
cmd
|
|
26
|
+
.command('list')
|
|
27
|
+
.description('list all workspaces')
|
|
28
|
+
.action(() => {
|
|
29
|
+
printWorkspaces();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
cmd
|
|
33
|
+
.command('create <name>')
|
|
34
|
+
.description('create a new workspace')
|
|
35
|
+
.option('--description <desc>', 'Workspace description')
|
|
36
|
+
.action((name, opts) => {
|
|
37
|
+
const workspaces = config.get('workspaces') || [];
|
|
38
|
+
const ws = {
|
|
39
|
+
id: genId(),
|
|
40
|
+
name,
|
|
41
|
+
description: opts.description || '',
|
|
42
|
+
shares: [],
|
|
43
|
+
paths: [],
|
|
44
|
+
createdAt: new Date().toISOString(),
|
|
45
|
+
};
|
|
46
|
+
workspaces.push(ws);
|
|
47
|
+
config.set('workspaces', workspaces);
|
|
48
|
+
console.log(chalk.green(`\u2714 Workspace created: ${ws.name}`));
|
|
49
|
+
console.log(` ID: ${chalk.white(ws.id)}`);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
cmd
|
|
53
|
+
.command('delete <id>')
|
|
54
|
+
.description('delete a workspace')
|
|
55
|
+
.action((id) => {
|
|
56
|
+
const workspaces = config.get('workspaces') || [];
|
|
57
|
+
const ws = workspaces.find(w => w.id === id);
|
|
58
|
+
if (!ws) {
|
|
59
|
+
console.log(chalk.red('Workspace not found.'));
|
|
60
|
+
process.exitCode = 1;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
config.set('workspaces', workspaces.filter(w => w.id !== id));
|
|
64
|
+
console.log(chalk.green(`\u2714 Workspace "${ws.name}" deleted.`));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
cmd
|
|
68
|
+
.command('add <workspaceId> <shareId>')
|
|
69
|
+
.description('add a share to a workspace')
|
|
70
|
+
.action((workspaceId, shareId) => {
|
|
71
|
+
const workspaces = config.get('workspaces') || [];
|
|
72
|
+
const ws = workspaces.find(w => w.id === workspaceId);
|
|
73
|
+
if (!ws) {
|
|
74
|
+
console.log(chalk.red('Workspace not found.'));
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (ws.shares.includes(shareId)) {
|
|
79
|
+
console.log(chalk.yellow('Share already in workspace.'));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
ws.shares.push(shareId);
|
|
83
|
+
config.set('workspaces', workspaces);
|
|
84
|
+
console.log(chalk.green(`\u2714 Share ${shareId} added to "${ws.name}".`));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
cmd
|
|
88
|
+
.command('remove <workspaceId> <shareId>')
|
|
89
|
+
.description('remove a share from a workspace')
|
|
90
|
+
.action((workspaceId, shareId) => {
|
|
91
|
+
const workspaces = config.get('workspaces') || [];
|
|
92
|
+
const ws = workspaces.find(w => w.id === workspaceId);
|
|
93
|
+
if (!ws) {
|
|
94
|
+
console.log(chalk.red('Workspace not found.'));
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (!ws.shares.includes(shareId)) {
|
|
99
|
+
console.log(chalk.yellow('Share not in workspace.'));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
ws.shares = ws.shares.filter(id => id !== shareId);
|
|
103
|
+
config.set('workspaces', workspaces);
|
|
104
|
+
console.log(chalk.green(`\u2714 Share ${shareId} removed from "${ws.name}".`));
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function printWorkspaces() {
|
|
109
|
+
const workspaces = config.get('workspaces') || [];
|
|
110
|
+
if (workspaces.length === 0) {
|
|
111
|
+
console.log(chalk.gray('No workspaces. Use `pal workspace create <name>` to create one.'));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
console.log('');
|
|
115
|
+
console.log(chalk.cyan('Workspaces:'));
|
|
116
|
+
for (const ws of workspaces) {
|
|
117
|
+
console.log(` ${chalk.white(ws.id)} ${chalk.yellow(ws.name)}`);
|
|
118
|
+
if (ws.description) console.log(` ${chalk.gray(ws.description)}`);
|
|
119
|
+
console.log(` Shares: ${chalk.white(ws.shares.length)} Created: ${chalk.gray(ws.createdAt)}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
package/lib/core/billing.js
CHANGED
|
@@ -35,6 +35,7 @@ export const PLANS = {
|
|
|
35
35
|
vfsMount: false, explorerMenu: false, autoSync: false, deltaSync: false,
|
|
36
36
|
batchTransfers: false, prioritySeeding: false, analytics90d: false,
|
|
37
37
|
encryptedBackup: false, priorityRelay: false, customThemes: false, vanityHandle: false,
|
|
38
|
+
customAvatar: false, shareImages: false, customStatusText: false, invisibleStatus: false, autoAway: false,
|
|
38
39
|
},
|
|
39
40
|
},
|
|
40
41
|
pro_monthly: {
|
|
@@ -49,6 +50,7 @@ export const PLANS = {
|
|
|
49
50
|
vfsMount: true, explorerMenu: true, autoSync: true, deltaSync: true,
|
|
50
51
|
batchTransfers: true, prioritySeeding: true, analytics90d: true,
|
|
51
52
|
encryptedBackup: true, priorityRelay: true, customThemes: true, vanityHandle: true,
|
|
53
|
+
customAvatar: true, shareImages: true, customStatusText: true, invisibleStatus: true, autoAway: true,
|
|
52
54
|
},
|
|
53
55
|
},
|
|
54
56
|
pro_annual: {
|
|
@@ -63,6 +65,7 @@ export const PLANS = {
|
|
|
63
65
|
vfsMount: true, explorerMenu: true, autoSync: true, deltaSync: true,
|
|
64
66
|
batchTransfers: true, prioritySeeding: true, analytics90d: true,
|
|
65
67
|
encryptedBackup: true, priorityRelay: true, customThemes: true, vanityHandle: true,
|
|
68
|
+
customAvatar: true, shareImages: true, customStatusText: true, invisibleStatus: true, autoAway: true,
|
|
66
69
|
},
|
|
67
70
|
},
|
|
68
71
|
enterprise: {
|
|
@@ -77,6 +80,7 @@ export const PLANS = {
|
|
|
77
80
|
vfsMount: true, explorerMenu: true, autoSync: true, deltaSync: true,
|
|
78
81
|
batchTransfers: true, prioritySeeding: true, analytics90d: true,
|
|
79
82
|
encryptedBackup: true, priorityRelay: true, customThemes: true, vanityHandle: true,
|
|
83
|
+
customAvatar: true, shareImages: true, customStatusText: true, invisibleStatus: true, autoAway: true,
|
|
80
84
|
oauth: true, sso: true, auditExport: true, dlpPolicies: true,
|
|
81
85
|
retentionRules: true, brandedThemes: true, webhooks: true,
|
|
82
86
|
dedicatedRelay: true, prioritySupport: true,
|
|
@@ -273,13 +277,20 @@ function downgradeToFree(reason) {
|
|
|
273
277
|
let _revalidationPromise = null;
|
|
274
278
|
|
|
275
279
|
export function getActivePlan() {
|
|
276
|
-
|
|
280
|
+
const data = getBillingData();
|
|
281
|
+
if (data && data.plan && PLANS[data.plan]) {
|
|
282
|
+
return {
|
|
283
|
+
id: data.plan,
|
|
284
|
+
...PLANS[data.plan],
|
|
285
|
+
expiresAt: data.expiresAt,
|
|
286
|
+
source: 'license',
|
|
287
|
+
};
|
|
288
|
+
}
|
|
277
289
|
return {
|
|
278
|
-
|
|
279
|
-
...PLANS.
|
|
290
|
+
id: 'free',
|
|
291
|
+
...PLANS.free,
|
|
280
292
|
expiresAt: null,
|
|
281
|
-
source: '
|
|
282
|
-
customerEmail: null,
|
|
293
|
+
source: 'default',
|
|
283
294
|
};
|
|
284
295
|
}
|
|
285
296
|
|
package/lib/core/dhtDiscovery.js
CHANGED
|
@@ -67,10 +67,17 @@ export class DHTDiscovery {
|
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
async publish(handle, publicKey, privateKey) {
|
|
70
|
+
async publish(handle, publicKey, privateKey, profile) {
|
|
71
71
|
await this.ready;
|
|
72
72
|
const { pk, sk } = extractKeyPair(privateKey);
|
|
73
|
-
const
|
|
73
|
+
const payload = { handle, publicKey, timestamp: Date.now() };
|
|
74
|
+
if (profile) {
|
|
75
|
+
if (profile.displayName) payload.dn = profile.displayName.slice(0, 50);
|
|
76
|
+
if (profile.presenceStatus) payload.ps = profile.presenceStatus;
|
|
77
|
+
if (profile.country) payload.co = profile.country.slice(0, 2);
|
|
78
|
+
if (profile.language) payload.la = profile.language.slice(0, 5);
|
|
79
|
+
}
|
|
80
|
+
const value = Buffer.from(JSON.stringify(payload));
|
|
74
81
|
|
|
75
82
|
if (value.length > 1000) {
|
|
76
83
|
throw new Error('DHT payload too large (max 1000 bytes for BEP 44)');
|
|
@@ -27,18 +27,24 @@ export function parseHandle(handle) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export function getServers(role) {
|
|
30
|
-
|
|
31
|
-
const roleServers = getServersForRole(role);
|
|
32
|
-
if (roleServers.length > 0) return roleServers;
|
|
33
|
-
// fallback: return all servers (backward compat with servers that have no roles)
|
|
34
|
-
}
|
|
30
|
+
// User-configured discovery servers take highest precedence
|
|
35
31
|
const settings = config.get('settings') || {};
|
|
36
32
|
const servers = settings.discovery_servers;
|
|
37
33
|
if (Array.isArray(servers) && servers.length > 0) return [...servers];
|
|
38
34
|
if (typeof servers === 'string' && servers.trim()) return servers.split(',').map(s => s.trim()).filter(Boolean);
|
|
39
35
|
const single = settings.discovery_url || process.env.PAL_DISCOVERY_URL;
|
|
40
36
|
if (single) return [single];
|
|
41
|
-
|
|
37
|
+
|
|
38
|
+
// Then try role-based servers (from addServerWithRoles)
|
|
39
|
+
if (role) {
|
|
40
|
+
const roleServers = getServersForRole(role);
|
|
41
|
+
if (roleServers.length > 0) return roleServers;
|
|
42
|
+
}
|
|
43
|
+
const bootstrap = getBootstrapServers();
|
|
44
|
+
if (!bootstrap.includes('http://138.2.142.188:7474')) {
|
|
45
|
+
bootstrap.unshift('http://138.2.142.188:7474');
|
|
46
|
+
}
|
|
47
|
+
return bootstrap;
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
export function getPrimaryServer() {
|
|
@@ -388,7 +394,7 @@ export async function pinServerKey(serverUrl) {
|
|
|
388
394
|
const err = new Error(
|
|
389
395
|
`Server key changed for ${serverUrl}! ` +
|
|
390
396
|
`Pinned: ${keyFingerprint(existing)}, Current: ${keyFingerprint(publicKey)}. ` +
|
|
391
|
-
`Run '
|
|
397
|
+
`Run 'pal server verify' to inspect and accept the new key.`
|
|
392
398
|
);
|
|
393
399
|
err.code = 'SERVER_KEY_CHANGED';
|
|
394
400
|
err.pinnedKey = existing;
|
package/lib/core/extensions.js
CHANGED
|
@@ -47,6 +47,17 @@ const HIGH_RISK_PERMISSIONS = new Set([
|
|
|
47
47
|
// Whether to require signatures on community extensions (default: true in production)
|
|
48
48
|
const REQUIRE_SIGNATURE = config.get('extensionRequireSignature') ?? (process.env.NODE_ENV === 'production');
|
|
49
49
|
|
|
50
|
+
// Extension command registry: { extName -> { cmdName -> handler } }
|
|
51
|
+
const commandRegistry = new Map();
|
|
52
|
+
|
|
53
|
+
// Command invocation rate limiting: { extName -> { count, resetTime } }
|
|
54
|
+
const commandRateLimits = new Map();
|
|
55
|
+
const COMMAND_RATE_LIMIT = 60; // per minute per extension
|
|
56
|
+
const COMMAND_TIMEOUT = 30000; // 30 seconds
|
|
57
|
+
|
|
58
|
+
// Valid context types for GUI actions
|
|
59
|
+
const VALID_ACTION_CONTEXTS = new Set(['file', 'folder', 'share', 'toolbar', 'page']);
|
|
60
|
+
|
|
50
61
|
const TIER_RANK = { free: 0, pro: 1, enterprise: 2 };
|
|
51
62
|
|
|
52
63
|
function checkExtensionTier(manifest) {
|
|
@@ -192,6 +203,41 @@ function validateManifest(manifest) {
|
|
|
192
203
|
}
|
|
193
204
|
}
|
|
194
205
|
if (manifest.hooks && manifest.hooks.length > 20) return 'Too many hooks (max 20)';
|
|
206
|
+
|
|
207
|
+
// Validate contributes.commands
|
|
208
|
+
if (manifest.contributes?.commands) {
|
|
209
|
+
const cmds = manifest.contributes.commands;
|
|
210
|
+
if (!Array.isArray(cmds)) return 'contributes.commands must be an array';
|
|
211
|
+
if (cmds.length > 10) return 'Too many contributed commands (max 10)';
|
|
212
|
+
for (const cmd of cmds) {
|
|
213
|
+
if (!cmd.name || typeof cmd.name !== 'string') return 'Command missing name';
|
|
214
|
+
if (!/^[a-z][a-z0-9- ]{0,29}$/.test(cmd.name)) return `Invalid command name: ${cmd.name}`;
|
|
215
|
+
if (!cmd.description) return `Command ${cmd.name} missing description`;
|
|
216
|
+
if (cmd.args && cmd.args.length > 5) return `Command ${cmd.name} has too many args (max 5)`;
|
|
217
|
+
if (cmd.options && cmd.options.length > 10) return `Command ${cmd.name} has too many options (max 10)`;
|
|
218
|
+
if (cmd.options) {
|
|
219
|
+
for (const opt of cmd.options) {
|
|
220
|
+
if (/^--(help|version)/.test(opt.flags)) return `Command ${cmd.name} cannot override --help/--version`;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Validate contributes.actions
|
|
227
|
+
if (manifest.contributes?.actions) {
|
|
228
|
+
const actions = manifest.contributes.actions;
|
|
229
|
+
if (!Array.isArray(actions)) return 'contributes.actions must be an array';
|
|
230
|
+
if (actions.length > 20) return 'Too many contributed actions (max 20)';
|
|
231
|
+
for (const action of actions) {
|
|
232
|
+
if (!action.id || !/^[a-z][a-z0-9-]{0,49}$/.test(action.id)) return `Invalid action id: ${action.id}`;
|
|
233
|
+
if (!action.label) return `Action ${action.id} missing label`;
|
|
234
|
+
if (!action.context || !VALID_ACTION_CONTEXTS.has(action.context)) {
|
|
235
|
+
return `Action ${action.id} has invalid context (must be: ${[...VALID_ACTION_CONTEXTS].join(', ')})`;
|
|
236
|
+
}
|
|
237
|
+
if (!action.command) return `Action ${action.id} missing command reference`;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
195
241
|
return null;
|
|
196
242
|
}
|
|
197
243
|
|
|
@@ -353,6 +399,18 @@ function buildContext(manifest) {
|
|
|
353
399
|
mode: globalThis.__palMode || 'cli',
|
|
354
400
|
},
|
|
355
401
|
store: createStore(manifest.name),
|
|
402
|
+
commands: {
|
|
403
|
+
register(name, handler) {
|
|
404
|
+
const declaredCmds = manifest.contributes?.commands || [];
|
|
405
|
+
if (!declaredCmds.some(c => c.name === name)) {
|
|
406
|
+
throw new Error(`Command "${name}" not declared in manifest contributes.commands`);
|
|
407
|
+
}
|
|
408
|
+
if (typeof handler !== 'function') throw new Error(`Command handler must be a function`);
|
|
409
|
+
const extName = manifest.bundled ? `@palexplorer/${manifest.name}` : manifest.name;
|
|
410
|
+
if (!commandRegistry.has(extName)) commandRegistry.set(extName, new Map());
|
|
411
|
+
commandRegistry.get(extName).set(name, handler);
|
|
412
|
+
},
|
|
413
|
+
},
|
|
356
414
|
};
|
|
357
415
|
|
|
358
416
|
if (perms.has('identity:read')) {
|
|
@@ -601,6 +659,15 @@ function buildSandboxContext(manifest) {
|
|
|
601
659
|
new Notification(title, { body });
|
|
602
660
|
}
|
|
603
661
|
},
|
|
662
|
+
registerCommand(name, handler) {
|
|
663
|
+
const declaredCmds = manifest.contributes?.commands || [];
|
|
664
|
+
if (!declaredCmds.some(c => c.name === name)) {
|
|
665
|
+
throw new Error(`Command "${name}" not declared in manifest contributes.commands`);
|
|
666
|
+
}
|
|
667
|
+
if (typeof handler !== 'function') throw new Error(`Command handler must be a function`);
|
|
668
|
+
if (!commandRegistry.has(manifest.name)) commandRegistry.set(manifest.name, new Map());
|
|
669
|
+
commandRegistry.get(manifest.name).set(name, handler);
|
|
670
|
+
},
|
|
604
671
|
};
|
|
605
672
|
}
|
|
606
673
|
|
|
@@ -642,7 +709,7 @@ async function loadExtension(extPath, manifest) {
|
|
|
642
709
|
const currentHash = computeIntegrityHash(extPath, manifest);
|
|
643
710
|
if (storedHash && storedHash !== currentHash) {
|
|
644
711
|
console.error(`[extensions] SECURITY: ${fullName} files modified since install. Refusing to load.`);
|
|
645
|
-
console.error(`[extensions] Run '
|
|
712
|
+
console.error(`[extensions] Run 'pal ext remove ${manifest.name} && pal ext install ...' to reinstall.`);
|
|
646
713
|
return;
|
|
647
714
|
}
|
|
648
715
|
|
|
@@ -979,6 +1046,76 @@ function getExtension(name) {
|
|
|
979
1046
|
return loadedExtensions.get(name);
|
|
980
1047
|
}
|
|
981
1048
|
|
|
1049
|
+
function getContributedCommands() {
|
|
1050
|
+
const extensions = getInstalledExtensions();
|
|
1051
|
+
const result = [];
|
|
1052
|
+
for (const ext of extensions) {
|
|
1053
|
+
if (!ext.enabled) continue;
|
|
1054
|
+
if (!ext.contributes?.commands?.length) continue;
|
|
1055
|
+
const fullName = ext.bundled ? `@palexplorer/${ext.name}` : ext.name;
|
|
1056
|
+
result.push({
|
|
1057
|
+
extension: fullName,
|
|
1058
|
+
commands: ext.contributes.commands.map(cmd => ({
|
|
1059
|
+
name: cmd.name,
|
|
1060
|
+
description: cmd.description,
|
|
1061
|
+
args: cmd.args || [],
|
|
1062
|
+
options: cmd.options || [],
|
|
1063
|
+
})),
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
return result;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function getContributedActions() {
|
|
1070
|
+
const extensions = getInstalledExtensions();
|
|
1071
|
+
const actions = [];
|
|
1072
|
+
for (const ext of extensions) {
|
|
1073
|
+
if (!ext.enabled) continue;
|
|
1074
|
+
if (!ext.contributes?.actions?.length) continue;
|
|
1075
|
+
const fullName = ext.bundled ? `@palexplorer/${ext.name}` : ext.name;
|
|
1076
|
+
for (const action of ext.contributes.actions) {
|
|
1077
|
+
actions.push({
|
|
1078
|
+
id: action.id,
|
|
1079
|
+
label: action.label,
|
|
1080
|
+
icon: action.icon || null,
|
|
1081
|
+
context: action.context,
|
|
1082
|
+
command: action.command,
|
|
1083
|
+
extension: fullName,
|
|
1084
|
+
minLevel: action.minLevel || null,
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
return actions;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
async function invokeExtensionCommand(extName, cmdName, args, opts) {
|
|
1092
|
+
// Rate limiting
|
|
1093
|
+
const now = Date.now();
|
|
1094
|
+
let limit = commandRateLimits.get(extName);
|
|
1095
|
+
if (!limit || now > limit.resetTime) {
|
|
1096
|
+
limit = { count: 0, resetTime: now + 60000 };
|
|
1097
|
+
commandRateLimits.set(extName, limit);
|
|
1098
|
+
}
|
|
1099
|
+
limit.count++;
|
|
1100
|
+
if (limit.count > COMMAND_RATE_LIMIT) {
|
|
1101
|
+
throw new Error(`Rate limit exceeded for extension ${extName} (max ${COMMAND_RATE_LIMIT}/min)`);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
const extCommands = commandRegistry.get(extName);
|
|
1105
|
+
if (!extCommands || !extCommands.has(cmdName)) {
|
|
1106
|
+
throw new Error(`Command "${cmdName}" not registered by extension ${extName}`);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
const handler = extCommands.get(cmdName);
|
|
1110
|
+
const result = await Promise.race([
|
|
1111
|
+
handler(args, opts),
|
|
1112
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Command timeout (${COMMAND_TIMEOUT / 1000}s)`)), COMMAND_TIMEOUT)),
|
|
1113
|
+
]);
|
|
1114
|
+
|
|
1115
|
+
// Sanitize output — must be JSON-serializable
|
|
1116
|
+
return JSON.parse(JSON.stringify(result ?? { ok: true }));
|
|
1117
|
+
}
|
|
1118
|
+
|
|
982
1119
|
function getContributedPages() {
|
|
983
1120
|
const extensions = getInstalledExtensions();
|
|
984
1121
|
const pages = [];
|
|
@@ -1028,6 +1165,10 @@ export {
|
|
|
1028
1165
|
loadedExtensions,
|
|
1029
1166
|
getExtension,
|
|
1030
1167
|
getContributedPages,
|
|
1168
|
+
getContributedCommands,
|
|
1169
|
+
getContributedActions,
|
|
1170
|
+
invokeExtensionCommand,
|
|
1171
|
+
commandRegistry,
|
|
1031
1172
|
getInstalledExtensions,
|
|
1032
1173
|
loadAllExtensions,
|
|
1033
1174
|
loadExtension,
|
package/lib/core/identity.js
CHANGED
|
@@ -218,20 +218,51 @@ export function setIdentityHandle(handle) {
|
|
|
218
218
|
return updated;
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
export const PRESENCE_STATUSES = ['online', 'away', 'dnd', 'invisible'];
|
|
222
|
+
|
|
223
|
+
export const PROFILE_FIELDS = [
|
|
224
|
+
'avatar', 'bio', 'status', 'displayName',
|
|
225
|
+
'avatarHash', 'avatarMagnet',
|
|
226
|
+
'presenceStatus', 'statusText',
|
|
227
|
+
'age', 'gender', 'country', 'language', 'interests',
|
|
228
|
+
];
|
|
229
|
+
|
|
221
230
|
export function updateProfile(fields) {
|
|
222
231
|
const identity = config.get('identity');
|
|
223
232
|
if (!identity) return null;
|
|
224
233
|
|
|
225
|
-
const allowed = ['avatar', 'bio', 'status', 'displayName'];
|
|
226
234
|
const updated = { ...identity };
|
|
227
|
-
for (const key of
|
|
235
|
+
for (const key of PROFILE_FIELDS) {
|
|
228
236
|
if (fields[key] !== undefined) updated[key] = fields[key];
|
|
229
237
|
}
|
|
238
|
+
if (fields.presenceStatus && !PRESENCE_STATUSES.includes(fields.presenceStatus)) {
|
|
239
|
+
throw new Error(`Invalid presence status: ${fields.presenceStatus}`);
|
|
240
|
+
}
|
|
241
|
+
if (fields.interests) {
|
|
242
|
+
if (!Array.isArray(fields.interests)) throw new Error('Interests must be an array');
|
|
243
|
+
updated.interests = fields.interests.slice(0, 20).map(i => String(i).slice(0, 30));
|
|
244
|
+
}
|
|
245
|
+
if (fields.age !== undefined) {
|
|
246
|
+
const age = Number(fields.age);
|
|
247
|
+
if (fields.age !== null && (isNaN(age) || age < 0 || age > 150)) throw new Error('Invalid age');
|
|
248
|
+
updated.age = fields.age === null ? undefined : age;
|
|
249
|
+
}
|
|
230
250
|
updated.updatedAt = new Date().toISOString();
|
|
231
251
|
config.set('identity', updated);
|
|
232
252
|
return updated;
|
|
233
253
|
}
|
|
234
254
|
|
|
255
|
+
export function getProfile() {
|
|
256
|
+
const identity = config.get('identity');
|
|
257
|
+
if (!identity) return null;
|
|
258
|
+
const profile = {};
|
|
259
|
+
for (const key of PROFILE_FIELDS) {
|
|
260
|
+
if (identity[key] !== undefined) profile[key] = identity[key];
|
|
261
|
+
}
|
|
262
|
+
profile.joinedAt = identity.createdAt;
|
|
263
|
+
return profile;
|
|
264
|
+
}
|
|
265
|
+
|
|
235
266
|
function keypairFromSeed(seed) {
|
|
236
267
|
const publicKey = Buffer.alloc(sodium.crypto_sign_PUBLICKEYBYTES);
|
|
237
268
|
const privateKey = Buffer.alloc(sodium.crypto_sign_SECRETKEYBYTES);
|