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.
- package/LICENSE.md +18 -0
- package/README.md +314 -0
- package/bin/pal.js +230 -0
- package/extensions/@palexplorer/analytics/README.md +45 -0
- package/extensions/@palexplorer/analytics/docs/MONETIZATION.md +14 -0
- package/extensions/@palexplorer/analytics/docs/PLAN.md +23 -0
- package/extensions/@palexplorer/analytics/docs/PRIVACY.md +38 -0
- package/extensions/@palexplorer/analytics/extension.json +27 -0
- package/extensions/@palexplorer/analytics/index.js +186 -0
- package/extensions/@palexplorer/analytics/test/analytics.test.js +82 -0
- package/extensions/@palexplorer/audit/extension.json +17 -0
- package/extensions/@palexplorer/audit/index.js +2 -0
- package/extensions/@palexplorer/auth-email/extension.json +17 -0
- package/extensions/@palexplorer/auth-email/index.js +102 -0
- package/extensions/@palexplorer/auth-oauth/extension.json +16 -0
- package/extensions/@palexplorer/auth-oauth/index.js +199 -0
- package/extensions/@palexplorer/chat/extension.json +17 -0
- package/extensions/@palexplorer/chat/index.js +2 -0
- package/extensions/@palexplorer/discovery/extension.json +16 -0
- package/extensions/@palexplorer/discovery/index.js +111 -0
- package/extensions/@palexplorer/email-notifications/extension.json +23 -0
- package/extensions/@palexplorer/email-notifications/index.js +242 -0
- package/extensions/@palexplorer/explorer-integration/extension.json +13 -0
- package/extensions/@palexplorer/explorer-integration/index.js +122 -0
- package/extensions/@palexplorer/groups/extension.json +17 -0
- package/extensions/@palexplorer/groups/index.js +2 -0
- package/extensions/@palexplorer/networks/extension.json +17 -0
- package/extensions/@palexplorer/networks/index.js +2 -0
- package/extensions/@palexplorer/share-links/extension.json +17 -0
- package/extensions/@palexplorer/share-links/index.js +2 -0
- package/extensions/@palexplorer/sync/extension.json +17 -0
- package/extensions/@palexplorer/sync/index.js +2 -0
- package/extensions/@palexplorer/user-mgmt/extension.json +17 -0
- package/extensions/@palexplorer/user-mgmt/index.js +2 -0
- package/extensions/@palexplorer/vfs/extension.json +17 -0
- package/extensions/@palexplorer/vfs/index.js +167 -0
- package/lib/capabilities.js +263 -0
- package/lib/commands/analytics.js +175 -0
- package/lib/commands/api-keys.js +131 -0
- package/lib/commands/audit.js +235 -0
- package/lib/commands/auth.js +137 -0
- package/lib/commands/backup.js +76 -0
- package/lib/commands/billing.js +148 -0
- package/lib/commands/chat.js +217 -0
- package/lib/commands/cloud-backup.js +231 -0
- package/lib/commands/comment.js +99 -0
- package/lib/commands/completion.js +203 -0
- package/lib/commands/compliance.js +218 -0
- package/lib/commands/config.js +136 -0
- package/lib/commands/connect.js +44 -0
- package/lib/commands/dept.js +294 -0
- package/lib/commands/device.js +146 -0
- package/lib/commands/download.js +226 -0
- package/lib/commands/explorer.js +178 -0
- package/lib/commands/extension.js +970 -0
- package/lib/commands/favorite.js +90 -0
- package/lib/commands/federation.js +270 -0
- package/lib/commands/file.js +533 -0
- package/lib/commands/group.js +271 -0
- package/lib/commands/gui-share.js +29 -0
- package/lib/commands/init.js +61 -0
- package/lib/commands/invite.js +59 -0
- package/lib/commands/list.js +59 -0
- package/lib/commands/log.js +116 -0
- package/lib/commands/nearby.js +108 -0
- package/lib/commands/network.js +251 -0
- package/lib/commands/notify.js +198 -0
- package/lib/commands/org.js +273 -0
- package/lib/commands/pal.js +180 -0
- package/lib/commands/permissions.js +216 -0
- package/lib/commands/pin.js +97 -0
- package/lib/commands/protocol.js +357 -0
- package/lib/commands/rbac.js +147 -0
- package/lib/commands/recover.js +36 -0
- package/lib/commands/register.js +171 -0
- package/lib/commands/relay.js +131 -0
- package/lib/commands/remote.js +368 -0
- package/lib/commands/revoke.js +50 -0
- package/lib/commands/scanner.js +280 -0
- package/lib/commands/schedule.js +344 -0
- package/lib/commands/scim.js +203 -0
- package/lib/commands/search.js +181 -0
- package/lib/commands/serve.js +438 -0
- package/lib/commands/server.js +350 -0
- package/lib/commands/share-link.js +199 -0
- package/lib/commands/share.js +323 -0
- package/lib/commands/sso.js +200 -0
- package/lib/commands/status.js +136 -0
- package/lib/commands/stream.js +562 -0
- package/lib/commands/su.js +187 -0
- package/lib/commands/sync.js +827 -0
- package/lib/commands/transfers.js +152 -0
- package/lib/commands/uninstall.js +188 -0
- package/lib/commands/update.js +204 -0
- package/lib/commands/user.js +276 -0
- package/lib/commands/vfs.js +84 -0
- package/lib/commands/web.js +52 -0
- package/lib/commands/webhook.js +180 -0
- package/lib/commands/whoami.js +59 -0
- package/lib/commands/workspace.js +121 -0
- package/lib/core/accessLog.js +54 -0
- package/lib/core/analytics.js +99 -0
- package/lib/core/backup.js +84 -0
- package/lib/core/billing.js +336 -0
- package/lib/core/bitfieldStore.js +53 -0
- package/lib/core/connectionManager.js +182 -0
- package/lib/core/dhtDiscovery.js +148 -0
- package/lib/core/discoveryClient.js +408 -0
- package/lib/core/extensionAnalyzer.js +357 -0
- package/lib/core/extensionSandbox.js +250 -0
- package/lib/core/extensionWorkerHost.js +166 -0
- package/lib/core/extensions.js +1082 -0
- package/lib/core/fileDiff.js +69 -0
- package/lib/core/groups.js +119 -0
- package/lib/core/identity.js +340 -0
- package/lib/core/mdnsService.js +126 -0
- package/lib/core/networks.js +81 -0
- package/lib/core/permissions.js +109 -0
- package/lib/core/pro.js +27 -0
- package/lib/core/resolver.js +74 -0
- package/lib/core/serverList.js +224 -0
- package/lib/core/sharePolicy.js +69 -0
- package/lib/core/shares.js +325 -0
- package/lib/core/signalingServer.js +441 -0
- package/lib/core/streamTransport.js +106 -0
- package/lib/core/su.js +55 -0
- package/lib/core/syncEngine.js +264 -0
- package/lib/core/syncState.js +159 -0
- package/lib/core/transfers.js +259 -0
- package/lib/core/users.js +225 -0
- package/lib/core/vfs.js +216 -0
- package/lib/core/webServer.js +702 -0
- package/lib/core/webrtcStream.js +396 -0
- package/lib/crypto/chatEncryption.js +57 -0
- package/lib/crypto/shareEncryption.js +195 -0
- package/lib/crypto/sharePassword.js +35 -0
- package/lib/crypto/streamEncryption.js +189 -0
- package/lib/package.json +1 -0
- package/lib/protocol/envelope.js +271 -0
- package/lib/protocol/handler.js +191 -0
- package/lib/protocol/index.js +27 -0
- package/lib/protocol/messages.js +247 -0
- package/lib/protocol/negotiation.js +127 -0
- package/lib/protocol/policy.js +142 -0
- package/lib/protocol/router.js +86 -0
- package/lib/protocol/sync.js +122 -0
- package/lib/utils/cli.js +15 -0
- package/lib/utils/config.js +123 -0
- package/lib/utils/configIntegrity.js +87 -0
- package/lib/utils/downloadDir.js +9 -0
- package/lib/utils/explorer.js +83 -0
- package/lib/utils/format.js +12 -0
- package/lib/utils/help.js +357 -0
- package/lib/utils/logger.js +103 -0
- package/lib/utils/torrent.js +203 -0
- package/package.json +71 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import { addShare, storeShareKey } from '../core/shares.js';
|
|
5
|
+
import { getGroup } from '../core/groups.js';
|
|
6
|
+
import config from '../utils/config.js';
|
|
7
|
+
import logger from '../utils/logger.js';
|
|
8
|
+
import { requireRole, getFriends } from '../core/users.js';
|
|
9
|
+
import { checkLimit } from '../core/pro.js';
|
|
10
|
+
|
|
11
|
+
export default function shareCommand(program) {
|
|
12
|
+
program
|
|
13
|
+
.command('share-rename <id> <name>')
|
|
14
|
+
.description('rename a shared resource')
|
|
15
|
+
.action((id, name) => {
|
|
16
|
+
if (!name || !name.trim()) {
|
|
17
|
+
console.log(chalk.red('Error: name cannot be empty.'));
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const shares = config.get('shares') || [];
|
|
22
|
+
const share = shares.find(s => s.id === id);
|
|
23
|
+
if (!share) {
|
|
24
|
+
console.log(chalk.red('Share not found.'));
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
share.name = name;
|
|
29
|
+
config.set('shares', shares);
|
|
30
|
+
console.log(chalk.green(`✔ Share renamed to: ${name}`));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
program
|
|
34
|
+
.command('share <file>')
|
|
35
|
+
.description('share a file, folder, or drive via P2P')
|
|
36
|
+
.option('-v, --visibility <type>', 'Visibility: global, private, group, network, link-only', 'global')
|
|
37
|
+
.option('-w, --with <pals...>', 'Specific pals to share with (names or IDs)')
|
|
38
|
+
.option('--with-group <group>', 'Share with all members of a group')
|
|
39
|
+
.option('--with-network <network>', 'Share with a network')
|
|
40
|
+
.option('--streamable', 'Enable media streaming (audio/video playback without download)')
|
|
41
|
+
.option('--no-recursive', 'Share only top-level files in folder (no subfolders)')
|
|
42
|
+
.option('--expires <duration>', 'Auto-expire after duration (e.g. 1h, 3d, 7d, 30d)')
|
|
43
|
+
.option('--max-downloads <n>', 'Auto-expire after N downloads (0 = unlimited)', '0')
|
|
44
|
+
.option('--password <pwd>', 'Password-protect this share')
|
|
45
|
+
.addHelpText('after', `
|
|
46
|
+
Examples:
|
|
47
|
+
$ pe share ./photos Share a folder publicly (recursive)
|
|
48
|
+
$ pe share ./photos --no-recursive Share only top-level files in folder
|
|
49
|
+
$ pe share ./docs -v private -w alice Share privately with pal "alice"
|
|
50
|
+
$ pe share ./project --with-group team Share with entire group
|
|
51
|
+
$ pe share ./file.zip --password secret123 Password-protected share
|
|
52
|
+
$ pe share ./file.zip Share a single file
|
|
53
|
+
$ pe share ./music --streamable Share as streaming media server
|
|
54
|
+
$ pe share ./report.pdf --expires 3d Share for 3 days then auto-expire
|
|
55
|
+
$ pe share ./file.zip --max-downloads 5 Expire after 5 downloads
|
|
56
|
+
$ pe share ./docs -v network --with-network mynet
|
|
57
|
+
|
|
58
|
+
Note: Private shares create an encrypted copy of your files for E2E seeding.
|
|
59
|
+
This uses additional disk space equal to the original share size.
|
|
60
|
+
`)
|
|
61
|
+
.action(async (filePath, options) => {
|
|
62
|
+
if (!filePath || !filePath.trim()) {
|
|
63
|
+
console.log(chalk.red('Error: file path cannot be empty.'));
|
|
64
|
+
process.exitCode = 1;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const VALID_VISIBILITIES = ['global', 'private', 'group', 'network', 'link-only', 'public'];
|
|
68
|
+
if (!VALID_VISIBILITIES.includes(options.visibility)) {
|
|
69
|
+
console.log(chalk.red(`Invalid visibility '${options.visibility}'. Must be one of: ${VALID_VISIBILITIES.join(', ')}`));
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const maxDl = parseInt(options.maxDownloads);
|
|
74
|
+
if (isNaN(maxDl) || maxDl < 0) {
|
|
75
|
+
console.log(chalk.red('--max-downloads must be a non-negative integer.'));
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const absolutePath = path.resolve(filePath);
|
|
80
|
+
try {
|
|
81
|
+
try { requireRole('user'); } catch (e) {
|
|
82
|
+
console.log(chalk.red(e.message));
|
|
83
|
+
process.exitCode = 1;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (!fs.existsSync(absolutePath)) {
|
|
87
|
+
console.log(chalk.red(`Path does not exist: ${absolutePath}`));
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const friends = getFriends();
|
|
92
|
+
|
|
93
|
+
// Validate pals if provided
|
|
94
|
+
let recipients = [];
|
|
95
|
+
if (options.withGroup) {
|
|
96
|
+
const group = getGroup(options.withGroup);
|
|
97
|
+
if (!group) {
|
|
98
|
+
console.log(chalk.red(`Group '${options.withGroup}' not found.`));
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (group.members.length === 0) {
|
|
103
|
+
console.log(chalk.yellow(`Group '${group.name}' has no members.`));
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
recipients = group.members.map(m => {
|
|
108
|
+
const pal = friends.find(f => f.id === m.id);
|
|
109
|
+
return pal || { name: m.name, id: m.id, handle: m.handle };
|
|
110
|
+
});
|
|
111
|
+
console.log(chalk.cyan(`Sharing with group '${group.name}' (${recipients.length} members)`));
|
|
112
|
+
}
|
|
113
|
+
if (options.with) {
|
|
114
|
+
const palRecipients = options.with.map(nameOrId => {
|
|
115
|
+
const stripped = nameOrId.replace(/^@/, '');
|
|
116
|
+
const pal = friends.find(f =>
|
|
117
|
+
f.id === stripped || f.name === stripped || f.handle === stripped || f.publicKey === stripped
|
|
118
|
+
);
|
|
119
|
+
if (!pal) {
|
|
120
|
+
console.warn(chalk.yellow(`Warning: Pal '${stripped}' not found in your list.`));
|
|
121
|
+
return { name: stripped, id: 'unknown' };
|
|
122
|
+
}
|
|
123
|
+
return pal;
|
|
124
|
+
});
|
|
125
|
+
// Merge, avoid duplicates by id
|
|
126
|
+
for (const r of palRecipients) {
|
|
127
|
+
if (!recipients.find(e => e.id === r.id)) {
|
|
128
|
+
recipients.push(r);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
checkLimit('maxShareRecipients', recipients.length);
|
|
134
|
+
|
|
135
|
+
console.log(chalk.blue(`Preparing to share: ${absolutePath}`));
|
|
136
|
+
|
|
137
|
+
let type = 'file';
|
|
138
|
+
try {
|
|
139
|
+
const stat = fs.statSync(absolutePath);
|
|
140
|
+
if (stat.isDirectory()) {
|
|
141
|
+
const parsed = path.parse(absolutePath);
|
|
142
|
+
type = parsed.dir === parsed.root || absolutePath === parsed.root ? 'drive' : 'folder';
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
// path doesn't exist yet — default to 'file'
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const recursive = options.recursive !== false;
|
|
149
|
+
let expiresIn = 0;
|
|
150
|
+
if (options.expires) {
|
|
151
|
+
expiresIn = parseDuration(options.expires);
|
|
152
|
+
if (!expiresIn) {
|
|
153
|
+
console.log(chalk.red('Invalid --expires format. Use: 1h, 3d, 7d, 30d'));
|
|
154
|
+
process.exitCode = 1;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const maxDownloads = parseInt(options.maxDownloads) || 0;
|
|
159
|
+
const password = options.password || null;
|
|
160
|
+
const share = addShare(absolutePath, type, options.visibility, { read: true }, recipients, { recursive, expiresIn, maxDownloads, password });
|
|
161
|
+
|
|
162
|
+
// Set streamable flag
|
|
163
|
+
if (options.streamable) {
|
|
164
|
+
const shares = config.get('shares');
|
|
165
|
+
const idx = shares.findIndex(s => s.id === share.id);
|
|
166
|
+
if (idx !== -1) {
|
|
167
|
+
shares[idx].streamable = true;
|
|
168
|
+
config.set('shares', shares);
|
|
169
|
+
}
|
|
170
|
+
console.log(chalk.magenta(' Media streaming enabled for this share'));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Set network association
|
|
174
|
+
if (options.withNetwork) {
|
|
175
|
+
const shares = config.get('shares');
|
|
176
|
+
const idx = shares.findIndex(s => s.id === share.id);
|
|
177
|
+
if (idx !== -1) {
|
|
178
|
+
shares[idx].sharedWithNetworks = [options.withNetwork];
|
|
179
|
+
config.set('shares', shares);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Persist group association
|
|
184
|
+
if (options.withGroup) {
|
|
185
|
+
const group = getGroup(options.withGroup);
|
|
186
|
+
if (group) {
|
|
187
|
+
const shares = config.get('shares');
|
|
188
|
+
const idx = shares.findIndex(s => s.id === share.id);
|
|
189
|
+
if (idx !== -1) {
|
|
190
|
+
shares[idx].sharedWithGroups = [group.id];
|
|
191
|
+
config.set('shares', shares);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Encrypt for private shares that have named recipients
|
|
197
|
+
if (options.visibility === 'private' && share.recipients.length > 0) {
|
|
198
|
+
const { generateShareKey, encryptDirectory, getEncryptedShareDir, encryptShareKeyForRecipient } = await import('../crypto/shareEncryption.js');
|
|
199
|
+
const shareKey = generateShareKey();
|
|
200
|
+
const encDir = getEncryptedShareDir(share.id);
|
|
201
|
+
console.log(chalk.gray('Encrypting files for private share...'));
|
|
202
|
+
const encStart = Date.now();
|
|
203
|
+
const encSpinner = setInterval(() => {
|
|
204
|
+
const elapsed = ((Date.now() - encStart) / 1000).toFixed(0);
|
|
205
|
+
process.stdout.write(`\r${chalk.gray(` Encrypting... ${elapsed}s`)}`);
|
|
206
|
+
}, 1000);
|
|
207
|
+
try {
|
|
208
|
+
encryptDirectory(absolutePath, encDir, shareKey);
|
|
209
|
+
} finally {
|
|
210
|
+
clearInterval(encSpinner);
|
|
211
|
+
process.stdout.write('\r' + ' '.repeat(40) + '\r');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Wrap the shareKey for each recipient
|
|
215
|
+
const encryptedShareKeys = {};
|
|
216
|
+
for (const recipient of share.recipients) {
|
|
217
|
+
const pal = friends.find(f => f.name === recipient.name || f.id === recipient.id || f.handle === recipient.handle);
|
|
218
|
+
if (pal?.id && pal.id !== 'unknown') {
|
|
219
|
+
encryptedShareKeys[pal.id] = encryptShareKeyForRecipient(shareKey, pal.id);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Store share key securely in credential store
|
|
224
|
+
await storeShareKey(share.id, shareKey.toString('hex'));
|
|
225
|
+
|
|
226
|
+
// Persist encryption metadata onto the stored share (without shareKeyHex)
|
|
227
|
+
const shares = config.get('shares');
|
|
228
|
+
const idx = shares.findIndex(s => s.id === share.id);
|
|
229
|
+
if (idx !== -1) {
|
|
230
|
+
shares[idx].encryptedPath = encDir;
|
|
231
|
+
shares[idx].encryptedShareKeys = encryptedShareKeys;
|
|
232
|
+
config.set('shares', shares);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
console.log(chalk.green('Encryption complete.'));
|
|
236
|
+
|
|
237
|
+
// Warn about disk overhead for private shares
|
|
238
|
+
try {
|
|
239
|
+
const stat = fs.statSync(absolutePath);
|
|
240
|
+
if (stat.isDirectory()) {
|
|
241
|
+
let totalSize = 0;
|
|
242
|
+
const walk = (dir) => {
|
|
243
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
244
|
+
const full = path.join(dir, entry.name);
|
|
245
|
+
if (entry.isFile()) totalSize += fs.statSync(full).size;
|
|
246
|
+
else if (entry.isDirectory()) walk(full);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
walk(absolutePath);
|
|
250
|
+
const sizeMB = (totalSize / 1024 / 1024).toFixed(0);
|
|
251
|
+
if (totalSize > 1024 * 1024 * 1024) {
|
|
252
|
+
const sizeGB = (totalSize / 1024 / 1024 / 1024).toFixed(1);
|
|
253
|
+
console.log(chalk.yellow(` Warning: Private shares create an encrypted copy (~${sizeGB} GB additional disk space).`));
|
|
254
|
+
console.log(chalk.yellow(` Total disk usage for this share: ~${(totalSize * 2 / 1024 / 1024 / 1024).toFixed(1)} GB (original + encrypted copy).`));
|
|
255
|
+
} else if (totalSize > 50 * 1024 * 1024) {
|
|
256
|
+
console.log(chalk.yellow(` Note: Private shares create an encrypted copy (~${sizeMB} MB additional disk space).`));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
} catch {}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
logger.info(`Share added: ${absolutePath}`, { visibility: options.visibility, type, id: share.id });
|
|
263
|
+
console.log(chalk.green(`✔ Resource added to ${options.visibility} share list!`));
|
|
264
|
+
if (recipients.length > 0) {
|
|
265
|
+
console.log(chalk.cyan(` Shared with: ${recipients.map(r => r.name).join(', ')}`));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Send PAL/1.0 share.offer envelopes to recipients
|
|
269
|
+
if (options.visibility === 'private' && recipients.length > 0) {
|
|
270
|
+
try {
|
|
271
|
+
const { getIdentity } = await import('../core/identity.js');
|
|
272
|
+
const identity = await getIdentity();
|
|
273
|
+
if (identity?.privateKey) {
|
|
274
|
+
const { initiateShare } = await import('../protocol/negotiation.js');
|
|
275
|
+
const { postTo, getPrimaryServer } = await import('../core/discoveryClient.js');
|
|
276
|
+
const keyPair = {
|
|
277
|
+
publicKey: Buffer.from(identity.publicKey, 'hex'),
|
|
278
|
+
privateKey: Buffer.from(identity.privateKey, 'hex'),
|
|
279
|
+
};
|
|
280
|
+
let sent = 0;
|
|
281
|
+
for (const r of recipients) {
|
|
282
|
+
const pal = friends.find(f => f.id === r.id);
|
|
283
|
+
if (pal?.id && pal.id !== 'unknown') {
|
|
284
|
+
const result = await initiateShare(keyPair, pal.id, share, options.policy || {});
|
|
285
|
+
if (result.ok) {
|
|
286
|
+
await postTo(getPrimaryServer(), '/api/v1/messages', {
|
|
287
|
+
toHandle: pal.handle || pal.name,
|
|
288
|
+
fromHandle: identity.handle || identity.name,
|
|
289
|
+
payload: result.envelope,
|
|
290
|
+
});
|
|
291
|
+
sent++;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (sent > 0) console.log(chalk.gray(` PAL/1.0 share offers sent to ${sent} recipient(s)`));
|
|
296
|
+
}
|
|
297
|
+
} catch {
|
|
298
|
+
// Protocol notifications are best-effort
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (share.password) console.log(chalk.yellow(' Password protected'));
|
|
303
|
+
if (share.expiresAt) console.log(chalk.gray(` Expires: ${new Date(share.expiresAt).toLocaleString()}`));
|
|
304
|
+
if (share.maxDownloads) console.log(chalk.gray(` Max downloads: ${share.maxDownloads}`));
|
|
305
|
+
console.log(chalk.white('To start seeding, use: pe serve'));
|
|
306
|
+
console.log(chalk.gray(`ID: ${share.id}`));
|
|
307
|
+
} catch (err) {
|
|
308
|
+
logger.error(`Share failed: ${err.message}`, { path: absolutePath });
|
|
309
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function parseDuration(str) {
|
|
315
|
+
const match = str.match(/^(\d+)([dhm])$/);
|
|
316
|
+
if (!match) return null;
|
|
317
|
+
const n = parseInt(match[1]);
|
|
318
|
+
const unit = match[2];
|
|
319
|
+
if (unit === 'd') return n * 86400000;
|
|
320
|
+
if (unit === 'h') return n * 3600000;
|
|
321
|
+
if (unit === 'm') return n * 60000;
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
export default function ssoCommand(program) {
|
|
4
|
+
const cmd = program
|
|
5
|
+
.command('sso')
|
|
6
|
+
.description('manage enterprise SSO (SAML/OIDC)')
|
|
7
|
+
.addHelpText('after', `
|
|
8
|
+
Examples:
|
|
9
|
+
$ pe sso configure --provider oidc --issuer https://auth.example.com --client-id abc --client-secret xyz
|
|
10
|
+
$ pe sso configure --provider saml --issuer https://idp.example.com/metadata
|
|
11
|
+
$ pe sso login Initiate SSO login
|
|
12
|
+
$ pe sso status Show current SSO configuration
|
|
13
|
+
$ pe sso enforce --on Enforce mandatory SSO
|
|
14
|
+
$ pe sso enforce --off Disable mandatory SSO
|
|
15
|
+
`)
|
|
16
|
+
.action(() => {
|
|
17
|
+
cmd.outputHelp();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
cmd
|
|
21
|
+
.command('configure')
|
|
22
|
+
.description('configure SSO provider (SAML or OIDC)')
|
|
23
|
+
.requiredOption('--provider <type>', 'SSO provider type (saml or oidc)')
|
|
24
|
+
.option('--issuer <url>', 'IdP issuer URL')
|
|
25
|
+
.option('--client-id <id>', 'OIDC client ID')
|
|
26
|
+
.option('--client-secret <secret>', 'OIDC client secret')
|
|
27
|
+
.action(async (opts) => {
|
|
28
|
+
try {
|
|
29
|
+
const provider = opts.provider.toLowerCase();
|
|
30
|
+
if (provider !== 'saml' && provider !== 'oidc') {
|
|
31
|
+
console.log(chalk.red('Provider must be "saml" or "oidc".'));
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (provider === 'oidc' && (!opts.clientId || !opts.clientSecret)) {
|
|
37
|
+
console.log(chalk.red('OIDC requires --client-id and --client-secret.'));
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!opts.issuer) {
|
|
43
|
+
console.log(chalk.red('--issuer is required.'));
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const extConfig = (await import('../utils/config.js')).default;
|
|
49
|
+
const existing = extConfig.get('ext.sso-connector') || {};
|
|
50
|
+
|
|
51
|
+
const clientSecret = opts.clientSecret || existing.clientSecret || null;
|
|
52
|
+
const ssoConfig = {
|
|
53
|
+
...existing,
|
|
54
|
+
provider,
|
|
55
|
+
issuer: opts.issuer,
|
|
56
|
+
clientId: opts.clientId || existing.clientId || null,
|
|
57
|
+
clientSecret: null,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (clientSecret) {
|
|
61
|
+
try {
|
|
62
|
+
const keytar = (await import('keytar')).default;
|
|
63
|
+
await keytar.setPassword('palexplorer', 'sso.clientSecret', clientSecret);
|
|
64
|
+
} catch {
|
|
65
|
+
ssoConfig.clientSecret = clientSecret;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
extConfig.set('ext.sso-connector', ssoConfig);
|
|
70
|
+
|
|
71
|
+
console.log(chalk.green(`✔ SSO configured`));
|
|
72
|
+
console.log(` Provider: ${chalk.white(provider.toUpperCase())}`);
|
|
73
|
+
console.log(` Issuer: ${chalk.white(opts.issuer)}`);
|
|
74
|
+
if (provider === 'oidc') {
|
|
75
|
+
console.log(` Client: ${chalk.white(opts.clientId)}`);
|
|
76
|
+
}
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.log(chalk.red(`Failed to configure SSO: ${err.message}`));
|
|
79
|
+
process.exitCode = 1;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
cmd
|
|
84
|
+
.command('login')
|
|
85
|
+
.description('initiate SSO login')
|
|
86
|
+
.action(async () => {
|
|
87
|
+
try {
|
|
88
|
+
const extConfig = (await import('../utils/config.js')).default;
|
|
89
|
+
const cfg = extConfig.get('ext.sso-connector');
|
|
90
|
+
|
|
91
|
+
if (!cfg || !cfg.provider) {
|
|
92
|
+
console.log(chalk.red('SSO not configured. Run: pe sso configure'));
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let redirectUrl;
|
|
98
|
+
if (cfg.provider === 'oidc') {
|
|
99
|
+
let clientId = cfg.clientId;
|
|
100
|
+
let clientSecret = cfg.clientSecret;
|
|
101
|
+
if (!clientSecret) {
|
|
102
|
+
try {
|
|
103
|
+
const keytar = (await import('keytar')).default;
|
|
104
|
+
clientSecret = await keytar.getPassword('palexplorer', 'sso.clientSecret');
|
|
105
|
+
} catch {}
|
|
106
|
+
}
|
|
107
|
+
const state = Math.random().toString(36).slice(2);
|
|
108
|
+
const params = new URLSearchParams({
|
|
109
|
+
response_type: 'code',
|
|
110
|
+
client_id: clientId,
|
|
111
|
+
redirect_uri: 'http://localhost:9876/callback',
|
|
112
|
+
scope: 'openid profile email',
|
|
113
|
+
state,
|
|
114
|
+
});
|
|
115
|
+
redirectUrl = `${cfg.issuer}/authorize?${params}`;
|
|
116
|
+
} else {
|
|
117
|
+
redirectUrl = `${cfg.issuer}/sso/saml?RelayState=palexplorer`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log('');
|
|
121
|
+
console.log(chalk.cyan('SSO Login'));
|
|
122
|
+
console.log(` Provider: ${chalk.white(cfg.provider.toUpperCase())}`);
|
|
123
|
+
console.log(` Redirect: ${chalk.white(redirectUrl)}`);
|
|
124
|
+
console.log('');
|
|
125
|
+
console.log(chalk.gray('Open the URL above in your browser to authenticate.'));
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.log(chalk.red(`SSO login failed: ${err.message}`));
|
|
128
|
+
process.exitCode = 1;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
cmd
|
|
133
|
+
.command('status')
|
|
134
|
+
.description('show current SSO configuration')
|
|
135
|
+
.action(async () => {
|
|
136
|
+
try {
|
|
137
|
+
const extConfig = (await import('../utils/config.js')).default;
|
|
138
|
+
const cfg = extConfig.get('ext.sso-connector');
|
|
139
|
+
|
|
140
|
+
console.log('');
|
|
141
|
+
console.log(chalk.cyan.bold('SSO Configuration'));
|
|
142
|
+
|
|
143
|
+
if (!cfg || !cfg.provider) {
|
|
144
|
+
console.log(chalk.gray(' Not configured. Run: pe sso configure'));
|
|
145
|
+
console.log('');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log(` Provider: ${chalk.white(cfg.provider.toUpperCase())}`);
|
|
150
|
+
console.log(` Issuer: ${chalk.white(cfg.issuer || 'not set')}`);
|
|
151
|
+
if (cfg.provider === 'oidc') {
|
|
152
|
+
console.log(` Client ID: ${chalk.white(cfg.clientId || 'not set')}`);
|
|
153
|
+
console.log(` Client Secret: ${cfg.clientSecret ? chalk.gray('********') : chalk.yellow('not set')}`);
|
|
154
|
+
}
|
|
155
|
+
console.log(` Enforce SSO: ${cfg.enforceSSO ? chalk.green('yes') : chalk.gray('no')}`);
|
|
156
|
+
console.log('');
|
|
157
|
+
} catch (err) {
|
|
158
|
+
console.log(chalk.red(`Failed to get SSO status: ${err.message}`));
|
|
159
|
+
process.exitCode = 1;
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
cmd
|
|
164
|
+
.command('enforce')
|
|
165
|
+
.description('enable or disable mandatory SSO')
|
|
166
|
+
.option('--on', 'Enable mandatory SSO')
|
|
167
|
+
.option('--off', 'Disable mandatory SSO')
|
|
168
|
+
.action(async (opts) => {
|
|
169
|
+
try {
|
|
170
|
+
if (!opts.on && !opts.off) {
|
|
171
|
+
console.log(chalk.red('Specify --on or --off.'));
|
|
172
|
+
process.exitCode = 1;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const extConfig = (await import('../utils/config.js')).default;
|
|
177
|
+
const cfg = extConfig.get('ext.sso-connector');
|
|
178
|
+
|
|
179
|
+
if (!cfg || !cfg.provider) {
|
|
180
|
+
console.log(chalk.red('SSO not configured. Run: pe sso configure'));
|
|
181
|
+
process.exitCode = 1;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const enforce = !!opts.on;
|
|
186
|
+
extConfig.set('ext.sso-connector', { ...cfg, enforceSSO: enforce });
|
|
187
|
+
|
|
188
|
+
if (enforce) {
|
|
189
|
+
console.log(chalk.green('✔ Mandatory SSO enabled'));
|
|
190
|
+
console.log(chalk.gray(' All users must authenticate via SSO.'));
|
|
191
|
+
} else {
|
|
192
|
+
console.log(chalk.green('✔ Mandatory SSO disabled'));
|
|
193
|
+
console.log(chalk.gray(' Users can authenticate with local credentials.'));
|
|
194
|
+
}
|
|
195
|
+
} catch (err) {
|
|
196
|
+
console.log(chalk.red(`Failed to update enforcement: ${err.message}`));
|
|
197
|
+
process.exitCode = 1;
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import config from '../utils/config.js';
|
|
5
|
+
import { getIdentity } from '../core/identity.js';
|
|
6
|
+
import { listShares } from '../core/shares.js';
|
|
7
|
+
import { getTransfers } from '../core/transfers.js';
|
|
8
|
+
import { getServers, checkServer } from '../core/discoveryClient.js';
|
|
9
|
+
import { getConnectionState } from '../core/connectionManager.js';
|
|
10
|
+
|
|
11
|
+
export default function statusCommand(program) {
|
|
12
|
+
program
|
|
13
|
+
.command('status')
|
|
14
|
+
.description('show system health dashboard')
|
|
15
|
+
.addHelpText('after', `
|
|
16
|
+
Examples:
|
|
17
|
+
$ pe status Show system health dashboard
|
|
18
|
+
`)
|
|
19
|
+
.action(async () => {
|
|
20
|
+
try {
|
|
21
|
+
const opts = program.opts();
|
|
22
|
+
const identity = await getIdentity();
|
|
23
|
+
|
|
24
|
+
if (opts.json) {
|
|
25
|
+
const shares = listShares();
|
|
26
|
+
const active = getTransfers() || [];
|
|
27
|
+
const pidPath = path.join(path.dirname(config.path), 'serve.pid');
|
|
28
|
+
const servers = getServers();
|
|
29
|
+
const checks = await Promise.all(servers.map(s => checkServer(s)));
|
|
30
|
+
const conn = getConnectionState();
|
|
31
|
+
const data = {
|
|
32
|
+
identity: identity ? { name: identity.name, handle: identity.handle, publicKey: identity.publicKey?.slice(0, 16) } : null,
|
|
33
|
+
network: { status: conn.status, connectedCount: conn.connectedCount || 0 },
|
|
34
|
+
daemon: fs.existsSync(pidPath) ? 'running' : 'stopped',
|
|
35
|
+
shares: { count: shares.length },
|
|
36
|
+
transfers: { active: active.length },
|
|
37
|
+
discoveryServers: checks
|
|
38
|
+
};
|
|
39
|
+
console.log(JSON.stringify(data, null, 2));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Identity
|
|
44
|
+
console.log('');
|
|
45
|
+
console.log(chalk.cyan.bold('Identity'));
|
|
46
|
+
if (identity) {
|
|
47
|
+
console.log(` Name: ${chalk.green(identity.name || 'Unknown')}`);
|
|
48
|
+
console.log(` Handle: ${chalk.green(identity.handle || 'Not linked')}`);
|
|
49
|
+
console.log(` Public Key: ${chalk.yellow(identity.publicKey?.slice(0, 16) + '...')}`);
|
|
50
|
+
} else {
|
|
51
|
+
console.log(chalk.gray(' Not initialized. Run `pe init <name>` to get started.'));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Network Connection
|
|
55
|
+
console.log('');
|
|
56
|
+
console.log(chalk.cyan.bold('Network'));
|
|
57
|
+
const conn = getConnectionState();
|
|
58
|
+
const connColor = conn.status === 'connected' ? chalk.green
|
|
59
|
+
: conn.status === 'connecting' ? chalk.yellow
|
|
60
|
+
: chalk.red;
|
|
61
|
+
console.log(` Status: ${connColor(conn.status)}`);
|
|
62
|
+
if (conn.protocol) {
|
|
63
|
+
console.log(` Protocol: ${chalk.white(conn.protocol)} v${conn.protocolVersion}`);
|
|
64
|
+
}
|
|
65
|
+
if (conn.connectedCount > 0) {
|
|
66
|
+
console.log(` Servers: ${chalk.white(conn.connectedCount)} reachable`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Daemon
|
|
70
|
+
console.log('');
|
|
71
|
+
console.log(chalk.cyan.bold('Daemon'));
|
|
72
|
+
const pidPath = path.join(path.dirname(config.path), 'serve.pid');
|
|
73
|
+
if (fs.existsSync(pidPath)) {
|
|
74
|
+
const pid = fs.readFileSync(pidPath, 'utf8').trim();
|
|
75
|
+
try {
|
|
76
|
+
process.kill(parseInt(pid, 10), 0);
|
|
77
|
+
console.log(` Status: ${chalk.green('Running')} (PID ${pid})`);
|
|
78
|
+
} catch {
|
|
79
|
+
console.log(` Status: ${chalk.yellow('Stale PID file')} (process ${pid} not found)`);
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
console.log(` Status: ${chalk.gray('Not running')}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Shares
|
|
86
|
+
console.log('');
|
|
87
|
+
console.log(chalk.cyan.bold('Shares'));
|
|
88
|
+
const shares = listShares();
|
|
89
|
+
if (shares.length === 0) {
|
|
90
|
+
console.log(chalk.gray(' No active shares.'));
|
|
91
|
+
} else {
|
|
92
|
+
let totalSize = 0;
|
|
93
|
+
for (const s of shares) {
|
|
94
|
+
try {
|
|
95
|
+
const stat = fs.statSync(s.path);
|
|
96
|
+
totalSize += stat.isDirectory() ? 0 : stat.size;
|
|
97
|
+
} catch { /* skip */ }
|
|
98
|
+
}
|
|
99
|
+
console.log(` Active: ${chalk.white(shares.length)}`);
|
|
100
|
+
if (totalSize > 0) {
|
|
101
|
+
console.log(` Size: ${chalk.white((totalSize / 1024 / 1024).toFixed(1) + ' MB')}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Transfers
|
|
106
|
+
console.log('');
|
|
107
|
+
console.log(chalk.cyan.bold('Transfers'));
|
|
108
|
+
const active = getTransfers() || [];
|
|
109
|
+
if (active.length === 0) {
|
|
110
|
+
console.log(chalk.gray(' No active transfers.'));
|
|
111
|
+
} else {
|
|
112
|
+
console.log(` Active: ${chalk.white(active.length)}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Discovery servers
|
|
116
|
+
console.log('');
|
|
117
|
+
console.log(chalk.cyan.bold('Discovery Servers'));
|
|
118
|
+
const servers = getServers();
|
|
119
|
+
const checks = await Promise.all(servers.map(s => checkServer(s)));
|
|
120
|
+
for (let i = 0; i < checks.length; i++) {
|
|
121
|
+
const c = checks[i];
|
|
122
|
+
const primary = i === 0 ? chalk.cyan(' (primary)') : '';
|
|
123
|
+
if (c.reachable) {
|
|
124
|
+
console.log(` ${chalk.green('●')} ${c.url}${primary}`);
|
|
125
|
+
} else {
|
|
126
|
+
console.log(` ${chalk.red('●')} ${c.url} ${chalk.red('unreachable')}${primary}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log('');
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.error(chalk.red('Status check failed:'), err.message);
|
|
133
|
+
process.exitCode = 1;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|