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,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "discovery",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Discover servers via DHT — adds @handles, online presence, and server-assisted peer finding",
|
|
5
|
+
"author": "Palexplorer Team",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"hooks": ["on:app:ready"],
|
|
9
|
+
"permissions": ["config:read", "config:write", "identity:read"],
|
|
10
|
+
"config": {
|
|
11
|
+
"refreshInterval": { "type": "number", "default": 1800000, "description": "Server list refresh interval in ms (default: 30 min)" },
|
|
12
|
+
"enableGossip": { "type": "boolean", "default": true, "description": "Exchange server lists with connected peers" }
|
|
13
|
+
},
|
|
14
|
+
"pro": false,
|
|
15
|
+
"minAppVersion": "0.4.0"
|
|
16
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import sodium from 'sodium-native';
|
|
3
|
+
|
|
4
|
+
// Well-known Ed25519 public key for verifying the server list.
|
|
5
|
+
// The server list is published to DHT (BEP44 mutable) signed with the corresponding private key.
|
|
6
|
+
// This is the ONLY constant — no hardcoded server URLs.
|
|
7
|
+
const SERVER_LIST_PUBLIC_KEY = '39832cac9958f83e15c31d2c93ae2fd7338417bd74aa8e7bb0dbe3c45e857560';
|
|
8
|
+
const SERVER_LIST_SALT = 'pal:servers';
|
|
9
|
+
|
|
10
|
+
let ctx = null;
|
|
11
|
+
let refreshTimer = null;
|
|
12
|
+
let dhtInstance = null;
|
|
13
|
+
|
|
14
|
+
export async function activate(context) {
|
|
15
|
+
ctx = context;
|
|
16
|
+
|
|
17
|
+
const firstRun = !ctx.config.get('initialized');
|
|
18
|
+
if (firstRun) {
|
|
19
|
+
ctx.config.set('initialized', true);
|
|
20
|
+
ctx.logger.info('Discovery extension enabled. Fetching server list from DHT...');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
await fetchServerList();
|
|
24
|
+
|
|
25
|
+
const interval = ctx.config.get('refreshInterval') || 1800000;
|
|
26
|
+
refreshTimer = setInterval(() => fetchServerList(), interval);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function deactivate() {
|
|
30
|
+
if (refreshTimer) {
|
|
31
|
+
clearInterval(refreshTimer);
|
|
32
|
+
refreshTimer = null;
|
|
33
|
+
}
|
|
34
|
+
if (dhtInstance) {
|
|
35
|
+
try { dhtInstance.destroy(); } catch {}
|
|
36
|
+
dhtInstance = null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function getDHT() {
|
|
41
|
+
if (dhtInstance) return dhtInstance;
|
|
42
|
+
const { DHTDiscovery } = await import('../../../lib/core/dhtDiscovery.js');
|
|
43
|
+
dhtInstance = new DHTDiscovery();
|
|
44
|
+
await dhtInstance.ready;
|
|
45
|
+
return dhtInstance;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function fetchServerList() {
|
|
49
|
+
try {
|
|
50
|
+
const dht = await getDHT();
|
|
51
|
+
const pk = Buffer.from(SERVER_LIST_PUBLIC_KEY, 'hex');
|
|
52
|
+
const salt = Buffer.from(SERVER_LIST_SALT);
|
|
53
|
+
const target = crypto.createHash('sha1').update(Buffer.concat([pk, salt])).digest();
|
|
54
|
+
|
|
55
|
+
const result = await new Promise((resolve, reject) => {
|
|
56
|
+
const timeout = setTimeout(() => resolve(null), 15000);
|
|
57
|
+
dht.dht.get(target, (err, res) => {
|
|
58
|
+
clearTimeout(timeout);
|
|
59
|
+
if (err) return resolve(null);
|
|
60
|
+
resolve(res);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (!result || !result.v) {
|
|
65
|
+
ctx.logger.warn('No server list found in DHT (not published yet?)');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const data = JSON.parse(result.v.toString());
|
|
70
|
+
if (!data.servers || !Array.isArray(data.servers)) {
|
|
71
|
+
ctx.logger.warn('Invalid server list format from DHT');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Signature is verified by the DHT layer (bittorrent-dht verify callback)
|
|
76
|
+
const { addBootstrapServer } = await import('../../../lib/core/serverList.js');
|
|
77
|
+
let added = 0;
|
|
78
|
+
for (const entry of data.servers) {
|
|
79
|
+
const url = typeof entry === 'string' ? entry : entry?.url;
|
|
80
|
+
if (url) {
|
|
81
|
+
addBootstrapServer(url);
|
|
82
|
+
added++;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (added > 0) {
|
|
87
|
+
ctx.logger.info(`Discovered ${added} server(s) via DHT`);
|
|
88
|
+
}
|
|
89
|
+
} catch (err) {
|
|
90
|
+
ctx.logger.warn(`DHT server list fetch failed: ${err.message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Called by the gossip protocol when a peer shares their server list
|
|
95
|
+
export async function onGossipServers(servers) {
|
|
96
|
+
if (!ctx || !Array.isArray(servers)) return;
|
|
97
|
+
const enableGossip = ctx.config.get('enableGossip') !== false;
|
|
98
|
+
if (!enableGossip) return;
|
|
99
|
+
|
|
100
|
+
const { addBootstrapServer } = await import('../../../lib/core/serverList.js');
|
|
101
|
+
for (const url of servers) {
|
|
102
|
+
if (typeof url === 'string' && url.startsWith('https://')) {
|
|
103
|
+
addBootstrapServer(url);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Export for pe server list to show source
|
|
109
|
+
export function getPublicKey() {
|
|
110
|
+
return SERVER_LIST_PUBLIC_KEY;
|
|
111
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "email-notifications",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Email notifications for share invites, transfers, peer activity, and security alerts via SMTP or API providers",
|
|
5
|
+
"author": "Palexplorer Team",
|
|
6
|
+
"license": "Proprietary",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"hooks": ["after:share:create", "after:download:complete", "on:peer:connect", "on:peer:disconnect", "on:auth:login"],
|
|
9
|
+
"permissions": ["config:read", "config:write", "net:http", "notifications", "identity:read"],
|
|
10
|
+
"config": {
|
|
11
|
+
"provider": { "type": "string", "default": "smtp", "description": "Email provider: smtp, sendgrid, mailgun, or resend" },
|
|
12
|
+
"smtpHost": { "type": "string", "default": "", "description": "SMTP server hostname" },
|
|
13
|
+
"smtpPort": { "type": "number", "default": 587, "description": "SMTP server port" },
|
|
14
|
+
"smtpUser": { "type": "string", "default": "", "description": "SMTP username" },
|
|
15
|
+
"smtpPass": { "type": "string", "default": "", "description": "SMTP password" },
|
|
16
|
+
"apiKey": { "type": "string", "default": "", "description": "API key for SendGrid/Mailgun/Resend" },
|
|
17
|
+
"fromAddress": { "type": "string", "default": "noreply@palexplorer.com", "description": "Sender email address" },
|
|
18
|
+
"toAddress": { "type": "string", "default": "", "description": "Recipient email address" },
|
|
19
|
+
"events": { "type": "array", "default": ["share:invite", "transfer:complete", "security:alert"], "description": "Enabled event types" }
|
|
20
|
+
},
|
|
21
|
+
"tier": "free",
|
|
22
|
+
"minAppVersion": "0.5.0"
|
|
23
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { createConnection } from 'node:net';
|
|
2
|
+
import { connect as tlsConnect } from 'node:tls';
|
|
3
|
+
|
|
4
|
+
const EVENT_TEMPLATES = {
|
|
5
|
+
'share:invite': {
|
|
6
|
+
subject: 'New Share Invite — Palexplorer',
|
|
7
|
+
body: (d) => `You received a share invite: "${d.name}" from ${d.from || 'a peer'}.`,
|
|
8
|
+
},
|
|
9
|
+
'transfer:complete': {
|
|
10
|
+
subject: 'Transfer Complete — Palexplorer',
|
|
11
|
+
body: (d) => `Transfer "${d.name}" completed (${formatSize(d.size)}).`,
|
|
12
|
+
},
|
|
13
|
+
'peer:online': {
|
|
14
|
+
subject: 'Peer Online — Palexplorer',
|
|
15
|
+
body: (d) => `Your pal ${d.peerName || d.peerId?.slice(0, 12)} came online.`,
|
|
16
|
+
},
|
|
17
|
+
'peer:offline': {
|
|
18
|
+
subject: 'Peer Offline — Palexplorer',
|
|
19
|
+
body: (d) => `Your pal ${d.peerName || d.peerId?.slice(0, 12)} went offline.`,
|
|
20
|
+
},
|
|
21
|
+
'share:expiring': {
|
|
22
|
+
subject: 'Share Expiring Soon — Palexplorer',
|
|
23
|
+
body: (d) => `Share "${d.name}" expires in ${d.hoursLeft} hours.`,
|
|
24
|
+
},
|
|
25
|
+
'security:alert': {
|
|
26
|
+
subject: 'Security Alert — Palexplorer',
|
|
27
|
+
body: (d) => `Security alert: ${d.reason || 'Unknown event'}.`,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
let ctx = null;
|
|
32
|
+
|
|
33
|
+
export function activate(context) {
|
|
34
|
+
ctx = context;
|
|
35
|
+
const { hooks, config, logger } = context;
|
|
36
|
+
|
|
37
|
+
const toAddress = config.get('toAddress');
|
|
38
|
+
if (!toAddress) {
|
|
39
|
+
logger.warn('Email notifications enabled but no recipient address configured');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
context.emailNotify = {
|
|
43
|
+
async send(eventType, data) {
|
|
44
|
+
return sendNotification(eventType, data, context);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
async testConnection() {
|
|
48
|
+
return testProvider(context);
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
hooks.on('after:share:create', async (details) => {
|
|
53
|
+
if (isEnabled('share:invite', config)) {
|
|
54
|
+
await sendNotification('share:invite', details, context).catch(err => {
|
|
55
|
+
logger.warn(`Email notification failed: ${err.message}`);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
hooks.on('after:download:complete', async (details) => {
|
|
61
|
+
if (isEnabled('transfer:complete', config)) {
|
|
62
|
+
await sendNotification('transfer:complete', details, context).catch(err => {
|
|
63
|
+
logger.warn(`Email notification failed: ${err.message}`);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
hooks.on('on:peer:connect', async (details) => {
|
|
69
|
+
if (isEnabled('peer:online', config)) {
|
|
70
|
+
await sendNotification('peer:online', details, context).catch(err => {
|
|
71
|
+
logger.warn(`Email notification failed: ${err.message}`);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
hooks.on('on:peer:disconnect', async (details) => {
|
|
77
|
+
if (isEnabled('peer:offline', config)) {
|
|
78
|
+
await sendNotification('peer:offline', details, context).catch(err => {
|
|
79
|
+
logger.warn(`Email notification failed: ${err.message}`);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
hooks.on('on:auth:login', async (details) => {
|
|
85
|
+
if (details.failed && isEnabled('security:alert', config)) {
|
|
86
|
+
await sendNotification('security:alert', { reason: `Failed login attempt from ${details.ip || 'unknown'}` }, context).catch(() => {});
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
logger.info('Email notifications enabled', { provider: config.get('provider') });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function deactivate() {
|
|
94
|
+
ctx = null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isEnabled(eventType, config) {
|
|
98
|
+
const events = config.get('events') || [];
|
|
99
|
+
return events.includes(eventType);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function sendNotification(eventType, data, ctx) {
|
|
103
|
+
const template = EVENT_TEMPLATES[eventType];
|
|
104
|
+
if (!template) throw new Error(`Unknown event type: ${eventType}`);
|
|
105
|
+
|
|
106
|
+
const to = ctx.config.get('toAddress');
|
|
107
|
+
if (!to) throw new Error('No recipient address configured');
|
|
108
|
+
|
|
109
|
+
const subject = template.subject;
|
|
110
|
+
const body = template.body(data || {});
|
|
111
|
+
const from = ctx.config.get('fromAddress');
|
|
112
|
+
const provider = ctx.config.get('provider');
|
|
113
|
+
|
|
114
|
+
ctx.logger.info(`Sending ${eventType} notification to ${to} via ${provider}`);
|
|
115
|
+
|
|
116
|
+
if (provider === 'sendgrid') {
|
|
117
|
+
return sendViaSendGrid(from, to, subject, body, ctx);
|
|
118
|
+
} else if (provider === 'mailgun') {
|
|
119
|
+
return sendViaMailgun(from, to, subject, body, ctx);
|
|
120
|
+
} else if (provider === 'resend') {
|
|
121
|
+
return sendViaResend(from, to, subject, body, ctx);
|
|
122
|
+
} else {
|
|
123
|
+
return sendViaSMTP(from, to, subject, body, ctx);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function sendViaSendGrid(from, to, subject, body, ctx) {
|
|
128
|
+
const apiKey = ctx.config.get('apiKey');
|
|
129
|
+
if (!apiKey) throw new Error('SendGrid API key not configured');
|
|
130
|
+
|
|
131
|
+
const res = await ctx.http('https://api.sendgrid.com/v3/mail/send', {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: {
|
|
134
|
+
Authorization: `Bearer ${apiKey}`,
|
|
135
|
+
'Content-Type': 'application/json',
|
|
136
|
+
},
|
|
137
|
+
body: JSON.stringify({
|
|
138
|
+
personalizations: [{ to: [{ email: to }] }],
|
|
139
|
+
from: { email: from },
|
|
140
|
+
subject,
|
|
141
|
+
content: [{ type: 'text/plain', value: body }],
|
|
142
|
+
}),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (!res.ok) throw new Error(`SendGrid error: ${res.status}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function sendViaMailgun(from, to, subject, body, ctx) {
|
|
149
|
+
const apiKey = ctx.config.get('apiKey');
|
|
150
|
+
if (!apiKey) throw new Error('Mailgun API key not configured');
|
|
151
|
+
|
|
152
|
+
const domain = from.split('@')[1];
|
|
153
|
+
const res = await ctx.http(`https://api.mailgun.net/v3/${domain}/messages`, {
|
|
154
|
+
method: 'POST',
|
|
155
|
+
headers: {
|
|
156
|
+
Authorization: `Basic ${Buffer.from(`api:${apiKey}`).toString('base64')}`,
|
|
157
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
158
|
+
},
|
|
159
|
+
body: new URLSearchParams({ from, to, subject, text: body }).toString(),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (!res.ok) throw new Error(`Mailgun error: ${res.status}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function sendViaResend(from, to, subject, body, ctx) {
|
|
166
|
+
const apiKey = ctx.config.get('apiKey');
|
|
167
|
+
if (!apiKey) throw new Error('Resend API key not configured');
|
|
168
|
+
|
|
169
|
+
const res = await ctx.http('https://api.resend.com/emails', {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: {
|
|
172
|
+
Authorization: `Bearer ${apiKey}`,
|
|
173
|
+
'Content-Type': 'application/json',
|
|
174
|
+
},
|
|
175
|
+
body: JSON.stringify({ from, to, subject, text: body }),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (!res.ok) {
|
|
179
|
+
const err = await res.text().catch(() => '');
|
|
180
|
+
throw new Error(`Resend error: ${res.status} ${err}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function sendViaSMTP(from, to, subject, body, ctx) {
|
|
185
|
+
const host = ctx.config.get('smtpHost');
|
|
186
|
+
const port = ctx.config.get('smtpPort');
|
|
187
|
+
const user = ctx.config.get('smtpUser');
|
|
188
|
+
const pass = ctx.config.get('smtpPass');
|
|
189
|
+
|
|
190
|
+
if (!host) throw new Error('SMTP host not configured');
|
|
191
|
+
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
const socket = port === 465
|
|
194
|
+
? tlsConnect({ host, port }, () => doSMTP(socket))
|
|
195
|
+
: createConnection({ host, port }, () => doSMTP(socket));
|
|
196
|
+
|
|
197
|
+
const commands = [
|
|
198
|
+
`EHLO palexplorer\r\n`,
|
|
199
|
+
...(port !== 465 ? [`STARTTLS\r\n`] : []),
|
|
200
|
+
...(user ? [`AUTH LOGIN\r\n`, `${Buffer.from(user).toString('base64')}\r\n`, `${Buffer.from(pass).toString('base64')}\r\n`] : []),
|
|
201
|
+
`MAIL FROM:<${from}>\r\n`,
|
|
202
|
+
`RCPT TO:<${to}>\r\n`,
|
|
203
|
+
`DATA\r\n`,
|
|
204
|
+
`From: ${from}\r\nTo: ${to}\r\nSubject: ${subject}\r\n\r\n${body}\r\n.\r\n`,
|
|
205
|
+
`QUIT\r\n`,
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
let idx = 0;
|
|
209
|
+
function doSMTP(sock) {
|
|
210
|
+
sock.on('data', () => {
|
|
211
|
+
if (idx < commands.length) {
|
|
212
|
+
sock.write(commands[idx++]);
|
|
213
|
+
} else {
|
|
214
|
+
sock.end();
|
|
215
|
+
resolve();
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
sock.on('error', reject);
|
|
219
|
+
sock.setTimeout(10000, () => { sock.destroy(); reject(new Error('SMTP timeout')); });
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function testProvider(ctx) {
|
|
225
|
+
const provider = ctx.config.get('provider');
|
|
226
|
+
const to = ctx.config.get('toAddress');
|
|
227
|
+
if (!to) return { ok: false, error: 'No recipient configured' };
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
await sendNotification('security:alert', { reason: 'Test notification' }, ctx);
|
|
231
|
+
return { ok: true, provider };
|
|
232
|
+
} catch (err) {
|
|
233
|
+
return { ok: false, provider, error: err.message };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function formatSize(bytes) {
|
|
238
|
+
if (!bytes) return '0 B';
|
|
239
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
240
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
241
|
+
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
|
|
242
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "explorer-integration",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Add 'Share with Pal' to your file manager context menu (Explorer, Finder, Nautilus)",
|
|
5
|
+
"author": "Palexplorer Team",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"hooks": [],
|
|
9
|
+
"permissions": ["fs:write"],
|
|
10
|
+
"config": {},
|
|
11
|
+
"pro": false,
|
|
12
|
+
"minAppVersion": "0.5.0"
|
|
13
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
|
|
5
|
+
let ctx = null;
|
|
6
|
+
|
|
7
|
+
function isHeadless() {
|
|
8
|
+
return process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function activate(context) {
|
|
12
|
+
ctx = context;
|
|
13
|
+
if (!isHeadless()) {
|
|
14
|
+
ctx.logger.info('Explorer integration loaded. Use `pe explorer install` to set up.');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function deactivate() {}
|
|
19
|
+
|
|
20
|
+
export async function install() {
|
|
21
|
+
const p = process.platform;
|
|
22
|
+
if (p === 'win32') return installWindows();
|
|
23
|
+
if (p === 'darwin') return installFinder();
|
|
24
|
+
if (p === 'linux') return installNautilus();
|
|
25
|
+
throw new Error(`No file manager integration for ${p}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function uninstall() {
|
|
29
|
+
const p = process.platform;
|
|
30
|
+
if (p === 'win32') return uninstallWindows();
|
|
31
|
+
if (p === 'darwin') return uninstallFinder();
|
|
32
|
+
if (p === 'linux') return uninstallNautilus();
|
|
33
|
+
throw new Error(`No file manager integration for ${p}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function installWindows() {
|
|
37
|
+
// Delegates to existing Windows explorer util
|
|
38
|
+
const { installExplorerContextMenu } = await import('../../../lib/utils/explorer.js');
|
|
39
|
+
await installExplorerContextMenu();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function uninstallWindows() {
|
|
43
|
+
const { uninstallExplorerContextMenu } = await import('../../../lib/utils/explorer.js');
|
|
44
|
+
await uninstallExplorerContextMenu();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function installFinder() {
|
|
48
|
+
const workflowDir = path.join(
|
|
49
|
+
process.env.HOME,
|
|
50
|
+
'Library/Services/Share with Pal.workflow/Contents'
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
fs.mkdirSync(workflowDir, { recursive: true });
|
|
54
|
+
|
|
55
|
+
fs.writeFileSync(path.join(workflowDir, 'Info.plist'), `<?xml version="1.0" encoding="UTF-8"?>
|
|
56
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
57
|
+
<plist version="1.0">
|
|
58
|
+
<dict>
|
|
59
|
+
<key>NSServices</key>
|
|
60
|
+
<array>
|
|
61
|
+
<dict>
|
|
62
|
+
<key>NSMenuItem</key>
|
|
63
|
+
<dict>
|
|
64
|
+
<key>default</key>
|
|
65
|
+
<string>Share with Pal</string>
|
|
66
|
+
</dict>
|
|
67
|
+
<key>NSMessage</key>
|
|
68
|
+
<string>runWorkflowAsService</string>
|
|
69
|
+
<key>NSSendFileTypes</key>
|
|
70
|
+
<array>
|
|
71
|
+
<string>public.item</string>
|
|
72
|
+
</array>
|
|
73
|
+
</dict>
|
|
74
|
+
</array>
|
|
75
|
+
</dict>
|
|
76
|
+
</plist>
|
|
77
|
+
`);
|
|
78
|
+
|
|
79
|
+
fs.writeFileSync(path.join(workflowDir, 'share-with-pal.sh'), `#!/bin/bash
|
|
80
|
+
for f in "$@"; do
|
|
81
|
+
pe share "$f"
|
|
82
|
+
done
|
|
83
|
+
`, { mode: 0o755 });
|
|
84
|
+
|
|
85
|
+
ctx?.logger.info(`Finder Quick Action created at ${workflowDir}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function uninstallFinder() {
|
|
89
|
+
const dir = path.join(process.env.HOME, 'Library/Services/Share with Pal.workflow');
|
|
90
|
+
if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function installNautilus() {
|
|
94
|
+
const scriptDir = path.join(process.env.HOME, '.local/share/nautilus/scripts');
|
|
95
|
+
fs.mkdirSync(scriptDir, { recursive: true });
|
|
96
|
+
|
|
97
|
+
fs.writeFileSync(path.join(scriptDir, 'Share with Pal'), `#!/bin/bash
|
|
98
|
+
IFS=$'\\n'
|
|
99
|
+
for f in $NAUTILUS_SCRIPT_SELECTED_FILE_PATHS; do
|
|
100
|
+
pe share "$f"
|
|
101
|
+
done
|
|
102
|
+
`, { mode: 0o755 });
|
|
103
|
+
|
|
104
|
+
const actionsDir = path.join(process.env.HOME, '.local/share/file-manager/actions');
|
|
105
|
+
fs.mkdirSync(actionsDir, { recursive: true });
|
|
106
|
+
|
|
107
|
+
fs.writeFileSync(path.join(actionsDir, 'palexplorer-share.desktop'), `[Desktop Action]
|
|
108
|
+
Name=Share with Pal
|
|
109
|
+
Icon=palexplorer
|
|
110
|
+
Exec=pe share %f
|
|
111
|
+
MimeType=inode/directory;application/octet-stream;
|
|
112
|
+
`);
|
|
113
|
+
|
|
114
|
+
ctx?.logger.info('Nautilus script installed');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function uninstallNautilus() {
|
|
118
|
+
const script = path.join(process.env.HOME, '.local/share/nautilus/scripts/Share with Pal');
|
|
119
|
+
const action = path.join(process.env.HOME, '.local/share/file-manager/actions/palexplorer-share.desktop');
|
|
120
|
+
if (fs.existsSync(script)) fs.unlinkSync(script);
|
|
121
|
+
if (fs.existsSync(action)) fs.unlinkSync(action);
|
|
122
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "groups",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Create and manage groups of pals",
|
|
5
|
+
"author": "Palexplorer Team",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"hooks": ["on:app:ready"],
|
|
9
|
+
"permissions": [],
|
|
10
|
+
"contributes": {
|
|
11
|
+
"pages": [
|
|
12
|
+
{ "id": "groups", "label": "groups", "icon": "UsersRound", "section": "social" }
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"config": { "enabled": { "type": "boolean", "default": true } },
|
|
16
|
+
"pro": false
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "networks",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Private overlay networks for organizations",
|
|
5
|
+
"author": "Palexplorer Team",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"hooks": ["on:app:ready"],
|
|
9
|
+
"permissions": [],
|
|
10
|
+
"contributes": {
|
|
11
|
+
"pages": [
|
|
12
|
+
{ "id": "networks", "label": "networks", "icon": "Globe", "section": "system" }
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"config": { "enabled": { "type": "boolean", "default": false } },
|
|
16
|
+
"pro": false
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "share-links",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Create and manage shareable download links",
|
|
5
|
+
"author": "Palexplorer Team",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"hooks": ["on:app:ready"],
|
|
9
|
+
"permissions": [],
|
|
10
|
+
"contributes": {
|
|
11
|
+
"pages": [
|
|
12
|
+
{ "id": "links", "label": "links", "icon": "Key", "section": "content" }
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"config": { "enabled": { "type": "boolean", "default": true } },
|
|
16
|
+
"pro": false
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sync",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Two-way folder synchronization between devices",
|
|
5
|
+
"author": "Palexplorer Team",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"hooks": ["on:app:ready"],
|
|
9
|
+
"permissions": [],
|
|
10
|
+
"contributes": {
|
|
11
|
+
"pages": [
|
|
12
|
+
{ "id": "sync", "label": "sync", "icon": "FolderSync", "section": "content" }
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"config": { "enabled": { "type": "boolean", "default": true } },
|
|
16
|
+
"pro": false
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "user-mgmt",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Multi-user management and role assignment",
|
|
5
|
+
"author": "Palexplorer Team",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"hooks": ["on:app:ready"],
|
|
9
|
+
"permissions": [],
|
|
10
|
+
"contributes": {
|
|
11
|
+
"pages": [
|
|
12
|
+
{ "id": "users", "label": "users", "icon": "Users", "section": "system", "minLevel": "owner" }
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"config": { "enabled": { "type": "boolean", "default": false } },
|
|
16
|
+
"pro": false
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vfs",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Mount Palexplorer shares as a virtual drive (WebDAV)",
|
|
5
|
+
"author": "Palexplorer Team",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"hooks": ["on:app:ready", "on:app:shutdown", "after:share:create", "after:share:revoke"],
|
|
9
|
+
"permissions": ["shares:read", "config:read", "config:write"],
|
|
10
|
+
"config": {
|
|
11
|
+
"port": { "type": "number", "default": 1900, "description": "WebDAV server port" },
|
|
12
|
+
"driveLetter": { "type": "string", "default": "P", "description": "Windows drive letter" },
|
|
13
|
+
"autoMount": { "type": "boolean", "default": true, "description": "Auto-mount on startup" }
|
|
14
|
+
},
|
|
15
|
+
"pro": false,
|
|
16
|
+
"minAppVersion": "0.5.0"
|
|
17
|
+
}
|