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.
Files changed (156) hide show
  1. package/LICENSE.md +18 -0
  2. package/README.md +314 -0
  3. package/bin/pal.js +230 -0
  4. package/extensions/@palexplorer/analytics/README.md +45 -0
  5. package/extensions/@palexplorer/analytics/docs/MONETIZATION.md +14 -0
  6. package/extensions/@palexplorer/analytics/docs/PLAN.md +23 -0
  7. package/extensions/@palexplorer/analytics/docs/PRIVACY.md +38 -0
  8. package/extensions/@palexplorer/analytics/extension.json +27 -0
  9. package/extensions/@palexplorer/analytics/index.js +186 -0
  10. package/extensions/@palexplorer/analytics/test/analytics.test.js +82 -0
  11. package/extensions/@palexplorer/audit/extension.json +17 -0
  12. package/extensions/@palexplorer/audit/index.js +2 -0
  13. package/extensions/@palexplorer/auth-email/extension.json +17 -0
  14. package/extensions/@palexplorer/auth-email/index.js +102 -0
  15. package/extensions/@palexplorer/auth-oauth/extension.json +16 -0
  16. package/extensions/@palexplorer/auth-oauth/index.js +199 -0
  17. package/extensions/@palexplorer/chat/extension.json +17 -0
  18. package/extensions/@palexplorer/chat/index.js +2 -0
  19. package/extensions/@palexplorer/discovery/extension.json +16 -0
  20. package/extensions/@palexplorer/discovery/index.js +111 -0
  21. package/extensions/@palexplorer/email-notifications/extension.json +23 -0
  22. package/extensions/@palexplorer/email-notifications/index.js +242 -0
  23. package/extensions/@palexplorer/explorer-integration/extension.json +13 -0
  24. package/extensions/@palexplorer/explorer-integration/index.js +122 -0
  25. package/extensions/@palexplorer/groups/extension.json +17 -0
  26. package/extensions/@palexplorer/groups/index.js +2 -0
  27. package/extensions/@palexplorer/networks/extension.json +17 -0
  28. package/extensions/@palexplorer/networks/index.js +2 -0
  29. package/extensions/@palexplorer/share-links/extension.json +17 -0
  30. package/extensions/@palexplorer/share-links/index.js +2 -0
  31. package/extensions/@palexplorer/sync/extension.json +17 -0
  32. package/extensions/@palexplorer/sync/index.js +2 -0
  33. package/extensions/@palexplorer/user-mgmt/extension.json +17 -0
  34. package/extensions/@palexplorer/user-mgmt/index.js +2 -0
  35. package/extensions/@palexplorer/vfs/extension.json +17 -0
  36. package/extensions/@palexplorer/vfs/index.js +167 -0
  37. package/lib/capabilities.js +263 -0
  38. package/lib/commands/analytics.js +175 -0
  39. package/lib/commands/api-keys.js +131 -0
  40. package/lib/commands/audit.js +235 -0
  41. package/lib/commands/auth.js +137 -0
  42. package/lib/commands/backup.js +76 -0
  43. package/lib/commands/billing.js +148 -0
  44. package/lib/commands/chat.js +217 -0
  45. package/lib/commands/cloud-backup.js +231 -0
  46. package/lib/commands/comment.js +99 -0
  47. package/lib/commands/completion.js +203 -0
  48. package/lib/commands/compliance.js +218 -0
  49. package/lib/commands/config.js +136 -0
  50. package/lib/commands/connect.js +44 -0
  51. package/lib/commands/dept.js +294 -0
  52. package/lib/commands/device.js +146 -0
  53. package/lib/commands/download.js +226 -0
  54. package/lib/commands/explorer.js +178 -0
  55. package/lib/commands/extension.js +970 -0
  56. package/lib/commands/favorite.js +90 -0
  57. package/lib/commands/federation.js +270 -0
  58. package/lib/commands/file.js +533 -0
  59. package/lib/commands/group.js +271 -0
  60. package/lib/commands/gui-share.js +29 -0
  61. package/lib/commands/init.js +61 -0
  62. package/lib/commands/invite.js +59 -0
  63. package/lib/commands/list.js +59 -0
  64. package/lib/commands/log.js +116 -0
  65. package/lib/commands/nearby.js +108 -0
  66. package/lib/commands/network.js +251 -0
  67. package/lib/commands/notify.js +198 -0
  68. package/lib/commands/org.js +273 -0
  69. package/lib/commands/pal.js +180 -0
  70. package/lib/commands/permissions.js +216 -0
  71. package/lib/commands/pin.js +97 -0
  72. package/lib/commands/protocol.js +357 -0
  73. package/lib/commands/rbac.js +147 -0
  74. package/lib/commands/recover.js +36 -0
  75. package/lib/commands/register.js +171 -0
  76. package/lib/commands/relay.js +131 -0
  77. package/lib/commands/remote.js +368 -0
  78. package/lib/commands/revoke.js +50 -0
  79. package/lib/commands/scanner.js +280 -0
  80. package/lib/commands/schedule.js +344 -0
  81. package/lib/commands/scim.js +203 -0
  82. package/lib/commands/search.js +181 -0
  83. package/lib/commands/serve.js +438 -0
  84. package/lib/commands/server.js +350 -0
  85. package/lib/commands/share-link.js +199 -0
  86. package/lib/commands/share.js +323 -0
  87. package/lib/commands/sso.js +200 -0
  88. package/lib/commands/status.js +136 -0
  89. package/lib/commands/stream.js +562 -0
  90. package/lib/commands/su.js +187 -0
  91. package/lib/commands/sync.js +827 -0
  92. package/lib/commands/transfers.js +152 -0
  93. package/lib/commands/uninstall.js +188 -0
  94. package/lib/commands/update.js +204 -0
  95. package/lib/commands/user.js +276 -0
  96. package/lib/commands/vfs.js +84 -0
  97. package/lib/commands/web.js +52 -0
  98. package/lib/commands/webhook.js +180 -0
  99. package/lib/commands/whoami.js +59 -0
  100. package/lib/commands/workspace.js +121 -0
  101. package/lib/core/accessLog.js +54 -0
  102. package/lib/core/analytics.js +99 -0
  103. package/lib/core/backup.js +84 -0
  104. package/lib/core/billing.js +336 -0
  105. package/lib/core/bitfieldStore.js +53 -0
  106. package/lib/core/connectionManager.js +182 -0
  107. package/lib/core/dhtDiscovery.js +148 -0
  108. package/lib/core/discoveryClient.js +408 -0
  109. package/lib/core/extensionAnalyzer.js +357 -0
  110. package/lib/core/extensionSandbox.js +250 -0
  111. package/lib/core/extensionWorkerHost.js +166 -0
  112. package/lib/core/extensions.js +1082 -0
  113. package/lib/core/fileDiff.js +69 -0
  114. package/lib/core/groups.js +119 -0
  115. package/lib/core/identity.js +340 -0
  116. package/lib/core/mdnsService.js +126 -0
  117. package/lib/core/networks.js +81 -0
  118. package/lib/core/permissions.js +109 -0
  119. package/lib/core/pro.js +27 -0
  120. package/lib/core/resolver.js +74 -0
  121. package/lib/core/serverList.js +224 -0
  122. package/lib/core/sharePolicy.js +69 -0
  123. package/lib/core/shares.js +325 -0
  124. package/lib/core/signalingServer.js +441 -0
  125. package/lib/core/streamTransport.js +106 -0
  126. package/lib/core/su.js +55 -0
  127. package/lib/core/syncEngine.js +264 -0
  128. package/lib/core/syncState.js +159 -0
  129. package/lib/core/transfers.js +259 -0
  130. package/lib/core/users.js +225 -0
  131. package/lib/core/vfs.js +216 -0
  132. package/lib/core/webServer.js +702 -0
  133. package/lib/core/webrtcStream.js +396 -0
  134. package/lib/crypto/chatEncryption.js +57 -0
  135. package/lib/crypto/shareEncryption.js +195 -0
  136. package/lib/crypto/sharePassword.js +35 -0
  137. package/lib/crypto/streamEncryption.js +189 -0
  138. package/lib/package.json +1 -0
  139. package/lib/protocol/envelope.js +271 -0
  140. package/lib/protocol/handler.js +191 -0
  141. package/lib/protocol/index.js +27 -0
  142. package/lib/protocol/messages.js +247 -0
  143. package/lib/protocol/negotiation.js +127 -0
  144. package/lib/protocol/policy.js +142 -0
  145. package/lib/protocol/router.js +86 -0
  146. package/lib/protocol/sync.js +122 -0
  147. package/lib/utils/cli.js +15 -0
  148. package/lib/utils/config.js +123 -0
  149. package/lib/utils/configIntegrity.js +87 -0
  150. package/lib/utils/downloadDir.js +9 -0
  151. package/lib/utils/explorer.js +83 -0
  152. package/lib/utils/format.js +12 -0
  153. package/lib/utils/help.js +357 -0
  154. package/lib/utils/logger.js +103 -0
  155. package/lib/utils/torrent.js +203 -0
  156. package/package.json +71 -0
@@ -0,0 +1,247 @@
1
+ import crypto from 'crypto';
2
+ import sodium from 'sodium-native';
3
+ import { create, createEncrypted } from './envelope.js';
4
+
5
+ // ── Identity & Discovery ──
6
+
7
+ export function identityAnnounce(keyPair, { handle, deviceId, deviceName, version, lanAddresses, capabilities }) {
8
+ return create('identity.announce', keyPair, null, {
9
+ handle,
10
+ deviceId,
11
+ deviceName,
12
+ capabilities: capabilities || ['share', 'sync', 'chat', 'relay'],
13
+ version,
14
+ addresses: {
15
+ lan: lanAddresses || [],
16
+ public: null,
17
+ },
18
+ });
19
+ }
20
+
21
+ export function identityResolve(keyPair, handle) {
22
+ return create('identity.resolve', keyPair, null, { handle });
23
+ }
24
+
25
+ export function identityResponse(keyPair, requesterPK, { handle, publicKey, deviceId, capabilities, lastSeen, source }) {
26
+ return create('identity.response', keyPair, requesterPK, {
27
+ handle,
28
+ publicKey,
29
+ deviceId: deviceId || null,
30
+ capabilities: capabilities || [],
31
+ lastSeen: lastSeen || new Date().toISOString(),
32
+ source: source || 'server',
33
+ });
34
+ }
35
+
36
+ export function heartbeat(keyPair, { handle, deviceId }) {
37
+ return create('heartbeat', keyPair, null, { handle, deviceId });
38
+ }
39
+
40
+ // ── Share Negotiation ──
41
+
42
+ export function shareOffer(keyPair, recipientPK, { shareId, name, type, totalSize, fileCount, policy, preview }) {
43
+ return createEncrypted('share.offer', keyPair, recipientPK, {
44
+ shareId,
45
+ name,
46
+ type,
47
+ totalSize,
48
+ fileCount: fileCount || 0,
49
+ preview: preview || null,
50
+ policy: policy || {},
51
+ encryption: {
52
+ algorithm: 'xchacha20-poly1305',
53
+ chunkSize: 65536,
54
+ manifestEncrypted: true,
55
+ },
56
+ });
57
+ }
58
+
59
+ export function shareAccept(keyPair, sharerPK, { shareId }) {
60
+ const curve25519PK = Buffer.alloc(sodium.crypto_box_PUBLICKEYBYTES);
61
+ const edPK = Buffer.isBuffer(keyPair.publicKey) ? keyPair.publicKey : Buffer.from(keyPair.publicKey, 'hex');
62
+ sodium.crypto_sign_ed25519_pk_to_curve25519(curve25519PK, edPK);
63
+
64
+ return create('share.accept', keyPair, sharerPK, {
65
+ shareId,
66
+ recipientCurve25519PK: curve25519PK.toString('hex'),
67
+ });
68
+ }
69
+
70
+ export function shareKey(keyPair, recipientPK, { shareId, magnet, wrappedKey, manifestHash }) {
71
+ return createEncrypted('share.key', keyPair, recipientPK, {
72
+ shareId,
73
+ magnet,
74
+ wrappedKey,
75
+ manifestHash,
76
+ });
77
+ }
78
+
79
+ export function shareRevoke(keyPair, recipientPK, { shareId, reason, rotated }) {
80
+ return create('share.revoke', keyPair, recipientPK, {
81
+ shareId,
82
+ reason: reason || 'revoked',
83
+ rotated: rotated || false,
84
+ });
85
+ }
86
+
87
+ export function sharePolicyUpdate(keyPair, { shareId, policy }) {
88
+ return create('share.policy.update', keyPair, null, { shareId, policy });
89
+ }
90
+
91
+ // ── Transfer Control ──
92
+
93
+ export function transferRequest(keyPair, seederPK, { shareId, requestedFiles, resumeFrom, clientVersion }) {
94
+ return create('transfer.request', keyPair, seederPK, {
95
+ shareId,
96
+ requestedFiles: requestedFiles || ['*'],
97
+ resumeFrom: resumeFrom || null,
98
+ clientVersion,
99
+ });
100
+ }
101
+
102
+ export function transferAuthorize(keyPair, downloaderPK, { shareId, magnet, wrappedKey, route, policy }) {
103
+ return createEncrypted('transfer.authorize', keyPair, downloaderPK, {
104
+ shareId,
105
+ magnet,
106
+ wrappedKey,
107
+ route: route || { preferred: 'direct', alternatives: [] },
108
+ policy: policy || {},
109
+ });
110
+ }
111
+
112
+ export function transferDeny(keyPair, downloaderPK, { shareId, reason, message }) {
113
+ return create('transfer.deny', keyPair, downloaderPK, {
114
+ shareId,
115
+ reason,
116
+ message,
117
+ });
118
+ }
119
+
120
+ export function transferProgress(keyPair, seederPK, { shareId, bytesTransferred, totalBytes, speed, percent }) {
121
+ return createEncrypted('transfer.progress', keyPair, seederPK, {
122
+ shareId,
123
+ bytesTransferred,
124
+ totalBytes,
125
+ speed: speed || 0,
126
+ percent: percent || 0,
127
+ });
128
+ }
129
+
130
+ export function transferComplete(keyPair, seederPK, { shareId, totalBytes, manifestHash }) {
131
+ return createEncrypted('transfer.complete', keyPair, seederPK, {
132
+ shareId,
133
+ totalBytes,
134
+ manifestHash: manifestHash || null,
135
+ });
136
+ }
137
+
138
+ export function transferReceipt(keyPair, downloaderPK, { shareId, downloadedBy, totalBytes, fileCount, manifestHash }) {
139
+ return createEncrypted('transfer.receipt', keyPair, downloaderPK, {
140
+ shareId,
141
+ downloadedBy,
142
+ completedAt: new Date().toISOString(),
143
+ totalBytes,
144
+ fileCount,
145
+ manifestHash,
146
+ });
147
+ }
148
+
149
+ // ── Sync ──
150
+
151
+ export function syncManifest(keyPair, peerPK, { syncPairId, generation, entries, checksum }) {
152
+ return createEncrypted('sync.manifest', keyPair, peerPK, {
153
+ syncPairId,
154
+ generation,
155
+ entries,
156
+ checksum,
157
+ });
158
+ }
159
+
160
+ export function syncDiff(keyPair, peerPK, { syncPairId, files }) {
161
+ return createEncrypted('sync.diff', keyPair, peerPK, {
162
+ syncPairId,
163
+ files,
164
+ });
165
+ }
166
+
167
+ export function syncRequest(keyPair, peerPK, { syncPairId, files }) {
168
+ return createEncrypted('sync.request', keyPair, peerPK, {
169
+ syncPairId,
170
+ files,
171
+ });
172
+ }
173
+
174
+ export function syncConflict(keyPair, peerPK, { syncPairId, conflicts }) {
175
+ return createEncrypted('sync.conflict', keyPair, peerPK, {
176
+ syncPairId,
177
+ conflicts,
178
+ });
179
+ }
180
+
181
+ // ── Chat ──
182
+
183
+ export function chatMessage(keyPair, recipientPK, { text, replyTo }) {
184
+ return createEncrypted('chat.message', keyPair, recipientPK, {
185
+ text,
186
+ replyTo: replyTo || null,
187
+ });
188
+ }
189
+
190
+ export function chatAck(keyPair, senderPK, { messageId }) {
191
+ return create('chat.ack', keyPair, senderPK, { messageId });
192
+ }
193
+
194
+ export function chatTyping(keyPair, recipientPK) {
195
+ return create('chat.typing', keyPair, recipientPK, {});
196
+ }
197
+
198
+ // ── Routing ──
199
+
200
+ export function routeProbe(keyPair, peerPK, { shareId, lanAddresses, stunResult }) {
201
+ return create('route.probe', keyPair, peerPK, {
202
+ shareId,
203
+ probeId: crypto.randomUUID(),
204
+ lanAddresses: lanAddresses || [],
205
+ stunResult: stunResult || null,
206
+ });
207
+ }
208
+
209
+ export function routeProbeResponse(keyPair, peerPK, { probeId, reachable, latency, recommended }) {
210
+ return create('route.probe.response', keyPair, peerPK, {
211
+ probeId,
212
+ reachable,
213
+ latency,
214
+ recommended,
215
+ });
216
+ }
217
+
218
+ export function relayRequest(keyPair, { shareId, estimatedSize, tier }) {
219
+ return create('relay.request', keyPair, null, {
220
+ shareId,
221
+ estimatedSize,
222
+ tier: tier || 'free',
223
+ });
224
+ }
225
+
226
+ export function relayAllocated(keyPair, requesterPK, { urls, username, credential, ttl, bandwidthLimit, priority }) {
227
+ return createEncrypted('relay.allocated', keyPair, requesterPK, {
228
+ urls,
229
+ username,
230
+ credential,
231
+ ttl,
232
+ bandwidthLimit: bandwidthLimit || null,
233
+ priority: priority || 'normal',
234
+ });
235
+ }
236
+
237
+ // ── Server Gossip ──
238
+
239
+ export function gossipServers(keyPair, peerPK, { servers }) {
240
+ return create('gossip.servers', keyPair, peerPK, {
241
+ servers: (servers || []).slice(0, 5).map(s => ({
242
+ url: s.url,
243
+ roles: s.roles || [],
244
+ publicKey: s.publicKey || null,
245
+ })),
246
+ });
247
+ }
@@ -0,0 +1,127 @@
1
+ import sodium from 'sodium-native';
2
+ import { shareOffer, shareAccept, shareKey, shareRevoke, transferAuthorize, transferDeny, transferReceipt } from './messages.js';
3
+ import { enforcePolicy, validatePolicy } from './policy.js';
4
+ import { probeRoutes, buildRouteInfo } from './router.js';
5
+ import { verify, decrypt } from './envelope.js';
6
+ import { getSharePolicy, incrementDownloadCount } from '../core/sharePolicy.js';
7
+ import { isPro } from '../core/pro.js';
8
+
9
+ export async function initiateShare(keyPair, recipientPK, share, policy) {
10
+ // Validate policy
11
+ if (policy) {
12
+ const validation = validatePolicy(policy);
13
+ if (!validation.valid) {
14
+ return { ok: false, errors: validation.errors };
15
+ }
16
+ }
17
+
18
+ const envelope = shareOffer(keyPair, recipientPK, {
19
+ shareId: share.id,
20
+ name: share.name || share.path?.split('/').pop() || 'share',
21
+ type: share.type,
22
+ totalSize: share.totalSize || 0,
23
+ fileCount: share.fileCount || 0,
24
+ policy: policy || {},
25
+ });
26
+
27
+ return { ok: true, envelope };
28
+ }
29
+
30
+ export function handleShareAccept(envelope, keyPair) {
31
+ const result = verify(envelope);
32
+ if (!result.valid) return { ok: false, errors: result.errors };
33
+
34
+ const payload = envelope.payload._enc ? decrypt(envelope, keyPair) : envelope.payload;
35
+ return {
36
+ ok: true,
37
+ shareId: payload.shareId,
38
+ recipientPK: envelope.from,
39
+ recipientCurve25519PK: payload.recipientCurve25519PK,
40
+ };
41
+ }
42
+
43
+ export function wrapShareKey(shareKeyHex, recipientCurve25519PK) {
44
+ const recipientPK = Buffer.from(recipientCurve25519PK, 'hex');
45
+ const shareKeyBuf = Buffer.from(shareKeyHex, 'hex');
46
+ const cipher = Buffer.alloc(shareKeyBuf.length + sodium.crypto_box_SEALBYTES);
47
+ sodium.crypto_box_seal(cipher, shareKeyBuf, recipientPK);
48
+ return cipher.toString('hex');
49
+ }
50
+
51
+ export function unwrapShareKey(wrappedKeyHex, keyPair) {
52
+ const cipher = Buffer.from(wrappedKeyHex, 'hex');
53
+ const curve25519PK = Buffer.alloc(sodium.crypto_box_PUBLICKEYBYTES);
54
+ const curve25519SK = Buffer.alloc(sodium.crypto_box_SECRETKEYBYTES);
55
+ sodium.crypto_sign_ed25519_pk_to_curve25519(curve25519PK, keyPair.publicKey);
56
+ sodium.crypto_sign_ed25519_sk_to_curve25519(curve25519SK, keyPair.privateKey);
57
+
58
+ const plaintext = Buffer.alloc(cipher.length - sodium.crypto_box_SEALBYTES);
59
+ const ok = sodium.crypto_box_seal_open(plaintext, cipher, curve25519PK, curve25519SK);
60
+ if (!ok) throw new Error('Failed to unwrap share key');
61
+ return plaintext.toString('hex');
62
+ }
63
+
64
+ export async function handleTransferRequest(envelope, keyPair, share) {
65
+ const result = verify(envelope);
66
+ if (!result.valid) {
67
+ return { ok: false, envelope: transferDeny(keyPair, envelope.from, {
68
+ shareId: envelope.payload.shareId,
69
+ reason: 'unauthorized',
70
+ message: result.errors.join(', '),
71
+ })};
72
+ }
73
+
74
+ const shareId = envelope.payload.shareId;
75
+
76
+ // Check policy
77
+ const policy = getSharePolicy(shareId);
78
+ const access = enforcePolicy(policy, { from: envelope.from });
79
+ if (!access.allowed) {
80
+ return { ok: false, envelope: transferDeny(keyPair, envelope.from, {
81
+ shareId,
82
+ reason: access.code,
83
+ message: access.message,
84
+ })};
85
+ }
86
+
87
+ // Probe routes for best path
88
+ const routes = await probeRoutes(envelope.from);
89
+ const routeInfo = buildRouteInfo(routes);
90
+
91
+ // Wrap share key for requester
92
+ let wrappedKey = null;
93
+ if (share.shareKey) {
94
+ const recipientEdPK = Buffer.from(envelope.from, 'hex');
95
+ const recipientCurve = Buffer.alloc(sodium.crypto_box_PUBLICKEYBYTES);
96
+ sodium.crypto_sign_ed25519_pk_to_curve25519(recipientCurve, recipientEdPK);
97
+ wrappedKey = wrapShareKey(share.shareKey, recipientCurve.toString('hex'));
98
+ }
99
+
100
+ // Increment download count
101
+ incrementDownloadCount(shareId);
102
+
103
+ return {
104
+ ok: true,
105
+ envelope: transferAuthorize(keyPair, envelope.from, {
106
+ shareId,
107
+ magnet: share.magnet,
108
+ wrappedKey,
109
+ route: routeInfo,
110
+ policy: {
111
+ expiresAt: policy?.expiresAt || null,
112
+ remainingDownloads: access.remainingDownloads,
113
+ },
114
+ }),
115
+ };
116
+ }
117
+
118
+ export function createReceipt(keyPair, downloaderPK, { shareId, totalBytes, fileCount, manifestHash }) {
119
+ if (!isPro()) return null; // receipts are Pro only
120
+ return transferReceipt(keyPair, downloaderPK, {
121
+ shareId,
122
+ downloadedBy: downloaderPK,
123
+ totalBytes,
124
+ fileCount,
125
+ manifestHash,
126
+ });
127
+ }
@@ -0,0 +1,142 @@
1
+ import { isPro } from '../core/pro.js';
2
+
3
+ export const FREE_POLICY_LIMITS = {
4
+ maxDownloads: null,
5
+ maxExpiry: 24 * 60 * 60 * 1000, // 24 hours
6
+ allowScheduleWindow: false,
7
+ allowIPRestriction: false,
8
+ allowBandwidthLimit: false,
9
+ allowReceipts: false,
10
+ };
11
+
12
+ export const PRO_POLICY_LIMITS = {
13
+ maxDownloads: null,
14
+ maxExpiry: null,
15
+ allowScheduleWindow: true,
16
+ allowIPRestriction: true,
17
+ allowBandwidthLimit: true,
18
+ allowReceipts: true,
19
+ };
20
+
21
+ function getLimits() {
22
+ return isPro() ? PRO_POLICY_LIMITS : FREE_POLICY_LIMITS;
23
+ }
24
+
25
+ export function validatePolicy(policy) {
26
+ const limits = getLimits();
27
+ const errors = [];
28
+
29
+ if (policy.expiresAt) {
30
+ const ttl = new Date(policy.expiresAt).getTime() - Date.now();
31
+ if (ttl <= 0) {
32
+ errors.push('Policy expiresAt is in the past');
33
+ }
34
+ if (limits.maxExpiry && ttl > limits.maxExpiry) {
35
+ errors.push(`Free tier: max expiry is ${limits.maxExpiry / 3600000}h. Upgrade to Pro for custom expiry.`);
36
+ }
37
+ }
38
+
39
+ if (policy.scheduleWindow && !limits.allowScheduleWindow) {
40
+ errors.push('Schedule windows require Pro. Upgrade to Pro for transfer scheduling.');
41
+ }
42
+
43
+ if (policy.allowedIPs && policy.allowedIPs.length > 0 && !limits.allowIPRestriction) {
44
+ errors.push('IP restrictions require Pro. Upgrade to Pro for IP-based access control.');
45
+ }
46
+
47
+ if (policy.bandwidthLimit && !limits.allowBandwidthLimit) {
48
+ errors.push('Bandwidth limits require Pro.');
49
+ }
50
+
51
+ if (policy.requireReceipt && !limits.allowReceipts) {
52
+ errors.push('Download receipts require Pro.');
53
+ }
54
+
55
+ return { valid: errors.length === 0, errors };
56
+ }
57
+
58
+ export function enforcePolicy(policy, request) {
59
+ if (!policy) return { allowed: true };
60
+
61
+ // Expiry check
62
+ if (policy.expiresAt && new Date(policy.expiresAt) < new Date()) {
63
+ return { allowed: false, code: 'E_EXPIRED', message: `Share expired on ${policy.expiresAt}` };
64
+ }
65
+
66
+ // Download limit
67
+ if (policy.maxDownloads != null && (policy.downloadCount || 0) >= policy.maxDownloads) {
68
+ return {
69
+ allowed: false,
70
+ code: 'E_POLICY_VIOLATION',
71
+ message: `Download limit reached (${policy.maxDownloads})`,
72
+ };
73
+ }
74
+
75
+ // Recipient check
76
+ if (policy.allowedRecipients && policy.allowedRecipients.length > 0) {
77
+ if (!request?.from || !policy.allowedRecipients.includes(request.from)) {
78
+ return { allowed: false, code: 'E_UNAUTHORIZED', message: 'Not an authorized recipient' };
79
+ }
80
+ }
81
+
82
+ // IP check
83
+ if (policy.allowedIPs && policy.allowedIPs.length > 0) {
84
+ if (!request?.ip || !matchIP(request.ip, policy.allowedIPs)) {
85
+ return { allowed: false, code: 'E_POLICY_VIOLATION', message: 'IP not in allowed list' };
86
+ }
87
+ }
88
+
89
+ // Schedule window
90
+ if (policy.scheduleWindow) {
91
+ if (!isWithinWindow(policy.scheduleWindow)) {
92
+ return {
93
+ allowed: false,
94
+ code: 'E_POLICY_VIOLATION',
95
+ message: `Transfers allowed ${policy.scheduleWindow.start}-${policy.scheduleWindow.end} ${policy.scheduleWindow.timezone || 'UTC'}`,
96
+ };
97
+ }
98
+ }
99
+
100
+ return {
101
+ allowed: true,
102
+ remainingDownloads: policy.maxDownloads != null ? policy.maxDownloads - (policy.downloadCount || 0) : null,
103
+ };
104
+ }
105
+
106
+ function matchIP(ip, allowedIPs) {
107
+ for (const allowed of allowedIPs) {
108
+ if (allowed.includes('/')) {
109
+ if (isInSubnet(ip, allowed)) return true;
110
+ } else {
111
+ if (ip === allowed) return true;
112
+ }
113
+ }
114
+ return false;
115
+ }
116
+
117
+ function isInSubnet(ip, cidr) {
118
+ const [subnet, bits] = cidr.split('/');
119
+ const mask = ~((1 << (32 - parseInt(bits))) - 1) >>> 0;
120
+ const ipNum = ipToNum(ip);
121
+ const subnetNum = ipToNum(subnet);
122
+ return (ipNum & mask) === (subnetNum & mask);
123
+ }
124
+
125
+ function ipToNum(ip) {
126
+ return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet), 0) >>> 0;
127
+ }
128
+
129
+ function isWithinWindow(window) {
130
+ const now = new Date();
131
+ const [startH, startM] = window.start.split(':').map(Number);
132
+ const [endH, endM] = window.end.split(':').map(Number);
133
+ const currentMinutes = now.getUTCHours() * 60 + now.getUTCMinutes();
134
+ const startMinutes = startH * 60 + startM;
135
+ const endMinutes = endH * 60 + endM;
136
+
137
+ if (startMinutes <= endMinutes) {
138
+ return currentMinutes >= startMinutes && currentMinutes < endMinutes;
139
+ }
140
+ // Overnight window (e.g. 22:00 - 06:00)
141
+ return currentMinutes >= startMinutes || currentMinutes < endMinutes;
142
+ }
@@ -0,0 +1,86 @@
1
+ import { getNearbyPeers } from '../core/mdnsService.js';
2
+ import { fetchTurnCredentials } from '../core/discoveryClient.js';
3
+ import { isPro } from '../core/pro.js';
4
+ import { sendRequest } from '../core/signalingServer.js';
5
+
6
+ export const ROUTE_PRIORITY = ['lan', 'direct', 'relay'];
7
+
8
+ const RELAY_LIMITS = {
9
+ free: { bandwidth: 5 * 1024 * 1024, sessionDuration: 3600, concurrent: 1 },
10
+ pro: { bandwidth: null, sessionDuration: 86400, concurrent: 10 },
11
+ };
12
+
13
+ export function getRelayLimits() {
14
+ return isPro() ? RELAY_LIMITS.pro : RELAY_LIMITS.free;
15
+ }
16
+
17
+ export async function probeRoutes(targetPK, { lanAddresses } = {}) {
18
+ const results = { lan: null, direct: null, relay: null };
19
+
20
+ // LAN probe — check if target is on local network via mDNS (pure TCP)
21
+ try {
22
+ const nearby = getNearbyPeers();
23
+ const lanPeer = nearby.find(p => p.publicKey === targetPK);
24
+ if (lanPeer) {
25
+ const start = Date.now();
26
+ const resp = await sendRequest(lanPeer.ip, 7474, { type: 'status' }, 2000);
27
+ if (resp.ok) {
28
+ results.lan = { reachable: true, latency: Date.now() - start, address: `${lanPeer.ip}:7474` };
29
+ }
30
+ }
31
+ } catch {
32
+ results.lan = { reachable: false };
33
+ }
34
+
35
+ // Direct probe — try known addresses (pure TCP)
36
+ if (lanAddresses?.length) {
37
+ for (const addr of lanAddresses) {
38
+ try {
39
+ const [host, port] = addr.includes(':') ? addr.split(':') : [addr, 7474];
40
+ const start = Date.now();
41
+ const resp = await sendRequest(host, parseInt(port, 10), { type: 'status' }, 3000);
42
+ if (resp.ok) {
43
+ results.direct = { reachable: true, latency: Date.now() - start, address: addr };
44
+ break;
45
+ }
46
+ } catch {}
47
+ }
48
+ }
49
+ if (!results.direct) results.direct = { reachable: false };
50
+
51
+ // Relay is always available (via TURN)
52
+ try {
53
+ const creds = await fetchTurnCredentials();
54
+ if (creds) {
55
+ results.relay = { reachable: true, latency: null, credentials: creds };
56
+ } else {
57
+ results.relay = { reachable: false };
58
+ }
59
+ } catch {
60
+ results.relay = { reachable: false };
61
+ }
62
+
63
+ return results;
64
+ }
65
+
66
+ export function selectRoute(probeResults) {
67
+ for (const route of ROUTE_PRIORITY) {
68
+ if (probeResults[route]?.reachable) {
69
+ return {
70
+ type: route,
71
+ ...probeResults[route],
72
+ };
73
+ }
74
+ }
75
+ return null;
76
+ }
77
+
78
+ export function buildRouteInfo(probeResults) {
79
+ const selected = selectRoute(probeResults);
80
+ return {
81
+ preferred: selected?.type || null,
82
+ alternatives: ROUTE_PRIORITY.filter(r => r !== selected?.type && probeResults[r]?.reachable),
83
+ lanAddress: probeResults.lan?.address || null,
84
+ relayToken: probeResults.relay?.credentials || null,
85
+ };
86
+ }