@sentropic/h2a-runtime 0.91.0 → 0.92.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.
Files changed (94) hide show
  1. package/dist/config.d.ts +2 -0
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +16 -3
  4. package/dist/config.js.map +1 -1
  5. package/dist/index.d.ts +4 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +79 -58
  8. package/dist/index.js.map +1 -1
  9. package/dist/llm-gateway-runtime/index.d.ts +6 -0
  10. package/dist/llm-gateway-runtime/index.d.ts.map +1 -1
  11. package/dist/llm-gateway-runtime/index.js +134 -60
  12. package/dist/llm-gateway-runtime/index.js.map +1 -1
  13. package/dist/llm-gateway-runtime/session-ledger.d.ts +38 -55
  14. package/dist/llm-gateway-runtime/session-ledger.d.ts.map +1 -1
  15. package/dist/llm-gateway-runtime/session-ledger.js +102 -250
  16. package/dist/llm-gateway-runtime/session-ledger.js.map +1 -1
  17. package/dist/llm-gateway-runtime/sticky.d.ts +25 -52
  18. package/dist/llm-gateway-runtime/sticky.d.ts.map +1 -1
  19. package/dist/llm-gateway-runtime/sticky.js +36 -605
  20. package/dist/llm-gateway-runtime/sticky.js.map +1 -1
  21. package/dist/llm-mesh.d.ts +19 -72
  22. package/dist/llm-mesh.d.ts.map +1 -1
  23. package/dist/llm-mesh.js +42 -399
  24. package/dist/llm-mesh.js.map +1 -1
  25. package/dist/llm-routing-config.d.ts +28 -0
  26. package/dist/llm-routing-config.d.ts.map +1 -0
  27. package/dist/llm-routing-config.js +100 -0
  28. package/dist/llm-routing-config.js.map +1 -0
  29. package/dist/native-terminal/client.d.ts +23 -0
  30. package/dist/native-terminal/client.d.ts.map +1 -0
  31. package/dist/native-terminal/client.js +203 -0
  32. package/dist/native-terminal/client.js.map +1 -0
  33. package/dist/native-terminal/host.d.ts +78 -0
  34. package/dist/native-terminal/host.d.ts.map +1 -0
  35. package/dist/native-terminal/host.js +275 -0
  36. package/dist/native-terminal/host.js.map +1 -0
  37. package/dist/native-terminal/process.d.ts +11 -0
  38. package/dist/native-terminal/process.d.ts.map +1 -0
  39. package/dist/native-terminal/process.js +137 -0
  40. package/dist/native-terminal/process.js.map +1 -0
  41. package/dist/native-terminal/protocol.d.ts +57 -0
  42. package/dist/native-terminal/protocol.d.ts.map +1 -0
  43. package/dist/native-terminal/protocol.js +102 -0
  44. package/dist/native-terminal/protocol.js.map +1 -0
  45. package/dist/native-terminal/replay-buffer.d.ts +26 -0
  46. package/dist/native-terminal/replay-buffer.d.ts.map +1 -0
  47. package/dist/native-terminal/replay-buffer.js +84 -0
  48. package/dist/native-terminal/replay-buffer.js.map +1 -0
  49. package/dist/native-terminal/server.d.ts +13 -0
  50. package/dist/native-terminal/server.d.ts.map +1 -0
  51. package/dist/native-terminal/server.js +555 -0
  52. package/dist/native-terminal/server.js.map +1 -0
  53. package/dist/native-terminal/socket-path.d.ts +8 -0
  54. package/dist/native-terminal/socket-path.d.ts.map +1 -0
  55. package/dist/native-terminal/socket-path.js +41 -0
  56. package/dist/native-terminal/socket-path.js.map +1 -0
  57. package/dist/native-terminal/supervisor.d.ts +26 -0
  58. package/dist/native-terminal/supervisor.d.ts.map +1 -0
  59. package/dist/native-terminal/supervisor.js +273 -0
  60. package/dist/native-terminal/supervisor.js.map +1 -0
  61. package/dist/pty.d.ts +1 -0
  62. package/dist/pty.d.ts.map +1 -1
  63. package/dist/pty.js +78 -2
  64. package/dist/pty.js.map +1 -1
  65. package/dist/registry.d.ts +7 -0
  66. package/dist/registry.d.ts.map +1 -1
  67. package/dist/registry.js +37 -1
  68. package/dist/registry.js.map +1 -1
  69. package/dist/status-projection.d.ts.map +1 -1
  70. package/dist/tmux.d.ts +7 -5
  71. package/dist/tmux.d.ts.map +1 -1
  72. package/dist/tmux.js +33 -7
  73. package/dist/tmux.js.map +1 -1
  74. package/package.json +3 -3
  75. package/dist/llm-gateway-runtime/accounts.d.ts +0 -62
  76. package/dist/llm-gateway-runtime/accounts.d.ts.map +0 -1
  77. package/dist/llm-gateway-runtime/accounts.js +0 -323
  78. package/dist/llm-gateway-runtime/accounts.js.map +0 -1
  79. package/dist/llm-gateway-runtime/model-catalog.d.ts +0 -44
  80. package/dist/llm-gateway-runtime/model-catalog.d.ts.map +0 -1
  81. package/dist/llm-gateway-runtime/model-catalog.js +0 -191
  82. package/dist/llm-gateway-runtime/model-catalog.js.map +0 -1
  83. package/dist/llm-gateway-runtime/proxy-anthropic.d.ts +0 -3
  84. package/dist/llm-gateway-runtime/proxy-anthropic.d.ts.map +0 -1
  85. package/dist/llm-gateway-runtime/proxy-anthropic.js +0 -318
  86. package/dist/llm-gateway-runtime/proxy-anthropic.js.map +0 -1
  87. package/dist/llm-gateway-runtime/proxy-cloud-code.d.ts +0 -11
  88. package/dist/llm-gateway-runtime/proxy-cloud-code.d.ts.map +0 -1
  89. package/dist/llm-gateway-runtime/proxy-cloud-code.js +0 -215
  90. package/dist/llm-gateway-runtime/proxy-cloud-code.js.map +0 -1
  91. package/dist/llm-gateway-runtime/proxy-openai.d.ts +0 -118
  92. package/dist/llm-gateway-runtime/proxy-openai.d.ts.map +0 -1
  93. package/dist/llm-gateway-runtime/proxy-openai.js +0 -1305
  94. package/dist/llm-gateway-runtime/proxy-openai.js.map +0 -1
@@ -0,0 +1,84 @@
1
+ export const NATIVE_TERMINAL_MAX_REPLAY_CHUNKS_PER_SESSION = 65_536;
2
+ // Leaves 8 MiB of the 32 MiB protocol frame for the response envelope and
3
+ // bounded identities, while charging the actual JSON-escaped chunk encoding.
4
+ export const NATIVE_TERMINAL_MAX_REPLAY_WIRE_BYTES_PER_SESSION = 24 * 1024 * 1024;
5
+ export class TerminalReplayBuffer {
6
+ #maxBytes;
7
+ #maxChunks;
8
+ #maxWireBytes;
9
+ #chunks = [];
10
+ #head = 0;
11
+ #bytes = 0;
12
+ #wireBytes = 0;
13
+ #latestSeq = 0;
14
+ constructor(maxBytes, limits = {}) {
15
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
16
+ throw new RangeError("maxBytes must be a positive safe integer");
17
+ }
18
+ const maxChunks = limits.maxChunks ?? NATIVE_TERMINAL_MAX_REPLAY_CHUNKS_PER_SESSION;
19
+ const maxWireBytes = limits.maxWireBytes ??
20
+ NATIVE_TERMINAL_MAX_REPLAY_WIRE_BYTES_PER_SESSION;
21
+ if (!Number.isSafeInteger(maxChunks) || maxChunks <= 0) {
22
+ throw new RangeError("maxChunks must be a positive safe integer");
23
+ }
24
+ if (!Number.isSafeInteger(maxWireBytes) || maxWireBytes <= 0) {
25
+ throw new RangeError("maxWireBytes must be a positive safe integer");
26
+ }
27
+ this.#maxBytes = maxBytes;
28
+ this.#maxChunks = maxChunks;
29
+ this.#maxWireBytes = maxWireBytes;
30
+ }
31
+ get latestSeq() {
32
+ return this.#latestSeq;
33
+ }
34
+ append(data) {
35
+ if (data.length === 0) {
36
+ throw new RangeError("terminal output chunks must not be empty");
37
+ }
38
+ const chunk = Object.freeze({ seq: ++this.#latestSeq, data });
39
+ const entry = Object.freeze({
40
+ chunk,
41
+ bytes: Buffer.byteLength(data, "utf8"),
42
+ // One byte reserves either the array comma or a conservative bracket.
43
+ wireBytes: Buffer.byteLength(JSON.stringify(chunk), "utf8") + 1,
44
+ });
45
+ this.#chunks.push(entry);
46
+ this.#bytes += entry.bytes;
47
+ this.#wireBytes += entry.wireBytes;
48
+ while ((this.#bytes > this.#maxBytes ||
49
+ this.#wireBytes > this.#maxWireBytes ||
50
+ this.#chunks.length - this.#head > this.#maxChunks) &&
51
+ this.#head < this.#chunks.length) {
52
+ const evicted = this.#chunks[this.#head++];
53
+ if (evicted) {
54
+ this.#bytes -= evicted.bytes;
55
+ this.#wireBytes -= evicted.wireBytes;
56
+ }
57
+ }
58
+ if (this.#head >= 1_024 && this.#head * 2 >= this.#chunks.length) {
59
+ this.#chunks.splice(0, this.#head);
60
+ this.#head = 0;
61
+ }
62
+ return chunk;
63
+ }
64
+ readAfter(afterSeq) {
65
+ if (!Number.isSafeInteger(afterSeq) ||
66
+ afterSeq < 0 ||
67
+ afterSeq > this.#latestSeq) {
68
+ throw new RangeError("afterSeq must identify this buffer's history");
69
+ }
70
+ const oldestAvailable = this.#chunks[this.#head]?.chunk.seq ?? this.#latestSeq + 1;
71
+ const gap = afterSeq < oldestAvailable - 1
72
+ ? Object.freeze({ fromSeq: afterSeq + 1, toSeq: oldestAvailable - 1 })
73
+ : null;
74
+ return Object.freeze({
75
+ chunks: this.#chunks
76
+ .slice(this.#head)
77
+ .filter(({ chunk }) => chunk.seq > afterSeq)
78
+ .map(({ chunk }) => chunk),
79
+ gap,
80
+ latestSeq: this.#latestSeq,
81
+ });
82
+ }
83
+ }
84
+ //# sourceMappingURL=replay-buffer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay-buffer.js","sourceRoot":"","sources":["../../src/native-terminal/replay-buffer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import { NativeTerminalHost } from "./host.js";
2
+ export type NativeTerminalHostServer = Readonly<{
3
+ socketPath: string;
4
+ close(options?: {
5
+ stopSessions?: boolean;
6
+ signal?: string;
7
+ }): Promise<void>;
8
+ }>;
9
+ export declare function startNativeTerminalHostServer(options: {
10
+ socketPath: string;
11
+ host: NativeTerminalHost;
12
+ }): Promise<NativeTerminalHostServer>;
13
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/native-terminal/server.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,kBAAkB,EAGnB,MAAM,WAAW,CAAC;AAwhBnB,MAAM,MAAM,wBAAwB,GAAG,QAAQ,CAAC;IAC9C,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,OAAO,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7E,CAAC,CAAC;AAEH,wBAAsB,6BAA6B,CAAC,OAAO,EAAE;IAC3D,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,kBAAkB,CAAC;CAC1B,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAuGpC"}
@@ -0,0 +1,555 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { chmod, link, lstat, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
3
+ import { createConnection, createServer } from "node:net";
4
+ import { basename, dirname, isAbsolute, join } from "node:path";
5
+ import { NativeTerminalHost, } from "./host.js";
6
+ import { NATIVE_TERMINAL_MAX_FRAME_BYTES, NATIVE_TERMINAL_MAX_CONNECTIONS, NATIVE_TERMINAL_MAX_ERROR_MESSAGE_CHARS, NATIVE_TERMINAL_MAX_IDENTIFIER_CHARS, NATIVE_TERMINAL_MAX_PENDING_RESPONSE_BYTES_PER_CONNECTION, NATIVE_TERMINAL_MAX_PENDING_RESPONSE_BYTES_TOTAL, NATIVE_TERMINAL_MAX_PENDING_RESPONSES_PER_CONNECTION, NATIVE_TERMINAL_PROTOCOL_VERSION, isNativeTerminalStopSignal, isRecord, parseNativeTerminalRequest, } from "./protocol.js";
7
+ import { assertPrivateNativeTerminalSocketDirectory, inspectPrivateNativeTerminalSocket, sameNativeTerminalSocket, } from "./socket-path.js";
8
+ const SOCKET_PUBLICATION_LOCK_TIMEOUT_MS = 5_000;
9
+ const SOCKET_PUBLICATION_LOCK_RETRY_MS = 25;
10
+ const SOCKET_PUBLICATION_LOCK_ID_FILE = ".h2a-native-terminal.lock-id";
11
+ function requiredRecord(value, label) {
12
+ if (!isRecord(value))
13
+ throw new TypeError(`${label} must be an object`);
14
+ return value;
15
+ }
16
+ function requiredString(value, label) {
17
+ if (typeof value !== "string" || value.length === 0) {
18
+ throw new TypeError(`${label} must be a non-empty string`);
19
+ }
20
+ return value;
21
+ }
22
+ function requiredIdentifier(value, label) {
23
+ const identifier = requiredString(value, label);
24
+ if (identifier.length > NATIVE_TERMINAL_MAX_IDENTIFIER_CHARS) {
25
+ throw new TypeError(`${label} must contain at most ${NATIVE_TERMINAL_MAX_IDENTIFIER_CHARS} characters`);
26
+ }
27
+ return identifier;
28
+ }
29
+ function requiredInteger(value, label) {
30
+ if (!Number.isSafeInteger(value) || value <= 0) {
31
+ throw new TypeError(`${label} must be a positive safe integer`);
32
+ }
33
+ return value;
34
+ }
35
+ function sessionId(params) {
36
+ return requiredIdentifier(requiredRecord(params, "params").id, "session id");
37
+ }
38
+ function controllerLease(value) {
39
+ const lease = requiredRecord(value, "controller lease");
40
+ if (lease.role !== "controller")
41
+ throw new TypeError("invalid controller lease role");
42
+ return {
43
+ role: "controller",
44
+ id: requiredIdentifier(lease.id, "controller lease session id"),
45
+ generation: requiredIdentifier(lease.generation, "controller lease generation"),
46
+ incarnation: requiredIdentifier(lease.incarnation, "controller lease incarnation"),
47
+ controllerId: requiredIdentifier(lease.controllerId, "controller lease id"),
48
+ epoch: requiredInteger(lease.epoch, "controller lease epoch"),
49
+ };
50
+ }
51
+ function sameLease(left, right) {
52
+ return left.id === right.id
53
+ && left.generation === right.generation
54
+ && left.incarnation === right.incarnation
55
+ && left.controllerId === right.controllerId
56
+ && left.epoch === right.epoch;
57
+ }
58
+ function ownedLease(context, params) {
59
+ const lease = controllerLease(requiredRecord(params, "params").lease);
60
+ const owned = context.leases.get(lease.id);
61
+ if (!owned || !sameLease(owned, lease)) {
62
+ throw new Error("controller lease is not owned by this connection");
63
+ }
64
+ return lease;
65
+ }
66
+ function createOptions(params) {
67
+ const options = requiredRecord(params, "create params");
68
+ if (!Array.isArray(options.args) || !options.args.every((arg) => typeof arg === "string")) {
69
+ throw new TypeError("terminal args must be an array of strings");
70
+ }
71
+ const environment = requiredRecord(options.env, "terminal env");
72
+ if (!Object.values(environment).every((value) => typeof value === "string")) {
73
+ throw new TypeError("terminal env values must be strings");
74
+ }
75
+ return {
76
+ id: requiredIdentifier(options.id, "terminal session id"),
77
+ command: requiredString(options.command, "terminal command"),
78
+ args: options.args,
79
+ cwd: requiredString(options.cwd, "terminal cwd"),
80
+ env: environment,
81
+ cols: requiredInteger(options.cols, "terminal cols"),
82
+ rows: requiredInteger(options.rows, "terminal rows"),
83
+ };
84
+ }
85
+ function releaseConnectionLeases(host, context) {
86
+ for (const lease of context.leases.values()) {
87
+ try {
88
+ host.releaseController(lease);
89
+ }
90
+ catch {
91
+ // Exit/stop already fences the lease. Disconnect cleanup is idempotent.
92
+ }
93
+ }
94
+ context.leases.clear();
95
+ }
96
+ function dispatch(host, context, request) {
97
+ const params = request.params;
98
+ switch (request.operation) {
99
+ case "ping":
100
+ return {
101
+ generation: host.generation,
102
+ hostPid: process.pid,
103
+ protocolVersion: NATIVE_TERMINAL_PROTOCOL_VERSION,
104
+ };
105
+ case "create":
106
+ return host.create(createOptions(params));
107
+ case "list":
108
+ return host.list();
109
+ case "state":
110
+ return host.state(sessionId(params));
111
+ case "read-output": {
112
+ const record = requiredRecord(params, "params");
113
+ const afterSeq = record.afterSeq;
114
+ if (!Number.isSafeInteger(afterSeq) || afterSeq < 0) {
115
+ throw new TypeError("afterSeq must be a non-negative safe integer");
116
+ }
117
+ return host.readOutput(requiredIdentifier(record.id, "session id"), afterSeq);
118
+ }
119
+ case "attach-observer":
120
+ return host.attachObserver(sessionId(params));
121
+ case "acquire-controller": {
122
+ const record = requiredRecord(params, "params");
123
+ const lease = host.acquireController(requiredIdentifier(record.id, "session id"), requiredIdentifier(record.controllerId, "controller id"));
124
+ context.leases.set(lease.id, lease);
125
+ return lease;
126
+ }
127
+ case "release-controller": {
128
+ const lease = ownedLease(context, params);
129
+ const result = host.releaseController(lease);
130
+ context.leases.delete(lease.id);
131
+ return result;
132
+ }
133
+ case "write": {
134
+ const record = requiredRecord(params, "params");
135
+ const lease = ownedLease(context, record);
136
+ host.write(lease, requiredString(record.data, "terminal input"));
137
+ return null;
138
+ }
139
+ case "resize": {
140
+ const record = requiredRecord(params, "params");
141
+ const lease = ownedLease(context, record);
142
+ host.resize(lease, requiredInteger(record.cols, "terminal cols"), requiredInteger(record.rows, "terminal rows"));
143
+ return null;
144
+ }
145
+ case "stop": {
146
+ const record = requiredRecord(params, "params");
147
+ const lease = ownedLease(context, record);
148
+ if (record.signal !== undefined &&
149
+ !isNativeTerminalStopSignal(record.signal)) {
150
+ throw new TypeError("terminal stop signal must be SIGHUP, SIGINT, SIGTERM or SIGKILL");
151
+ }
152
+ return host.stop(lease, record.signal);
153
+ }
154
+ }
155
+ }
156
+ function responseFrame(response) {
157
+ const frame = `${JSON.stringify(response)}\n`;
158
+ if (Buffer.byteLength(frame) > NATIVE_TERMINAL_MAX_FRAME_BYTES) {
159
+ throw new RangeError("terminal response exceeds the frame limit");
160
+ }
161
+ return frame;
162
+ }
163
+ function safeResponseId(value) {
164
+ return typeof value === "string" &&
165
+ value.length > 0 &&
166
+ value.length <= NATIVE_TERMINAL_MAX_IDENTIFIER_CHARS
167
+ ? value
168
+ : "invalid";
169
+ }
170
+ function boundedErrorMessage(error) {
171
+ const message = error instanceof Error ? error.message : String(error);
172
+ if (message.length === 0)
173
+ return "terminal operation failed";
174
+ return message.slice(0, NATIVE_TERMINAL_MAX_ERROR_MESSAGE_CHARS);
175
+ }
176
+ function writeResponse(context, response) {
177
+ let frame;
178
+ try {
179
+ frame = responseFrame(response);
180
+ }
181
+ catch {
182
+ context.socket.destroy();
183
+ return false;
184
+ }
185
+ const bytes = Buffer.byteLength(frame);
186
+ if (context.socket.destroyed ||
187
+ context.pendingResponses >=
188
+ NATIVE_TERMINAL_MAX_PENDING_RESPONSES_PER_CONNECTION ||
189
+ context.pendingResponseBytes + bytes >
190
+ NATIVE_TERMINAL_MAX_PENDING_RESPONSE_BYTES_PER_CONNECTION ||
191
+ context.responseBudget.pendingBytes + bytes >
192
+ NATIVE_TERMINAL_MAX_PENDING_RESPONSE_BYTES_TOTAL) {
193
+ context.socket.destroy();
194
+ return false;
195
+ }
196
+ context.pendingResponses += 1;
197
+ context.pendingResponseBytes += bytes;
198
+ context.responseBudget.pendingBytes += bytes;
199
+ try {
200
+ context.socket.write(frame, (error) => {
201
+ releasePendingResponse(context, bytes);
202
+ if (error)
203
+ context.socket.destroy();
204
+ });
205
+ return true;
206
+ }
207
+ catch {
208
+ releasePendingResponse(context, bytes);
209
+ context.socket.destroy();
210
+ return false;
211
+ }
212
+ }
213
+ function releasePendingResponse(context, bytes) {
214
+ if (context.pendingResponses <= 0)
215
+ return;
216
+ context.pendingResponses -= 1;
217
+ const released = Math.min(bytes, context.pendingResponseBytes);
218
+ context.pendingResponseBytes -= released;
219
+ context.responseBudget.pendingBytes = Math.max(0, context.responseBudget.pendingBytes - released);
220
+ }
221
+ function releaseAllPendingResponses(context) {
222
+ context.responseBudget.pendingBytes = Math.max(0, context.responseBudget.pendingBytes - context.pendingResponseBytes);
223
+ context.pendingResponseBytes = 0;
224
+ context.pendingResponses = 0;
225
+ }
226
+ function writeError(context, id, code, error) {
227
+ const response = {
228
+ version: NATIVE_TERMINAL_PROTOCOL_VERSION,
229
+ id: safeResponseId(id),
230
+ ok: false,
231
+ error: {
232
+ code,
233
+ message: boundedErrorMessage(error),
234
+ },
235
+ };
236
+ return writeResponse(context, response);
237
+ }
238
+ function consumeFrames(host, context, chunk) {
239
+ context.buffer += chunk;
240
+ for (;;) {
241
+ const newline = context.buffer.indexOf("\n");
242
+ if (newline < 0)
243
+ break;
244
+ const line = context.buffer.slice(0, newline);
245
+ context.buffer = context.buffer.slice(newline + 1);
246
+ if (line.length === 0)
247
+ continue;
248
+ if (Buffer.byteLength(line) > NATIVE_TERMINAL_MAX_FRAME_BYTES) {
249
+ writeError(context, "invalid", "invalid-request", new Error("terminal request exceeds the frame limit"));
250
+ context.socket.destroy();
251
+ return;
252
+ }
253
+ let raw;
254
+ try {
255
+ raw = JSON.parse(line);
256
+ }
257
+ catch (error) {
258
+ if (!writeError(context, "invalid", "invalid-request", error))
259
+ return;
260
+ continue;
261
+ }
262
+ let request;
263
+ try {
264
+ request = parseNativeTerminalRequest(raw);
265
+ }
266
+ catch (error) {
267
+ const id = isRecord(raw) ? raw.id : "invalid";
268
+ if (!writeError(context, id, "invalid-request", error))
269
+ return;
270
+ continue;
271
+ }
272
+ try {
273
+ const response = {
274
+ version: NATIVE_TERMINAL_PROTOCOL_VERSION,
275
+ id: request.id,
276
+ ok: true,
277
+ result: dispatch(host, context, request),
278
+ };
279
+ if (!writeResponse(context, response))
280
+ return;
281
+ }
282
+ catch (error) {
283
+ if (!writeError(context, request.id, "operation-failed", error))
284
+ return;
285
+ }
286
+ }
287
+ if (Buffer.byteLength(context.buffer) > NATIVE_TERMINAL_MAX_FRAME_BYTES) {
288
+ writeError(context, "invalid", "invalid-request", new Error("terminal request exceeds the frame limit"));
289
+ context.socket.destroy();
290
+ }
291
+ }
292
+ async function socketAcceptsConnections(socketPath) {
293
+ return new Promise((resolve) => {
294
+ const socket = createConnection(socketPath);
295
+ socket.once("connect", () => {
296
+ socket.destroy();
297
+ resolve(true);
298
+ });
299
+ socket.once("error", () => resolve(false));
300
+ });
301
+ }
302
+ async function socketPublicationLockSecret(socketPath) {
303
+ if (process.platform !== "linux" || process.getuid === undefined) {
304
+ throw new Error("native terminal socket publication requires Linux abstract Unix sockets");
305
+ }
306
+ const directory = dirname(socketPath);
307
+ const identityPath = join(directory, SOCKET_PUBLICATION_LOCK_ID_FILE);
308
+ const temporaryPath = join(directory, `.${SOCKET_PUBLICATION_LOCK_ID_FILE}.${process.pid}.${randomUUID()}.tmp`);
309
+ try {
310
+ await writeFile(temporaryPath, `${randomUUID()}\n`, {
311
+ flag: "wx",
312
+ mode: 0o600,
313
+ });
314
+ await chmod(temporaryPath, 0o600);
315
+ try {
316
+ await link(temporaryPath, identityPath);
317
+ }
318
+ catch (error) {
319
+ if (error.code !== "EEXIST")
320
+ throw error;
321
+ }
322
+ }
323
+ finally {
324
+ try {
325
+ await unlink(temporaryPath);
326
+ }
327
+ catch (error) {
328
+ if (error.code !== "ENOENT")
329
+ throw error;
330
+ }
331
+ }
332
+ const identity = await lstat(identityPath);
333
+ if (!identity.isFile()
334
+ || identity.isSymbolicLink()
335
+ || identity.uid !== process.getuid()
336
+ || (identity.mode & 0o777) !== 0o600) {
337
+ throw new Error(`terminal socket publication lock identity is not a private owned file: ${identityPath}`);
338
+ }
339
+ const secret = (await readFile(identityPath, "utf8")).trim();
340
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(secret)) {
341
+ throw new Error(`terminal socket publication lock identity is malformed: ${identityPath}`);
342
+ }
343
+ return secret;
344
+ }
345
+ async function acquireSocketPublicationLock(socketPath) {
346
+ const secret = await socketPublicationLockSecret(socketPath);
347
+ const address = `\0h2a-terminal-lock-${createHash("sha256")
348
+ .update(`${process.getuid?.() ?? "unknown"}:${socketPath}:${secret}`)
349
+ .digest("hex")}`;
350
+ const deadline = Date.now() + SOCKET_PUBLICATION_LOCK_TIMEOUT_MS;
351
+ for (;;) {
352
+ const lock = createServer();
353
+ try {
354
+ await new Promise((resolve, reject) => {
355
+ lock.once("error", reject);
356
+ lock.listen(address, () => {
357
+ lock.removeListener("error", reject);
358
+ resolve();
359
+ });
360
+ });
361
+ return lock;
362
+ }
363
+ catch (error) {
364
+ if (error.code !== "EADDRINUSE"
365
+ || Date.now() >= deadline) {
366
+ throw error;
367
+ }
368
+ await new Promise((resolve) => setTimeout(resolve, SOCKET_PUBLICATION_LOCK_RETRY_MS));
369
+ }
370
+ }
371
+ }
372
+ async function withSocketPublicationLock(socketPath, operation) {
373
+ const lock = await acquireSocketPublicationLock(socketPath);
374
+ try {
375
+ return await operation();
376
+ }
377
+ finally {
378
+ await new Promise((resolve, reject) => {
379
+ lock.close((error) => error ? reject(error) : resolve());
380
+ });
381
+ }
382
+ }
383
+ async function removeStaleSocket(socketPath) {
384
+ for (;;) {
385
+ let stale;
386
+ try {
387
+ stale = await inspectPrivateNativeTerminalSocket(socketPath);
388
+ }
389
+ catch (error) {
390
+ if (error.code === "ENOENT")
391
+ return;
392
+ throw error;
393
+ }
394
+ if (await socketAcceptsConnections(socketPath)) {
395
+ throw new Error(`terminal host socket is already active: ${socketPath}`);
396
+ }
397
+ try {
398
+ const current = await inspectPrivateNativeTerminalSocket(socketPath);
399
+ if (sameNativeTerminalSocket(stale, current)) {
400
+ await unlink(socketPath);
401
+ return;
402
+ }
403
+ }
404
+ catch (error) {
405
+ if (error.code === "ENOENT")
406
+ return;
407
+ throw error;
408
+ }
409
+ }
410
+ }
411
+ async function ensurePrivateSocketDirectory(socketPath) {
412
+ const directory = dirname(socketPath);
413
+ try {
414
+ await assertPrivateNativeTerminalSocketDirectory(socketPath);
415
+ }
416
+ catch (error) {
417
+ if (error.code !== "ENOENT")
418
+ throw error;
419
+ await mkdir(directory, { recursive: true, mode: 0o700 });
420
+ await chmod(directory, 0o700);
421
+ await assertPrivateNativeTerminalSocketDirectory(socketPath);
422
+ }
423
+ }
424
+ function stagingSocketPath(socketPath) {
425
+ const directory = dirname(socketPath);
426
+ const stem = basename(socketPath).slice(0, 24).replace(/[^A-Za-z0-9_.-]/g, "_");
427
+ return join(directory, `.${stem}.${process.pid}.${randomUUID().slice(0, 8)}.sock`);
428
+ }
429
+ async function publishSocket(stagedPath, socketPath) {
430
+ const staged = await inspectPrivateNativeTerminalSocket(stagedPath);
431
+ for (let attempt = 0; attempt < 200; attempt += 1) {
432
+ try {
433
+ await link(stagedPath, socketPath);
434
+ const published = await inspectPrivateNativeTerminalSocket(socketPath);
435
+ if (!sameNativeTerminalSocket(staged, published)) {
436
+ throw new Error("terminal host published socket identity does not match its staged socket");
437
+ }
438
+ await unlink(stagedPath);
439
+ return published;
440
+ }
441
+ catch (error) {
442
+ if (error.code !== "EEXIST")
443
+ throw error;
444
+ await removeStaleSocket(socketPath);
445
+ }
446
+ }
447
+ throw new Error("terminal host socket publication retry limit exceeded");
448
+ }
449
+ export async function startNativeTerminalHostServer(options) {
450
+ if (!isAbsolute(options.socketPath))
451
+ throw new Error("terminal host socket path must be absolute");
452
+ await ensurePrivateSocketDirectory(options.socketPath);
453
+ const sockets = new Set();
454
+ const responseBudget = { pendingBytes: 0 };
455
+ const server = createServer((socket) => {
456
+ if (sockets.size >= NATIVE_TERMINAL_MAX_CONNECTIONS) {
457
+ socket.destroy();
458
+ return;
459
+ }
460
+ const context = {
461
+ socket,
462
+ leases: new Map(),
463
+ responseBudget,
464
+ buffer: "",
465
+ pendingResponseBytes: 0,
466
+ pendingResponses: 0,
467
+ };
468
+ sockets.add(socket);
469
+ socket.setEncoding("utf8");
470
+ socket.setNoDelay(true);
471
+ socket.on("error", () => {
472
+ // Per-connection transport failures are contained to this client.
473
+ });
474
+ socket.on("data", (chunk) => {
475
+ try {
476
+ consumeFrames(options.host, context, chunk);
477
+ }
478
+ catch {
479
+ socket.destroy();
480
+ }
481
+ });
482
+ socket.on("close", () => {
483
+ releaseAllPendingResponses(context);
484
+ releaseConnectionLeases(options.host, context);
485
+ sockets.delete(socket);
486
+ });
487
+ });
488
+ const stagedPath = stagingSocketPath(options.socketPath);
489
+ let ownedSocket;
490
+ try {
491
+ await new Promise((resolve, reject) => {
492
+ server.once("error", reject);
493
+ server.listen(stagedPath, () => {
494
+ server.removeListener("error", reject);
495
+ resolve();
496
+ });
497
+ });
498
+ await chmod(stagedPath, 0o600);
499
+ ownedSocket = await withSocketPublicationLock(options.socketPath, () => publishSocket(stagedPath, options.socketPath));
500
+ }
501
+ catch (error) {
502
+ await new Promise((resolve) => server.close(() => resolve()));
503
+ try {
504
+ await unlink(stagedPath);
505
+ }
506
+ catch (cleanupError) {
507
+ if (cleanupError.code !== "ENOENT") {
508
+ throw cleanupError;
509
+ }
510
+ }
511
+ throw error;
512
+ }
513
+ let closing;
514
+ return Object.freeze({
515
+ socketPath: options.socketPath,
516
+ close(closeOptions = {}) {
517
+ closing ??= (async () => {
518
+ let stopError;
519
+ if (closeOptions.stopSessions) {
520
+ try {
521
+ const signal = closeOptions.signal ?? "SIGTERM";
522
+ if (!isNativeTerminalStopSignal(signal)) {
523
+ throw new TypeError("invalid terminal host shutdown signal");
524
+ }
525
+ options.host.stopAll(signal);
526
+ }
527
+ catch (error) {
528
+ stopError = error;
529
+ }
530
+ }
531
+ for (const socket of sockets)
532
+ socket.destroy();
533
+ await new Promise((resolve, reject) => {
534
+ server.close((error) => error ? reject(error) : resolve());
535
+ });
536
+ await withSocketPublicationLock(options.socketPath, async () => {
537
+ try {
538
+ const current = await inspectPrivateNativeTerminalSocket(options.socketPath);
539
+ if (sameNativeTerminalSocket(current, ownedSocket)) {
540
+ await unlink(options.socketPath);
541
+ }
542
+ }
543
+ catch (error) {
544
+ if (error.code !== "ENOENT")
545
+ throw error;
546
+ }
547
+ });
548
+ if (stopError !== undefined)
549
+ throw stopError;
550
+ })();
551
+ return closing;
552
+ },
553
+ });
554
+ }
555
+ //# sourceMappingURL=server.js.map