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
package/LICENSE.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Copyright (c) 2015-2026 Palexplorer. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This software is provided under a proprietary license. You may use the
|
|
4
|
+
compiled/installed version of this software for personal and commercial
|
|
5
|
+
purposes. You may NOT:
|
|
6
|
+
|
|
7
|
+
- Redistribute, sublicense, or sell copies of the source code
|
|
8
|
+
- Modify the source code and distribute derivative works
|
|
9
|
+
- Reverse engineer, decompile, or disassemble the software
|
|
10
|
+
- Remove or alter any proprietary notices or labels
|
|
11
|
+
|
|
12
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
13
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
14
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
15
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
16
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
17
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
18
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# Pal Explorer (`pe`)
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/pal-explorer-cli)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://nodejs.org/)
|
|
6
|
+
|
|
7
|
+
> Peer-to-peer file sharing with end-to-end encryption. No cloud. No middleman.
|
|
8
|
+
|
|
9
|
+
Pal Explorer lets you share files directly with friends using P2P protocols. Files are encrypted before leaving your device, transferred via WebTorrent, and only decryptable by intended recipients.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
### File Sharing
|
|
14
|
+
- Share files, folders, or entire drives via WebTorrent
|
|
15
|
+
- Private shares with per-recipient E2EE (XChaCha20-Poly1305)
|
|
16
|
+
- Public shares with global magnet links
|
|
17
|
+
- Non-recursive folder sharing (top-level only)
|
|
18
|
+
- Web share links with expiry and download limits
|
|
19
|
+
- Share invites sent automatically via discovery server inbox
|
|
20
|
+
|
|
21
|
+
### Groups
|
|
22
|
+
- Create named groups and organize pals
|
|
23
|
+
- Share with entire groups in one command (`--with-group`)
|
|
24
|
+
- Broadcast messages to all group members
|
|
25
|
+
- Pro limits on group count and member count
|
|
26
|
+
|
|
27
|
+
### Comments
|
|
28
|
+
- Per-share comment threads
|
|
29
|
+
- Add, list, and delete comments on any share
|
|
30
|
+
- Comments linked to author identity
|
|
31
|
+
|
|
32
|
+
### Sync
|
|
33
|
+
- Push/pull directory sync between pals
|
|
34
|
+
- SHA-256 manifest-based delta sync
|
|
35
|
+
- Watch mode for automatic sync on file changes
|
|
36
|
+
- Conflict detection with `.sync-conflict` copies
|
|
37
|
+
- Sync history tracking
|
|
38
|
+
|
|
39
|
+
### File Explorer
|
|
40
|
+
- Browse, search, and manage files with permission checks
|
|
41
|
+
- Directory tree visualization
|
|
42
|
+
- File info with share status
|
|
43
|
+
- Copy, move, rename, delete operations
|
|
44
|
+
- Audit log for all file operations
|
|
45
|
+
|
|
46
|
+
### Remote Browsing
|
|
47
|
+
- Browse pals' shares without downloading
|
|
48
|
+
- List files in remote shares
|
|
49
|
+
- Selective file download from remote shares
|
|
50
|
+
- Server signature verification
|
|
51
|
+
|
|
52
|
+
### Chat
|
|
53
|
+
- Real-time encrypted messaging between pals
|
|
54
|
+
- Chat history with conversation list
|
|
55
|
+
- Fetch new messages from the server
|
|
56
|
+
|
|
57
|
+
### Security
|
|
58
|
+
- **Ed25519 identities** with private keys stored in OS credential manager (keytar)
|
|
59
|
+
- **XChaCha20-Poly1305** authenticated encryption for all private shares
|
|
60
|
+
- **Per-recipient key wrapping** -- each recipient gets a uniquely encrypted share key
|
|
61
|
+
- **Key rotation on revocation** -- removing a recipient triggers re-encryption
|
|
62
|
+
- **BIP-39 recovery** -- 24-word mnemonic phrase for identity backup
|
|
63
|
+
- **Challenge-response registration** -- server never sees your private key
|
|
64
|
+
- **Signed server responses** -- clients verify discovery server authenticity
|
|
65
|
+
- **Encrypted messages** -- inbox payloads encrypted end-to-end
|
|
66
|
+
- **Metadata protection** -- opaque filenames and encrypted manifests in torrents
|
|
67
|
+
- **TOFU server key pinning** -- federated servers pinned on first contact
|
|
68
|
+
- **Config integrity** -- HMAC-SHA256 signing for critical config sections
|
|
69
|
+
- **PIN lock** -- protect the GUI with a PIN code
|
|
70
|
+
|
|
71
|
+
### Pro Features
|
|
72
|
+
- Multi-user profiles with role-based access (owner, admin, member)
|
|
73
|
+
- Encrypted backup and restore
|
|
74
|
+
- API keys for programmatic access
|
|
75
|
+
- Web share links
|
|
76
|
+
- Scheduled tasks (shares, downloads, auto-revokes)
|
|
77
|
+
- Extended inbox and transfer analytics
|
|
78
|
+
- Ad-free experience
|
|
79
|
+
|
|
80
|
+
### Multi-User
|
|
81
|
+
- Multiple identity profiles on one device
|
|
82
|
+
- Owner/admin/member role hierarchy
|
|
83
|
+
- Login switching between profiles
|
|
84
|
+
- Profile promotion and demotion
|
|
85
|
+
|
|
86
|
+
### Discovery & Presence
|
|
87
|
+
- LAN peer discovery via mDNS (Bonjour)
|
|
88
|
+
- Federated handle system (`alice@server.com`)
|
|
89
|
+
- DHT-based decentralized fallback
|
|
90
|
+
- Online/offline presence status for pals
|
|
91
|
+
- Multi-device support with per-device registration
|
|
92
|
+
|
|
93
|
+
## Quick Start
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# Install CLI globally
|
|
97
|
+
npm install -g pal-explorer-cli
|
|
98
|
+
|
|
99
|
+
# Or clone and link for development
|
|
100
|
+
# git clone https://github.com/hn2/palexplorer && cd palexplorer && npm install && npm link
|
|
101
|
+
|
|
102
|
+
# Create your identity
|
|
103
|
+
pe init "YourName"
|
|
104
|
+
# Save the 24-word recovery phrase!
|
|
105
|
+
|
|
106
|
+
# Share a folder with a friend
|
|
107
|
+
pe share ~/Documents --visibility private --with Alice
|
|
108
|
+
|
|
109
|
+
# Start seeding
|
|
110
|
+
pe serve
|
|
111
|
+
|
|
112
|
+
# Download from a peer
|
|
113
|
+
pe download "magnet:?xt=urn:btih:..."
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Adding Friends
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
pe invite --qr # Generate invite link + QR code
|
|
120
|
+
pe pal add @alice # Add by handle
|
|
121
|
+
pe pal add pal://eyJ... # Add by invite link
|
|
122
|
+
pe nearby --add # Auto-discover on LAN
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## CLI Reference
|
|
126
|
+
|
|
127
|
+
The `pe` CLI includes 100+ subcommands. See [docs/CLI.md](docs/CLI.md) for the complete reference.
|
|
128
|
+
|
|
129
|
+
### Key Commands
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
pe init <name> # Create identity
|
|
133
|
+
pe register <handle> # Register on discovery network
|
|
134
|
+
pe share <path> -v private -w bob # Share encrypted with a pal
|
|
135
|
+
pe serve # Start seeding
|
|
136
|
+
pe download <magnet> # Download from magnet link
|
|
137
|
+
pe pal add @alice # Add a pal by handle
|
|
138
|
+
pe sync push ./project alice # Push sync to a pal
|
|
139
|
+
pe status # System health dashboard
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Global Flags
|
|
143
|
+
|
|
144
|
+
| Flag | Description |
|
|
145
|
+
|:---|:---|
|
|
146
|
+
| `--json` | Output as JSON |
|
|
147
|
+
| `--verbose` | Verbose logging |
|
|
148
|
+
| `--quiet` | Suppress non-essential output |
|
|
149
|
+
|
|
150
|
+
## GUI
|
|
151
|
+
|
|
152
|
+
The Electron desktop app lives in the `gui/` directory:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
cd gui
|
|
156
|
+
npm install
|
|
157
|
+
npm run electron
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Features: setup wizard, splash screen with startup progress, dark/light themes, system tray, drag-and-drop sharing, P2P chat, PIN lock, media streaming, workspaces, extensions, file explorer with favorites, command palette, and sidebar grouped into Content/Social/System sections.
|
|
161
|
+
|
|
162
|
+
## Web Dashboard
|
|
163
|
+
|
|
164
|
+
Start the web dashboard alongside the seeder:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
pe serve --web
|
|
168
|
+
pe web # Opens in browser
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
REST API + WebSocket for real-time transfer monitoring, chat, and Pro/billing management.
|
|
172
|
+
|
|
173
|
+
## Configuration
|
|
174
|
+
|
|
175
|
+
| Key | Default | Description |
|
|
176
|
+
|:---|:---|:---|
|
|
177
|
+
| `port` | auto | Local seeder port |
|
|
178
|
+
| `storage_path` | `./downloads` | Default download directory |
|
|
179
|
+
| `max_connections` | `50` | Max P2P connections |
|
|
180
|
+
| `bandwidth_cap` | `0` | Upload cap in KB/s (0 = unlimited) |
|
|
181
|
+
| `discovery_servers` | `http://localhost:3000` | Discovery server URLs (comma-separated) |
|
|
182
|
+
| `announce_interval` | -- | DHT announce interval in seconds |
|
|
183
|
+
|
|
184
|
+
Config file location: `~/.config/palexplorer-cli/config.json`
|
|
185
|
+
|
|
186
|
+
Override the discovery server URL with the `PAL_DISCOVERY_URL` environment variable.
|
|
187
|
+
|
|
188
|
+
## Free vs Pro
|
|
189
|
+
|
|
190
|
+
| Feature | Free | Pro |
|
|
191
|
+
|:---|:---:|:---:|
|
|
192
|
+
| File sharing, E2EE, chat | Yes | Yes |
|
|
193
|
+
| Groups (max 3, 5 members each) | Yes | Unlimited |
|
|
194
|
+
| Comments (max 10 per share) | Yes | Unlimited |
|
|
195
|
+
| Share recipients (max 5) | Yes | Unlimited |
|
|
196
|
+
| Ad-free experience | No | Yes |
|
|
197
|
+
| Multi-user profiles | No | Yes |
|
|
198
|
+
| Vanity handles | No | Yes |
|
|
199
|
+
| Extended inbox (1000 msgs) | No | Yes |
|
|
200
|
+
| Transfer analytics | No | Yes |
|
|
201
|
+
| Encrypted backup/restore | No | Yes |
|
|
202
|
+
| API keys | No | Yes |
|
|
203
|
+
| Scheduled tasks | No | Yes |
|
|
204
|
+
|
|
205
|
+
**Pricing:** Pro $2.99/mo | Pro Annual $29.99/yr | Enterprise $9.99/seat/mo
|
|
206
|
+
|
|
207
|
+
See [docs/MONETIZATION.md](docs/MONETIZATION.md) for full comparison, enforcement architecture, and details.
|
|
208
|
+
|
|
209
|
+
## Architecture
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
+------------------+ +------------------+ +--------------------+
|
|
213
|
+
| CLI (pe) | | Electron GUI | | Web Dashboard |
|
|
214
|
+
| bin/pal.js | | gui/ | | web/ |
|
|
215
|
+
+--------+---------+ +--------+---------+ +--------+-----------+
|
|
216
|
+
| | |
|
|
217
|
+
+------------------------+------------------------+
|
|
218
|
+
| Core Libraries |
|
|
219
|
+
| lib/core/ - identity, shares, groups, sync |
|
|
220
|
+
| lib/crypto/ - E2EE, key exchange |
|
|
221
|
+
| lib/utils/ - config, logging |
|
|
222
|
+
+-------------------------+-----------------------+
|
|
223
|
+
|
|
|
224
|
+
+--------------+--------------+
|
|
225
|
+
| Discovery Server |
|
|
226
|
+
| palexplorer-server (repo) |
|
|
227
|
+
| LevelDB + Express |
|
|
228
|
+
| OAuth + Lemon Squeezy billing |
|
|
229
|
+
+------------------------------+
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**CLI** -- 100+ subcommands, shell completion (bash/zsh), JSON output.
|
|
233
|
+
|
|
234
|
+
**GUI** -- Electron desktop app with command palette, setup wizard, splash screen, dark/light themes, media streaming, extensions, workspaces, file explorer, P2P chat, PIN lock.
|
|
235
|
+
|
|
236
|
+
**Web Dashboard** -- Browser-based node management. REST API + WebSocket, OAuth login, chat, billing.
|
|
237
|
+
|
|
238
|
+
**Discovery Server** -- Separate repo: [palexplorer-server](../palexplorer-server). Identity resolution, message relay, OAuth, Lemon Squeezy billing, self-update server. Self-hostable via Docker.
|
|
239
|
+
|
|
240
|
+
**PAL/1.0 Protocol** -- Proprietary control protocol on top of BitTorrent/DHT/mDNS. 27 message types, policy engine, smart routing.
|
|
241
|
+
|
|
242
|
+
## Ecosystem
|
|
243
|
+
|
|
244
|
+
| Project | Description |
|
|
245
|
+
|:---|:---|
|
|
246
|
+
| [palexplorer](.) | Desktop app (Electron + React + CLI) |
|
|
247
|
+
| [palexplorer-server](../palexplorer-server) | Discovery server (Express 5 + LevelDB) |
|
|
248
|
+
| [palexplorer-www](../palexplorer-www) | Web dashboard (React + Express) |
|
|
249
|
+
| [palexplorer-mobile](../palexplorer-mobile) | Mobile app (React Native) |
|
|
250
|
+
| [palexplorer-sdk](../palexplorer-sdk) | JavaScript SDK for integrations |
|
|
251
|
+
|
|
252
|
+
## Repo Review
|
|
253
|
+
|
|
254
|
+
A focused architecture and product review was added on 2026-03-23.
|
|
255
|
+
|
|
256
|
+
Executive summary:
|
|
257
|
+
- `palexplorer` is the core desktop product, not just a thin client.
|
|
258
|
+
- Do not rename `palexplorer` to `palexplorer-client`.
|
|
259
|
+
- Keep `palexplorer-mobile` as a separate app, but treat it as a companion surface with shared core packages.
|
|
260
|
+
- Highest-priority fixes are packaged CLI installation, cross-platform packaging parity, startup page loading order, and stale GUI audit documentation.
|
|
261
|
+
|
|
262
|
+
See:
|
|
263
|
+
- [docs/REPO-REVIEW-2026-03-23.md](docs/REPO-REVIEW-2026-03-23.md)
|
|
264
|
+
- [docs/REPO-REVIEW-ACTION-PLAN-2026-03-23.md](docs/REPO-REVIEW-ACTION-PLAN-2026-03-23.md)
|
|
265
|
+
|
|
266
|
+
## Development
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
cd palexplorer
|
|
270
|
+
npm install
|
|
271
|
+
npm link
|
|
272
|
+
|
|
273
|
+
# Run tests
|
|
274
|
+
npm test
|
|
275
|
+
|
|
276
|
+
# Start the discovery server (separate repo)
|
|
277
|
+
cd ../palexplorer-server
|
|
278
|
+
npm install
|
|
279
|
+
npm start
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Project Structure
|
|
283
|
+
|
|
284
|
+
```
|
|
285
|
+
bin/ CLI entry point
|
|
286
|
+
lib/commands/ One file per command group (63 files)
|
|
287
|
+
lib/core/ Core logic (identity, shares, groups, sync, transfers, users, backup, extensions, billing, vfs, streaming)
|
|
288
|
+
lib/crypto/ Encryption (stream cipher, share key wrapping)
|
|
289
|
+
lib/protocol/ PAL/1.0 protocol (envelope, messages, policy, router, negotiation, sync, handler)
|
|
290
|
+
lib/utils/ Config, logging, config integrity
|
|
291
|
+
gui/ Electron desktop app (React + Vite)
|
|
292
|
+
web/ Web dashboard (HTML/JS)
|
|
293
|
+
extensions/ Bundled extensions (@palexplorer/vfs, explorer-integration, transfer-analytics, media-streaming)
|
|
294
|
+
test/ Test suite (node:test, incl. stress tests)
|
|
295
|
+
scripts/ Build and publish scripts
|
|
296
|
+
docs/ Documentation
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Security
|
|
300
|
+
|
|
301
|
+
For full details, see [SECURITY.md](docs/SECURITY.md).
|
|
302
|
+
|
|
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
|
+
## License
|
|
312
|
+
|
|
313
|
+
MIT
|
|
314
|
+
|
package/bin/pal.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Enable V8 code caching for faster subsequent boots
|
|
4
|
+
import 'v8-compile-cache-lib';
|
|
5
|
+
|
|
6
|
+
import { program } from 'commander';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
|
|
9
|
+
// Utility for lazy loading commands
|
|
10
|
+
const lazy = (path) => async (program) => {
|
|
11
|
+
const mod = await import(path);
|
|
12
|
+
if (mod.default) mod.default(program);
|
|
13
|
+
else if (typeof mod === 'function') mod(program);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// --- Eagerly load ONLY what is needed for initial setup/parsing ---
|
|
17
|
+
import logger from '../lib/utils/logger.js';
|
|
18
|
+
import { installGlobalErrorHandler } from '../lib/core/analytics.js';
|
|
19
|
+
installGlobalErrorHandler();
|
|
20
|
+
|
|
21
|
+
globalThis.__palMode = 'cli';
|
|
22
|
+
|
|
23
|
+
// --- Lazy load the big migration/core logic only if needed ---
|
|
24
|
+
const runMigrations = async () => {
|
|
25
|
+
const { migrateShareKeys } = await import('../lib/core/shares.js');
|
|
26
|
+
const { migrateToMultiUser } = await import('../lib/core/users.js');
|
|
27
|
+
await migrateShareKeys();
|
|
28
|
+
migrateToMultiUser();
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Apply persisted log level from config before anything else
|
|
32
|
+
import config from '../lib/utils/config.js';
|
|
33
|
+
const settings = config.get('settings') || {};
|
|
34
|
+
const configuredLevel = settings.logLevel;
|
|
35
|
+
if (configuredLevel) logger.setLevel(configuredLevel);
|
|
36
|
+
|
|
37
|
+
// Parse --log-level early (before Commander) so it works before subcommands
|
|
38
|
+
const logLevelIdx = process.argv.indexOf('--log-level');
|
|
39
|
+
if (logLevelIdx !== -1 && process.argv[logLevelIdx + 1]) {
|
|
40
|
+
const cliLevel = process.argv[logLevelIdx + 1];
|
|
41
|
+
if (logger.LEVELS.includes(cliLevel)) {
|
|
42
|
+
logger.setLevel(cliLevel);
|
|
43
|
+
process.argv.splice(logLevelIdx, 2);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
logger.applyGlobalOverride();
|
|
48
|
+
|
|
49
|
+
program
|
|
50
|
+
.name('pe')
|
|
51
|
+
.description('p2p file sharing for friends')
|
|
52
|
+
.version('0.4.0')
|
|
53
|
+
.option('--json', 'output in JSON format')
|
|
54
|
+
.option('--log-level <level>', 'set log level (debug, info, warn, error, silent)');
|
|
55
|
+
|
|
56
|
+
// Register Commands LAZILY
|
|
57
|
+
// Note: Commander requires commands to be defined upfront for help/autocompletion,
|
|
58
|
+
// but the actual .js files containing the complex registration logic (with their own imports)
|
|
59
|
+
// are what we are lazy-loading here.
|
|
60
|
+
const commands = [
|
|
61
|
+
['init', '../lib/commands/init.js'],
|
|
62
|
+
['whoami', '../lib/commands/whoami.js'],
|
|
63
|
+
['register', '../lib/commands/register.js'],
|
|
64
|
+
['recover', '../lib/commands/recover.js'],
|
|
65
|
+
['share', '../lib/commands/share.js'],
|
|
66
|
+
['download', '../lib/commands/download.js'],
|
|
67
|
+
['list', '../lib/commands/list.js'],
|
|
68
|
+
['revoke', '../lib/commands/revoke.js'],
|
|
69
|
+
['serve', '../lib/commands/serve.js'],
|
|
70
|
+
['pal', '../lib/commands/pal.js'],
|
|
71
|
+
['invite', '../lib/commands/invite.js'],
|
|
72
|
+
['nearby', '../lib/commands/nearby.js'],
|
|
73
|
+
['group', '../lib/commands/group.js'],
|
|
74
|
+
['sync', '../lib/commands/sync.js'],
|
|
75
|
+
['transfers', '../lib/commands/transfers.js'],
|
|
76
|
+
['config', '../lib/commands/config.js'],
|
|
77
|
+
['device', '../lib/commands/device.js'],
|
|
78
|
+
['server', '../lib/commands/server.js'],
|
|
79
|
+
['explorer', '../lib/commands/explorer.js'],
|
|
80
|
+
['status', '../lib/commands/status.js'],
|
|
81
|
+
['log', '../lib/commands/log.js'],
|
|
82
|
+
['completion', '../lib/commands/completion.js'],
|
|
83
|
+
['web', '../lib/commands/web.js'],
|
|
84
|
+
['gui-share', '../lib/commands/gui-share.js'],
|
|
85
|
+
['vfs', '../lib/commands/vfs.js'],
|
|
86
|
+
['file', '../lib/commands/file.js'],
|
|
87
|
+
['remote', '../lib/commands/remote.js'],
|
|
88
|
+
['user', '../lib/commands/user.js'],
|
|
89
|
+
['schedule', '../lib/commands/schedule.js'],
|
|
90
|
+
['chat', '../lib/commands/chat.js'],
|
|
91
|
+
['backup', '../lib/commands/backup.js'],
|
|
92
|
+
['api-keys', '../lib/commands/api-keys.js'],
|
|
93
|
+
['share-link', '../lib/commands/share-link.js'],
|
|
94
|
+
['comment', '../lib/commands/comment.js'],
|
|
95
|
+
['update', '../lib/commands/update.js'],
|
|
96
|
+
['auth', '../lib/commands/auth.js'],
|
|
97
|
+
['network', '../lib/commands/network.js'],
|
|
98
|
+
['search', '../lib/commands/search.js'],
|
|
99
|
+
['connect', '../lib/commands/connect.js'],
|
|
100
|
+
['protocol', '../lib/commands/protocol.js'],
|
|
101
|
+
['workspace', '../lib/commands/workspace.js'],
|
|
102
|
+
['favorite', '../lib/commands/favorite.js'],
|
|
103
|
+
['pin', '../lib/commands/pin.js'],
|
|
104
|
+
['stream', '../lib/commands/stream.js'],
|
|
105
|
+
['uninstall', '../lib/commands/uninstall.js'],
|
|
106
|
+
['extension', '../lib/commands/extension.js'],
|
|
107
|
+
['ext', '../lib/commands/extension.js'],
|
|
108
|
+
['billing', '../lib/commands/billing.js'],
|
|
109
|
+
['permissions', '../lib/commands/permissions.js'],
|
|
110
|
+
['org', '../lib/commands/org.js'],
|
|
111
|
+
['su', '../lib/commands/su.js'],
|
|
112
|
+
['federation', '../lib/commands/federation.js'],
|
|
113
|
+
['analytics', '../lib/commands/analytics.js'],
|
|
114
|
+
['webhook', '../lib/commands/webhook.js'],
|
|
115
|
+
['relay', '../lib/commands/relay.js'],
|
|
116
|
+
['compliance', '../lib/commands/compliance.js'],
|
|
117
|
+
['sso', '../lib/commands/sso.js'],
|
|
118
|
+
['rbac', '../lib/commands/rbac.js'],
|
|
119
|
+
['scim', '../lib/commands/scim.js'],
|
|
120
|
+
['audit', '../lib/commands/audit.js'],
|
|
121
|
+
['scanner', '../lib/commands/scanner.js'],
|
|
122
|
+
['notify', '../lib/commands/notify.js'],
|
|
123
|
+
['cloud-backup', '../lib/commands/cloud-backup.js'],
|
|
124
|
+
['dept', '../lib/commands/dept.js'],
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
// To keep it truly fast, we should only register the specific command being run.
|
|
128
|
+
// Commander's parseAsync will handle the matching.
|
|
129
|
+
const currentCmd = process.argv[2];
|
|
130
|
+
const cmdMatch = commands.find(c => c[0] === currentCmd);
|
|
131
|
+
|
|
132
|
+
// Commands that don't modify data — skip migrations for speed
|
|
133
|
+
const readOnlyCommands = new Set([
|
|
134
|
+
'whoami', 'status', 'list', 'log', 'completion', 'nearby',
|
|
135
|
+
'protocol', 'pin', 'favorite', 'search', 'workspace',
|
|
136
|
+
]);
|
|
137
|
+
const isVersionOrHelp = !currentCmd || currentCmd === '--help' || currentCmd === '-h'
|
|
138
|
+
|| currentCmd === '--version' || currentCmd === '-V';
|
|
139
|
+
const needsMigrations = cmdMatch && !readOnlyCommands.has(cmdMatch[0]) && !isVersionOrHelp;
|
|
140
|
+
|
|
141
|
+
if (cmdMatch) {
|
|
142
|
+
if (needsMigrations) await runMigrations();
|
|
143
|
+
const register = await lazy(cmdMatch[1]);
|
|
144
|
+
await register(program);
|
|
145
|
+
} else if (isVersionOrHelp) {
|
|
146
|
+
// Help/version — load all commands for full help text, skip migrations
|
|
147
|
+
await Promise.all(commands.map(c => lazy(c[1])(program)));
|
|
148
|
+
} else {
|
|
149
|
+
// Unknown command — load all for Commander to handle
|
|
150
|
+
await runMigrations();
|
|
151
|
+
await Promise.all(commands.map(c => lazy(c[1])(program)));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Grouped help text
|
|
155
|
+
program.addHelpText('after', `
|
|
156
|
+
${chalk.cyan.bold('Command Groups:')}
|
|
157
|
+
|
|
158
|
+
${chalk.yellow('Identity:')} init, whoami, register, verify, recover
|
|
159
|
+
${chalk.yellow('Users:')} user list/add/remove/promote, login
|
|
160
|
+
${chalk.yellow('Sharing:')} share, list, revoke, serve, download, share-link, share-rename, permissions
|
|
161
|
+
${chalk.yellow('Sync:')} sync push/pull/status/watch/list/remove/diff
|
|
162
|
+
${chalk.yellow('Files:')} file ls/tree/info/copy/move/rename/mkdir/delete/search/open/reveal/audit
|
|
163
|
+
${chalk.yellow('Remote:')} remote browse/files/download
|
|
164
|
+
${chalk.yellow('Social:')} pal, invite, nearby, group, chat, comment
|
|
165
|
+
${chalk.yellow('Search:')} search (files, pals, groups)
|
|
166
|
+
${chalk.yellow('Network:')} network create/list/info/invite/join/members/groups/connect/disconnect
|
|
167
|
+
${chalk.yellow('Workspace:')} workspace list/create/delete/add/remove
|
|
168
|
+
${chalk.yellow('Management:')} transfers, config, device, server, explorer, vfs, web, schedule, favorite
|
|
169
|
+
${chalk.yellow('Protocol:')} protocol info/policy/route/envelope/keys
|
|
170
|
+
${chalk.yellow('PIN:')} pin set/remove/status
|
|
171
|
+
${chalk.yellow('Billing:')} billing status/plans/activate/deactivate/checkout
|
|
172
|
+
${chalk.yellow('Pro:')} backup, api-keys
|
|
173
|
+
${chalk.yellow('Streaming:')} stream local/remote/stop/status/broadcast/join/transport
|
|
174
|
+
${chalk.yellow('Extensions:')} ext list/install/remove/enable/disable/info/config/create
|
|
175
|
+
${chalk.yellow('Orgs:')} org create/list/info/invite/remove/subscribe/unsubscribe/billing
|
|
176
|
+
${chalk.yellow('Utilities:')} status, log, completion, gui-share, uninstall
|
|
177
|
+
|
|
178
|
+
${chalk.gray('Aliases: pe connect → pe network connect, pe disconnect → pe network disconnect')}
|
|
179
|
+
`);
|
|
180
|
+
|
|
181
|
+
// Load extensions and emit lifecycle hooks
|
|
182
|
+
let extensionsLoaded = false;
|
|
183
|
+
if (!isVersionOrHelp) {
|
|
184
|
+
try {
|
|
185
|
+
const { loadAllExtensions, hooks, ensureDefaultExtensions } = await import('../lib/core/extensions.js');
|
|
186
|
+
ensureDefaultExtensions();
|
|
187
|
+
await loadAllExtensions();
|
|
188
|
+
extensionsLoaded = true;
|
|
189
|
+
|
|
190
|
+
// Clean stale transfers (0% for >24h)
|
|
191
|
+
const { cleanStaleTransfers } = await import('../lib/core/transfers.js');
|
|
192
|
+
const cleaned = cleanStaleTransfers(24);
|
|
193
|
+
if (cleaned > 0) console.log(`[transfers] Cleaned ${cleaned} stale transfer(s)`);
|
|
194
|
+
|
|
195
|
+
const identity = (await import('../lib/utils/config.js')).default.get('identity');
|
|
196
|
+
await hooks.emit('on:app:ready', {
|
|
197
|
+
version: program.version(),
|
|
198
|
+
identity: identity ? { handle: identity.handle, publicKey: identity.publicKey } : null,
|
|
199
|
+
mode: 'cli',
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const shutdown = async () => {
|
|
203
|
+
await hooks.emit('on:app:shutdown', {});
|
|
204
|
+
process.exit(process.exitCode || 0);
|
|
205
|
+
};
|
|
206
|
+
process.on('SIGINT', shutdown);
|
|
207
|
+
process.on('SIGTERM', shutdown);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
// Extension loading is non-fatal
|
|
210
|
+
logger.warn?.(`Extension loading failed: ${err.message}`) || console.warn(`[extensions] ${err.message}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!process.argv.slice(2).length) {
|
|
215
|
+
program.outputHelp();
|
|
216
|
+
} else {
|
|
217
|
+
try {
|
|
218
|
+
await program.parseAsync(process.argv);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
if (err.code !== 'commander.helpDisplayed' && err.code !== 'commander.unknownCommand') {
|
|
221
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// keytar's native D-Bus handles block the event loop on headless Linux
|
|
227
|
+
const longRunning = new Set(['serve', 'nearby', 'stream']);
|
|
228
|
+
if (!longRunning.has(currentCmd)) {
|
|
229
|
+
process.exit(process.exitCode || 0);
|
|
230
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Analytics Extension
|
|
2
|
+
|
|
3
|
+
Anonymous, opt-in usage analytics for Palexplorer via PostHog.
|
|
4
|
+
|
|
5
|
+
## Privacy Guarantees
|
|
6
|
+
|
|
7
|
+
- **Opt-in only** — disabled by default, must be explicitly enabled
|
|
8
|
+
- **No PII** — device ID is a random UUID, not tied to identity
|
|
9
|
+
- **No content** — never tracks file names, share names, peer identities, or message content
|
|
10
|
+
- **No IP logging** — PostHog configured to anonymize IPs
|
|
11
|
+
- **Transparent** — all tracked events listed below
|
|
12
|
+
|
|
13
|
+
## Events Tracked
|
|
14
|
+
|
|
15
|
+
| Event | Properties | When |
|
|
16
|
+
|-------|-----------|------|
|
|
17
|
+
| `app_open` | platform, version, arch | App starts |
|
|
18
|
+
| `app_close` | sessionDurationSec | App closes |
|
|
19
|
+
| `share_created` | visibility, recipientCount | Share created |
|
|
20
|
+
| `share_revoked` | — | Share revoked |
|
|
21
|
+
| `transfer_complete` | size, duration, speed | Download finishes |
|
|
22
|
+
| `peer_connected` | — | Peer connects |
|
|
23
|
+
| `peer_disconnected` | — | Peer disconnects |
|
|
24
|
+
| `settings_changed` | key (setting name only) | Setting modified |
|
|
25
|
+
|
|
26
|
+
## Configuration
|
|
27
|
+
|
|
28
|
+
| Key | Type | Default | Description |
|
|
29
|
+
|-----|------|---------|-------------|
|
|
30
|
+
| `enabled` | boolean | `false` | Enable analytics (opt-in) |
|
|
31
|
+
| `posthogKey` | string | (built-in) | PostHog project API key |
|
|
32
|
+
| `posthogHost` | string | `https://us.i.posthog.com` | PostHog host |
|
|
33
|
+
| `sessionTracking` | boolean | `true` | Track session duration |
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pe ext config analytics enabled true
|
|
37
|
+
pe ext config analytics sessionTracking false
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## How to Opt Out
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pe ext config analytics enabled false
|
|
44
|
+
```
|
|
45
|
+
Or toggle "Usage Analytics" in Settings > Privacy.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Analytics Extension — Monetization
|
|
2
|
+
|
|
3
|
+
## Tier
|
|
4
|
+
|
|
5
|
+
- [x] Free — available to all users
|
|
6
|
+
|
|
7
|
+
## Rationale
|
|
8
|
+
|
|
9
|
+
Analytics benefits us (product decisions, bug detection, growth tracking), not the user. It must be free and opt-in to maintain trust. Charging for analytics would be counterproductive — we want maximum opt-in rate.
|
|
10
|
+
|
|
11
|
+
## Revenue Potential
|
|
12
|
+
|
|
13
|
+
- No direct revenue
|
|
14
|
+
- Indirect value: data-driven feature prioritization, retention insights, conversion tracking for Pro upsells
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Analytics Extension — Plan
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Move PostHog analytics out of core into an extension. Core should be pure P2P with no server dependencies. Analytics phones home to PostHog, so it must be an extension.
|
|
6
|
+
|
|
7
|
+
## Design
|
|
8
|
+
|
|
9
|
+
- Bundled extension (`@palexplorer/analytics`) — runs in-process, no sandbox
|
|
10
|
+
- Listens to existing core hooks — no changes to core event emission needed
|
|
11
|
+
- PostHog client initialized only when enabled (opt-in)
|
|
12
|
+
- Device ID stored in extension's own store (not core config)
|
|
13
|
+
- Offline queue with periodic flush — same pattern as before
|
|
14
|
+
- Error reporting (`reportCrash`/`reportError`) stays in core as a safety feature
|
|
15
|
+
|
|
16
|
+
## What Changed
|
|
17
|
+
|
|
18
|
+
- Moved from: `lib/core/analytics.js` (PostHog + track functions)
|
|
19
|
+
- Moved to: `extensions/@palexplorer/analytics/index.js`
|
|
20
|
+
- Core `analytics.js` slimmed to error reporting only
|
|
21
|
+
- `posthog-node` dependency moved from root to extension
|
|
22
|
+
- Removed direct `track()` imports from `shares.js`, `transfers.js`, `billing.js`
|
|
23
|
+
- Those events now flow through hooks → analytics extension
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Analytics Extension — Privacy
|
|
2
|
+
|
|
3
|
+
## What We Collect
|
|
4
|
+
|
|
5
|
+
- App open/close events with session duration
|
|
6
|
+
- Feature usage counts (shares, transfers, peer connections)
|
|
7
|
+
- Setting change events (key name only, not values)
|
|
8
|
+
- Platform, app version, architecture
|
|
9
|
+
|
|
10
|
+
## What We Never Collect
|
|
11
|
+
|
|
12
|
+
- File names, paths, or content
|
|
13
|
+
- Share names, IDs, or recipients
|
|
14
|
+
- Peer identities, handles, or public keys
|
|
15
|
+
- IP addresses (PostHog IP anonymization enabled)
|
|
16
|
+
- Messages or chat content
|
|
17
|
+
- Encryption keys or credentials
|
|
18
|
+
- Browsing/usage patterns that could identify individuals
|
|
19
|
+
|
|
20
|
+
## Device ID
|
|
21
|
+
|
|
22
|
+
A random UUID generated on first use. Not derived from hardware, identity, or any personal data. Stored locally in the extension's store. Can be reset by disabling and re-enabling the extension.
|
|
23
|
+
|
|
24
|
+
## Data Flow
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
App Events → Core Hooks → Analytics Extension → PostHog (US Cloud)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
No data is sent to Palexplorer servers. Only PostHog receives analytics data.
|
|
31
|
+
|
|
32
|
+
## User Control
|
|
33
|
+
|
|
34
|
+
- Disabled by default (opt-in required)
|
|
35
|
+
- Toggle in Settings > Privacy
|
|
36
|
+
- First-launch setup wizard asks for consent
|
|
37
|
+
- Can be disabled at any time via GUI or CLI
|
|
38
|
+
- Disabling immediately stops all data collection
|