fied 0.1.2 → 0.1.4

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/dist/bin.js +101 -73
  2. package/package.json +2 -2
package/dist/bin.js CHANGED
@@ -94,11 +94,12 @@ function listSessions() {
94
94
  }
95
95
  }
96
96
  function attachSession(sessionName, cols, rows) {
97
+ const { TMUX: _, TMUX_PANE: __, ...env } = process.env;
97
98
  return spawn("tmux", ["attach-session", "-t", sessionName], {
98
99
  name: "xterm-256color",
99
100
  cols,
100
101
  rows,
101
- env: process.env
102
+ env
102
103
  });
103
104
  }
104
105
 
@@ -107,6 +108,8 @@ var DEFAULT_RELAY = "https://fied.app";
107
108
  var MSG_TERMINAL_OUTPUT = 1;
108
109
  var MSG_TERMINAL_INPUT = 2;
109
110
  var MSG_RESIZE = 3;
111
+ var RECONNECT_BASE_MS = 1e3;
112
+ var RECONNECT_MAX_MS = 3e4;
110
113
  async function share(options2) {
111
114
  const relay = options2.relay ?? DEFAULT_RELAY;
112
115
  const sessions = listSessions();
@@ -142,22 +145,27 @@ async function share(options2) {
142
145
  const rawKey = await generateKey();
143
146
  const cryptoKey = await importKey(rawKey);
144
147
  const keyFragment = toBase64Url(rawKey);
145
- const sessionId = await createSession(relay);
146
- const url = `${relay}/s/${sessionId}#${keyFragment}`;
148
+ const pty = attachSession(targetSession, cols, rows);
147
149
  console.log("");
148
150
  console.log(" \x1B[1m\x1B[32mfied\x1B[0m \u2014 encrypted terminal sharing");
149
151
  console.log("");
150
152
  console.log(` Session: ${targetSession}`);
151
153
  console.log(` Size: ${cols}x${rows}`);
152
154
  console.log("");
153
- console.log(` \x1B[1mShare this link:\x1B[0m`);
154
- console.log(` \x1B[4m\x1B[36m${url}\x1B[0m`);
155
- console.log("");
156
- console.log(" \x1B[2mThe encryption key is in the URL fragment (#) \u2014 the server never sees it.\x1B[0m");
157
- console.log(" \x1B[2mPress Ctrl+C to stop sharing.\x1B[0m");
158
- console.log("");
159
- const pty = attachSession(targetSession, cols, rows);
160
- await connectToRelay(relay, sessionId, cryptoKey, pty);
155
+ const bridge = new RelayBridge(relay, cryptoKey, keyFragment, pty);
156
+ await bridge.connect();
157
+ const shutdown = () => {
158
+ bridge.destroy();
159
+ pty.kill();
160
+ };
161
+ process.on("SIGINT", shutdown);
162
+ process.on("SIGTERM", shutdown);
163
+ await new Promise((resolve) => {
164
+ pty.onExit(() => {
165
+ bridge.destroy();
166
+ resolve();
167
+ });
168
+ });
161
169
  }
162
170
  async function createSession(relay) {
163
171
  const res = await fetch(`${relay}/api/sessions`, { method: "POST" });
@@ -167,39 +175,54 @@ async function createSession(relay) {
167
175
  const data = await res.json();
168
176
  return data.sessionId;
169
177
  }
170
- async function connectToRelay(relay, sessionId, key, pty) {
171
- return new Promise((resolve, reject) => {
172
- const wsUrl = relay.replace(/^http/, "ws") + `/api/sessions/${sessionId}/ws?role=host`;
178
+ var RelayBridge = class {
179
+ constructor(relay, key, keyFragment, pty) {
180
+ this.relay = relay;
181
+ this.key = key;
182
+ this.keyFragment = keyFragment;
183
+ this.pty = pty;
184
+ }
185
+ ws = null;
186
+ destroyed = false;
187
+ backoff = RECONNECT_BASE_MS;
188
+ reconnectTimer = null;
189
+ encoder = new TextEncoder();
190
+ decoder = new TextDecoder();
191
+ async connect() {
192
+ if (this.destroyed) return;
193
+ let sessionId;
194
+ try {
195
+ sessionId = await createSession(this.relay);
196
+ } catch {
197
+ console.error(" \x1B[33mRelay unreachable, retrying...\x1B[0m");
198
+ this.scheduleReconnect();
199
+ return;
200
+ }
201
+ const url = `${this.relay}/s/${sessionId}#${this.keyFragment}`;
202
+ console.log(` \x1B[1mShare this link:\x1B[0m`);
203
+ console.log(` \x1B[4m\x1B[36m${url}\x1B[0m`);
204
+ console.log("");
205
+ console.log(" \x1B[2mThe encryption key is in the URL fragment (#) \u2014 the server never sees it.\x1B[0m");
206
+ console.log(" \x1B[2mPress Ctrl+C to stop sharing.\x1B[0m");
207
+ console.log("");
208
+ const wsUrl = this.relay.replace(/^http/, "ws") + `/api/sessions/${sessionId}/ws?role=host`;
173
209
  const ws = new WebSocket(wsUrl);
174
210
  ws.binaryType = "arraybuffer";
175
- let alive = true;
176
- let heartbeatTimer;
211
+ this.ws = ws;
177
212
  ws.on("open", () => {
178
- heartbeatTimer = setInterval(() => {
179
- if (ws.readyState === WebSocket.OPEN) {
180
- ws.ping();
181
- }
182
- }, 3e4);
183
- const resizePayload = JSON.stringify({
184
- cols: pty.cols,
185
- rows: pty.rows
186
- });
187
- sendEncrypted(ws, key, MSG_RESIZE, new TextEncoder().encode(resizePayload));
213
+ this.backoff = RECONNECT_BASE_MS;
214
+ const resizePayload = JSON.stringify({ cols: this.pty.cols, rows: this.pty.rows });
215
+ this.sendEncrypted(MSG_RESIZE, this.encoder.encode(resizePayload));
188
216
  });
189
- pty.onData((data) => {
190
- if (ws.readyState === WebSocket.OPEN) {
191
- sendEncrypted(ws, key, MSG_TERMINAL_OUTPUT, new TextEncoder().encode(data));
217
+ this.pty.onData((data) => {
218
+ if (this.ws?.readyState === WebSocket.OPEN) {
219
+ this.sendEncrypted(MSG_TERMINAL_OUTPUT, this.encoder.encode(data));
192
220
  }
193
221
  });
194
- pty.onExit(({ exitCode }) => {
195
- alive = false;
196
- if (heartbeatTimer) clearInterval(heartbeatTimer);
197
- ws.close();
198
- resolve();
199
- });
200
- ws.on("message", async (raw) => {
201
- if (typeof raw === "string") {
202
- if (raw === "__fied_ping__") {
222
+ ws.on("message", async (raw, isBinary) => {
223
+ if (!isBinary) {
224
+ const text = this.decoder.decode(raw);
225
+ if (text === "__fied_ping__") {
203
226
  ws.send("__fied_pong__");
204
227
  }
205
228
  return;
@@ -208,50 +231,55 @@ async function connectToRelay(relay, sessionId, key, pty) {
208
231
  const data = new Uint8Array(raw);
209
232
  const frame = parseFrame(data);
210
233
  if (frame.type === MSG_TERMINAL_INPUT) {
211
- const plaintext = await decrypt(key, frame.iv, frame.ciphertext);
212
- pty.write(new TextDecoder().decode(plaintext));
234
+ const plaintext = await decrypt(this.key, frame.iv, frame.ciphertext);
235
+ this.pty.write(this.decoder.decode(plaintext));
213
236
  } else if (frame.type === MSG_RESIZE) {
214
- const plaintext = await decrypt(key, frame.iv, frame.ciphertext);
215
- const { cols, rows } = JSON.parse(new TextDecoder().decode(plaintext));
216
- pty.resize(cols, rows);
237
+ const plaintext = await decrypt(this.key, frame.iv, frame.ciphertext);
238
+ const { cols, rows } = JSON.parse(this.decoder.decode(plaintext));
239
+ this.pty.resize(cols, rows);
217
240
  }
218
- } catch (err) {
219
- console.error("Failed to process incoming message:", err);
241
+ } catch {
220
242
  }
221
243
  });
222
244
  ws.on("close", () => {
223
- if (heartbeatTimer) clearInterval(heartbeatTimer);
224
- if (alive) {
225
- pty.kill();
245
+ this.ws = null;
246
+ if (!this.destroyed) {
247
+ console.error(" \x1B[33mConnection lost, reconnecting...\x1B[0m");
248
+ this.scheduleReconnect();
226
249
  }
227
- resolve();
228
250
  });
229
- ws.on("error", (err) => {
230
- console.error("WebSocket error:", err.message);
251
+ ws.on("error", () => {
231
252
  });
232
- process.on("SIGINT", () => {
233
- alive = false;
234
- if (heartbeatTimer) clearInterval(heartbeatTimer);
235
- ws.close();
236
- pty.kill();
237
- });
238
- process.on("SIGTERM", () => {
239
- alive = false;
240
- if (heartbeatTimer) clearInterval(heartbeatTimer);
241
- ws.close();
242
- pty.kill();
243
- });
244
- });
245
- }
246
- async function sendEncrypted(ws, key, type, plaintext) {
247
- try {
248
- const { iv, ciphertext } = await encrypt(key, plaintext);
249
- const frame = frameMessage(type, iv, ciphertext);
250
- ws.send(frame);
251
- } catch (err) {
252
- console.error("Encryption failed:", err);
253
253
  }
254
- }
254
+ destroy() {
255
+ this.destroyed = true;
256
+ if (this.reconnectTimer) {
257
+ clearTimeout(this.reconnectTimer);
258
+ this.reconnectTimer = null;
259
+ }
260
+ if (this.ws) {
261
+ this.ws.close();
262
+ this.ws = null;
263
+ }
264
+ }
265
+ scheduleReconnect() {
266
+ if (this.destroyed || this.reconnectTimer) return;
267
+ this.reconnectTimer = setTimeout(() => {
268
+ this.reconnectTimer = null;
269
+ this.connect();
270
+ }, this.backoff);
271
+ this.backoff = Math.min(this.backoff * 2, RECONNECT_MAX_MS);
272
+ }
273
+ async sendEncrypted(type, plaintext) {
274
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
275
+ try {
276
+ const { iv, ciphertext } = await encrypt(this.key, plaintext);
277
+ const frame = frameMessage(type, iv, ciphertext);
278
+ this.ws.send(frame);
279
+ } catch {
280
+ }
281
+ }
282
+ };
255
283
 
256
284
  // src/bin.ts
257
285
  var args = process.argv.slice(2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fied",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Share your tmux session in the browser with end-to-end encryption",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,7 +14,7 @@
14
14
  "dev": "tsc --watch"
15
15
  },
16
16
  "dependencies": {
17
- "node-pty": "^1.0.0",
17
+ "node-pty": "^1.2.0-beta.11",
18
18
  "ws": "^8.18.0"
19
19
  },
20
20
  "devDependencies": {