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
package/lib/commands/protocol.js
CHANGED
|
@@ -1,357 +1,357 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import { getIdentity } from '../core/identity.js';
|
|
3
|
-
import { listShares, getShareKey } from '../core/shares.js';
|
|
4
|
-
import { getSharePolicy, setSharePolicy, listPolicies } from '../core/sharePolicy.js';
|
|
5
|
-
import { getFriends } from '../core/users.js';
|
|
6
|
-
import config from '../utils/config.js';
|
|
7
|
-
|
|
8
|
-
export default function protocolCommand(program) {
|
|
9
|
-
const proto = program
|
|
10
|
-
.command('protocol')
|
|
11
|
-
.description('pAL/1.0 protocol management')
|
|
12
|
-
.addHelpText('after', `
|
|
13
|
-
Examples:
|
|
14
|
-
$
|
|
15
|
-
$
|
|
16
|
-
$
|
|
17
|
-
$
|
|
18
|
-
$
|
|
19
|
-
$
|
|
20
|
-
`);
|
|
21
|
-
|
|
22
|
-
// ──
|
|
23
|
-
proto
|
|
24
|
-
.command('info')
|
|
25
|
-
.description('show protocol version, capabilities, and stats')
|
|
26
|
-
.action(async () => {
|
|
27
|
-
try {
|
|
28
|
-
const { PROTOCOL_VERSION, PROTOCOL_NAME } = await import('../protocol/index.js');
|
|
29
|
-
const identity = await getIdentity();
|
|
30
|
-
const { isPro } = await import('../core/pro.js');
|
|
31
|
-
const { getRelayLimits } = await import('../protocol/router.js');
|
|
32
|
-
const { FREE_POLICY_LIMITS, PRO_POLICY_LIMITS } = await import('../protocol/policy.js');
|
|
33
|
-
|
|
34
|
-
console.log('');
|
|
35
|
-
console.log(chalk.cyan.bold('PAL Protocol'));
|
|
36
|
-
console.log(` Protocol: ${chalk.white(PROTOCOL_NAME)}`);
|
|
37
|
-
console.log(` Version: ${chalk.white(PROTOCOL_VERSION)}`);
|
|
38
|
-
console.log(` Tier: ${isPro() ? chalk.green('Pro') : chalk.yellow('Free')}`);
|
|
39
|
-
|
|
40
|
-
console.log('');
|
|
41
|
-
console.log(chalk.cyan.bold('Capabilities'));
|
|
42
|
-
const caps = ['share', 'sync', 'chat', 'relay'];
|
|
43
|
-
if (isPro()) caps.push('delta-sync', 'receipts');
|
|
44
|
-
console.log(` Supported: ${caps.map(c => chalk.green(c)).join(', ')}`);
|
|
45
|
-
|
|
46
|
-
console.log('');
|
|
47
|
-
console.log(chalk.cyan.bold('Relay Limits'));
|
|
48
|
-
const limits = getRelayLimits();
|
|
49
|
-
console.log(` Bandwidth: ${limits.bandwidth ? (limits.bandwidth / 1024 / 1024) + ' MB/s' : chalk.green('Unlimited')}`);
|
|
50
|
-
console.log(` Session: ${limits.sessionDuration ? (limits.sessionDuration / 3600) + 'h' : chalk.green('Unlimited')}`);
|
|
51
|
-
console.log(` Concurrent: ${limits.concurrent}`);
|
|
52
|
-
|
|
53
|
-
console.log('');
|
|
54
|
-
console.log(chalk.cyan.bold('Policy Limits'));
|
|
55
|
-
const pl = isPro() ? PRO_POLICY_LIMITS : FREE_POLICY_LIMITS;
|
|
56
|
-
console.log(` Max expiry: ${pl.maxExpiry ? (pl.maxExpiry / 3600000) + 'h' : chalk.green('Unlimited')}`);
|
|
57
|
-
console.log(` IP restrict: ${pl.allowIPRestriction ? chalk.green('Yes') : chalk.gray('Pro only')}`);
|
|
58
|
-
console.log(` Schedule: ${pl.allowScheduleWindow ? chalk.green('Yes') : chalk.gray('Pro only')}`);
|
|
59
|
-
console.log(` Receipts: ${pl.allowReceipts ? chalk.green('Yes') : chalk.gray('Pro only')}`);
|
|
60
|
-
|
|
61
|
-
if (identity) {
|
|
62
|
-
console.log('');
|
|
63
|
-
console.log(chalk.cyan.bold('Identity'));
|
|
64
|
-
console.log(` Public Key: ${chalk.yellow(identity.publicKey?.slice(0, 32) + '...')}`);
|
|
65
|
-
console.log(` Handle: ${chalk.white(identity.handle || 'Not registered')}`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
console.log('');
|
|
69
|
-
} catch (err) {
|
|
70
|
-
console.error(chalk.red('Error:'), err.message);
|
|
71
|
-
process.exitCode = 1;
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// ──
|
|
76
|
-
const policy = proto
|
|
77
|
-
.command('policy')
|
|
78
|
-
.description('manage share policies');
|
|
79
|
-
|
|
80
|
-
policy
|
|
81
|
-
.command('list')
|
|
82
|
-
.description('list all share policies')
|
|
83
|
-
.action(async () => {
|
|
84
|
-
try {
|
|
85
|
-
const policies = listPolicies();
|
|
86
|
-
const shares = listShares();
|
|
87
|
-
const entries = Object.entries(policies);
|
|
88
|
-
|
|
89
|
-
if (entries.length === 0) {
|
|
90
|
-
console.log(chalk.gray('No policies configured. Use `
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
console.log('');
|
|
95
|
-
for (const [shareId, p] of entries) {
|
|
96
|
-
const share = shares.find(s => s.id === shareId);
|
|
97
|
-
const name = share?.name || share?.path?.split(/[/\\]/).pop() || shareId.slice(0, 8);
|
|
98
|
-
console.log(chalk.cyan.bold(`${name}`));
|
|
99
|
-
console.log(` Share ID: ${chalk.gray(shareId)}`);
|
|
100
|
-
if (p.expiresAt) {
|
|
101
|
-
const expired = new Date(p.expiresAt) < new Date();
|
|
102
|
-
console.log(` Expires: ${expired ? chalk.red(p.expiresAt) : chalk.white(p.expiresAt)}`);
|
|
103
|
-
}
|
|
104
|
-
if (p.maxDownloads != null) {
|
|
105
|
-
console.log(` Downloads: ${chalk.white(p.downloadCount || 0)} / ${chalk.white(p.maxDownloads)}`);
|
|
106
|
-
}
|
|
107
|
-
if (p.allowedIPs?.length) {
|
|
108
|
-
console.log(` Allowed IPs: ${chalk.white(p.allowedIPs.join(', '))}`);
|
|
109
|
-
}
|
|
110
|
-
if (p.scheduleWindow) {
|
|
111
|
-
console.log(` Schedule: ${chalk.white(p.scheduleWindow.start + ' - ' + p.scheduleWindow.end)}`);
|
|
112
|
-
}
|
|
113
|
-
console.log('');
|
|
114
|
-
}
|
|
115
|
-
} catch (err) {
|
|
116
|
-
console.error(chalk.red('Error:'), err.message);
|
|
117
|
-
process.exitCode = 1;
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
policy
|
|
122
|
-
.command('set <shareId>')
|
|
123
|
-
.description('set or update policy on a share')
|
|
124
|
-
.option('--expires <duration>', 'Expiry duration (e.g. 24h, 7d, 30d)')
|
|
125
|
-
.option('--max-downloads <n>', 'Maximum number of downloads', parseInt)
|
|
126
|
-
.option('--allowed-ips <ips...>', 'Restrict to specific IPs or CIDRs')
|
|
127
|
-
.option('--schedule <window>', 'Transfer window (e.g. 22:00-06:00)')
|
|
128
|
-
.option('--require-receipt', 'Require signed download receipt (Pro)')
|
|
129
|
-
.option('--no-redistribute', 'Disallow redistribution')
|
|
130
|
-
.action(async (shareId, options) => {
|
|
131
|
-
try {
|
|
132
|
-
const { validatePolicy } = await import('../protocol/policy.js');
|
|
133
|
-
const policyData = {};
|
|
134
|
-
|
|
135
|
-
if (options.expires) {
|
|
136
|
-
const ms = parseDuration(options.expires);
|
|
137
|
-
if (!ms) {
|
|
138
|
-
console.log(chalk.red('Invalid duration. Use e.g. 24h, 7d, 30d'));
|
|
139
|
-
process.exitCode = 1;
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
policyData.expiresAt = new Date(Date.now() + ms).toISOString();
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (options.maxDownloads != null) policyData.maxDownloads = options.maxDownloads;
|
|
146
|
-
if (options.allowedIps) policyData.allowedIPs = options.allowedIps;
|
|
147
|
-
if (options.schedule) {
|
|
148
|
-
const [start, end] = options.schedule.split('-');
|
|
149
|
-
if (!start || !end) {
|
|
150
|
-
console.log(chalk.red('Invalid schedule. Use format: HH:MM-HH:MM'));
|
|
151
|
-
process.exitCode = 1;
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
policyData.scheduleWindow = { start: start.trim(), end: end.trim(), timezone: 'UTC' };
|
|
155
|
-
}
|
|
156
|
-
if (options.requireReceipt) policyData.requireReceipt = true;
|
|
157
|
-
if (options.redistribute === false) policyData.allowRedistribute = false;
|
|
158
|
-
|
|
159
|
-
const validation = validatePolicy(policyData);
|
|
160
|
-
if (!validation.valid) {
|
|
161
|
-
for (const err of validation.errors) {
|
|
162
|
-
console.log(chalk.red(` ${err}`));
|
|
163
|
-
}
|
|
164
|
-
process.exitCode = 1;
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
setSharePolicy(shareId, policyData);
|
|
169
|
-
console.log(chalk.green(`Policy updated for share ${shareId.slice(0, 8)}...`));
|
|
170
|
-
|
|
171
|
-
const updated = getSharePolicy(shareId);
|
|
172
|
-
if (updated.expiresAt) console.log(` Expires: ${chalk.white(updated.expiresAt)}`);
|
|
173
|
-
if (updated.maxDownloads) console.log(` Max downloads: ${chalk.white(updated.maxDownloads)}`);
|
|
174
|
-
if (updated.allowedIPs) console.log(` Allowed IPs: ${chalk.white(updated.allowedIPs.join(', '))}`);
|
|
175
|
-
} catch (err) {
|
|
176
|
-
console.error(chalk.red('Error:'), err.message);
|
|
177
|
-
process.exitCode = 1;
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
policy
|
|
182
|
-
.command('remove <shareId>')
|
|
183
|
-
.description('remove policy from a share')
|
|
184
|
-
.action(async (shareId) => {
|
|
185
|
-
try {
|
|
186
|
-
const { removeSharePolicy } = await import('../core/sharePolicy.js');
|
|
187
|
-
removeSharePolicy(shareId);
|
|
188
|
-
console.log(chalk.green(`Policy removed for share ${shareId.slice(0, 8)}...`));
|
|
189
|
-
} catch (err) {
|
|
190
|
-
console.error(chalk.red('Error:'), err.message);
|
|
191
|
-
process.exitCode = 1;
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
// ──
|
|
196
|
-
proto
|
|
197
|
-
.command('route <peer>')
|
|
198
|
-
.description('probe connectivity routes to a peer')
|
|
199
|
-
.action(async (peer) => {
|
|
200
|
-
try {
|
|
201
|
-
const { probeRoutes, selectRoute, ROUTE_PRIORITY } = await import('../protocol/router.js');
|
|
202
|
-
|
|
203
|
-
// Resolve peer — could be handle or public key
|
|
204
|
-
let targetPK = peer;
|
|
205
|
-
if (peer.length !== 64) {
|
|
206
|
-
const friends = getFriends();
|
|
207
|
-
const pal = friends.find(f => f.name === peer || f.handle === peer);
|
|
208
|
-
if (pal) {
|
|
209
|
-
targetPK = pal.id;
|
|
210
|
-
} else {
|
|
211
|
-
console.log(chalk.red(`Peer '${peer}' not found. Use a pal name, handle, or public key.`));
|
|
212
|
-
process.exitCode = 1;
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
console.log(chalk.cyan(`Probing routes to ${peer}...`));
|
|
218
|
-
console.log('');
|
|
219
|
-
|
|
220
|
-
const results = await probeRoutes(targetPK);
|
|
221
|
-
for (const route of ROUTE_PRIORITY) {
|
|
222
|
-
const r = results[route];
|
|
223
|
-
if (!r) continue;
|
|
224
|
-
const icon = r.reachable ? chalk.green('●') : chalk.red('●');
|
|
225
|
-
const latency = r.latency != null ? chalk.gray(` (${r.latency}ms)`) : '';
|
|
226
|
-
const addr = r.address ? chalk.gray(` ${r.address}`) : '';
|
|
227
|
-
console.log(` ${icon} ${route.toUpperCase()}${addr}${latency}`);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const best = selectRoute(results);
|
|
231
|
-
console.log('');
|
|
232
|
-
if (best) {
|
|
233
|
-
console.log(` Recommended: ${chalk.green.bold(best.type.toUpperCase())}`);
|
|
234
|
-
} else {
|
|
235
|
-
console.log(chalk.red(' No reachable route found.'));
|
|
236
|
-
}
|
|
237
|
-
console.log('');
|
|
238
|
-
} catch (err) {
|
|
239
|
-
console.error(chalk.red('Error:'), err.message);
|
|
240
|
-
process.exitCode = 1;
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
// ──
|
|
245
|
-
proto
|
|
246
|
-
.command('envelope <file>')
|
|
247
|
-
.description('inspect a PAL/1.0 envelope from a JSON file')
|
|
248
|
-
.option('--verify', 'Verify signature')
|
|
249
|
-
.option('--decrypt', 'Decrypt payload (requires identity)')
|
|
250
|
-
.action(async (file, options) => {
|
|
251
|
-
try {
|
|
252
|
-
const fs = await import('fs');
|
|
253
|
-
const pathMod = await import('path');
|
|
254
|
-
const resolvedPath = pathMod.resolve(file);
|
|
255
|
-
if (!resolvedPath.endsWith('.json')) {
|
|
256
|
-
console.log(chalk.red('Only .json envelope files are supported.'));
|
|
257
|
-
process.exitCode = 1;
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
const cwd = process.cwd();
|
|
261
|
-
if (!resolvedPath.startsWith(cwd) && !resolvedPath.startsWith(pathMod.resolve(process.env.HOME || process.env.USERPROFILE || ''))) {
|
|
262
|
-
console.log(chalk.red('Path must be within working directory or home folder.'));
|
|
263
|
-
process.exitCode = 1;
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
const data = JSON.parse(fs.readFileSync(resolvedPath, 'utf8'));
|
|
267
|
-
|
|
268
|
-
console.log('');
|
|
269
|
-
console.log(chalk.cyan.bold('PAL Envelope'));
|
|
270
|
-
console.log(` Version: ${chalk.white('PAL/' + data.v)}`);
|
|
271
|
-
console.log(` Type: ${chalk.yellow(data.type)}`);
|
|
272
|
-
console.log(` From: ${chalk.white(data.from?.slice(0, 16) + '...')}`);
|
|
273
|
-
console.log(` To: ${data.to ? chalk.white(data.to.slice(0, 16) + '...') : chalk.gray('broadcast')}`);
|
|
274
|
-
console.log(` ID: ${chalk.gray(data.id)}`);
|
|
275
|
-
console.log(` Timestamp: ${chalk.white(data.ts)}`);
|
|
276
|
-
console.log(` Encrypted: ${data.payload?._enc ? chalk.yellow('Yes') : chalk.gray('No')}`);
|
|
277
|
-
|
|
278
|
-
if (options.verify) {
|
|
279
|
-
const { verify } = await import('../protocol/envelope.js');
|
|
280
|
-
const result = verify(data);
|
|
281
|
-
console.log(` Signature: ${result.valid ? chalk.green('Valid ✓') : chalk.red('Invalid ✗')}`);
|
|
282
|
-
if (!result.valid) {
|
|
283
|
-
for (const e of result.errors) console.log(` ${chalk.red(e)}`);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (options.decrypt && data.payload?._enc) {
|
|
288
|
-
const { decrypt } = await import('../protocol/envelope.js');
|
|
289
|
-
const identity = await getIdentity();
|
|
290
|
-
if (!identity?.privateKey) {
|
|
291
|
-
console.log(chalk.red(' Cannot decrypt: no identity found'));
|
|
292
|
-
} else {
|
|
293
|
-
try {
|
|
294
|
-
const keyPair = {
|
|
295
|
-
publicKey: Buffer.from(identity.publicKey, 'hex'),
|
|
296
|
-
privateKey: Buffer.from(identity.privateKey, 'hex'),
|
|
297
|
-
};
|
|
298
|
-
const payload = decrypt(data, keyPair);
|
|
299
|
-
console.log('');
|
|
300
|
-
console.log(chalk.cyan.bold('Decrypted Payload'));
|
|
301
|
-
console.log(JSON.stringify(payload, null, 2));
|
|
302
|
-
} catch (e) {
|
|
303
|
-
console.log(chalk.red(` Decryption failed: ${e.message}`));
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (!data.payload?._enc) {
|
|
309
|
-
console.log('');
|
|
310
|
-
console.log(chalk.cyan.bold('Payload'));
|
|
311
|
-
console.log(JSON.stringify(data.payload, null, 2));
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
console.log('');
|
|
315
|
-
} catch (err) {
|
|
316
|
-
console.error(chalk.red('Error:'), err.message);
|
|
317
|
-
process.exitCode = 1;
|
|
318
|
-
}
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
// ──
|
|
322
|
-
proto
|
|
323
|
-
.command('keys')
|
|
324
|
-
.description('show protocol key info')
|
|
325
|
-
.action(async () => {
|
|
326
|
-
try {
|
|
327
|
-
const identity = await getIdentity();
|
|
328
|
-
if (!identity) {
|
|
329
|
-
console.log(chalk.gray('No identity. Run `
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const sodium = (await import('sodium-native')).default;
|
|
334
|
-
const edPK = Buffer.from(identity.publicKey, 'hex');
|
|
335
|
-
const curve25519PK = Buffer.alloc(sodium.crypto_box_PUBLICKEYBYTES);
|
|
336
|
-
sodium.crypto_sign_ed25519_pk_to_curve25519(curve25519PK, edPK);
|
|
337
|
-
|
|
338
|
-
console.log('');
|
|
339
|
-
console.log(chalk.cyan.bold('Protocol Keys'));
|
|
340
|
-
console.log(` Ed25519 PK (signing): ${chalk.yellow(identity.publicKey)}`);
|
|
341
|
-
console.log(` Curve25519 PK (encrypt): ${chalk.yellow(curve25519PK.toString('hex'))}`);
|
|
342
|
-
console.log(` Private key: ${chalk.gray('stored in OS credential manager')}`);
|
|
343
|
-
console.log('');
|
|
344
|
-
} catch (err) {
|
|
345
|
-
console.error(chalk.red('Error:'), err.message);
|
|
346
|
-
process.exitCode = 1;
|
|
347
|
-
}
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function parseDuration(s) {
|
|
352
|
-
const m = s.match(/^(\d+)(h|d|m|w)$/);
|
|
353
|
-
if (!m) return null;
|
|
354
|
-
const n = parseInt(m[1]);
|
|
355
|
-
const unit = { m: 60000, h: 3600000, d: 86400000, w: 604800000 }[m[2]];
|
|
356
|
-
return n * unit;
|
|
357
|
-
}
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getIdentity } from '../core/identity.js';
|
|
3
|
+
import { listShares, getShareKey } from '../core/shares.js';
|
|
4
|
+
import { getSharePolicy, setSharePolicy, listPolicies } from '../core/sharePolicy.js';
|
|
5
|
+
import { getFriends } from '../core/users.js';
|
|
6
|
+
import config from '../utils/config.js';
|
|
7
|
+
|
|
8
|
+
export default function protocolCommand(program) {
|
|
9
|
+
const proto = program
|
|
10
|
+
.command('protocol')
|
|
11
|
+
.description('pAL/1.0 protocol management')
|
|
12
|
+
.addHelpText('after', `
|
|
13
|
+
Examples:
|
|
14
|
+
$ pal protocol info Show protocol version and capabilities
|
|
15
|
+
$ pal protocol policy list List all share policies
|
|
16
|
+
$ pal protocol policy set <id> Set policy on a share
|
|
17
|
+
$ pal protocol route <peer> Probe routes to a peer
|
|
18
|
+
$ pal protocol envelope <file> Inspect a PAL envelope
|
|
19
|
+
$ pal protocol keys Show protocol key info
|
|
20
|
+
`);
|
|
21
|
+
|
|
22
|
+
// ── pal protocol info ──
|
|
23
|
+
proto
|
|
24
|
+
.command('info')
|
|
25
|
+
.description('show protocol version, capabilities, and stats')
|
|
26
|
+
.action(async () => {
|
|
27
|
+
try {
|
|
28
|
+
const { PROTOCOL_VERSION, PROTOCOL_NAME } = await import('../protocol/index.js');
|
|
29
|
+
const identity = await getIdentity();
|
|
30
|
+
const { isPro } = await import('../core/pro.js');
|
|
31
|
+
const { getRelayLimits } = await import('../protocol/router.js');
|
|
32
|
+
const { FREE_POLICY_LIMITS, PRO_POLICY_LIMITS } = await import('../protocol/policy.js');
|
|
33
|
+
|
|
34
|
+
console.log('');
|
|
35
|
+
console.log(chalk.cyan.bold('PAL Protocol'));
|
|
36
|
+
console.log(` Protocol: ${chalk.white(PROTOCOL_NAME)}`);
|
|
37
|
+
console.log(` Version: ${chalk.white(PROTOCOL_VERSION)}`);
|
|
38
|
+
console.log(` Tier: ${isPro() ? chalk.green('Pro') : chalk.yellow('Free')}`);
|
|
39
|
+
|
|
40
|
+
console.log('');
|
|
41
|
+
console.log(chalk.cyan.bold('Capabilities'));
|
|
42
|
+
const caps = ['share', 'sync', 'chat', 'relay'];
|
|
43
|
+
if (isPro()) caps.push('delta-sync', 'receipts');
|
|
44
|
+
console.log(` Supported: ${caps.map(c => chalk.green(c)).join(', ')}`);
|
|
45
|
+
|
|
46
|
+
console.log('');
|
|
47
|
+
console.log(chalk.cyan.bold('Relay Limits'));
|
|
48
|
+
const limits = getRelayLimits();
|
|
49
|
+
console.log(` Bandwidth: ${limits.bandwidth ? (limits.bandwidth / 1024 / 1024) + ' MB/s' : chalk.green('Unlimited')}`);
|
|
50
|
+
console.log(` Session: ${limits.sessionDuration ? (limits.sessionDuration / 3600) + 'h' : chalk.green('Unlimited')}`);
|
|
51
|
+
console.log(` Concurrent: ${limits.concurrent}`);
|
|
52
|
+
|
|
53
|
+
console.log('');
|
|
54
|
+
console.log(chalk.cyan.bold('Policy Limits'));
|
|
55
|
+
const pl = isPro() ? PRO_POLICY_LIMITS : FREE_POLICY_LIMITS;
|
|
56
|
+
console.log(` Max expiry: ${pl.maxExpiry ? (pl.maxExpiry / 3600000) + 'h' : chalk.green('Unlimited')}`);
|
|
57
|
+
console.log(` IP restrict: ${pl.allowIPRestriction ? chalk.green('Yes') : chalk.gray('Pro only')}`);
|
|
58
|
+
console.log(` Schedule: ${pl.allowScheduleWindow ? chalk.green('Yes') : chalk.gray('Pro only')}`);
|
|
59
|
+
console.log(` Receipts: ${pl.allowReceipts ? chalk.green('Yes') : chalk.gray('Pro only')}`);
|
|
60
|
+
|
|
61
|
+
if (identity) {
|
|
62
|
+
console.log('');
|
|
63
|
+
console.log(chalk.cyan.bold('Identity'));
|
|
64
|
+
console.log(` Public Key: ${chalk.yellow(identity.publicKey?.slice(0, 32) + '...')}`);
|
|
65
|
+
console.log(` Handle: ${chalk.white(identity.handle || 'Not registered')}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log('');
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error(chalk.red('Error:'), err.message);
|
|
71
|
+
process.exitCode = 1;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ── pal protocol policy ──
|
|
76
|
+
const policy = proto
|
|
77
|
+
.command('policy')
|
|
78
|
+
.description('manage share policies');
|
|
79
|
+
|
|
80
|
+
policy
|
|
81
|
+
.command('list')
|
|
82
|
+
.description('list all share policies')
|
|
83
|
+
.action(async () => {
|
|
84
|
+
try {
|
|
85
|
+
const policies = listPolicies();
|
|
86
|
+
const shares = listShares();
|
|
87
|
+
const entries = Object.entries(policies);
|
|
88
|
+
|
|
89
|
+
if (entries.length === 0) {
|
|
90
|
+
console.log(chalk.gray('No policies configured. Use `pal protocol policy set <shareId>` to add one.'));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log('');
|
|
95
|
+
for (const [shareId, p] of entries) {
|
|
96
|
+
const share = shares.find(s => s.id === shareId);
|
|
97
|
+
const name = share?.name || share?.path?.split(/[/\\]/).pop() || shareId.slice(0, 8);
|
|
98
|
+
console.log(chalk.cyan.bold(`${name}`));
|
|
99
|
+
console.log(` Share ID: ${chalk.gray(shareId)}`);
|
|
100
|
+
if (p.expiresAt) {
|
|
101
|
+
const expired = new Date(p.expiresAt) < new Date();
|
|
102
|
+
console.log(` Expires: ${expired ? chalk.red(p.expiresAt) : chalk.white(p.expiresAt)}`);
|
|
103
|
+
}
|
|
104
|
+
if (p.maxDownloads != null) {
|
|
105
|
+
console.log(` Downloads: ${chalk.white(p.downloadCount || 0)} / ${chalk.white(p.maxDownloads)}`);
|
|
106
|
+
}
|
|
107
|
+
if (p.allowedIPs?.length) {
|
|
108
|
+
console.log(` Allowed IPs: ${chalk.white(p.allowedIPs.join(', '))}`);
|
|
109
|
+
}
|
|
110
|
+
if (p.scheduleWindow) {
|
|
111
|
+
console.log(` Schedule: ${chalk.white(p.scheduleWindow.start + ' - ' + p.scheduleWindow.end)}`);
|
|
112
|
+
}
|
|
113
|
+
console.log('');
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
console.error(chalk.red('Error:'), err.message);
|
|
117
|
+
process.exitCode = 1;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
policy
|
|
122
|
+
.command('set <shareId>')
|
|
123
|
+
.description('set or update policy on a share')
|
|
124
|
+
.option('--expires <duration>', 'Expiry duration (e.g. 24h, 7d, 30d)')
|
|
125
|
+
.option('--max-downloads <n>', 'Maximum number of downloads', parseInt)
|
|
126
|
+
.option('--allowed-ips <ips...>', 'Restrict to specific IPs or CIDRs')
|
|
127
|
+
.option('--schedule <window>', 'Transfer window (e.g. 22:00-06:00)')
|
|
128
|
+
.option('--require-receipt', 'Require signed download receipt (Pro)')
|
|
129
|
+
.option('--no-redistribute', 'Disallow redistribution')
|
|
130
|
+
.action(async (shareId, options) => {
|
|
131
|
+
try {
|
|
132
|
+
const { validatePolicy } = await import('../protocol/policy.js');
|
|
133
|
+
const policyData = {};
|
|
134
|
+
|
|
135
|
+
if (options.expires) {
|
|
136
|
+
const ms = parseDuration(options.expires);
|
|
137
|
+
if (!ms) {
|
|
138
|
+
console.log(chalk.red('Invalid duration. Use e.g. 24h, 7d, 30d'));
|
|
139
|
+
process.exitCode = 1;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
policyData.expiresAt = new Date(Date.now() + ms).toISOString();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (options.maxDownloads != null) policyData.maxDownloads = options.maxDownloads;
|
|
146
|
+
if (options.allowedIps) policyData.allowedIPs = options.allowedIps;
|
|
147
|
+
if (options.schedule) {
|
|
148
|
+
const [start, end] = options.schedule.split('-');
|
|
149
|
+
if (!start || !end) {
|
|
150
|
+
console.log(chalk.red('Invalid schedule. Use format: HH:MM-HH:MM'));
|
|
151
|
+
process.exitCode = 1;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
policyData.scheduleWindow = { start: start.trim(), end: end.trim(), timezone: 'UTC' };
|
|
155
|
+
}
|
|
156
|
+
if (options.requireReceipt) policyData.requireReceipt = true;
|
|
157
|
+
if (options.redistribute === false) policyData.allowRedistribute = false;
|
|
158
|
+
|
|
159
|
+
const validation = validatePolicy(policyData);
|
|
160
|
+
if (!validation.valid) {
|
|
161
|
+
for (const err of validation.errors) {
|
|
162
|
+
console.log(chalk.red(` ${err}`));
|
|
163
|
+
}
|
|
164
|
+
process.exitCode = 1;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
setSharePolicy(shareId, policyData);
|
|
169
|
+
console.log(chalk.green(`Policy updated for share ${shareId.slice(0, 8)}...`));
|
|
170
|
+
|
|
171
|
+
const updated = getSharePolicy(shareId);
|
|
172
|
+
if (updated.expiresAt) console.log(` Expires: ${chalk.white(updated.expiresAt)}`);
|
|
173
|
+
if (updated.maxDownloads) console.log(` Max downloads: ${chalk.white(updated.maxDownloads)}`);
|
|
174
|
+
if (updated.allowedIPs) console.log(` Allowed IPs: ${chalk.white(updated.allowedIPs.join(', '))}`);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
console.error(chalk.red('Error:'), err.message);
|
|
177
|
+
process.exitCode = 1;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
policy
|
|
182
|
+
.command('remove <shareId>')
|
|
183
|
+
.description('remove policy from a share')
|
|
184
|
+
.action(async (shareId) => {
|
|
185
|
+
try {
|
|
186
|
+
const { removeSharePolicy } = await import('../core/sharePolicy.js');
|
|
187
|
+
removeSharePolicy(shareId);
|
|
188
|
+
console.log(chalk.green(`Policy removed for share ${shareId.slice(0, 8)}...`));
|
|
189
|
+
} catch (err) {
|
|
190
|
+
console.error(chalk.red('Error:'), err.message);
|
|
191
|
+
process.exitCode = 1;
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// ── pal protocol route ──
|
|
196
|
+
proto
|
|
197
|
+
.command('route <peer>')
|
|
198
|
+
.description('probe connectivity routes to a peer')
|
|
199
|
+
.action(async (peer) => {
|
|
200
|
+
try {
|
|
201
|
+
const { probeRoutes, selectRoute, ROUTE_PRIORITY } = await import('../protocol/router.js');
|
|
202
|
+
|
|
203
|
+
// Resolve peer — could be handle or public key
|
|
204
|
+
let targetPK = peer;
|
|
205
|
+
if (peer.length !== 64) {
|
|
206
|
+
const friends = getFriends();
|
|
207
|
+
const pal = friends.find(f => f.name === peer || f.handle === peer);
|
|
208
|
+
if (pal) {
|
|
209
|
+
targetPK = pal.id;
|
|
210
|
+
} else {
|
|
211
|
+
console.log(chalk.red(`Peer '${peer}' not found. Use a pal name, handle, or public key.`));
|
|
212
|
+
process.exitCode = 1;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
console.log(chalk.cyan(`Probing routes to ${peer}...`));
|
|
218
|
+
console.log('');
|
|
219
|
+
|
|
220
|
+
const results = await probeRoutes(targetPK);
|
|
221
|
+
for (const route of ROUTE_PRIORITY) {
|
|
222
|
+
const r = results[route];
|
|
223
|
+
if (!r) continue;
|
|
224
|
+
const icon = r.reachable ? chalk.green('●') : chalk.red('●');
|
|
225
|
+
const latency = r.latency != null ? chalk.gray(` (${r.latency}ms)`) : '';
|
|
226
|
+
const addr = r.address ? chalk.gray(` ${r.address}`) : '';
|
|
227
|
+
console.log(` ${icon} ${route.toUpperCase()}${addr}${latency}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const best = selectRoute(results);
|
|
231
|
+
console.log('');
|
|
232
|
+
if (best) {
|
|
233
|
+
console.log(` Recommended: ${chalk.green.bold(best.type.toUpperCase())}`);
|
|
234
|
+
} else {
|
|
235
|
+
console.log(chalk.red(' No reachable route found.'));
|
|
236
|
+
}
|
|
237
|
+
console.log('');
|
|
238
|
+
} catch (err) {
|
|
239
|
+
console.error(chalk.red('Error:'), err.message);
|
|
240
|
+
process.exitCode = 1;
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// ── pal protocol envelope ──
|
|
245
|
+
proto
|
|
246
|
+
.command('envelope <file>')
|
|
247
|
+
.description('inspect a PAL/1.0 envelope from a JSON file')
|
|
248
|
+
.option('--verify', 'Verify signature')
|
|
249
|
+
.option('--decrypt', 'Decrypt payload (requires identity)')
|
|
250
|
+
.action(async (file, options) => {
|
|
251
|
+
try {
|
|
252
|
+
const fs = await import('fs');
|
|
253
|
+
const pathMod = await import('path');
|
|
254
|
+
const resolvedPath = pathMod.resolve(file);
|
|
255
|
+
if (!resolvedPath.endsWith('.json')) {
|
|
256
|
+
console.log(chalk.red('Only .json envelope files are supported.'));
|
|
257
|
+
process.exitCode = 1;
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const cwd = process.cwd();
|
|
261
|
+
if (!resolvedPath.startsWith(cwd) && !resolvedPath.startsWith(pathMod.resolve(process.env.HOME || process.env.USERPROFILE || ''))) {
|
|
262
|
+
console.log(chalk.red('Path must be within working directory or home folder.'));
|
|
263
|
+
process.exitCode = 1;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const data = JSON.parse(fs.readFileSync(resolvedPath, 'utf8'));
|
|
267
|
+
|
|
268
|
+
console.log('');
|
|
269
|
+
console.log(chalk.cyan.bold('PAL Envelope'));
|
|
270
|
+
console.log(` Version: ${chalk.white('PAL/' + data.v)}`);
|
|
271
|
+
console.log(` Type: ${chalk.yellow(data.type)}`);
|
|
272
|
+
console.log(` From: ${chalk.white(data.from?.slice(0, 16) + '...')}`);
|
|
273
|
+
console.log(` To: ${data.to ? chalk.white(data.to.slice(0, 16) + '...') : chalk.gray('broadcast')}`);
|
|
274
|
+
console.log(` ID: ${chalk.gray(data.id)}`);
|
|
275
|
+
console.log(` Timestamp: ${chalk.white(data.ts)}`);
|
|
276
|
+
console.log(` Encrypted: ${data.payload?._enc ? chalk.yellow('Yes') : chalk.gray('No')}`);
|
|
277
|
+
|
|
278
|
+
if (options.verify) {
|
|
279
|
+
const { verify } = await import('../protocol/envelope.js');
|
|
280
|
+
const result = verify(data);
|
|
281
|
+
console.log(` Signature: ${result.valid ? chalk.green('Valid ✓') : chalk.red('Invalid ✗')}`);
|
|
282
|
+
if (!result.valid) {
|
|
283
|
+
for (const e of result.errors) console.log(` ${chalk.red(e)}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (options.decrypt && data.payload?._enc) {
|
|
288
|
+
const { decrypt } = await import('../protocol/envelope.js');
|
|
289
|
+
const identity = await getIdentity();
|
|
290
|
+
if (!identity?.privateKey) {
|
|
291
|
+
console.log(chalk.red(' Cannot decrypt: no identity found'));
|
|
292
|
+
} else {
|
|
293
|
+
try {
|
|
294
|
+
const keyPair = {
|
|
295
|
+
publicKey: Buffer.from(identity.publicKey, 'hex'),
|
|
296
|
+
privateKey: Buffer.from(identity.privateKey, 'hex'),
|
|
297
|
+
};
|
|
298
|
+
const payload = decrypt(data, keyPair);
|
|
299
|
+
console.log('');
|
|
300
|
+
console.log(chalk.cyan.bold('Decrypted Payload'));
|
|
301
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
302
|
+
} catch (e) {
|
|
303
|
+
console.log(chalk.red(` Decryption failed: ${e.message}`));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (!data.payload?._enc) {
|
|
309
|
+
console.log('');
|
|
310
|
+
console.log(chalk.cyan.bold('Payload'));
|
|
311
|
+
console.log(JSON.stringify(data.payload, null, 2));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
console.log('');
|
|
315
|
+
} catch (err) {
|
|
316
|
+
console.error(chalk.red('Error:'), err.message);
|
|
317
|
+
process.exitCode = 1;
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// ── pal protocol keys ──
|
|
322
|
+
proto
|
|
323
|
+
.command('keys')
|
|
324
|
+
.description('show protocol key info')
|
|
325
|
+
.action(async () => {
|
|
326
|
+
try {
|
|
327
|
+
const identity = await getIdentity();
|
|
328
|
+
if (!identity) {
|
|
329
|
+
console.log(chalk.gray('No identity. Run `pal init <name>` first.'));
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const sodium = (await import('sodium-native')).default;
|
|
334
|
+
const edPK = Buffer.from(identity.publicKey, 'hex');
|
|
335
|
+
const curve25519PK = Buffer.alloc(sodium.crypto_box_PUBLICKEYBYTES);
|
|
336
|
+
sodium.crypto_sign_ed25519_pk_to_curve25519(curve25519PK, edPK);
|
|
337
|
+
|
|
338
|
+
console.log('');
|
|
339
|
+
console.log(chalk.cyan.bold('Protocol Keys'));
|
|
340
|
+
console.log(` Ed25519 PK (signing): ${chalk.yellow(identity.publicKey)}`);
|
|
341
|
+
console.log(` Curve25519 PK (encrypt): ${chalk.yellow(curve25519PK.toString('hex'))}`);
|
|
342
|
+
console.log(` Private key: ${chalk.gray('stored in OS credential manager')}`);
|
|
343
|
+
console.log('');
|
|
344
|
+
} catch (err) {
|
|
345
|
+
console.error(chalk.red('Error:'), err.message);
|
|
346
|
+
process.exitCode = 1;
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function parseDuration(s) {
|
|
352
|
+
const m = s.match(/^(\d+)(h|d|m|w)$/);
|
|
353
|
+
if (!m) return null;
|
|
354
|
+
const n = parseInt(m[1]);
|
|
355
|
+
const unit = { m: 60000, h: 3600000, d: 86400000, w: 604800000 }[m[2]];
|
|
356
|
+
return n * unit;
|
|
357
|
+
}
|