peer-term 1.0.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/README.md +52 -0
- package/bin/peer-term.js +53 -0
- package/package.json +47 -0
- package/src/check-deps.js +83 -0
- package/src/crypto.js +147 -0
- package/src/index.js +839 -0
- package/src/logger.js +102 -0
- package/src/session-viewer.js +79 -0
- package/src/ui.js +137 -0
- package/src/webrtc.js +309 -0
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# PeerTerm
|
|
2
|
+
|
|
3
|
+
Share your terminal instantly over WebRTC using a 6-digit code. Fully end-to-end encrypted. No configuration or accounts required.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
You don't even need to install it. Just run:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx peer-term
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This will start a terminal sharing session and give you a 6-digit code.
|
|
14
|
+
|
|
15
|
+
Share this code with your peer. They can view your terminal by going to https://peer-term-relay-production-9b7a.up.railway.app or https://peer-term-relay.onrender.com and entering the code.
|
|
16
|
+
|
|
17
|
+
## Global Installation
|
|
18
|
+
|
|
19
|
+
If you prefer to install it globally:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g peer-term
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then you can just run:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
peer-term
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
peer-term # starts at home directory
|
|
35
|
+
peer-term --path ~/projects # starts at ~/projects
|
|
36
|
+
peer-term --path . # starts in current directory
|
|
37
|
+
peer-term --readonly # view-only session
|
|
38
|
+
peer-term --expiry 10m # custom expiry time
|
|
39
|
+
peer-term --verbose # enable debug logging
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
- **No Config**: Works instantly. No port forwarding or firewall configuration needed.
|
|
45
|
+
- **End-to-End Encrypted**: Terminal data is encrypted locally using AES-GCM before being sent.
|
|
46
|
+
- **WebRTC P2P**: Creates a direct Peer-to-Peer connection when possible for minimal latency.
|
|
47
|
+
- **Read-Only Mode**: Guests can view your terminal but cannot type commands, ensuring your system remains secure.
|
|
48
|
+
- **Custom Start Path**: Set the starting directory with `--path` so guests land right where you want them.
|
|
49
|
+
|
|
50
|
+
## License
|
|
51
|
+
|
|
52
|
+
MIT
|
package/bin/peer-term.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PeerTerm — CLI Entry Point
|
|
5
|
+
*
|
|
6
|
+
* This is the shebang entry point for `npx peer-term` and global installs.
|
|
7
|
+
* It imports the main host agent and handles uncaught exceptions gracefully.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import os from 'os';
|
|
13
|
+
|
|
14
|
+
// ─── Global Error Handling ───────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
const LOG_DIR = path.join(os.homedir(), '.peerterm', 'logs');
|
|
17
|
+
const ERROR_LOG = path.join(LOG_DIR, 'error.log');
|
|
18
|
+
|
|
19
|
+
function logFatalError(label, err) {
|
|
20
|
+
// Write full stack to file
|
|
21
|
+
try {
|
|
22
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
23
|
+
const ts = new Date().toISOString();
|
|
24
|
+
const entry = `[${ts}] ${label}: ${err.message || err}\n${err.stack || ''}\n\n`;
|
|
25
|
+
fs.appendFileSync(ERROR_LOG, entry);
|
|
26
|
+
} catch {
|
|
27
|
+
// Can't even log — nothing to do
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Print friendly message to terminal
|
|
31
|
+
console.error('');
|
|
32
|
+
console.error(` ✖ ${label}: ${err.message || err}`);
|
|
33
|
+
console.error(` Details logged to: ${ERROR_LOG}`);
|
|
34
|
+
console.error('');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
process.on('uncaughtException', (err) => {
|
|
38
|
+
logFatalError('Uncaught exception', err);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
process.on('unhandledRejection', (reason) => {
|
|
43
|
+
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
44
|
+
logFatalError('Unhandled promise rejection', err);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// ─── Launch Main ─────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
import('../src/index.js').catch((err) => {
|
|
51
|
+
logFatalError('Failed to start PeerTerm', err);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "peer-term",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Share your terminal instantly using a 6-digit code. Encrypted. No config.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"peer-term": "./bin/peer-term.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18.0.0"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin/",
|
|
14
|
+
"src/",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"type": "module",
|
|
18
|
+
"scripts": {
|
|
19
|
+
"start": "node bin/peer-term.js",
|
|
20
|
+
"build": "npx @yao-pkg/pkg . --compress GZip",
|
|
21
|
+
"build:linux-x64": "npx @yao-pkg/pkg . --targets node18-linux-x64 --output dist/peer-term-linux-x64",
|
|
22
|
+
"build:linux-arm64": "npx @yao-pkg/pkg . --targets node18-linux-arm64 --output dist/peer-term-linux-arm64",
|
|
23
|
+
"build:macos-x64": "npx @yao-pkg/pkg . --targets node18-macos-x64 --output dist/peer-term-macos-x64",
|
|
24
|
+
"build:macos-arm64": "npx @yao-pkg/pkg . --targets node18-macos-arm64 --output dist/peer-term-macos-arm64",
|
|
25
|
+
"build:win": "npx @yao-pkg/pkg . --targets node18-win-x64 --output dist/peer-term-win-x64.exe",
|
|
26
|
+
"postinstall": "node src/check-deps.js"
|
|
27
|
+
},
|
|
28
|
+
"pkg": {
|
|
29
|
+
"scripts": "src/**/*.js",
|
|
30
|
+
"assets": [],
|
|
31
|
+
"targets": [
|
|
32
|
+
"node18-linux-x64",
|
|
33
|
+
"node18-linux-arm64",
|
|
34
|
+
"node18-macos-x64",
|
|
35
|
+
"node18-macos-arm64",
|
|
36
|
+
"node18-win-x64"
|
|
37
|
+
],
|
|
38
|
+
"outputPath": "dist/"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"dotenv": "^16.4.5",
|
|
42
|
+
"minimist": "^1.2.8",
|
|
43
|
+
"node-datachannel": "^0.32.3",
|
|
44
|
+
"node-pty": "^1.0.0",
|
|
45
|
+
"ws": "^8.18.0"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PeerTerm — Native Dependency Check
|
|
3
|
+
*
|
|
4
|
+
* Postinstall script that verifies native modules (node-pty, node-datachannel)
|
|
5
|
+
* are available. Prints clear errors with platform-specific fix instructions
|
|
6
|
+
* if they're missing. Exits cleanly (warning, not error) so npm install
|
|
7
|
+
* doesn't fail.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createRequire } from 'module';
|
|
11
|
+
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
|
|
14
|
+
let allGood = true;
|
|
15
|
+
|
|
16
|
+
// ─── Check node-pty ──────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
require('node-pty');
|
|
20
|
+
} catch {
|
|
21
|
+
allGood = false;
|
|
22
|
+
console.warn('');
|
|
23
|
+
console.warn(' ⚠ node-pty failed to load.');
|
|
24
|
+
console.warn('');
|
|
25
|
+
console.warn(' node-pty is a native C++ module that requires build tools.');
|
|
26
|
+
console.warn('');
|
|
27
|
+
|
|
28
|
+
if (process.platform === 'win32') {
|
|
29
|
+
console.warn(' Windows fix:');
|
|
30
|
+
console.warn(' npm install -g windows-build-tools');
|
|
31
|
+
console.warn(' — or —');
|
|
32
|
+
console.warn(' Install Visual Studio Build Tools with "Desktop development with C++"');
|
|
33
|
+
} else if (process.platform === 'darwin') {
|
|
34
|
+
console.warn(' macOS fix:');
|
|
35
|
+
console.warn(' xcode-select --install');
|
|
36
|
+
} else {
|
|
37
|
+
console.warn(' Linux fix:');
|
|
38
|
+
console.warn(' sudo apt install build-essential python3 (Debian/Ubuntu)');
|
|
39
|
+
console.warn(' sudo yum groupinstall "Development Tools" (RHEL/CentOS)');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.warn('');
|
|
43
|
+
console.warn(' Then run: npm rebuild node-pty');
|
|
44
|
+
console.warn('');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── Check node-datachannel ──────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
require('node-datachannel');
|
|
51
|
+
} catch {
|
|
52
|
+
allGood = false;
|
|
53
|
+
console.warn('');
|
|
54
|
+
console.warn(' ⚠ node-datachannel failed to load.');
|
|
55
|
+
console.warn('');
|
|
56
|
+
console.warn(' node-datachannel is a native module for WebRTC DataChannels.');
|
|
57
|
+
console.warn(' It requires CMake and C++ build tools.');
|
|
58
|
+
console.warn('');
|
|
59
|
+
|
|
60
|
+
if (process.platform === 'win32') {
|
|
61
|
+
console.warn(' Windows fix:');
|
|
62
|
+
console.warn(' Install CMake from https://cmake.org/download/');
|
|
63
|
+
console.warn(' Install Visual Studio Build Tools with "Desktop development with C++"');
|
|
64
|
+
} else if (process.platform === 'darwin') {
|
|
65
|
+
console.warn(' macOS fix:');
|
|
66
|
+
console.warn(' brew install cmake');
|
|
67
|
+
console.warn(' xcode-select --install');
|
|
68
|
+
} else {
|
|
69
|
+
console.warn(' Linux fix:');
|
|
70
|
+
console.warn(' sudo apt install cmake build-essential (Debian/Ubuntu)');
|
|
71
|
+
console.warn(' sudo yum install cmake gcc-c++ (RHEL/CentOS)');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.warn('');
|
|
75
|
+
console.warn(' Then run: npm rebuild node-datachannel');
|
|
76
|
+
console.warn('');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Summary ─────────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
if (allGood) {
|
|
82
|
+
console.log(' ✓ All native dependencies loaded successfully.');
|
|
83
|
+
}
|
package/src/crypto.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PeerTerm — Encryption Helpers (Host-side)
|
|
3
|
+
*
|
|
4
|
+
* Implements ECDH P-256 key exchange and AES-256-GCM encryption/decryption
|
|
5
|
+
* using Node.js crypto.webcrypto (SubtleCrypto API).
|
|
6
|
+
*
|
|
7
|
+
* Encryption spec:
|
|
8
|
+
* - Key exchange: ECDH with P-256 curve
|
|
9
|
+
* - Derived key: AES-GCM, 256-bit, extractable: false
|
|
10
|
+
* - Per message: 12-byte random IV prepended to ciphertext, encoded as base64
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { webcrypto } from 'crypto';
|
|
14
|
+
const subtle = webcrypto.subtle;
|
|
15
|
+
|
|
16
|
+
// ─── Key Pair Generation ─────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate an ephemeral ECDH P-256 key pair.
|
|
20
|
+
* Returns { publicKey, privateKey } as CryptoKey objects.
|
|
21
|
+
*/
|
|
22
|
+
export async function generateKeyPair() {
|
|
23
|
+
return await subtle.generateKey(
|
|
24
|
+
{ name: 'ECDH', namedCurve: 'P-256' },
|
|
25
|
+
false, // not extractable (private key stays in memory)
|
|
26
|
+
['deriveKey']
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ─── Public Key Export / Import ──────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Export a public CryptoKey to a base64 string (raw format).
|
|
34
|
+
* This is what gets sent over the wire to the peer.
|
|
35
|
+
*/
|
|
36
|
+
export async function exportPublicKey(publicKey) {
|
|
37
|
+
const raw = await subtle.exportKey('raw', publicKey);
|
|
38
|
+
return Buffer.from(raw).toString('base64');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Import a peer's public key from a base64 string.
|
|
43
|
+
* Returns a CryptoKey suitable for ECDH key derivation.
|
|
44
|
+
*/
|
|
45
|
+
export async function importPublicKey(base64) {
|
|
46
|
+
const raw = Buffer.from(base64, 'base64');
|
|
47
|
+
return await subtle.importKey(
|
|
48
|
+
'raw',
|
|
49
|
+
raw,
|
|
50
|
+
{ name: 'ECDH', namedCurve: 'P-256' },
|
|
51
|
+
false,
|
|
52
|
+
[]
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── Shared Key Derivation ──────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Derive a shared AES-256-GCM key from our private key and the peer's public key.
|
|
60
|
+
* Both sides independently derive the same key (ECDH magic).
|
|
61
|
+
*
|
|
62
|
+
* The derived key is:
|
|
63
|
+
* - Algorithm: AES-GCM
|
|
64
|
+
* - Length: 256 bits
|
|
65
|
+
* - Extractable: false (cannot be exported, only used for encrypt/decrypt)
|
|
66
|
+
* - Usages: ['encrypt', 'decrypt']
|
|
67
|
+
*/
|
|
68
|
+
export async function deriveSharedKey(privateKey, peerPublicKey) {
|
|
69
|
+
return await subtle.deriveKey(
|
|
70
|
+
{ name: 'ECDH', public: peerPublicKey },
|
|
71
|
+
privateKey,
|
|
72
|
+
{ name: 'AES-GCM', length: 256 },
|
|
73
|
+
false, // not extractable
|
|
74
|
+
['encrypt', 'decrypt']
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Encryption ─────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Encrypt plaintext with AES-256-GCM.
|
|
82
|
+
*
|
|
83
|
+
* Steps:
|
|
84
|
+
* 1. Generate a random 12-byte IV
|
|
85
|
+
* 2. Encrypt the plaintext with AES-GCM using the shared key and IV
|
|
86
|
+
* 3. Concatenate [IV (12 bytes) + ciphertext]
|
|
87
|
+
* 4. Encode the result as a base64 string
|
|
88
|
+
*
|
|
89
|
+
* @param {CryptoKey} sharedKey - The derived AES-GCM key
|
|
90
|
+
* @param {string} plaintext - The data to encrypt
|
|
91
|
+
* @returns {string} base64-encoded IV + ciphertext
|
|
92
|
+
*/
|
|
93
|
+
export async function encrypt(sharedKey, plaintext) {
|
|
94
|
+
// 1. Fresh random IV for every message (critical for AES-GCM security)
|
|
95
|
+
const iv = webcrypto.getRandomValues(new Uint8Array(12));
|
|
96
|
+
|
|
97
|
+
// 2. Encrypt
|
|
98
|
+
const encoded = new TextEncoder().encode(plaintext);
|
|
99
|
+
const ciphertext = await subtle.encrypt(
|
|
100
|
+
{ name: 'AES-GCM', iv },
|
|
101
|
+
sharedKey,
|
|
102
|
+
encoded
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// 3. Concatenate IV + ciphertext
|
|
106
|
+
const combined = new Uint8Array(iv.length + ciphertext.byteLength);
|
|
107
|
+
combined.set(iv, 0);
|
|
108
|
+
combined.set(new Uint8Array(ciphertext), iv.length);
|
|
109
|
+
|
|
110
|
+
// 4. Encode as base64
|
|
111
|
+
return Buffer.from(combined).toString('base64');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── Decryption ─────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Decrypt a base64-encoded AES-GCM message.
|
|
118
|
+
*
|
|
119
|
+
* Steps:
|
|
120
|
+
* 1. Base64 decode the input
|
|
121
|
+
* 2. Split: first 12 bytes = IV, rest = ciphertext
|
|
122
|
+
* 3. Decrypt with AES-GCM
|
|
123
|
+
* 4. Return plaintext string
|
|
124
|
+
*
|
|
125
|
+
* @param {CryptoKey} sharedKey - The derived AES-GCM key
|
|
126
|
+
* @param {string} base64data - The encrypted message (IV + ciphertext in base64)
|
|
127
|
+
* @returns {string} Decrypted plaintext
|
|
128
|
+
*/
|
|
129
|
+
export async function decrypt(sharedKey, base64data) {
|
|
130
|
+
// 1. Base64 decode
|
|
131
|
+
const combined = Buffer.from(base64data, 'base64');
|
|
132
|
+
|
|
133
|
+
// 2. Split IV and ciphertext
|
|
134
|
+
const iv = combined.slice(0, 12);
|
|
135
|
+
const ciphertext = combined.slice(12);
|
|
136
|
+
|
|
137
|
+
// 3. Decrypt
|
|
138
|
+
const plaintext = await subtle.decrypt(
|
|
139
|
+
{ name: 'AES-GCM', iv },
|
|
140
|
+
sharedKey,
|
|
141
|
+
ciphertext
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// 4. Return as string
|
|
145
|
+
return new TextDecoder().decode(plaintext);
|
|
146
|
+
}
|
|
147
|
+
|