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 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
@@ -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
+