peer-term 1.3.10 → 1.4.1
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/bin/peer-term.js +1 -1
- package/package.json +2 -1
- package/src/cli.js +128 -0
- package/src/config.js +91 -0
- package/src/file-transfer.js +230 -0
- package/src/index.js +1037 -1849
- package/src/prompts.js +143 -0
- package/src/ui.js +25 -1
- package/src/utils.js +113 -0
- package/src/viewer.js +259 -0
package/bin/peer-term.js
CHANGED
|
@@ -47,7 +47,7 @@ process.on('unhandledRejection', (reason) => {
|
|
|
47
47
|
|
|
48
48
|
// ─── Launch Main ─────────────────────────────────────────────────────────────
|
|
49
49
|
|
|
50
|
-
import('../src/
|
|
50
|
+
import('../src/cli.js').catch((err) => {
|
|
51
51
|
logFatalError('Failed to start PeerTerm', err);
|
|
52
52
|
process.exit(1);
|
|
53
53
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peer-term",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Share your terminal instantly using a 6-digit code. Encrypted. No config.",
|
|
6
6
|
"repository": {
|
|
@@ -87,6 +87,7 @@
|
|
|
87
87
|
"minimist": "^1.2.8",
|
|
88
88
|
"node-datachannel": "^0.32.3",
|
|
89
89
|
"node-pty": "^1.0.0",
|
|
90
|
+
"qrcode-terminal": "^0.12.0",
|
|
90
91
|
"ws": "^8.18.0"
|
|
91
92
|
}
|
|
92
93
|
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PeerTerm CLI — Interactive session manager
|
|
3
|
+
*
|
|
4
|
+
* Top-level orchestration:
|
|
5
|
+
* 1. Detects shell, resolves start path, runs interactive prompts
|
|
6
|
+
* 2. Creates a SessionManager and starts the first session
|
|
7
|
+
* 3. Provides a readline-based menu (n/l/k/a/q) for managing sessions
|
|
8
|
+
* 4. Handles SIGINT and global error handlers
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import readline from 'readline';
|
|
12
|
+
import logger from './logger.js';
|
|
13
|
+
import { printBanner } from './ui.js';
|
|
14
|
+
import { argv, RELAY_URLS } from './config.js';
|
|
15
|
+
import { formatDuration, detectShell, resolveStartPath } from './utils.js';
|
|
16
|
+
import { getExpiryInteractive, getRejoinInteractive } from './prompts.js';
|
|
17
|
+
import { SessionManager } from './index.js';
|
|
18
|
+
|
|
19
|
+
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
async function main() {
|
|
22
|
+
const shell = detectShell();
|
|
23
|
+
const readOnly = argv.readonly;
|
|
24
|
+
const startPath = resolveStartPath(argv.path);
|
|
25
|
+
|
|
26
|
+
// Print startup banner
|
|
27
|
+
printBanner();
|
|
28
|
+
|
|
29
|
+
// Interactive prompts — always ask unless flags are passed or non-interactive
|
|
30
|
+
const expiryMs = await getExpiryInteractive();
|
|
31
|
+
const rejoinMs = await getRejoinInteractive();
|
|
32
|
+
console.log('');
|
|
33
|
+
|
|
34
|
+
logger.info(`Shell: ${shell}`);
|
|
35
|
+
logger.info(`Relays: ${RELAY_URLS.join(', ')}`);
|
|
36
|
+
logger.info(`Path: ${startPath}`);
|
|
37
|
+
logger.info(`Expiry: ${formatDuration(expiryMs)}`);
|
|
38
|
+
logger.info(`Rejoin Window: ${formatDuration(rejoinMs)}`);
|
|
39
|
+
if (readOnly) logger.info('Mode: READ-ONLY');
|
|
40
|
+
if (argv.verbose) logger.info('Verbose logging enabled');
|
|
41
|
+
console.log('');
|
|
42
|
+
|
|
43
|
+
const manager = new SessionManager(shell, expiryMs, rejoinMs, readOnly, startPath);
|
|
44
|
+
|
|
45
|
+
// Create first session automatically
|
|
46
|
+
const firstCode = await manager.createSession();
|
|
47
|
+
if (!firstCode) {
|
|
48
|
+
logger.error('Failed to start. Is the relay server running?');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── Interactive CLI menu ──────────────────────────────────────────
|
|
53
|
+
console.log(' ─────────────────────────────────────────');
|
|
54
|
+
console.log(' Commands:');
|
|
55
|
+
console.log(' [n] New session');
|
|
56
|
+
console.log(' [l] List sessions');
|
|
57
|
+
console.log(' [a <code>] Authorize verified fingerprint');
|
|
58
|
+
console.log(' [k <code>] Kill session');
|
|
59
|
+
console.log(' [q] Quit all');
|
|
60
|
+
console.log(' ─────────────────────────────────────────');
|
|
61
|
+
console.log('');
|
|
62
|
+
|
|
63
|
+
const rl = readline.createInterface({
|
|
64
|
+
input: process.stdin,
|
|
65
|
+
output: process.stdout,
|
|
66
|
+
prompt: ' > ',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
rl.prompt();
|
|
70
|
+
|
|
71
|
+
rl.on('line', async (line) => {
|
|
72
|
+
const input = line.trim();
|
|
73
|
+
|
|
74
|
+
if (input === 'n') {
|
|
75
|
+
logger.info('Creating new session...');
|
|
76
|
+
await manager.createSession();
|
|
77
|
+
} else if (input === 'l') {
|
|
78
|
+
manager.listSessions();
|
|
79
|
+
} else if (input.startsWith('a ')) {
|
|
80
|
+
const code = input.slice(2).trim();
|
|
81
|
+
await manager.authorizeSession(code);
|
|
82
|
+
} else if (input.startsWith('k ')) {
|
|
83
|
+
const code = input.slice(2).trim();
|
|
84
|
+
manager.killSession(code);
|
|
85
|
+
} else if (input === 'q') {
|
|
86
|
+
logger.info('Shutting down all sessions...');
|
|
87
|
+
manager.killAll();
|
|
88
|
+
rl.close();
|
|
89
|
+
process.exit(0);
|
|
90
|
+
} else if (input) {
|
|
91
|
+
console.log(' Unknown command. Use n, l, a <code>, k <code>, or q.');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
rl.prompt();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
rl.on('close', () => {
|
|
98
|
+
manager.killAll();
|
|
99
|
+
process.exit(0);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
process.on('SIGINT', () => {
|
|
103
|
+
console.log('');
|
|
104
|
+
logger.info('Shutting down...');
|
|
105
|
+
manager.killAll();
|
|
106
|
+
rl.close();
|
|
107
|
+
process.exit(0);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── Global Error Handling ───────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
process.on('uncaughtException', (err) => {
|
|
114
|
+
logger.error(`Uncaught exception: ${err.message}`, err);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
process.on('unhandledRejection', (reason) => {
|
|
119
|
+
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
120
|
+
logger.error(`Unhandled rejection: ${err.message}`, err);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ─── Run ─────────────────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
main().catch((err) => {
|
|
126
|
+
logger.error(`Fatal error: ${err.message}`, err);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
});
|
package/src/config.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PeerTerm — Configuration Module
|
|
3
|
+
*
|
|
4
|
+
* CLI argument parsing, flag detection, and static configuration constants.
|
|
5
|
+
* Single source of truth for user-provided options and app-wide constants.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
import * as dotenv from 'dotenv';
|
|
12
|
+
import minimist from 'minimist';
|
|
13
|
+
import logger from './logger.js';
|
|
14
|
+
import { printHelp, printVersion } from './ui.js';
|
|
15
|
+
|
|
16
|
+
// ─── File paths ──────────────────────────────────────────────────────────────
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
const __dirname = path.dirname(__filename);
|
|
19
|
+
|
|
20
|
+
// Load .env from host/ directory (parent of src/)
|
|
21
|
+
dotenv.config({ path: path.join(__dirname, '..', '.env') });
|
|
22
|
+
|
|
23
|
+
// ─── Version ─────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
let version = 'unknown';
|
|
26
|
+
try {
|
|
27
|
+
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
28
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
29
|
+
version = pkg.version;
|
|
30
|
+
} catch {}
|
|
31
|
+
|
|
32
|
+
// ─── CLI Argument Parsing ────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
const argv = minimist(process.argv.slice(2), {
|
|
35
|
+
boolean: ['readonly', 'verbose', 'help', 'version', 'secure'],
|
|
36
|
+
string: ['expiry', 'rejoin', 'relay', 'path'],
|
|
37
|
+
alias: { h: 'help', v: 'version', V: 'verbose' },
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Detect if --expiry or --rejoin were explicitly passed on the command line
|
|
41
|
+
const expiryFlagPassed = process.argv.some(a => a === '--expiry' || a.startsWith('--expiry='));
|
|
42
|
+
const rejoinFlagPassed = process.argv.some(a => a === '--rejoin' || a.startsWith('--rejoin='));
|
|
43
|
+
|
|
44
|
+
// Handle --help
|
|
45
|
+
if (argv.help) {
|
|
46
|
+
printHelp();
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Handle --version
|
|
51
|
+
if (argv.version) {
|
|
52
|
+
printVersion();
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Enable verbose logging
|
|
57
|
+
if (argv.verbose) {
|
|
58
|
+
logger.setVerbose(true);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── Static Configuration ────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
const DEFAULT_RELAYS = [
|
|
64
|
+
'wss://relay.dhananjaybalekar.in'
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const RELAY_URLS = argv.relay
|
|
68
|
+
? argv.relay.split(',').map(s => s.trim())
|
|
69
|
+
: (process.env.RELAY_URL ? process.env.RELAY_URL.split(',').map(s => s.trim()) : DEFAULT_RELAYS);
|
|
70
|
+
|
|
71
|
+
const HEARTBEAT_INTERVAL_MS = 5000;
|
|
72
|
+
const MAX_MISSED_PINGS = 2;
|
|
73
|
+
|
|
74
|
+
const UPLOAD_MAX_FILE_SIZE = 100 * 1024 * 1024; // 100 MB max
|
|
75
|
+
const UPLOAD_MAX_CHUNKS_PER_FILE = Math.ceil(UPLOAD_MAX_FILE_SIZE / 16384);
|
|
76
|
+
const UPLOAD_MAX_CONCURRENT = 3;
|
|
77
|
+
|
|
78
|
+
// ─── Exports ─────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
export {
|
|
81
|
+
argv,
|
|
82
|
+
version,
|
|
83
|
+
expiryFlagPassed,
|
|
84
|
+
rejoinFlagPassed,
|
|
85
|
+
RELAY_URLS,
|
|
86
|
+
HEARTBEAT_INTERVAL_MS,
|
|
87
|
+
MAX_MISSED_PINGS,
|
|
88
|
+
UPLOAD_MAX_FILE_SIZE,
|
|
89
|
+
UPLOAD_MAX_CHUNKS_PER_FILE,
|
|
90
|
+
UPLOAD_MAX_CONCURRENT,
|
|
91
|
+
};
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PeerTerm — File Transfer Handler
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates all file upload protocol logic:
|
|
5
|
+
* - Routing file-start, file-chunk, file-end, file-cancel messages
|
|
6
|
+
* - Chunk validation and size enforcement
|
|
7
|
+
* - File reassembly and disk writes
|
|
8
|
+
* - Ack/error responses back to the client
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import os from 'os';
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { encrypt } from './crypto.js';
|
|
15
|
+
import {
|
|
16
|
+
UPLOAD_MAX_FILE_SIZE,
|
|
17
|
+
UPLOAD_MAX_CHUNKS_PER_FILE,
|
|
18
|
+
UPLOAD_MAX_CONCURRENT,
|
|
19
|
+
} from './config.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} SessionContext
|
|
23
|
+
* @property {CryptoKey|null} sharedKey - Current E2E shared key
|
|
24
|
+
* @property {boolean} readOnly - Whether the session is read-only
|
|
25
|
+
* @property {Object|null} ptyProcess - The PTY process instance
|
|
26
|
+
* @property {function} log - Session-scoped info logger
|
|
27
|
+
* @property {function} logDebug - Session-scoped debug logger
|
|
28
|
+
* @property {function} sendEncrypted - Send encrypted payload to client
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
export class FileTransferHandler {
|
|
32
|
+
/**
|
|
33
|
+
* @param {SessionContext} ctx - Session context for callbacks and state access
|
|
34
|
+
*/
|
|
35
|
+
constructor(ctx) {
|
|
36
|
+
this._ctx = ctx;
|
|
37
|
+
|
|
38
|
+
/** @type {Object.<string, {name: string, size: number, totalChunks: number, receivedSize: number, chunks: Map}>} */
|
|
39
|
+
this._incomingFiles = {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Update the session context (e.g. after key re-exchange).
|
|
44
|
+
* @param {SessionContext} ctx
|
|
45
|
+
*/
|
|
46
|
+
updateContext(ctx) {
|
|
47
|
+
this._ctx = ctx;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Clean up all in-progress transfers.
|
|
52
|
+
*/
|
|
53
|
+
cleanup() {
|
|
54
|
+
this._incomingFiles = {};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Route a file transfer message to the appropriate handler.
|
|
59
|
+
*
|
|
60
|
+
* @param {Object} msg - Parsed file message with type, id, and payload fields
|
|
61
|
+
*/
|
|
62
|
+
handleMessage(msg) {
|
|
63
|
+
// Defense-in-depth: reject all file operations in read-only sessions
|
|
64
|
+
if (this._ctx.readOnly) {
|
|
65
|
+
this._sendError(msg.id, 'File uploads are disabled in read-only mode');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
switch (msg.type) {
|
|
70
|
+
case 'file-start': {
|
|
71
|
+
// Validate required fields
|
|
72
|
+
if (!msg.id || !msg.name || msg.size === undefined || msg.totalChunks === undefined) {
|
|
73
|
+
this._sendError(msg.id, 'Invalid file-start message');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (msg.size > UPLOAD_MAX_FILE_SIZE) {
|
|
78
|
+
this._sendError(msg.id, `File exceeds maximum size of ${UPLOAD_MAX_FILE_SIZE / 1024 / 1024}MB`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (msg.totalChunks > UPLOAD_MAX_CHUNKS_PER_FILE) {
|
|
83
|
+
this._sendError(msg.id, 'File requires too many chunks');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (Object.keys(this._incomingFiles).length >= UPLOAD_MAX_CONCURRENT) {
|
|
88
|
+
this._sendError(msg.id, 'Too many concurrent uploads');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this._ctx.log(`File upload started: "${msg.name}" (${msg.size} bytes, ${msg.totalChunks} chunks)`);
|
|
93
|
+
this._incomingFiles[msg.id] = {
|
|
94
|
+
name: msg.name,
|
|
95
|
+
size: msg.size,
|
|
96
|
+
totalChunks: msg.totalChunks,
|
|
97
|
+
receivedSize: 0,
|
|
98
|
+
chunks: new Map(),
|
|
99
|
+
};
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
case 'file-chunk': {
|
|
103
|
+
const transfer = this._incomingFiles[msg.id];
|
|
104
|
+
if (!transfer) {
|
|
105
|
+
this._sendError(msg.id, 'Unknown transfer ID');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const chunkData = Buffer.from(msg.data, 'base64');
|
|
110
|
+
|
|
111
|
+
if (transfer.chunks.size >= transfer.totalChunks) {
|
|
112
|
+
this._sendError(msg.id, 'Too many chunks received');
|
|
113
|
+
delete this._incomingFiles[msg.id];
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (transfer.receivedSize + chunkData.length > transfer.size) {
|
|
118
|
+
this._sendError(msg.id, 'Received size exceeds declared size');
|
|
119
|
+
delete this._incomingFiles[msg.id];
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
transfer.chunks.set(msg.index, chunkData);
|
|
124
|
+
transfer.receivedSize += chunkData.length;
|
|
125
|
+
this._ctx.logDebug(`File chunk ${msg.index + 1}/${transfer.totalChunks} received for "${transfer.name}"`);
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case 'file-end': {
|
|
129
|
+
this._finalizeFile(msg.id);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case 'file-cancel': {
|
|
133
|
+
if (this._incomingFiles[msg.id]) {
|
|
134
|
+
delete this._incomingFiles[msg.id];
|
|
135
|
+
this._ctx.log(`File upload cancelled by client: ${msg.id}`);
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Reassemble all chunks in order and write the completed file to disk.
|
|
144
|
+
* Saves to ~/peerterm-uploads/<filename>.
|
|
145
|
+
*/
|
|
146
|
+
async _finalizeFile(id) {
|
|
147
|
+
const transfer = this._incomingFiles[id];
|
|
148
|
+
if (!transfer) {
|
|
149
|
+
this._sendError(id, 'Unknown transfer ID');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check all chunks are present
|
|
154
|
+
for (let i = 0; i < transfer.totalChunks; i++) {
|
|
155
|
+
if (!transfer.chunks.has(i)) {
|
|
156
|
+
this._ctx.log(`File "${transfer.name}" missing chunk ${i}`);
|
|
157
|
+
this._sendError(id, `Missing chunk ${i}`);
|
|
158
|
+
delete this._incomingFiles[id];
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Reassemble in order using Buffer.concat
|
|
164
|
+
const ordered = [];
|
|
165
|
+
for (let i = 0; i < transfer.totalChunks; i++) {
|
|
166
|
+
ordered.push(transfer.chunks.get(i));
|
|
167
|
+
}
|
|
168
|
+
const fileData = Buffer.concat(ordered);
|
|
169
|
+
|
|
170
|
+
// Determine save path: ~/peerterm-uploads/<filename>
|
|
171
|
+
const homeDir = os.homedir();
|
|
172
|
+
const uploadDir = path.join(homeDir, 'peerterm-uploads');
|
|
173
|
+
// Sanitize filename to prevent path traversal (e.g., "../../../etc/passwd")
|
|
174
|
+
const baseName = path.basename(transfer.name);
|
|
175
|
+
if (!baseName || baseName === '.' || baseName === '..') {
|
|
176
|
+
this._sendError(id, 'Invalid filename');
|
|
177
|
+
delete this._incomingFiles[id];
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const savePath = path.join(uploadDir, baseName);
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
await fs.promises.mkdir(uploadDir, { recursive: true });
|
|
184
|
+
await fs.promises.writeFile(savePath, fileData);
|
|
185
|
+
|
|
186
|
+
this._ctx.log(`File saved: ${savePath}`);
|
|
187
|
+
|
|
188
|
+
// Write green ANSI message into the PTY to show the save path
|
|
189
|
+
if (this._ctx.ptyProcess) {
|
|
190
|
+
this._ctx.ptyProcess.write(`\r\n\x1b[32m[PeerTerm] File saved: ${savePath}\x1b[0m\r\n`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Send ack to client
|
|
194
|
+
this._sendAck(id, transfer.name);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
this._ctx.log(`Failed to save file "${transfer.name}": ${err.message}`);
|
|
197
|
+
this._sendError(id, `Save failed: ${err.message}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
delete this._incomingFiles[id];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Send file-ack to the client confirming successful file save.
|
|
205
|
+
*/
|
|
206
|
+
async _sendAck(id, name) {
|
|
207
|
+
if (!this._ctx.sharedKey) return;
|
|
208
|
+
try {
|
|
209
|
+
const ackJson = JSON.stringify({ type: 'file-ack', id, name });
|
|
210
|
+
const payload = await encrypt(this._ctx.sharedKey, ackJson);
|
|
211
|
+
this._ctx.sendEncrypted(payload);
|
|
212
|
+
} catch (err) {
|
|
213
|
+
this._ctx.log(`Failed to send file-ack: ${err.message}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Send file-error to the client with an error message.
|
|
219
|
+
*/
|
|
220
|
+
async _sendError(id, msg) {
|
|
221
|
+
if (!this._ctx.sharedKey) return;
|
|
222
|
+
try {
|
|
223
|
+
const errJson = JSON.stringify({ type: 'file-error', id, msg });
|
|
224
|
+
const payload = await encrypt(this._ctx.sharedKey, errJson);
|
|
225
|
+
this._ctx.sendEncrypted(payload);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
this._ctx.log(`Failed to send file-error: ${err.message}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|