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,350 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import config from '../utils/config.js';
|
|
3
|
+
import { spawn, exec } from 'child_process';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import readline from 'readline';
|
|
8
|
+
import { getPrimaryServer, getServers, checkServer, verifyAllServers, fetchServerKey, acceptServerKey, keyFingerprint } from '../core/discoveryClient.js';
|
|
9
|
+
import { refreshServerList, healthCheckServers, getServerRoles, SERVER_ROLES, isWriteTrusted } from '../core/serverList.js';
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
|
|
13
|
+
function confirm(question) {
|
|
14
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
15
|
+
return new Promise(resolve => rl.question(question, answer => { rl.close(); resolve(answer.trim().toLowerCase() === 'y'); }));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function installWindows() {
|
|
19
|
+
const pePath = process.execPath;
|
|
20
|
+
const scriptPath = path.resolve(__dirname, '../../bin/pal.js');
|
|
21
|
+
const taskName = 'PalexplorerServe';
|
|
22
|
+
const cmd = `schtasks /create /tn "${taskName}" /tr "\\"${pePath}\\" \\"${scriptPath}\\" serve" /sc onlogon /rl limited /f`;
|
|
23
|
+
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
exec(cmd, (err, stdout, stderr) => {
|
|
26
|
+
if (err) {
|
|
27
|
+
console.log(chalk.yellow('Could not create scheduled task automatically.'));
|
|
28
|
+
console.log(chalk.gray('To install manually, create a scheduled task:'));
|
|
29
|
+
console.log(chalk.gray(` schtasks /create /tn "${taskName}" /tr "pe serve" /sc onlogon /rl limited`));
|
|
30
|
+
return resolve();
|
|
31
|
+
}
|
|
32
|
+
console.log(chalk.green(`Scheduled task "${taskName}" created. pe serve will run on login.`));
|
|
33
|
+
resolve();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function installLinux() {
|
|
39
|
+
const serviceSource = path.resolve(__dirname, '../../installer/linux/palexplorer.service');
|
|
40
|
+
const serviceDir = path.join(process.env.HOME, '.config/systemd/user');
|
|
41
|
+
const serviceDest = path.join(serviceDir, 'palexplorer.service');
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
fs.mkdirSync(serviceDir, { recursive: true });
|
|
45
|
+
|
|
46
|
+
if (fs.existsSync(serviceSource)) {
|
|
47
|
+
fs.copyFileSync(serviceSource, serviceDest);
|
|
48
|
+
} else {
|
|
49
|
+
const serviceContent = `[Unit]
|
|
50
|
+
Description=Palexplorer P2P File Sharing Daemon
|
|
51
|
+
After=network-online.target
|
|
52
|
+
Wants=network-online.target
|
|
53
|
+
|
|
54
|
+
[Service]
|
|
55
|
+
Type=simple
|
|
56
|
+
ExecStart=${process.execPath} ${path.resolve(__dirname, '../../bin/pal.js')} serve
|
|
57
|
+
Restart=on-failure
|
|
58
|
+
RestartSec=10
|
|
59
|
+
Environment=NODE_ENV=production
|
|
60
|
+
|
|
61
|
+
[Install]
|
|
62
|
+
WantedBy=default.target
|
|
63
|
+
`;
|
|
64
|
+
fs.writeFileSync(serviceDest, serviceContent);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(chalk.green(`Service file written to ${serviceDest}`));
|
|
68
|
+
console.log(chalk.gray('Enable with:'));
|
|
69
|
+
console.log(chalk.gray(' systemctl --user daemon-reload'));
|
|
70
|
+
console.log(chalk.gray(' systemctl --user enable --now palexplorer'));
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.log(chalk.red('Failed to install systemd service:'), err.message);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function installMacOS() {
|
|
77
|
+
const plistDir = path.join(process.env.HOME, 'Library/LaunchAgents');
|
|
78
|
+
const plistPath = path.join(plistDir, 'com.palexplorer.serve.plist');
|
|
79
|
+
const pePath = process.execPath;
|
|
80
|
+
const scriptPath = path.resolve(__dirname, '../../bin/pal.js');
|
|
81
|
+
|
|
82
|
+
const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
83
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
84
|
+
<plist version="1.0">
|
|
85
|
+
<dict>
|
|
86
|
+
<key>Label</key>
|
|
87
|
+
<string>com.palexplorer.serve</string>
|
|
88
|
+
<key>ProgramArguments</key>
|
|
89
|
+
<array>
|
|
90
|
+
<string>${pePath}</string>
|
|
91
|
+
<string>${scriptPath}</string>
|
|
92
|
+
<string>serve</string>
|
|
93
|
+
</array>
|
|
94
|
+
<key>RunAtLoad</key>
|
|
95
|
+
<true/>
|
|
96
|
+
<key>KeepAlive</key>
|
|
97
|
+
<true/>
|
|
98
|
+
<key>StandardOutPath</key>
|
|
99
|
+
<string>/tmp/palexplorer.log</string>
|
|
100
|
+
<key>StandardErrorPath</key>
|
|
101
|
+
<string>/tmp/palexplorer.err</string>
|
|
102
|
+
</dict>
|
|
103
|
+
</plist>
|
|
104
|
+
`;
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
fs.mkdirSync(plistDir, { recursive: true });
|
|
108
|
+
fs.writeFileSync(plistPath, plistContent);
|
|
109
|
+
console.log(chalk.green(`LaunchAgent written to ${plistPath}`));
|
|
110
|
+
console.log(chalk.gray('Load with:'));
|
|
111
|
+
console.log(chalk.gray(` launchctl load ${plistPath}`));
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.log(chalk.red('Failed to install LaunchAgent:'), err.message);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export default function serverCommand(program) {
|
|
118
|
+
const server = program.command('server').description('provision and manage Palexplorer server nodes')
|
|
119
|
+
.addHelpText('after', `
|
|
120
|
+
Examples:
|
|
121
|
+
$ pe server install Install as system service
|
|
122
|
+
$ pe server status Check service status
|
|
123
|
+
$ pe server config Show server configuration
|
|
124
|
+
$ pe server register Register device with discovery server
|
|
125
|
+
`);
|
|
126
|
+
|
|
127
|
+
server
|
|
128
|
+
.command('install')
|
|
129
|
+
.description('install pe serve as a background service (cross-platform)')
|
|
130
|
+
.action(async () => {
|
|
131
|
+
const platform = process.platform;
|
|
132
|
+
console.log(chalk.blue(`Installing Palexplorer service for ${platform}...`));
|
|
133
|
+
|
|
134
|
+
if (platform === 'win32') {
|
|
135
|
+
await installWindows();
|
|
136
|
+
} else if (platform === 'linux') {
|
|
137
|
+
await installLinux();
|
|
138
|
+
} else if (platform === 'darwin') {
|
|
139
|
+
await installMacOS();
|
|
140
|
+
} else {
|
|
141
|
+
console.log(chalk.red(`Unsupported platform: ${platform}`));
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
server
|
|
146
|
+
.command('config <key> <value>')
|
|
147
|
+
.description('set server-specific operational parameters')
|
|
148
|
+
.action((key, value) => {
|
|
149
|
+
// Validate common keys
|
|
150
|
+
const validKeys = ['port', 'storage_path', 'max_connections', 'bandwidth_cap'];
|
|
151
|
+
if (!validKeys.includes(key)) {
|
|
152
|
+
console.log(chalk.yellow(`Warning: '${key}' is not a standard server parameter. Setting anyway.`));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const settings = config.get('settings') || {};
|
|
156
|
+
settings[key] = value;
|
|
157
|
+
config.set('settings', settings);
|
|
158
|
+
console.log(chalk.green(`✔ Server configuration updated: ${key} = ${value}`));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
server
|
|
162
|
+
.command('register <handle>')
|
|
163
|
+
.description('register this server node with a handle in the Discovery Network')
|
|
164
|
+
.action(async (handle) => {
|
|
165
|
+
const DISCOVERY_URL = getPrimaryServer();
|
|
166
|
+
const { getIdentity, setIdentityHandle } = await import('../core/identity.js');
|
|
167
|
+
const identity = await getIdentity();
|
|
168
|
+
if (!identity || !identity.privateKey) {
|
|
169
|
+
console.log(chalk.red('No identity found. Run `pe init <name>` first.'));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const sodium = (await import('sodium-native')).default;
|
|
174
|
+
const privateKey = Buffer.from(identity.privateKey, 'hex');
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
console.log(chalk.blue(`Registering handle @${handle} on discovery server...`));
|
|
178
|
+
// Step 1: Get challenge
|
|
179
|
+
const chalRes = await fetch(`${DISCOVERY_URL}/register/challenge`, {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
headers: { 'Content-Type': 'application/json' },
|
|
182
|
+
body: JSON.stringify({ handle, publicKey: identity.publicKey })
|
|
183
|
+
});
|
|
184
|
+
const chalData = await chalRes.json();
|
|
185
|
+
if (!chalData.challengeId) {
|
|
186
|
+
console.log(chalk.red(`Challenge failed: ${chalData.error || 'unknown error'}`));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Step 2: Sign challenge and register
|
|
191
|
+
const message = `register:${handle}:${chalData.nonce}`;
|
|
192
|
+
const sig = Buffer.alloc(sodium.crypto_sign_BYTES);
|
|
193
|
+
sodium.crypto_sign_detached(sig, Buffer.from(message), privateKey);
|
|
194
|
+
|
|
195
|
+
const regRes = await fetch(`${DISCOVERY_URL}/register`, {
|
|
196
|
+
method: 'POST',
|
|
197
|
+
headers: { 'Content-Type': 'application/json' },
|
|
198
|
+
body: JSON.stringify({ handle, publicKey: identity.publicKey, challengeId: chalData.challengeId, signature: sig.toString('hex') })
|
|
199
|
+
});
|
|
200
|
+
const result = await regRes.json();
|
|
201
|
+
if (result.success) {
|
|
202
|
+
console.log(chalk.green(`✔ Node registered as @${handle}`));
|
|
203
|
+
setIdentityHandle(handle);
|
|
204
|
+
} else {
|
|
205
|
+
console.log(chalk.red(`Registration failed: ${result.error}`));
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
console.log(chalk.red(`Could not reach discovery server at ${DISCOVERY_URL}`));
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
server
|
|
213
|
+
.command('status')
|
|
214
|
+
.description('check server health and network connectivity')
|
|
215
|
+
.action(() => {
|
|
216
|
+
const identity = config.get('identity');
|
|
217
|
+
const settings = config.get('settings') || {};
|
|
218
|
+
const shares = config.get('shares') || [];
|
|
219
|
+
|
|
220
|
+
console.log('');
|
|
221
|
+
console.log(chalk.cyan('Palexplorer Server Status:'));
|
|
222
|
+
console.log(`- Node Identity: ${identity ? chalk.green(identity.name) : chalk.red('Not Initialized')}`);
|
|
223
|
+
console.log(`- Public Key: ${identity ? chalk.gray(identity.publicKey) : 'N/A'}`);
|
|
224
|
+
console.log(`- Active Shares: ${chalk.white(shares.length)}`);
|
|
225
|
+
console.log(`- Bound Port: ${chalk.white(settings.port || 'Auto (1900)')}`);
|
|
226
|
+
console.log(`- OS: ${chalk.white(process.platform)}`);
|
|
227
|
+
console.log('');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
server
|
|
231
|
+
.command('list')
|
|
232
|
+
.description('list all configured discovery servers with health status and roles')
|
|
233
|
+
.action(async () => {
|
|
234
|
+
const servers = getServers();
|
|
235
|
+
console.log('');
|
|
236
|
+
console.log(chalk.cyan('Discovery Servers:'));
|
|
237
|
+
const checks = await Promise.all(servers.map(s => checkServer(s)));
|
|
238
|
+
for (let i = 0; i < checks.length; i++) {
|
|
239
|
+
const c = checks[i];
|
|
240
|
+
const primary = i === 0 ? chalk.cyan(' (primary)') : '';
|
|
241
|
+
const status = c.reachable ? chalk.green('reachable') : chalk.red('down');
|
|
242
|
+
const meta = getServerRoles(c.url);
|
|
243
|
+
const rolesStr = chalk.gray(`[${meta.roles.join(', ')}]`);
|
|
244
|
+
const addedBy = meta.addedBy !== 'bootstrap' ? chalk.gray(` (${meta.addedBy})`) : '';
|
|
245
|
+
const trust = isWriteTrusted(c.url) ? chalk.green('read+write') : chalk.yellow('read-only');
|
|
246
|
+
console.log(` ${i + 1}. ${chalk.white(c.url)} — ${status}${primary} ${rolesStr} ${trust}${addedBy}`);
|
|
247
|
+
}
|
|
248
|
+
console.log('');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
server
|
|
252
|
+
.command('refresh')
|
|
253
|
+
.description('fetch and update server list from all known servers')
|
|
254
|
+
.action(async () => {
|
|
255
|
+
console.log(chalk.blue('Refreshing server list...'));
|
|
256
|
+
const servers = await refreshServerList((step) => {
|
|
257
|
+
console.log(chalk.gray(` ${step}`));
|
|
258
|
+
});
|
|
259
|
+
console.log(chalk.green(`\nFound ${servers.length} server(s). Running health checks...`));
|
|
260
|
+
const checks = await healthCheckServers(servers);
|
|
261
|
+
for (const c of checks) {
|
|
262
|
+
const status = c.reachable
|
|
263
|
+
? chalk.green(`reachable (${c.latencyMs}ms)`)
|
|
264
|
+
: chalk.red('unreachable');
|
|
265
|
+
console.log(` ${chalk.white(c.url)} — ${status}`);
|
|
266
|
+
}
|
|
267
|
+
console.log('');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
server
|
|
271
|
+
.command('roles <url>')
|
|
272
|
+
.description('query and display a server\'s supported roles')
|
|
273
|
+
.action(async (url) => {
|
|
274
|
+
const normalized = url.replace(/\/+$/, '');
|
|
275
|
+
try {
|
|
276
|
+
const parsed = new URL(normalized);
|
|
277
|
+
const host = parsed.hostname;
|
|
278
|
+
if (host === 'localhost' || host.startsWith('127.') || host.startsWith('10.') ||
|
|
279
|
+
host.startsWith('192.168.') || host.startsWith('169.254.') || host === '0.0.0.0' ||
|
|
280
|
+
/^172\.(1[6-9]|2\d|3[01])\./.test(host)) {
|
|
281
|
+
console.log(chalk.yellow('⚠ Warning: querying a private/local network address.'));
|
|
282
|
+
}
|
|
283
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
284
|
+
console.log(chalk.red('Server URL must use http:// or https://'));
|
|
285
|
+
process.exitCode = 1;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
} catch { /* URL validation is best-effort */ }
|
|
289
|
+
console.log(chalk.blue(`Querying roles for ${normalized}...`));
|
|
290
|
+
try {
|
|
291
|
+
const res = await fetch(`${normalized}/status`, { signal: AbortSignal.timeout(5000) });
|
|
292
|
+
if (!res.ok) {
|
|
293
|
+
console.log(chalk.red(`Server returned status ${res.status}`));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const data = await res.json();
|
|
297
|
+
console.log('');
|
|
298
|
+
console.log(chalk.cyan(`Server: ${normalized}`));
|
|
299
|
+
console.log(` Status: ${chalk.green(data.status || 'unknown')}`);
|
|
300
|
+
console.log(` Version: ${chalk.white(data.version || 'unknown')}`);
|
|
301
|
+
if (Array.isArray(data.roles)) {
|
|
302
|
+
console.log(` Roles: ${chalk.white(data.roles.join(', '))}`);
|
|
303
|
+
} else {
|
|
304
|
+
console.log(` Roles: ${chalk.yellow('not reported (legacy server, assumed all)')}`);
|
|
305
|
+
}
|
|
306
|
+
if (data.network) {
|
|
307
|
+
console.log(` Network: ${chalk.white(data.network)}`);
|
|
308
|
+
}
|
|
309
|
+
console.log('');
|
|
310
|
+
} catch (err) {
|
|
311
|
+
console.log(chalk.red(`Could not reach server: ${err.message}`));
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
server
|
|
316
|
+
.command('verify')
|
|
317
|
+
.description('verify pinned keys for all configured servers')
|
|
318
|
+
.action(async () => {
|
|
319
|
+
console.log(chalk.blue('Verifying server keys...\n'));
|
|
320
|
+
const results = await verifyAllServers();
|
|
321
|
+
|
|
322
|
+
for (const r of results) {
|
|
323
|
+
if (r.status === 'ok') {
|
|
324
|
+
console.log(` ${chalk.green('✔')} ${chalk.white(r.url)} — ${chalk.green('key verified')} (${chalk.cyan(r.fingerprint)})`);
|
|
325
|
+
} else if (r.status === 'key-changed') {
|
|
326
|
+
console.log(` ${chalk.red('✘')} ${chalk.white(r.url)} — ${chalk.red('KEY CHANGED')}`);
|
|
327
|
+
console.log(` Pinned: ${chalk.yellow(r.pinnedFingerprint)}`);
|
|
328
|
+
console.log(` Current: ${chalk.red(r.currentFingerprint)}`);
|
|
329
|
+
console.log(chalk.red(` ${r.message}`));
|
|
330
|
+
|
|
331
|
+
const ok = await confirm(chalk.white(' Accept the new key? (y/N) '));
|
|
332
|
+
if (ok) {
|
|
333
|
+
const keyResult = await fetchServerKey(r.url);
|
|
334
|
+
if (keyResult) {
|
|
335
|
+
acceptServerKey(r.url, keyResult.publicKey);
|
|
336
|
+
console.log(chalk.green(' ✔ New key accepted and pinned.'));
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
console.log(chalk.yellow(' Key NOT accepted. Consider removing this server.'));
|
|
340
|
+
}
|
|
341
|
+
} else if (r.status === 'no-pinned-key') {
|
|
342
|
+
console.log(` ${chalk.yellow('?')} ${chalk.white(r.url)} — ${chalk.yellow('no pinned key')}`);
|
|
343
|
+
console.log(chalk.gray(` ${r.message}`));
|
|
344
|
+
} else if (r.status === 'unreachable') {
|
|
345
|
+
console.log(` ${chalk.red('—')} ${chalk.white(r.url)} — ${chalk.red('unreachable')}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
console.log('');
|
|
349
|
+
});
|
|
350
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import config from '../utils/config.js';
|
|
3
|
+
import { getIdentity } from '../core/identity.js';
|
|
4
|
+
import { listShares, getShareKey } from '../core/shares.js';
|
|
5
|
+
|
|
6
|
+
const DISCOVERY_URL = process.env.PAL_DISCOVERY_URL || config.get('discoveryUrl') || 'http://localhost:3000';
|
|
7
|
+
|
|
8
|
+
export default function shareLinkCommand(program) {
|
|
9
|
+
const cmd = program
|
|
10
|
+
.command('share-link')
|
|
11
|
+
.description('create and manage web share links')
|
|
12
|
+
.addHelpText('after', `
|
|
13
|
+
Examples:
|
|
14
|
+
$ pe share-link create --magnet "magnet:..." --name "report.pdf"
|
|
15
|
+
$ pe share-link create --share abc123 Auto-fill from share ID (includes E2EE key)
|
|
16
|
+
$ pe share-link create --magnet "magnet:..." --name "report.pdf" --expires 3d --max-downloads 5
|
|
17
|
+
$ pe share-link list
|
|
18
|
+
`)
|
|
19
|
+
.action(() => {
|
|
20
|
+
cmd.outputHelp();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
cmd
|
|
24
|
+
.command('create')
|
|
25
|
+
.description('create a web-accessible share link')
|
|
26
|
+
.option('--share <id>', 'Share ID (auto-fills magnet + E2EE key)')
|
|
27
|
+
.option('--magnet <uri>', 'Magnet URI of the shared content')
|
|
28
|
+
.option('--name <name>', 'File/share name')
|
|
29
|
+
.option('--message <msg>', 'Optional message for recipients')
|
|
30
|
+
.option('--expires <duration>', 'Expiry (e.g. 1d, 7d, 30d)', '7d')
|
|
31
|
+
.option('--max-downloads <n>', 'Max download count (0 = unlimited)', '0')
|
|
32
|
+
.option('--password <pwd>', 'Optional password protection')
|
|
33
|
+
.action(async (opts) => {
|
|
34
|
+
try {
|
|
35
|
+
const identity = await getIdentity();
|
|
36
|
+
const fromHandle = identity?.handle || 'anonymous';
|
|
37
|
+
|
|
38
|
+
let magnet = opts.magnet;
|
|
39
|
+
let fileName = opts.name;
|
|
40
|
+
let shareKeyHex = null;
|
|
41
|
+
let encrypted = false;
|
|
42
|
+
|
|
43
|
+
// Auto-fill from share ID
|
|
44
|
+
if (opts.share) {
|
|
45
|
+
const shares = listShares();
|
|
46
|
+
const share = shares.find(s => s.id === opts.share);
|
|
47
|
+
if (!share) {
|
|
48
|
+
console.log(chalk.red(`Share '${opts.share}' not found. Use 'pe list' to see shares.`));
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (!share.magnet) {
|
|
53
|
+
console.log(chalk.red('Share has no magnet URI yet. Start seeding first: pe serve'));
|
|
54
|
+
process.exitCode = 1;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
magnet = share.magnet;
|
|
58
|
+
fileName = fileName || share.name;
|
|
59
|
+
if (share.visibility === 'private') {
|
|
60
|
+
shareKeyHex = await getShareKey(share.id);
|
|
61
|
+
if (shareKeyHex) {
|
|
62
|
+
encrypted = true;
|
|
63
|
+
} else {
|
|
64
|
+
console.log(chalk.yellow('Warning: Private share but no encryption key found. Link will not include decryption key.'));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!magnet) {
|
|
70
|
+
console.log(chalk.red('Either --magnet or --share is required.'));
|
|
71
|
+
process.exitCode = 1;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (!fileName) {
|
|
75
|
+
console.log(chalk.red('--name is required when not using --share.'));
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const expiresMs = parseDuration(opts.expires);
|
|
81
|
+
if (!expiresMs) {
|
|
82
|
+
console.log(chalk.red('Invalid --expires format. Use: 1d, 7d, 30d'));
|
|
83
|
+
process.exitCode = 1;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const res = await fetch(`${DISCOVERY_URL}/api/v1/share-links`, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: { 'Content-Type': 'application/json' },
|
|
90
|
+
body: JSON.stringify({
|
|
91
|
+
magnet,
|
|
92
|
+
fileName,
|
|
93
|
+
fromHandle,
|
|
94
|
+
message: opts.message || '',
|
|
95
|
+
expiresIn: expiresMs,
|
|
96
|
+
maxDownloads: parseInt(opts.maxDownloads) || 0,
|
|
97
|
+
password: opts.password || null,
|
|
98
|
+
encrypted,
|
|
99
|
+
}),
|
|
100
|
+
signal: AbortSignal.timeout(10000),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (!res.ok) {
|
|
104
|
+
const err = await res.json().catch(() => ({}));
|
|
105
|
+
console.log(chalk.red(`Failed: ${err.error || res.statusText}`));
|
|
106
|
+
process.exitCode = 1;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const data = await res.json();
|
|
111
|
+
// Append E2EE key as URL fragment (never sent to server)
|
|
112
|
+
const url = shareKeyHex ? `${data.url}#key=${shareKeyHex}` : data.url;
|
|
113
|
+
console.log(chalk.green('✔ Share link created:'));
|
|
114
|
+
console.log(` URL: ${chalk.cyan(url)}`);
|
|
115
|
+
if (encrypted) console.log(chalk.red(' ⚠ This URL contains the decryption key. Share it securely!'));
|
|
116
|
+
console.log(` ID: ${chalk.gray(data.id)}`);
|
|
117
|
+
if (data.expiresAt) console.log(` Expires: ${chalk.gray(new Date(data.expiresAt).toLocaleString())}`);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.log(chalk.red(`Error: ${err.message}`));
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
cmd
|
|
125
|
+
.command('list')
|
|
126
|
+
.description('list your active share links')
|
|
127
|
+
.action(async () => {
|
|
128
|
+
try {
|
|
129
|
+
const identity = await getIdentity();
|
|
130
|
+
if (!identity?.handle) {
|
|
131
|
+
console.log(chalk.red('You must register a handle first: pe register'));
|
|
132
|
+
process.exitCode = 1;
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const res = await fetch(`${DISCOVERY_URL}/api/v1/share-links?handle=${encodeURIComponent(identity.handle)}`, {
|
|
137
|
+
signal: AbortSignal.timeout(10000),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (!res.ok) {
|
|
141
|
+
console.log(chalk.red('Failed to fetch share links.'));
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const links = await res.json();
|
|
147
|
+
if (!Array.isArray(links) || links.length === 0) {
|
|
148
|
+
console.log(chalk.gray('No active share links.'));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log('');
|
|
153
|
+
console.log(chalk.cyan('Share Links:'));
|
|
154
|
+
for (const l of links) {
|
|
155
|
+
console.log(` ${chalk.white(l.fileName || l.id)}`);
|
|
156
|
+
console.log(` URL: ${chalk.cyan(l.url)}`);
|
|
157
|
+
if (l.expiresAt) console.log(` Expires: ${chalk.gray(new Date(l.expiresAt).toLocaleString())}`);
|
|
158
|
+
if (l.downloads !== undefined) console.log(` Downloads: ${chalk.gray(l.downloads)}`);
|
|
159
|
+
}
|
|
160
|
+
} catch (err) {
|
|
161
|
+
console.log(chalk.red(`Error: ${err.message}`));
|
|
162
|
+
process.exitCode = 1;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
cmd
|
|
166
|
+
.command('delete <id>')
|
|
167
|
+
.description('delete a share link')
|
|
168
|
+
.action(async (id) => {
|
|
169
|
+
try {
|
|
170
|
+
const identity = await getIdentity();
|
|
171
|
+
const res = await fetch(`${DISCOVERY_URL}/api/v1/share-links/${encodeURIComponent(id)}`, {
|
|
172
|
+
method: 'DELETE',
|
|
173
|
+
headers: { 'Content-Type': 'application/json' },
|
|
174
|
+
body: JSON.stringify({ handle: identity?.handle }),
|
|
175
|
+
signal: AbortSignal.timeout(10000),
|
|
176
|
+
});
|
|
177
|
+
if (!res.ok) {
|
|
178
|
+
console.log(chalk.red('Failed to delete share link.'));
|
|
179
|
+
process.exitCode = 1;
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
console.log(chalk.green('✔ Share link deleted.'));
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.log(chalk.red(`Error: ${err.message}`));
|
|
185
|
+
process.exitCode = 1;
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function parseDuration(str) {
|
|
191
|
+
const match = str.match(/^(\d+)([dhm])$/);
|
|
192
|
+
if (!match) return null;
|
|
193
|
+
const n = parseInt(match[1]);
|
|
194
|
+
const unit = match[2];
|
|
195
|
+
if (unit === 'd') return n * 86400000;
|
|
196
|
+
if (unit === 'h') return n * 3600000;
|
|
197
|
+
if (unit === 'm') return n * 60000;
|
|
198
|
+
return null;
|
|
199
|
+
}
|