fied 0.1.3 → 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 +99 -72
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -108,6 +108,8 @@ var DEFAULT_RELAY = "https://fied.app";
108
108
  var MSG_TERMINAL_OUTPUT = 1;
109
109
  var MSG_TERMINAL_INPUT = 2;
110
110
  var MSG_RESIZE = 3;
111
+ var RECONNECT_BASE_MS = 1e3;
112
+ var RECONNECT_MAX_MS = 3e4;
111
113
  async function share(options2) {
112
114
  const relay = options2.relay ?? DEFAULT_RELAY;
113
115
  const sessions = listSessions();
@@ -143,22 +145,27 @@ async function share(options2) {
143
145
  const rawKey = await generateKey();
144
146
  const cryptoKey = await importKey(rawKey);
145
147
  const keyFragment = toBase64Url(rawKey);
146
- const sessionId = await createSession(relay);
147
- const url = `${relay}/s/${sessionId}#${keyFragment}`;
148
+ const pty = attachSession(targetSession, cols, rows);
148
149
  console.log("");
149
150
  console.log(" \x1B[1m\x1B[32mfied\x1B[0m \u2014 encrypted terminal sharing");
150
151
  console.log("");
151
152
  console.log(` Session: ${targetSession}`);
152
153
  console.log(` Size: ${cols}x${rows}`);
153
154
  console.log("");
154
- console.log(` \x1B[1mShare this link:\x1B[0m`);
155
- console.log(` \x1B[4m\x1B[36m${url}\x1B[0m`);
156
- console.log("");
157
- console.log(" \x1B[2mThe encryption key is in the URL fragment (#) \u2014 the server never sees it.\x1B[0m");
158
- console.log(" \x1B[2mPress Ctrl+C to stop sharing.\x1B[0m");
159
- console.log("");
160
- const pty = attachSession(targetSession, cols, rows);
161
- 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
+ });
162
169
  }
163
170
  async function createSession(relay) {
164
171
  const res = await fetch(`${relay}/api/sessions`, { method: "POST" });
@@ -168,39 +175,54 @@ async function createSession(relay) {
168
175
  const data = await res.json();
169
176
  return data.sessionId;
170
177
  }
171
- async function connectToRelay(relay, sessionId, key, pty) {
172
- return new Promise((resolve, reject) => {
173
- 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`;
174
209
  const ws = new WebSocket(wsUrl);
175
210
  ws.binaryType = "arraybuffer";
176
- let alive = true;
177
- let heartbeatTimer;
211
+ this.ws = ws;
178
212
  ws.on("open", () => {
179
- heartbeatTimer = setInterval(() => {
180
- if (ws.readyState === WebSocket.OPEN) {
181
- ws.ping();
182
- }
183
- }, 3e4);
184
- const resizePayload = JSON.stringify({
185
- cols: pty.cols,
186
- rows: pty.rows
187
- });
188
- 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));
189
216
  });
190
- pty.onData((data) => {
191
- if (ws.readyState === WebSocket.OPEN) {
192
- 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));
193
220
  }
194
221
  });
195
- pty.onExit(({ exitCode }) => {
196
- alive = false;
197
- if (heartbeatTimer) clearInterval(heartbeatTimer);
198
- ws.close();
199
- resolve();
200
- });
201
- ws.on("message", async (raw) => {
202
- if (typeof raw === "string") {
203
- 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__") {
204
226
  ws.send("__fied_pong__");
205
227
  }
206
228
  return;
@@ -209,50 +231,55 @@ async function connectToRelay(relay, sessionId, key, pty) {
209
231
  const data = new Uint8Array(raw);
210
232
  const frame = parseFrame(data);
211
233
  if (frame.type === MSG_TERMINAL_INPUT) {
212
- const plaintext = await decrypt(key, frame.iv, frame.ciphertext);
213
- 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));
214
236
  } else if (frame.type === MSG_RESIZE) {
215
- const plaintext = await decrypt(key, frame.iv, frame.ciphertext);
216
- const { cols, rows } = JSON.parse(new TextDecoder().decode(plaintext));
217
- 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);
218
240
  }
219
- } catch (err) {
220
- console.error("Failed to process incoming message:", err);
241
+ } catch {
221
242
  }
222
243
  });
223
244
  ws.on("close", () => {
224
- if (heartbeatTimer) clearInterval(heartbeatTimer);
225
- if (alive) {
226
- pty.kill();
245
+ this.ws = null;
246
+ if (!this.destroyed) {
247
+ console.error(" \x1B[33mConnection lost, reconnecting...\x1B[0m");
248
+ this.scheduleReconnect();
227
249
  }
228
- resolve();
229
250
  });
230
- ws.on("error", (err) => {
231
- console.error("WebSocket error:", err.message);
251
+ ws.on("error", () => {
232
252
  });
233
- process.on("SIGINT", () => {
234
- alive = false;
235
- if (heartbeatTimer) clearInterval(heartbeatTimer);
236
- ws.close();
237
- pty.kill();
238
- });
239
- process.on("SIGTERM", () => {
240
- alive = false;
241
- if (heartbeatTimer) clearInterval(heartbeatTimer);
242
- ws.close();
243
- pty.kill();
244
- });
245
- });
246
- }
247
- async function sendEncrypted(ws, key, type, plaintext) {
248
- try {
249
- const { iv, ciphertext } = await encrypt(key, plaintext);
250
- const frame = frameMessage(type, iv, ciphertext);
251
- ws.send(frame);
252
- } catch (err) {
253
- console.error("Encryption failed:", err);
254
253
  }
255
- }
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
+ };
256
283
 
257
284
  // src/bin.ts
258
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.3",
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": {