peer-term 1.2.13 → 1.3.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 (2) hide show
  1. package/package.json +10 -1
  2. package/src/index.js +145 -2
package/package.json CHANGED
@@ -1,8 +1,17 @@
1
1
  {
2
2
  "name": "peer-term",
3
- "version": "1.2.13",
3
+ "version": "1.3.0",
4
4
  "license": "MIT",
5
5
  "description": "Share your terminal instantly using a 6-digit code. Encrypted. No config.",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/jay6205/Peer_Term",
9
+ "directory": "host"
10
+ },
11
+ "homepage": "https://github.com/jay6205/Peer_Term#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/jay6205/Peer_Term/issues"
14
+ },
6
15
  "keywords": [
7
16
  "terminal",
8
17
  "pty",
package/src/index.js CHANGED
@@ -336,6 +336,10 @@ class Session {
336
336
  this.webrtc = null;
337
337
  this.useDataChannel = false;
338
338
 
339
+ // File upload: in-progress file transfers from client
340
+ // Shape: { [id]: { name, size, totalChunks, chunks: Map<index, Buffer> } }
341
+ this._incomingFiles = {};
342
+
339
343
  // Local TCP viewer (new terminal window)
340
344
  this.viewerServer = null;
341
345
  this.viewerSocket = null;
@@ -820,6 +824,129 @@ class Session {
820
824
  }
821
825
  }
822
826
 
827
+ // ─── File Upload Handling ─────────────────────────────────────────────
828
+
829
+ /**
830
+ * Route file transfer messages (file-start, file-chunk, file-end).
831
+ * All file messages arrive over the DataChannel only.
832
+ */
833
+ _handleFileMessage(msg) {
834
+ switch (msg.type) {
835
+ case 'file-start': {
836
+ // Validate required fields
837
+ if (!msg.id || !msg.name || !msg.size || !msg.totalChunks) {
838
+ this._sendFileError(msg.id, 'Invalid file-start message');
839
+ return;
840
+ }
841
+ this.log(`File upload started: "${msg.name}" (${msg.size} bytes, ${msg.totalChunks} chunks)`);
842
+ this._incomingFiles[msg.id] = {
843
+ name: msg.name,
844
+ size: msg.size,
845
+ totalChunks: msg.totalChunks,
846
+ chunks: new Map(),
847
+ };
848
+ break;
849
+ }
850
+ case 'file-chunk': {
851
+ const transfer = this._incomingFiles[msg.id];
852
+ if (!transfer) {
853
+ this._sendFileError(msg.id, 'Unknown transfer ID');
854
+ return;
855
+ }
856
+ // data is base64-encoded raw bytes
857
+ transfer.chunks.set(msg.index, Buffer.from(msg.data, 'base64'));
858
+ this.logDebug(`File chunk ${msg.index + 1}/${transfer.totalChunks} received for "${transfer.name}"`);
859
+ break;
860
+ }
861
+ case 'file-end': {
862
+ this._finalizeFile(msg.id);
863
+ break;
864
+ }
865
+ }
866
+ }
867
+
868
+ /**
869
+ * Reassemble all chunks in order and write the completed file to disk.
870
+ * Saves to ~/peerterm-uploads/<filename>.
871
+ */
872
+ async _finalizeFile(id) {
873
+ const transfer = this._incomingFiles[id];
874
+ if (!transfer) {
875
+ this._sendFileError(id, 'Unknown transfer ID');
876
+ return;
877
+ }
878
+
879
+ // Check all chunks are present
880
+ for (let i = 0; i < transfer.totalChunks; i++) {
881
+ if (!transfer.chunks.has(i)) {
882
+ this.log(`File "${transfer.name}" missing chunk ${i}`);
883
+ this._sendFileError(id, `Missing chunk ${i}`);
884
+ delete this._incomingFiles[id];
885
+ return;
886
+ }
887
+ }
888
+
889
+ // Reassemble in order using Buffer.concat
890
+ const ordered = [];
891
+ for (let i = 0; i < transfer.totalChunks; i++) {
892
+ ordered.push(transfer.chunks.get(i));
893
+ }
894
+ const fileData = Buffer.concat(ordered);
895
+
896
+ // Determine save path: ~/peerterm-uploads/<filename>
897
+ const homeDir = os.homedir();
898
+ const uploadDir = path.join(homeDir, 'peerterm-uploads');
899
+ const savePath = path.join(uploadDir, transfer.name);
900
+
901
+ try {
902
+ await fs.promises.mkdir(uploadDir, { recursive: true });
903
+ await fs.promises.writeFile(savePath, fileData);
904
+
905
+ this.log(`File saved: ${savePath}`);
906
+
907
+ // Write green ANSI message into the PTY to show the save path
908
+ if (this.ptyProcess) {
909
+ this.ptyProcess.write(`\r\n\x1b[32m[PeerTerm] File saved: ${savePath}\x1b[0m\r\n`);
910
+ }
911
+
912
+ // Send ack to client
913
+ this._sendFileAck(id, transfer.name);
914
+ } catch (err) {
915
+ this.log(`Failed to save file "${transfer.name}": ${err.message}`);
916
+ this._sendFileError(id, `Save failed: ${err.message}`);
917
+ }
918
+
919
+ delete this._incomingFiles[id];
920
+ }
921
+
922
+ /**
923
+ * Send file-ack to the client confirming successful file save.
924
+ */
925
+ async _sendFileAck(id, name) {
926
+ if (!this.sharedKey) return;
927
+ try {
928
+ const ackJson = JSON.stringify({ type: 'file-ack', id, name });
929
+ const payload = await encrypt(this.sharedKey, ackJson);
930
+ this._sendEncryptedToClient(payload);
931
+ } catch (err) {
932
+ this.log(`Failed to send file-ack: ${err.message}`);
933
+ }
934
+ }
935
+
936
+ /**
937
+ * Send file-error to the client with an error message.
938
+ */
939
+ async _sendFileError(id, msg) {
940
+ if (!this.sharedKey) return;
941
+ try {
942
+ const errJson = JSON.stringify({ type: 'file-error', id, msg });
943
+ const payload = await encrypt(this.sharedKey, errJson);
944
+ this._sendEncryptedToClient(payload);
945
+ } catch (err) {
946
+ this.log(`Failed to send file-error: ${err.message}`);
947
+ }
948
+ }
949
+
823
950
  _startViewerServer() {
824
951
  // Generate a random nonce for viewer authentication
825
952
  this.viewerToken = crypto.randomBytes(16).toString('hex');
@@ -1221,6 +1348,8 @@ class Session {
1221
1348
  this.intentionalClose = true;
1222
1349
  this.stopHeartbeat();
1223
1350
  this._stopReconnecting();
1351
+ // Clean up in-progress file uploads
1352
+ this._incomingFiles = {};
1224
1353
  // Clean up viewer
1225
1354
  if (this.viewerSocket) {
1226
1355
  try { this.viewerSocket.destroy(); } catch {}
@@ -1267,7 +1396,7 @@ class Session {
1267
1396
  });
1268
1397
 
1269
1398
  this.webrtc.onMessage(async (data) => {
1270
- if (!this.sharedKey || !this.ptyProcess) return;
1399
+ if (!this.sharedKey) return;
1271
1400
  try {
1272
1401
  // Check if this is a tagged message (resize)
1273
1402
  let parsed;
@@ -1286,8 +1415,22 @@ class Session {
1286
1415
  return;
1287
1416
  }
1288
1417
 
1289
- // Normal encrypted terminal data
1418
+ // Normal encrypted data — decrypt first
1290
1419
  const plaintext = await decrypt(this.sharedKey, data);
1420
+
1421
+ // Check if decrypted message is a file transfer message (file-start, file-chunk, file-end)
1422
+ try {
1423
+ const msg = JSON.parse(plaintext);
1424
+ if (msg && typeof msg.type === 'string' && msg.type.startsWith('file-')) {
1425
+ this._handleFileMessage(msg);
1426
+ return;
1427
+ }
1428
+ } catch {
1429
+ // Not JSON — normal keystroke data, fall through
1430
+ }
1431
+
1432
+ // Normal keystroke — drop if read-only or no PTY
1433
+ if (!this.ptyProcess) return;
1291
1434
  if (this.readOnly) return;
1292
1435
  this.ptyProcess.write(plaintext);
1293
1436
  } catch (err) {