pal-explorer-cli 0.4.1 → 0.4.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  # Pal Explorer (`pe`)
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/pal-explorer-cli)](https://www.npmjs.com/package/pal-explorer-cli)
4
- [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
4
+ [![license](https://img.shields.io/badge/license-proprietary-red)](LICENSE)
5
5
  [![node](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org/)
6
6
 
7
7
  > Peer-to-peer file sharing with end-to-end encryption. No cloud. No middleman.
@@ -96,8 +96,6 @@ Pal Explorer lets you share files directly with friends using P2P protocols. Fil
96
96
  # Install CLI globally
97
97
  npm install -g pal-explorer-cli
98
98
 
99
- # Or clone and link for development
100
- # git clone https://github.com/hn2/palexplorer && cd palexplorer && npm install && npm link
101
99
 
102
100
  # Create your identity
103
101
  pe init "YourName"
@@ -300,15 +298,7 @@ docs/ Documentation
300
298
 
301
299
  For full details, see [SECURITY.md](docs/SECURITY.md).
302
300
 
303
- ## Contributing
304
-
305
- 1. Fork the repository
306
- 2. Create a feature branch (`git checkout -b feature/my-feature`)
307
- 3. Write tests for new functionality
308
- 4. Ensure `npm test` passes
309
- 5. Submit a pull request
310
-
311
301
  ## License
312
302
 
313
- MIT
303
+ Proprietary. All rights reserved. See [LICENSE.md](LICENSE.md).
314
304
 
package/bin/pal.js CHANGED
@@ -81,6 +81,7 @@ const commands = [
81
81
  ['log', '../lib/commands/log.js'],
82
82
  ['completion', '../lib/commands/completion.js'],
83
83
  ['web', '../lib/commands/web.js'],
84
+ ['web-login', '../lib/commands/web-login.js'],
84
85
  ['gui-share', '../lib/commands/gui-share.js'],
85
86
  ['vfs', '../lib/commands/vfs.js'],
86
87
  ['file', '../lib/commands/file.js'],
@@ -17,11 +17,11 @@
17
17
  ],
18
18
  "permissions": ["config:read", "config:write", "net:http"],
19
19
  "config": {
20
- "enabled": { "type": "boolean", "default": false, "description": "Enable anonymous usage analytics (opt-in)" },
21
- "posthogKey": { "type": "string", "default": "", "description": "PostHog project API key (leave empty for default)" },
20
+ "enabled": { "type": "boolean", "default": true, "description": "Enable anonymous usage analytics" },
21
+ "posthogKey": { "type": "string", "default": "phc_PSslPgpRuFzQf6s5qbN9atXFGUDHzCvMlmpBTtdTkte", "description": "PostHog project API key (leave empty for default)" },
22
22
  "posthogHost": { "type": "string", "default": "https://us.i.posthog.com", "description": "PostHog ingestion host" },
23
23
  "sessionTracking": { "type": "boolean", "default": true, "description": "Track session duration" }
24
24
  },
25
- "pro": false,
25
+ "tier": "free",
26
26
  "minAppVersion": "0.4.0"
27
27
  }
@@ -1,7 +1,7 @@
1
1
  import crypto from 'crypto';
2
2
  import { createRequire } from 'module';
3
3
 
4
- const DEFAULT_POSTHOG_KEY = process.env.PAL_POSTHOG_KEY || '';
4
+ const DEFAULT_POSTHOG_KEY = process.env.PAL_POSTHOG_KEY || 'phc_PSslPgpRuFzQf6s5qbN9atXFGUDHzCvMlmpBTtdTkte';
5
5
  const FLUSH_INTERVAL = 30_000;
6
6
  const MAX_QUEUE = 100;
7
7
 
@@ -13,5 +13,5 @@
13
13
  ]
14
14
  },
15
15
  "config": { "enabled": { "type": "boolean", "default": false } },
16
- "pro": false
16
+ "tier": "free"
17
17
  }
@@ -12,6 +12,6 @@
12
12
  { "id": "chat", "label": "chat", "icon": "MessagesSquare", "section": "social" }
13
13
  ]
14
14
  },
15
- "config": { "enabled": { "type": "boolean", "default": true } },
16
- "pro": false
15
+ "config": { "enabled": { "type": "boolean", "default": false } },
16
+ "tier": "free"
17
17
  }
@@ -11,6 +11,6 @@
11
11
  "refreshInterval": { "type": "number", "default": 1800000, "description": "Server list refresh interval in ms (default: 30 min)" },
12
12
  "enableGossip": { "type": "boolean", "default": true, "description": "Exchange server lists with connected peers" }
13
13
  },
14
- "pro": false,
14
+ "tier": "free",
15
15
  "minAppVersion": "0.4.0"
16
16
  }
@@ -8,6 +8,6 @@
8
8
  "hooks": [],
9
9
  "permissions": ["fs:write"],
10
10
  "config": {},
11
- "pro": false,
11
+ "tier": "free",
12
12
  "minAppVersion": "0.5.0"
13
13
  }
@@ -12,6 +12,6 @@
12
12
  { "id": "groups", "label": "groups", "icon": "UsersRound", "section": "social" }
13
13
  ]
14
14
  },
15
- "config": { "enabled": { "type": "boolean", "default": true } },
16
- "pro": false
15
+ "config": { "enabled": { "type": "boolean", "default": false } },
16
+ "tier": "free"
17
17
  }
@@ -13,5 +13,5 @@
13
13
  ]
14
14
  },
15
15
  "config": { "enabled": { "type": "boolean", "default": false } },
16
- "pro": false
16
+ "tier": "free"
17
17
  }
@@ -12,6 +12,6 @@
12
12
  { "id": "links", "label": "links", "icon": "Key", "section": "content" }
13
13
  ]
14
14
  },
15
- "config": { "enabled": { "type": "boolean", "default": true } },
16
- "pro": false
15
+ "config": { "enabled": { "type": "boolean", "default": false } },
16
+ "tier": "free"
17
17
  }
@@ -12,6 +12,6 @@
12
12
  { "id": "sync", "label": "sync", "icon": "FolderSync", "section": "content" }
13
13
  ]
14
14
  },
15
- "config": { "enabled": { "type": "boolean", "default": true } },
16
- "pro": false
15
+ "config": { "enabled": { "type": "boolean", "default": false } },
16
+ "tier": "free"
17
17
  }
@@ -13,5 +13,5 @@
13
13
  ]
14
14
  },
15
15
  "config": { "enabled": { "type": "boolean", "default": false } },
16
- "pro": false
16
+ "tier": "free"
17
17
  }
@@ -12,6 +12,6 @@
12
12
  "driveLetter": { "type": "string", "default": "P", "description": "Windows drive letter" },
13
13
  "autoMount": { "type": "boolean", "default": true, "description": "Auto-mount on startup" }
14
14
  },
15
- "pro": false,
15
+ "tier": "free",
16
16
  "minAppVersion": "0.5.0"
17
17
  }
@@ -0,0 +1,79 @@
1
+ import chalk from 'chalk';
2
+ import { getIdentity } from '../core/identity.js';
3
+ import config from '../utils/config.js';
4
+
5
+ export default function webLoginCommand(program) {
6
+ program
7
+ .command('web-login <code>')
8
+ .description('sign in to palexplorer.com using your desktop identity')
9
+ .option('--host <host>', 'Web server host', 'palexplorer.com')
10
+ .addHelpText('after', `
11
+ Examples:
12
+ $ pe web-login A7X-3K9 Sign in to palexplorer.com
13
+ $ pe web-login A7X-3K9 --host localhost:4000 Use local dev server
14
+ `)
15
+ .action(async (code, opts) => {
16
+ try {
17
+ const identity = getIdentity();
18
+ if (!identity) {
19
+ console.log(chalk.red('No identity found. Run: pe init'));
20
+ return;
21
+ }
22
+
23
+ const host = opts.host;
24
+ const protocol = host.startsWith('localhost') || host.startsWith('127.0.0.1') ? 'http' : 'https';
25
+ const baseUrl = `${protocol}://${host}`;
26
+
27
+ console.log(chalk.gray(`Resolving code ${code}...`));
28
+
29
+ const resolveRes = await fetch(`${baseUrl}/api/auth/qr/resolve`, {
30
+ method: 'POST',
31
+ headers: { 'Content-Type': 'application/json' },
32
+ body: JSON.stringify({ code }),
33
+ });
34
+
35
+ if (!resolveRes.ok) {
36
+ const data = await resolveRes.json().catch(() => ({}));
37
+ console.log(chalk.red(`Failed: ${data.error || 'Code not found or expired'}`));
38
+ return;
39
+ }
40
+
41
+ const { sessionId, challenge } = await resolveRes.json();
42
+
43
+ console.log(chalk.gray('Signing challenge...'));
44
+
45
+ let sodium;
46
+ try {
47
+ sodium = (await import('sodium-native')).default;
48
+ } catch {
49
+ sodium = await import('sodium-native');
50
+ }
51
+
52
+ const sig = Buffer.alloc(sodium.crypto_sign_BYTES);
53
+ const challengeBuf = Buffer.from(challenge, 'hex');
54
+ const privateKey = Buffer.from(identity.privateKey, 'hex');
55
+ sodium.crypto_sign_detached(sig, challengeBuf, privateKey);
56
+
57
+ const verifyRes = await fetch(`${baseUrl}/api/auth/qr/verify`, {
58
+ method: 'POST',
59
+ headers: { 'Content-Type': 'application/json' },
60
+ body: JSON.stringify({
61
+ sessionId,
62
+ publicKey: identity.publicKey,
63
+ signature: sig.toString('hex'),
64
+ }),
65
+ });
66
+
67
+ if (!verifyRes.ok) {
68
+ const data = await verifyRes.json().catch(() => ({}));
69
+ console.log(chalk.red(`Verification failed: ${data.error || 'Unknown error'}`));
70
+ return;
71
+ }
72
+
73
+ console.log(chalk.green('Signed in to web successfully!'));
74
+ console.log(chalk.gray('Your browser should update automatically.'));
75
+ } catch (err) {
76
+ console.log(chalk.red(`Error: ${err.message}`));
77
+ }
78
+ });
79
+ }
@@ -273,28 +273,13 @@ function downgradeToFree(reason) {
273
273
  let _revalidationPromise = null;
274
274
 
275
275
  export function getActivePlan() {
276
- const billing = getBillingData();
277
- if (!billing) {
278
- return { key: 'free', ...PLANS.free, expiresAt: null, source: 'none' };
279
- }
280
-
281
- if (billing.expiresAt && new Date(billing.expiresAt) < new Date()) {
282
- return { key: 'free', ...PLANS.free, expiresAt: null, source: 'expired' };
283
- }
284
-
285
- // Trigger non-blocking revalidation if needed
286
- if (shouldRevalidate() && !_revalidationPromise) {
287
- _revalidationPromise = revalidateLicense().finally(() => { _revalidationPromise = null; });
288
- }
289
-
290
- const planKey = billing.plan || 'pro_monthly';
291
- const plan = PLANS[planKey] || PLANS.pro_monthly;
276
+ // BETA: all features unlocked — re-enable billing when ready
292
277
  return {
293
- key: planKey,
294
- ...plan,
295
- expiresAt: billing.expiresAt,
296
- source: 'license',
297
- customerEmail: billing.customerEmail || null,
278
+ key: 'enterprise',
279
+ ...PLANS.enterprise,
280
+ expiresAt: null,
281
+ source: 'beta',
282
+ customerEmail: null,
298
283
  };
299
284
  }
300
285
 
@@ -54,25 +54,17 @@ const REQUIRE_SIGNATURE = config.get('extensionRequireSignature') ?? (process.en
54
54
  const TIER_RANK = { free: 0, pro: 1, enterprise: 2 };
55
55
 
56
56
  function checkExtensionTier(manifest) {
57
+ // BETA: all extensions free — bypass tier enforcement
57
58
  const tier = manifest.tier || (manifest.pro ? 'pro' : 'free');
58
- if (tier === 'free') return { allowed: true, tier, planTier: 'free' };
59
-
60
- let plan;
61
- try { plan = getActivePlan(); } catch { plan = { key: 'free' }; }
62
-
63
- const planTier = plan.key === 'free' ? 'free'
64
- : plan.key.startsWith('enterprise') ? 'enterprise' : 'pro';
65
-
66
- if (TIER_RANK[planTier] >= TIER_RANK[tier]) {
67
- return { allowed: true, tier, planTier };
68
- }
69
-
70
- return { allowed: false, tier, planTier };
59
+ return { allowed: true, tier, planTier: 'free' };
71
60
  }
72
61
 
73
62
  const OFFLINE_GRACE_DAYS = 7;
74
63
 
75
64
  async function verifyExtensionLicense(manifest, extPath) {
65
+ // BETA: all extensions free — skip license verification
66
+ return true;
67
+
76
68
  const tier = manifest.tier || (manifest.pro ? 'pro' : 'free');
77
69
  if (tier === 'free' || manifest.bundled) return true;
78
70
 
@@ -821,12 +813,8 @@ async function installExtension(source, { skipPrompt = false } = {}) {
821
813
  throw new Error('Cannot install extensions using reserved @palexplorer namespace.');
822
814
  }
823
815
 
824
- // Tier enforcementpaid extensions require matching subscription
816
+ // BETA: all extensions free tier check always passes
825
817
  const tierCheck = checkExtensionTier(manifest);
826
- if (!tierCheck.allowed) {
827
- if (sourcePath !== source) fs.rmSync(sourcePath, { recursive: true, force: true });
828
- throw new Error(`Extension '${manifest.name}' requires a ${tierCheck.tier} subscription (current: ${tierCheck.planTier}). Upgrade at https://palexplorer.com/pro`);
829
- }
830
818
 
831
819
  // AST analysis (replaces old regex scanning)
832
820
  const analysis = analyzeExtension(sourcePath, manifest);
package/lib/core/pro.js CHANGED
@@ -5,23 +5,17 @@ export const FREE_LIMITS = {
5
5
  maxShareRecipients: PLANS.free.limits.maxRecipients,
6
6
  };
7
7
 
8
+ // BETA: all features unlocked — re-enable billing when ready
8
9
  export function isPro() {
9
- const plan = billingGetActivePlan();
10
- return plan.key !== 'free';
10
+ return true;
11
11
  }
12
12
 
13
13
  export function isEnterprise() {
14
- const plan = billingGetActivePlan();
15
- return plan.key === 'enterprise';
14
+ return true;
16
15
  }
17
16
 
18
17
  export function checkLimit(feature, currentCount) {
19
- if (isPro()) return;
20
- const limit = FREE_LIMITS[feature];
21
- if (limit == null) return;
22
- if (currentCount > limit) {
23
- throw new Error(`Free tier limit reached: ${feature} (max ${limit}). Upgrade to Pro for unlimited access.`);
24
- }
18
+ return;
25
19
  }
26
20
 
27
21
  export { getFeature, checkFeature, getPlanLimits };
@@ -558,14 +558,15 @@ export function startWebServer(port, torrentClient, { bindAddress = '127.0.0.1'
558
558
 
559
559
  // ── Billing / Pro Status ──
560
560
 
561
+ // BETA: all features unlocked — re-enable billing when ready
561
562
  app.get('/api/billing/status', (req, res) => {
562
- const pro = isPro();
563
- const license = config.get('license') || {};
564
563
  res.json({
565
- plan: pro ? 'pro' : 'free',
566
- active: pro,
567
- expiresAt: license.expiresAt || null,
568
- licenseKey: license.key ? `${license.key.slice(0, 8)}...` : null,
564
+ plan: 'enterprise',
565
+ active: true,
566
+ isPro: true,
567
+ expiresAt: null,
568
+ licenseKey: null,
569
+ source: 'beta',
569
570
  });
570
571
  });
571
572
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pal-explorer-cli",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "P2P encrypted file sharing CLI — share files directly with friends, not with the cloud",
5
5
  "main": "bin/pal.js",
6
6
  "bin": {
@@ -60,10 +60,6 @@
60
60
  "author": "Palexplorer",
61
61
  "license": "SEE LICENSE IN LICENSE.md",
62
62
  "homepage": "https://palexplorer.com",
63
- "repository": {
64
- "type": "git",
65
- "url": "https://github.com/hn2/palexplorer.git"
66
- },
67
63
  "type": "module",
68
64
  "devDependencies": {
69
65
  "@playwright/test": "^1.58.2"