@thenewlabs/entangle-serve 2.6.2 → 2.7.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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ipc.d.ts","sourceRoot":"","sources":["../src/ipc.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAgBrE,mEAAmE;AACnE,eAAO,MAAM,YAAY,IAAI,CAAC;AAE9B;;;;;GAKG;AACH,eAAO,MAAM,eAAe,QAAkB,CAAC;AAI/C,8EAA8E;AAC9E,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEpE,oEAAoE;AACpE,MAAM,MAAM,cAAc,GACtB;IAAE,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AAC5C,uDAAuD;GACrD;IAAE,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC5B;IAAE,CAAC,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC3C;IAAE,CAAC,EAAE,KAAK,CAAC;IAAC,EAAE,EAAE,QAAQ,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,CAAC,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEpB,oEAAoE;AACpE,MAAM,MAAM,cAAc;AACxB,4CAA4C;AAC1C;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AAC9B,iEAAiE;GAC/D;IAAE,CAAC,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,CAAC,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,eAAe,CAAA;CAAE;AAC/C,4CAA4C;GAC1C;IAAE,CAAC,EAAE,SAAS,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GAC3B;IAAE,CAAC,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC1B;IAAE,CAAC,EAAE,KAAK,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAEvC,mDAAmD;AACnD,MAAM,MAAM,UAAU,GAAG,cAAc,GAAG,cAAc,CAAC;AAIzD,2EAA2E;AAC3E,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAEpD;AAED,yEAAyE;AACzE,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEjD;AAID;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CASrD;AAED,4DAA4D;AAC5D,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAErE;AAID;;;;;;GAMG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAA2B;IAEzC;;;;OAIG;IACH,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU,EAAE;CAoBtC;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,CAAC,GAAG,EAAE,UAAU,KAAK,IAAI,EACpC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,GAC7B,MAAM,IAAI,CAgBZ"}
package/dist/ipc.js ADDED
@@ -0,0 +1,115 @@
1
+ // Local unix-socket IPC framing between the `entangle serve` daemon and an
2
+ // attached terminal client. This is a *local* transport (a filesystem socket
3
+ // secured by directory permissions — see session-registry.ts), so there is no
4
+ // encryption here: confidentiality is the registry's job.
5
+ //
6
+ // Framing: a 4-byte big-endian (network order) unsigned length prefix followed
7
+ // by exactly that many bytes of a UTF-8 JSON payload. Terminal data (which is
8
+ // binary) travels as base64 strings inside the JSON so the wire is always valid
9
+ // JSON and we never have to deal with mixed binary/text framing.
10
+ //
11
+ // +----------------+----------------------------+
12
+ // | uint32 BE len | JSON payload (len bytes) |
13
+ // +----------------+----------------------------+
14
+ /** Bytes of the length prefix that precedes every JSON payload. */
15
+ export const HEADER_BYTES = 4;
16
+ /**
17
+ * Maximum accepted payload size. A frame declaring a larger payload is rejected
18
+ * (the decoder errors) rather than buffering an unbounded amount of memory for
19
+ * a payload that may never fully arrive. 8 MB comfortably fits a full-screen
20
+ * replay while capping a hostile/framed-wrong peer.
21
+ */
22
+ export const MAX_FRAME_BYTES = 8 * 1024 * 1024;
23
+ // --- base64 helpers for binary chunks --------------------------------------
24
+ /** Encode a binary terminal chunk as a base64 string for the JSON wire. */
25
+ export function encodeChunk(data) {
26
+ return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString('base64');
27
+ }
28
+ /** Decode a base64 chunk string back into a Buffer of terminal bytes. */
29
+ export function decodeChunk(chunk) {
30
+ return Buffer.from(chunk, 'base64');
31
+ }
32
+ // --- Encoding --------------------------------------------------------------
33
+ /**
34
+ * Serialize a message to a length-prefixed frame: a 4-byte big-endian payload
35
+ * length followed by the UTF-8 JSON payload.
36
+ *
37
+ * @throws if the resulting payload exceeds {@link MAX_FRAME_BYTES}.
38
+ */
39
+ export function encodeMessage(msg) {
40
+ const payload = Buffer.from(JSON.stringify(msg), 'utf8');
41
+ if (payload.length > MAX_FRAME_BYTES) {
42
+ throw new Error(`IPC frame too large to encode: ${payload.length} > ${MAX_FRAME_BYTES}`);
43
+ }
44
+ const frame = Buffer.allocUnsafe(HEADER_BYTES + payload.length);
45
+ frame.writeUInt32BE(payload.length, 0);
46
+ payload.copy(frame, HEADER_BYTES);
47
+ return frame;
48
+ }
49
+ /** Encode and write a single message to a socket/stream. */
50
+ export function writeMessage(socket, msg) {
51
+ return socket.write(encodeMessage(msg));
52
+ }
53
+ // --- Decoding --------------------------------------------------------------
54
+ /**
55
+ * Incrementally decodes length-prefixed frames from a byte stream, tolerating
56
+ * partial reads (a frame split across chunks) and coalesced reads (many frames
57
+ * in one chunk). A frame whose declared length exceeds {@link MAX_FRAME_BYTES}
58
+ * makes {@link push} throw rather than buffering unbounded memory — callers
59
+ * should treat that as a fatal protocol error and close the socket.
60
+ */
61
+ export class FrameDecoder {
62
+ buffer = Buffer.alloc(0);
63
+ /**
64
+ * Feed received bytes; returns every message that became fully available.
65
+ * @throws if a frame declares a payload larger than {@link MAX_FRAME_BYTES}
66
+ * or if a completed payload is not valid JSON.
67
+ */
68
+ push(chunk) {
69
+ const messages = [];
70
+ this.buffer = this.buffer.length === 0
71
+ ? Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength)
72
+ : Buffer.concat([this.buffer, Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength)]);
73
+ while (this.buffer.length >= HEADER_BYTES) {
74
+ const length = this.buffer.readUInt32BE(0);
75
+ if (length > MAX_FRAME_BYTES) {
76
+ throw new Error(`IPC frame too large: ${length} > ${MAX_FRAME_BYTES}`);
77
+ }
78
+ if (this.buffer.length < HEADER_BYTES + length)
79
+ break; // wait for more bytes
80
+ const payload = this.buffer.subarray(HEADER_BYTES, HEADER_BYTES + length);
81
+ messages.push(JSON.parse(payload.toString('utf8')));
82
+ this.buffer = this.buffer.subarray(HEADER_BYTES + length);
83
+ }
84
+ return messages;
85
+ }
86
+ }
87
+ /**
88
+ * Attach a {@link FrameDecoder} to a socket and invoke `onMessage` for each
89
+ * decoded message. A decode error (oversize frame or malformed JSON) is routed
90
+ * to `onError` if provided, otherwise emitted on the socket as an `'error'`.
91
+ *
92
+ * @returns a detach function that removes the installed `'data'` listener.
93
+ */
94
+ export function createMessageReader(socket, onMessage, onError) {
95
+ const decoder = new FrameDecoder();
96
+ const onData = (chunk) => {
97
+ let messages;
98
+ try {
99
+ messages = decoder.push(chunk);
100
+ }
101
+ catch (err) {
102
+ const error = err instanceof Error ? err : new Error(String(err));
103
+ if (onError)
104
+ onError(error);
105
+ else
106
+ socket.emit('error', error);
107
+ return;
108
+ }
109
+ for (const msg of messages)
110
+ onMessage(msg);
111
+ };
112
+ socket.on('data', onData);
113
+ return () => socket.off('data', onData);
114
+ }
115
+ //# sourceMappingURL=ipc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ipc.js","sourceRoot":"","sources":["../src/ipc.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ import type { Socket } from 'net';
2
+ import type { WindowStateBody } from '@thenewlabs/entangle-protocol';
3
+ import type { HostSession } from './host-session.js';
4
+ /**
5
+ * The socket-backed {@link HostSession}: the CLIENT half of the daemon/client
6
+ * split. It renders the SAME host UI (host-terminal.ts) that {@link LocalHostSession}
7
+ * does, but every read is a cached value fed by the daemon over a local unix
8
+ * socket and every action is a framed message written back to it.
9
+ *
10
+ * On construction it sends a `hello` (so the daemon learns the client's terminal
11
+ * size) and starts decoding {@link DaemonToClient} frames: live output, the
12
+ * post-attach replay, window-state, viewer count, captured logs, the session URL
13
+ * and the terminal exit. State the UI reads synchronously ({@link getReplay},
14
+ * {@link windowState}, {@link viewerCount}, {@link getUrl}, {@link getLogBuffer})
15
+ * is cached from those frames; the daemon sends the current state immediately on
16
+ * connect so these populate before the first paint.
17
+ */
18
+ export declare class RemoteHostSession implements HostSession {
19
+ private readonly socket;
20
+ private replay;
21
+ private winState;
22
+ private viewers;
23
+ private url;
24
+ private readonly logBuf;
25
+ private hostDataCb?;
26
+ private windowStateCb?;
27
+ private viewersCb?;
28
+ private logCb?;
29
+ private urlCb?;
30
+ private exitCb?;
31
+ private exited;
32
+ private closed;
33
+ private pendingExitCode;
34
+ constructor(socket: Socket, initial: {
35
+ cols: number;
36
+ rows: number;
37
+ });
38
+ private dispatch;
39
+ /** Fire the one-shot exit callback (idempotent). */
40
+ private fireExit;
41
+ /** Socket gone: fire exit (with a clean code if an `exit` frame preceded it). */
42
+ private handleClose;
43
+ /** Write a framed message, swallowing sends after the socket has closed. */
44
+ private send;
45
+ onHostData(cb: (chunk: Buffer) => void): void;
46
+ write(data: Uint8Array | string): void;
47
+ resize(cols: number, rows: number): void;
48
+ getReplay(): Uint8Array;
49
+ onWindowState(cb: (s: WindowStateBody) => void): void;
50
+ windowState(): WindowStateBody;
51
+ newWindow(): void;
52
+ nextWindow(): void;
53
+ prevWindow(): void;
54
+ selectWindow(i: number): void;
55
+ closeWindow(i: number): void;
56
+ onViewersChange(cb: (n: number) => void): void;
57
+ viewerCount(): number;
58
+ onLog(cb: (line: string) => void): void;
59
+ getLogBuffer(): readonly string[];
60
+ getUrl(): string | null;
61
+ onUrl(cb: (url: string) => void): void;
62
+ onExit(cb: (code: number | null, signal: string | null) => void): void;
63
+ /**
64
+ * Detach without ending the daemon session: tell the daemon to drop this
65
+ * client (it keeps running), end the socket, then run the local exit path so
66
+ * the host UI tears its terminal down (host-terminal's restore() runs on exit).
67
+ */
68
+ detach(): void;
69
+ /** Tear down: end the socket (no exit fired — the caller is disposing us). */
70
+ dispose(): void;
71
+ }
72
+ //# sourceMappingURL=remote-host-session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-host-session.d.ts","sourceRoot":"","sources":["../src/remote-host-session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,KAAK,CAAC;AAClC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAqBrD;;;;;;;;;;;;;GAaG;AACH,qBAAa,iBAAkB,YAAW,WAAW;IACnD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAGhC,OAAO,CAAC,MAAM,CAAiC;IAC/C,OAAO,CAAC,QAAQ,CAAuC;IACvD,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,GAAG,CAAuB;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IAIvC,OAAO,CAAC,UAAU,CAAC,CAA0B;IAC7C,OAAO,CAAC,aAAa,CAAC,CAA+B;IACrD,OAAO,CAAC,SAAS,CAAC,CAAsB;IACxC,OAAO,CAAC,KAAK,CAAC,CAAyB;IACvC,OAAO,CAAC,KAAK,CAAC,CAAwB;IACtC,OAAO,CAAC,MAAM,CAAC,CAAuD;IAKtE,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,eAAe,CAAuB;gBAElC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE;IAgBnE,OAAO,CAAC,QAAQ;IAyChB,oDAAoD;IACpD,OAAO,CAAC,QAAQ;IAMhB,iFAAiF;IACjF,OAAO,CAAC,WAAW;IAOnB,4EAA4E;IAC5E,OAAO,CAAC,IAAI;IAWZ,UAAU,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI;IAC7C,KAAK,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI;IAItC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IACxC,SAAS,IAAI,UAAU;IAGvB,aAAa,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,eAAe,KAAK,IAAI,GAAG,IAAI;IACrD,WAAW,IAAI,eAAe;IAC9B,SAAS,IAAI,IAAI;IACjB,UAAU,IAAI,IAAI;IAClB,UAAU,IAAI,IAAI;IAClB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAC7B,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAG5B,eAAe,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI;IAC9C,WAAW,IAAI,MAAM;IAGrB,KAAK,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI;IACvC,YAAY,IAAI,SAAS,MAAM,EAAE;IAGjC,MAAM,IAAI,MAAM,GAAG,IAAI;IACvB,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI;IAGtC,MAAM,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI;IAEtE;;;;OAIG;IACH,MAAM,IAAI,IAAI;IAOd,8EAA8E;IAC9E,OAAO,IAAI,IAAI;CAIhB"}
@@ -0,0 +1,170 @@
1
+ import { createMessageReader, decodeChunk, encodeChunk, writeMessage, } from './ipc.js';
2
+ /** Cap on the client-side mirror of the captured-log ring buffer. */
3
+ const DEBUG_MAX_LINES = 1000;
4
+ /** Empty window-state used before the daemon's first window-state frame arrives. */
5
+ const EMPTY_WINDOW_STATE = {
6
+ v: 1,
7
+ kind: 'window-state',
8
+ windows: [],
9
+ activeIndex: 0,
10
+ };
11
+ /**
12
+ * The socket-backed {@link HostSession}: the CLIENT half of the daemon/client
13
+ * split. It renders the SAME host UI (host-terminal.ts) that {@link LocalHostSession}
14
+ * does, but every read is a cached value fed by the daemon over a local unix
15
+ * socket and every action is a framed message written back to it.
16
+ *
17
+ * On construction it sends a `hello` (so the daemon learns the client's terminal
18
+ * size) and starts decoding {@link DaemonToClient} frames: live output, the
19
+ * post-attach replay, window-state, viewer count, captured logs, the session URL
20
+ * and the terminal exit. State the UI reads synchronously ({@link getReplay},
21
+ * {@link windowState}, {@link viewerCount}, {@link getUrl}, {@link getLogBuffer})
22
+ * is cached from those frames; the daemon sends the current state immediately on
23
+ * connect so these populate before the first paint.
24
+ */
25
+ export class RemoteHostSession {
26
+ socket;
27
+ // Cached state fed by daemon frames; read synchronously by the host UI.
28
+ replay = new Uint8Array(0);
29
+ winState = EMPTY_WINDOW_STATE;
30
+ viewers = 0;
31
+ url = null;
32
+ logBuf = [];
33
+ // The host UI registers exactly one callback per event; mirror the
34
+ // SharedWorkspace single-callback surface it binds against.
35
+ hostDataCb;
36
+ windowStateCb;
37
+ viewersCb;
38
+ logCb;
39
+ urlCb;
40
+ exitCb;
41
+ // Guards: `exited` makes the onExit path fire at most once (a clean `exit`
42
+ // frame or an unexpected socket close, whichever comes first); `closed` stops
43
+ // any send after the socket is gone.
44
+ exited = false;
45
+ closed = false;
46
+ pendingExitCode = null;
47
+ constructor(socket, initial) {
48
+ this.socket = socket;
49
+ createMessageReader(socket, (msg) => this.dispatch(msg), () => this.handleClose(null));
50
+ socket.on('close', () => this.handleClose(this.pendingExitCode));
51
+ socket.on('error', () => { });
52
+ this.send({ t: 'hello', cols: initial.cols, rows: initial.rows });
53
+ }
54
+ // --- inbound daemon → client ---------------------------------------------
55
+ dispatch(msg) {
56
+ // Only DaemonToClient frames arrive on a client socket; the daemon never
57
+ // sends ClientToDaemon frames back, so narrow by the discriminants we own.
58
+ const m = msg;
59
+ switch (m.t) {
60
+ case 'data':
61
+ this.hostDataCb?.(decodeChunk(m.chunk));
62
+ break;
63
+ case 'replay':
64
+ // Cached for getReplay(); NOT fed to onHostData — the host UI writes the
65
+ // replay itself once, via getReplay(), at go-live.
66
+ this.replay = decodeChunk(m.chunk);
67
+ break;
68
+ case 'window-state':
69
+ this.winState = m.state;
70
+ this.windowStateCb?.(m.state);
71
+ break;
72
+ case 'viewers':
73
+ this.viewers = m.n;
74
+ this.viewersCb?.(m.n);
75
+ break;
76
+ case 'log':
77
+ this.logBuf.push(m.line);
78
+ if (this.logBuf.length > DEBUG_MAX_LINES) {
79
+ this.logBuf.splice(0, this.logBuf.length - DEBUG_MAX_LINES);
80
+ }
81
+ this.logCb?.(m.line);
82
+ break;
83
+ case 'url':
84
+ this.url = m.url;
85
+ this.urlCb?.(m.url);
86
+ break;
87
+ case 'exit':
88
+ // Record the clean exit code so a following socket 'close' reports it,
89
+ // then fire the exit path now.
90
+ this.pendingExitCode = m.code;
91
+ this.fireExit(m.code, null);
92
+ break;
93
+ }
94
+ }
95
+ /** Fire the one-shot exit callback (idempotent). */
96
+ fireExit(code, signal) {
97
+ if (this.exited)
98
+ return;
99
+ this.exited = true;
100
+ this.exitCb?.(code, signal);
101
+ }
102
+ /** Socket gone: fire exit (with a clean code if an `exit` frame preceded it). */
103
+ handleClose(code) {
104
+ this.closed = true;
105
+ this.fireExit(code, null);
106
+ }
107
+ // --- outbound client → daemon --------------------------------------------
108
+ /** Write a framed message, swallowing sends after the socket has closed. */
109
+ send(msg) {
110
+ if (this.closed)
111
+ return;
112
+ try {
113
+ writeMessage(this.socket, msg);
114
+ }
115
+ catch {
116
+ // A write on a half-closed socket can throw; treat as closed.
117
+ this.closed = true;
118
+ }
119
+ }
120
+ // --- terminal ------------------------------------------------------------
121
+ onHostData(cb) { this.hostDataCb = cb; }
122
+ write(data) {
123
+ const bytes = typeof data === 'string' ? Buffer.from(data, 'utf8') : data;
124
+ this.send({ t: 'input', data: encodeChunk(bytes) });
125
+ }
126
+ resize(cols, rows) { this.send({ t: 'resize', cols, rows }); }
127
+ getReplay() { return this.replay; }
128
+ // --- windows -------------------------------------------------------------
129
+ onWindowState(cb) { this.windowStateCb = cb; }
130
+ windowState() { return this.winState; }
131
+ newWindow() { this.send({ t: 'win', op: 'new' }); }
132
+ nextWindow() { this.send({ t: 'win', op: 'next' }); }
133
+ prevWindow() { this.send({ t: 'win', op: 'prev' }); }
134
+ selectWindow(i) { this.send({ t: 'win', op: 'select', index: i }); }
135
+ closeWindow(i) { this.send({ t: 'win', op: 'close', index: i }); }
136
+ // --- viewers -------------------------------------------------------------
137
+ onViewersChange(cb) { this.viewersCb = cb; }
138
+ viewerCount() { return this.viewers; }
139
+ // --- logs (debug tab) ----------------------------------------------------
140
+ onLog(cb) { this.logCb = cb; }
141
+ getLogBuffer() { return this.logBuf; }
142
+ // --- url -----------------------------------------------------------------
143
+ getUrl() { return this.url; }
144
+ onUrl(cb) { this.urlCb = cb; }
145
+ // --- lifecycle -----------------------------------------------------------
146
+ onExit(cb) { this.exitCb = cb; }
147
+ /**
148
+ * Detach without ending the daemon session: tell the daemon to drop this
149
+ * client (it keeps running), end the socket, then run the local exit path so
150
+ * the host UI tears its terminal down (host-terminal's restore() runs on exit).
151
+ */
152
+ detach() {
153
+ this.send({ t: 'detach' });
154
+ this.closed = true;
155
+ try {
156
+ this.socket.end();
157
+ }
158
+ catch { /* already ending */ }
159
+ this.fireExit(null, null);
160
+ }
161
+ /** Tear down: end the socket (no exit fired — the caller is disposing us). */
162
+ dispose() {
163
+ this.closed = true;
164
+ try {
165
+ this.socket.end();
166
+ }
167
+ catch { /* already ending */ }
168
+ }
169
+ }
170
+ //# sourceMappingURL=remote-host-session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-host-session.js","sourceRoot":"","sources":["../src/remote-host-session.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ /** A registered, discoverable daemon session. */
2
+ export interface SessionInfo {
3
+ name: string;
4
+ socket: string;
5
+ logFile: string;
6
+ pid: number;
7
+ capId: string;
8
+ url: string;
9
+ createdAt: number;
10
+ }
11
+ /**
12
+ * Resolve the runtime directory holding sockets, logs and the registry.
13
+ *
14
+ * Precedence:
15
+ * 1. `ENTANGLE_RUN_DIR` — explicit override (used by tests, and handy for the
16
+ * daemon to pin a location);
17
+ * 2. `$XDG_RUNTIME_DIR/entangle` — the standard per-user runtime dir;
18
+ * 3. `~/.entangle/run` — fallback when XDG is unset.
19
+ */
20
+ export declare function resolveRunDir(): string;
21
+ /** Ensure the runtime directory exists with 0700 perms, returning its path. */
22
+ export declare function ensureRunDir(): string;
23
+ /** Reduce an arbitrary name to a filesystem-safe token ([A-Za-z0-9._-]). */
24
+ export declare function sanitizeName(name: string): string;
25
+ /** Absolute path of the unix socket for a session. */
26
+ export declare function socketPath(name: string): string;
27
+ /** Absolute path of the log file for a session. */
28
+ export declare function logPath(name: string): string;
29
+ /**
30
+ * Stable short session name derived from a capability id, so an unnamed session
31
+ * is keyed on its capability. Deterministic: the same capId always yields the
32
+ * same name. Uses the sanitized leading characters of the capId (capIds are
33
+ * already url-safe base-ish tokens) to stay human-recognizable.
34
+ */
35
+ export declare function defaultSessionName(capId: string): string;
36
+ /** Read the registry. A missing or corrupt file is treated as empty. */
37
+ export declare function listSessions(): SessionInfo[];
38
+ /** Add (or replace by name) a session in the registry. */
39
+ export declare function addSession(info: SessionInfo): void;
40
+ /** Remove a session from the registry by name. No-op if absent. */
41
+ export declare function removeSession(name: string): void;
42
+ /** Find a registered session by name. */
43
+ export declare function findSession(name: string): SessionInfo | undefined;
44
+ /** True if the pid is alive AND the session's socket file still exists. */
45
+ export declare function isAlive(info: SessionInfo): boolean;
46
+ /**
47
+ * Prune dead state: drop registry entries whose pid is gone, and unlink any
48
+ * `.sock` file in the run dir that no live registered session owns.
49
+ */
50
+ export declare function cleanupStale(): void;
51
+ //# sourceMappingURL=session-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-registry.d.ts","sourceRoot":"","sources":["../src/session-registry.ts"],"names":[],"mappings":"AAeA,iDAAiD;AACjD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;CACnB;AAID;;;;;;;;GAQG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAMtC;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,IAAI,MAAM,CAIrC;AAED,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAIjD;AAED,sDAAsD;AACtD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,mDAAmD;AACnD,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGxD;AAsBD,wEAAwE;AACxE,wBAAgB,YAAY,IAAI,WAAW,EAAE,CAc5C;AAaD,0DAA0D;AAC1D,wBAAgB,UAAU,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAIlD;AAED,mEAAmE;AACnE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAIhD;AAED,yCAAyC;AACzC,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAEjE;AAID,2EAA2E;AAC3E,wBAAgB,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAElD;AAcD;;;GAGG;AACH,wBAAgB,YAAY,IAAI,IAAI,CAoBnC"}
@@ -0,0 +1,167 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ const REGISTRY_FILE = 'sessions.json';
5
+ /**
6
+ * Resolve the runtime directory holding sockets, logs and the registry.
7
+ *
8
+ * Precedence:
9
+ * 1. `ENTANGLE_RUN_DIR` — explicit override (used by tests, and handy for the
10
+ * daemon to pin a location);
11
+ * 2. `$XDG_RUNTIME_DIR/entangle` — the standard per-user runtime dir;
12
+ * 3. `~/.entangle/run` — fallback when XDG is unset.
13
+ */
14
+ export function resolveRunDir() {
15
+ const override = process.env.ENTANGLE_RUN_DIR;
16
+ if (override)
17
+ return override;
18
+ const xdg = process.env.XDG_RUNTIME_DIR;
19
+ if (xdg)
20
+ return path.join(xdg, 'entangle');
21
+ return path.join(os.homedir(), '.entangle', 'run');
22
+ }
23
+ /** Ensure the runtime directory exists with 0700 perms, returning its path. */
24
+ export function ensureRunDir() {
25
+ const dir = resolveRunDir();
26
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
27
+ return dir;
28
+ }
29
+ /** Reduce an arbitrary name to a filesystem-safe token ([A-Za-z0-9._-]). */
30
+ export function sanitizeName(name) {
31
+ const safe = name.replace(/[^A-Za-z0-9._-]/g, '-');
32
+ // Avoid an empty basename or a name that resolves to a directory entry.
33
+ return safe.length > 0 && safe !== '.' && safe !== '..' ? safe : 'session';
34
+ }
35
+ /** Absolute path of the unix socket for a session. */
36
+ export function socketPath(name) {
37
+ return path.join(resolveRunDir(), `${sanitizeName(name)}.sock`);
38
+ }
39
+ /** Absolute path of the log file for a session. */
40
+ export function logPath(name) {
41
+ return path.join(resolveRunDir(), `${sanitizeName(name)}.log`);
42
+ }
43
+ /**
44
+ * Stable short session name derived from a capability id, so an unnamed session
45
+ * is keyed on its capability. Deterministic: the same capId always yields the
46
+ * same name. Uses the sanitized leading characters of the capId (capIds are
47
+ * already url-safe base-ish tokens) to stay human-recognizable.
48
+ */
49
+ export function defaultSessionName(capId) {
50
+ const token = sanitizeName(capId).replace(/[._-]/g, '').slice(0, 10) || 'anon';
51
+ return `cap-${token}`;
52
+ }
53
+ // --- registry file I/O (best-effort, tolerant of missing/corrupt data) -----
54
+ function registryPath() {
55
+ return path.join(resolveRunDir(), REGISTRY_FILE);
56
+ }
57
+ function isSessionInfo(value) {
58
+ if (typeof value !== 'object' || value === null)
59
+ return false;
60
+ const v = value;
61
+ return (typeof v.name === 'string' &&
62
+ typeof v.socket === 'string' &&
63
+ typeof v.logFile === 'string' &&
64
+ typeof v.pid === 'number' &&
65
+ typeof v.capId === 'string' &&
66
+ typeof v.url === 'string' &&
67
+ typeof v.createdAt === 'number');
68
+ }
69
+ /** Read the registry. A missing or corrupt file is treated as empty. */
70
+ export function listSessions() {
71
+ let raw;
72
+ try {
73
+ raw = fs.readFileSync(registryPath(), 'utf8');
74
+ }
75
+ catch {
76
+ return []; // missing file (or unreadable) → no sessions
77
+ }
78
+ try {
79
+ const parsed = JSON.parse(raw);
80
+ if (!Array.isArray(parsed))
81
+ return [];
82
+ return parsed.filter(isSessionInfo);
83
+ }
84
+ catch {
85
+ return []; // corrupt JSON → treat as empty
86
+ }
87
+ }
88
+ function writeSessions(sessions) {
89
+ ensureRunDir();
90
+ const tmp = `${registryPath()}.${process.pid}.tmp`;
91
+ try {
92
+ fs.writeFileSync(tmp, JSON.stringify(sessions, null, 2), { mode: 0o600 });
93
+ fs.renameSync(tmp, registryPath()); // atomic replace
94
+ }
95
+ catch {
96
+ try {
97
+ fs.unlinkSync(tmp);
98
+ }
99
+ catch { /* nothing to clean up */ }
100
+ }
101
+ }
102
+ /** Add (or replace by name) a session in the registry. */
103
+ export function addSession(info) {
104
+ const sessions = listSessions().filter((s) => s.name !== info.name);
105
+ sessions.push(info);
106
+ writeSessions(sessions);
107
+ }
108
+ /** Remove a session from the registry by name. No-op if absent. */
109
+ export function removeSession(name) {
110
+ const sessions = listSessions();
111
+ const next = sessions.filter((s) => s.name !== name);
112
+ if (next.length !== sessions.length)
113
+ writeSessions(next);
114
+ }
115
+ /** Find a registered session by name. */
116
+ export function findSession(name) {
117
+ return listSessions().find((s) => s.name === name);
118
+ }
119
+ // --- liveness --------------------------------------------------------------
120
+ /** True if the pid is alive AND the session's socket file still exists. */
121
+ export function isAlive(info) {
122
+ return pidAlive(info.pid) && fs.existsSync(info.socket);
123
+ }
124
+ /** True if a process with `pid` exists and is signalable by us. */
125
+ function pidAlive(pid) {
126
+ if (!Number.isInteger(pid) || pid <= 0)
127
+ return false;
128
+ try {
129
+ process.kill(pid, 0);
130
+ return true;
131
+ }
132
+ catch (err) {
133
+ // EPERM means the process exists but we may not signal it → still alive.
134
+ return err.code === 'EPERM';
135
+ }
136
+ }
137
+ /**
138
+ * Prune dead state: drop registry entries whose pid is gone, and unlink any
139
+ * `.sock` file in the run dir that no live registered session owns.
140
+ */
141
+ export function cleanupStale() {
142
+ const dir = resolveRunDir();
143
+ const sessions = listSessions();
144
+ const live = sessions.filter((s) => pidAlive(s.pid));
145
+ if (live.length !== sessions.length)
146
+ writeSessions(live);
147
+ const liveSockets = new Set(live.map((s) => s.socket));
148
+ let entries;
149
+ try {
150
+ entries = fs.readdirSync(dir);
151
+ }
152
+ catch {
153
+ return; // dir gone → nothing to unlink
154
+ }
155
+ for (const entry of entries) {
156
+ if (!entry.endsWith('.sock'))
157
+ continue;
158
+ const full = path.join(dir, entry);
159
+ if (liveSockets.has(full))
160
+ continue; // owned by a live session
161
+ try {
162
+ fs.unlinkSync(full);
163
+ }
164
+ catch { /* raced / already gone */ }
165
+ }
166
+ }
167
+ //# sourceMappingURL=session-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-registry.js","sourceRoot":"","sources":["../src/session-registry.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thenewlabs/entangle-serve",
3
- "version": "2.6.2",
3
+ "version": "2.7.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "bin": {
@@ -17,9 +17,9 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
20
- "@thenewlabs/entangle-crypto": "^2.6.2",
21
- "@thenewlabs/entangle-protocol": "^2.6.2",
22
- "@thenewlabs/entangle-utils": "^2.6.2",
20
+ "@thenewlabs/entangle-crypto": "^2.7.0",
21
+ "@thenewlabs/entangle-protocol": "^2.7.0",
22
+ "@thenewlabs/entangle-utils": "^2.7.0",
23
23
  "cborg": "^4.5.8",
24
24
  "commander": "^15.0.0",
25
25
  "ws": "^8.21.0"