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,97 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import config from '../utils/config.js';
|
|
4
|
+
import readline from 'readline';
|
|
5
|
+
|
|
6
|
+
function promptPin(prompt) {
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
9
|
+
rl.question(prompt, (answer) => {
|
|
10
|
+
rl.close();
|
|
11
|
+
resolve(answer.trim());
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function pinCommand(program) {
|
|
17
|
+
const cmd = program
|
|
18
|
+
.command('pin')
|
|
19
|
+
.description('manage PIN lock for app security')
|
|
20
|
+
.addHelpText('after', `
|
|
21
|
+
Examples:
|
|
22
|
+
$ pe pin status Show if PIN is set
|
|
23
|
+
$ pe pin set Set a new PIN
|
|
24
|
+
$ pe pin remove Remove PIN lock
|
|
25
|
+
`)
|
|
26
|
+
.action(() => {
|
|
27
|
+
showPinStatus();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
cmd
|
|
31
|
+
.command('status')
|
|
32
|
+
.description('show if PIN lock is enabled')
|
|
33
|
+
.action(() => {
|
|
34
|
+
showPinStatus();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
cmd
|
|
38
|
+
.command('set')
|
|
39
|
+
.description('set or update PIN lock')
|
|
40
|
+
.action(async () => {
|
|
41
|
+
const pin = await promptPin('Enter new PIN: ');
|
|
42
|
+
if (!pin || pin.length < 4) {
|
|
43
|
+
console.log(chalk.red('PIN must be at least 4 characters.'));
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const confirm = await promptPin('Confirm PIN: ');
|
|
48
|
+
if (pin !== confirm) {
|
|
49
|
+
console.log(chalk.red('PINs do not match.'));
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const salt = crypto.randomBytes(16).toString('hex');
|
|
54
|
+
const hash = crypto.createHash('sha256').update(salt + pin).digest('hex');
|
|
55
|
+
config.set('pinHash', JSON.stringify({ salt, hash }));
|
|
56
|
+
console.log(chalk.green('\u2714 PIN lock enabled.'));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
cmd
|
|
60
|
+
.command('remove')
|
|
61
|
+
.description('remove PIN lock')
|
|
62
|
+
.action(async () => {
|
|
63
|
+
const existing = config.get('pinHash');
|
|
64
|
+
if (!existing) {
|
|
65
|
+
console.log(chalk.gray('No PIN is set.'));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const pin = await promptPin('Enter current PIN to remove: ');
|
|
69
|
+
let match = false;
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(existing);
|
|
72
|
+
if (parsed.salt && parsed.hash) {
|
|
73
|
+
const hash = crypto.createHash('sha256').update(parsed.salt + pin).digest('hex');
|
|
74
|
+
match = hash === parsed.hash;
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
const hash = crypto.createHash('sha256').update(pin).digest('hex');
|
|
78
|
+
match = hash === existing;
|
|
79
|
+
}
|
|
80
|
+
if (!match) {
|
|
81
|
+
console.log(chalk.red('Incorrect PIN.'));
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
config.delete('pinHash');
|
|
86
|
+
console.log(chalk.green('\u2714 PIN lock removed.'));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function showPinStatus() {
|
|
91
|
+
const pinHash = config.get('pinHash');
|
|
92
|
+
if (pinHash) {
|
|
93
|
+
console.log(`PIN lock: ${chalk.green('Enabled')}`);
|
|
94
|
+
} else {
|
|
95
|
+
console.log(`PIN lock: ${chalk.gray('Not set')}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +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
|
+
$ pe protocol info Show protocol version and capabilities
|
|
15
|
+
$ pe protocol policy list List all share policies
|
|
16
|
+
$ pe protocol policy set <id> Set policy on a share
|
|
17
|
+
$ pe protocol route <peer> Probe routes to a peer
|
|
18
|
+
$ pe protocol envelope <file> Inspect a PAL envelope
|
|
19
|
+
$ pe protocol keys Show protocol key info
|
|
20
|
+
`);
|
|
21
|
+
|
|
22
|
+
// ── pe 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
|
+
// ── pe 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 `pe 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
|
+
// ── pe 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
|
+
// ── pe 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
|
+
// ── pe 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 `pe 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
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
const BUILTIN_ROLES = {
|
|
4
|
+
'org-admin': ['view', 'download', 'reshare', 'delete', 'manage', 'audit', 'assign-roles'],
|
|
5
|
+
'dept-admin': ['view', 'download', 'reshare', 'delete', 'manage'],
|
|
6
|
+
'admin': ['view', 'download', 'reshare', 'delete', 'manage'],
|
|
7
|
+
'editor': ['view', 'download', 'reshare'],
|
|
8
|
+
'viewer': ['view', 'download'],
|
|
9
|
+
'member': ['view', 'download'],
|
|
10
|
+
'restricted': ['view'],
|
|
11
|
+
'external': ['view'],
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default function rbacCommand(program) {
|
|
15
|
+
const cmd = program
|
|
16
|
+
.command('rbac')
|
|
17
|
+
.description('enterprise role-based access control (Enterprise)')
|
|
18
|
+
.addHelpText('after', `
|
|
19
|
+
Examples:
|
|
20
|
+
$ pe rbac roles List all roles and permissions
|
|
21
|
+
$ pe rbac assign <publicKey> admin Assign role to user
|
|
22
|
+
$ pe rbac assign <publicKey> dept-admin --org acme --dept engineering
|
|
23
|
+
$ pe rbac revoke <publicKey> Remove role assignment
|
|
24
|
+
$ pe rbac check <publicKey> delete Check if user has permission
|
|
25
|
+
`)
|
|
26
|
+
.action(() => { cmd.outputHelp(); });
|
|
27
|
+
|
|
28
|
+
cmd
|
|
29
|
+
.command('roles')
|
|
30
|
+
.description('list all roles with permissions')
|
|
31
|
+
.action(async () => {
|
|
32
|
+
try {
|
|
33
|
+
console.log('');
|
|
34
|
+
console.log(chalk.cyan.bold('Flat Roles'));
|
|
35
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
36
|
+
for (const role of ['admin', 'editor', 'viewer', 'restricted']) {
|
|
37
|
+
const perms = BUILTIN_ROLES[role];
|
|
38
|
+
console.log(` ${chalk.white(role)}`);
|
|
39
|
+
console.log(` ${chalk.gray(perms.join(', '))}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log('');
|
|
43
|
+
console.log(chalk.cyan.bold('Enterprise Hierarchical Roles'));
|
|
44
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
45
|
+
for (const role of ['org-admin', 'dept-admin', 'member', 'external']) {
|
|
46
|
+
const perms = BUILTIN_ROLES[role];
|
|
47
|
+
const scope = role === 'dept-admin' ? chalk.yellow(' (scoped to dept)') : '';
|
|
48
|
+
const note = role === 'external' ? chalk.yellow(' (limited)') : '';
|
|
49
|
+
console.log(` ${chalk.white(role)}${scope}${note}`);
|
|
50
|
+
console.log(` ${chalk.gray(perms.join(', '))}`);
|
|
51
|
+
}
|
|
52
|
+
console.log('');
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.error(chalk.red(`Failed to list roles: ${err.message}`));
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
cmd
|
|
60
|
+
.command('assign <publicKey> <role>')
|
|
61
|
+
.description('assign role to user')
|
|
62
|
+
.option('--org <orgId>', 'scope to organization')
|
|
63
|
+
.option('--dept <deptName>', 'scope to department')
|
|
64
|
+
.action(async (publicKey, role, opts) => {
|
|
65
|
+
try {
|
|
66
|
+
if (!BUILTIN_ROLES[role]) {
|
|
67
|
+
console.error(chalk.red(`Unknown role "${role}". Valid roles: ${Object.keys(BUILTIN_ROLES).join(', ')}`));
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const extConfig = (await import('../utils/config.js')).default;
|
|
73
|
+
const store = extConfig.get('ext_store.enterprise-rbac') || {};
|
|
74
|
+
const key = `role:${publicKey}`;
|
|
75
|
+
store[key] = {
|
|
76
|
+
role,
|
|
77
|
+
org: opts.org || null,
|
|
78
|
+
dept: opts.dept || null,
|
|
79
|
+
assignedAt: new Date().toISOString(),
|
|
80
|
+
};
|
|
81
|
+
extConfig.set('ext_store.enterprise-rbac', store);
|
|
82
|
+
|
|
83
|
+
let scope = '';
|
|
84
|
+
if (opts.org) scope += ` org=${chalk.white(opts.org)}`;
|
|
85
|
+
if (opts.dept) scope += ` dept=${chalk.white(opts.dept)}`;
|
|
86
|
+
console.log(chalk.green(`✔ Assigned role "${role}" to ${publicKey.slice(0, 16)}...${scope}`));
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.error(chalk.red(`Assign failed: ${err.message}`));
|
|
89
|
+
process.exitCode = 1;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
cmd
|
|
94
|
+
.command('revoke <publicKey>')
|
|
95
|
+
.description('remove role assignment')
|
|
96
|
+
.action(async (publicKey) => {
|
|
97
|
+
try {
|
|
98
|
+
const extConfig = (await import('../utils/config.js')).default;
|
|
99
|
+
const store = extConfig.get('ext_store.enterprise-rbac') || {};
|
|
100
|
+
const key = `role:${publicKey}`;
|
|
101
|
+
|
|
102
|
+
if (!store[key]) {
|
|
103
|
+
console.log(chalk.yellow(`No role assignment found for ${publicKey.slice(0, 16)}...`));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const oldRole = store[key].role;
|
|
108
|
+
delete store[key];
|
|
109
|
+
extConfig.set('ext_store.enterprise-rbac', store);
|
|
110
|
+
console.log(chalk.green(`✔ Revoked role "${oldRole}" from ${publicKey.slice(0, 16)}...`));
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error(chalk.red(`Revoke failed: ${err.message}`));
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
cmd
|
|
118
|
+
.command('check <publicKey> <permission>')
|
|
119
|
+
.description('check if user has specific permission')
|
|
120
|
+
.action(async (publicKey, permission) => {
|
|
121
|
+
try {
|
|
122
|
+
const extConfig = (await import('../utils/config.js')).default;
|
|
123
|
+
const store = extConfig.get('ext_store.enterprise-rbac') || {};
|
|
124
|
+
const key = `role:${publicKey}`;
|
|
125
|
+
const assignment = store[key];
|
|
126
|
+
|
|
127
|
+
if (!assignment) {
|
|
128
|
+
console.log(chalk.red(`✘ No role assigned to ${publicKey.slice(0, 16)}...`));
|
|
129
|
+
process.exitCode = 1;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const perms = BUILTIN_ROLES[assignment.role] || [];
|
|
134
|
+
const has = perms.includes(permission);
|
|
135
|
+
|
|
136
|
+
if (has) {
|
|
137
|
+
console.log(chalk.green(`✔ ${publicKey.slice(0, 16)}... HAS "${permission}" (role: ${assignment.role})`));
|
|
138
|
+
} else {
|
|
139
|
+
console.log(chalk.red(`✘ ${publicKey.slice(0, 16)}... DOES NOT have "${permission}" (role: ${assignment.role})`));
|
|
140
|
+
process.exitCode = 1;
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.error(chalk.red(`Check failed: ${err.message}`));
|
|
144
|
+
process.exitCode = 1;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { recoverIdentity } from '../core/identity.js';
|
|
4
|
+
|
|
5
|
+
export default function recoverCommand(program) {
|
|
6
|
+
program
|
|
7
|
+
.command('recover <mnemonic...>')
|
|
8
|
+
.description('recover your identity from a BIP-39 mnemonic phrase')
|
|
9
|
+
.option('-n, --name <name>', 'Name for the recovered identity')
|
|
10
|
+
.addHelpText('after', `
|
|
11
|
+
Examples:
|
|
12
|
+
$ pe recover word1 word2 ... word24 Recover from 24-word phrase
|
|
13
|
+
$ pe recover word1 word2 ... word24 -n alice Recover with name "alice"
|
|
14
|
+
`)
|
|
15
|
+
.action(async (words, opts) => {
|
|
16
|
+
const mnemonic = words.join(' ').trim();
|
|
17
|
+
const wordCount = mnemonic.split(/\s+/).length;
|
|
18
|
+
if (wordCount !== 24) {
|
|
19
|
+
console.log(chalk.red(`Expected 24 words, got ${wordCount}.`));
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const spinner = ora('Recovering identity...').start();
|
|
25
|
+
try {
|
|
26
|
+
const identity = await recoverIdentity(mnemonic, opts.name);
|
|
27
|
+
spinner.succeed(chalk.green('Identity recovered!'));
|
|
28
|
+
console.log(chalk.cyan(`Public Key: ${identity.publicKey}`));
|
|
29
|
+
console.log(chalk.gray('Re-register your handle with `pe register <handle>` to associate it with this key.'));
|
|
30
|
+
} catch (err) {
|
|
31
|
+
spinner.fail(chalk.red('Recovery failed'));
|
|
32
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|