ai-remote 0.1.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 (114) hide show
  1. package/README.md +50 -0
  2. package/dist/index.js +7 -0
  3. package/dist/index.js.map +7 -0
  4. package/dist/protocols/index.js +35 -0
  5. package/dist/protocols/index.js.map +7 -0
  6. package/dist/protocols/rdp/buffer.js +170 -0
  7. package/dist/protocols/rdp/buffer.js.map +7 -0
  8. package/dist/protocols/rdp/caps.js +260 -0
  9. package/dist/protocols/rdp/caps.js.map +7 -0
  10. package/dist/protocols/rdp/cert.js +135 -0
  11. package/dist/protocols/rdp/cert.js.map +7 -0
  12. package/dist/protocols/rdp/client.js +1068 -0
  13. package/dist/protocols/rdp/client.js.map +7 -0
  14. package/dist/protocols/rdp/cliprdr.js +240 -0
  15. package/dist/protocols/rdp/cliprdr.js.map +7 -0
  16. package/dist/protocols/rdp/credssp.js +254 -0
  17. package/dist/protocols/rdp/credssp.js.map +7 -0
  18. package/dist/protocols/rdp/crypto.js +369 -0
  19. package/dist/protocols/rdp/crypto.js.map +7 -0
  20. package/dist/protocols/rdp/display.js +589 -0
  21. package/dist/protocols/rdp/display.js.map +7 -0
  22. package/dist/protocols/rdp/gcc.js +132 -0
  23. package/dist/protocols/rdp/gcc.js.map +7 -0
  24. package/dist/protocols/rdp/keymap.js +132 -0
  25. package/dist/protocols/rdp/keymap.js.map +7 -0
  26. package/dist/protocols/rdp/mcs.js +223 -0
  27. package/dist/protocols/rdp/mcs.js.map +7 -0
  28. package/dist/protocols/rdp/ntlm.js +305 -0
  29. package/dist/protocols/rdp/ntlm.js.map +7 -0
  30. package/dist/protocols/rdp/pdu.js +444 -0
  31. package/dist/protocols/rdp/pdu.js.map +7 -0
  32. package/dist/protocols/rdp/rail.js +383 -0
  33. package/dist/protocols/rdp/rail.js.map +7 -0
  34. package/dist/protocols/rdp/rle.js +276 -0
  35. package/dist/protocols/rdp/rle.js.map +7 -0
  36. package/dist/protocols/rdp/sec.js +156 -0
  37. package/dist/protocols/rdp/sec.js.map +7 -0
  38. package/dist/protocols/rdp/session.js +307 -0
  39. package/dist/protocols/rdp/session.js.map +7 -0
  40. package/dist/protocols/rdp/tls.js +354 -0
  41. package/dist/protocols/rdp/tls.js.map +7 -0
  42. package/dist/protocols/rdp/vchannel.js +59 -0
  43. package/dist/protocols/rdp/vchannel.js.map +7 -0
  44. package/dist/protocols/rdp/x224.js +110 -0
  45. package/dist/protocols/rdp/x224.js.map +7 -0
  46. package/dist/protocols/ssh/kex.js +177 -0
  47. package/dist/protocols/ssh/kex.js.map +7 -0
  48. package/dist/protocols/ssh/messages.js +55 -0
  49. package/dist/protocols/ssh/messages.js.map +7 -0
  50. package/dist/protocols/ssh/session.js +608 -0
  51. package/dist/protocols/ssh/session.js.map +7 -0
  52. package/dist/protocols/ssh/terminal.js +191 -0
  53. package/dist/protocols/ssh/terminal.js.map +7 -0
  54. package/dist/protocols/ssh/transport.js +611 -0
  55. package/dist/protocols/ssh/transport.js.map +7 -0
  56. package/dist/protocols/ssh/wire.js +189 -0
  57. package/dist/protocols/ssh/wire.js.map +7 -0
  58. package/dist/protocols/types.js +1 -0
  59. package/dist/protocols/types.js.map +7 -0
  60. package/dist/protocols/vnc/input.js +16 -0
  61. package/dist/protocols/vnc/input.js.map +7 -0
  62. package/dist/protocols/vnc/session.js +259 -0
  63. package/dist/protocols/vnc/session.js.map +7 -0
  64. package/dist/shared/connection.js +114 -0
  65. package/dist/shared/connection.js.map +7 -0
  66. package/dist/shared/device.js +1 -0
  67. package/dist/shared/device.js.map +7 -0
  68. package/dist/shared/hosts.js +133 -0
  69. package/dist/shared/hosts.js.map +7 -0
  70. package/dist/shared/icons.js +42 -0
  71. package/dist/shared/icons.js.map +7 -0
  72. package/dist/shared/protocol.js +40 -0
  73. package/dist/shared/protocol.js.map +7 -0
  74. package/dist/shared/signals.js +42 -0
  75. package/dist/shared/signals.js.map +7 -0
  76. package/package.json +82 -0
  77. package/src/index.ts +14 -0
  78. package/src/protocols/index.ts +72 -0
  79. package/src/protocols/rdp/buffer.ts +204 -0
  80. package/src/protocols/rdp/caps.ts +304 -0
  81. package/src/protocols/rdp/cert.ts +190 -0
  82. package/src/protocols/rdp/client.ts +1363 -0
  83. package/src/protocols/rdp/cliprdr.ts +344 -0
  84. package/src/protocols/rdp/credssp.ts +319 -0
  85. package/src/protocols/rdp/crypto.ts +416 -0
  86. package/src/protocols/rdp/display.ts +750 -0
  87. package/src/protocols/rdp/gcc.ts +162 -0
  88. package/src/protocols/rdp/keymap.ts +68 -0
  89. package/src/protocols/rdp/mcs.ts +283 -0
  90. package/src/protocols/rdp/ntlm.ts +380 -0
  91. package/src/protocols/rdp/pdu.ts +497 -0
  92. package/src/protocols/rdp/rail.ts +480 -0
  93. package/src/protocols/rdp/rle.ts +343 -0
  94. package/src/protocols/rdp/sec.ts +184 -0
  95. package/src/protocols/rdp/session.ts +393 -0
  96. package/src/protocols/rdp/tls.ts +452 -0
  97. package/src/protocols/rdp/vchannel.ts +89 -0
  98. package/src/protocols/rdp/x224.ts +139 -0
  99. package/src/protocols/ssh/kex.ts +227 -0
  100. package/src/protocols/ssh/messages.ts +54 -0
  101. package/src/protocols/ssh/session.ts +878 -0
  102. package/src/protocols/ssh/terminal.ts +242 -0
  103. package/src/protocols/ssh/transport.ts +732 -0
  104. package/src/protocols/ssh/wire.ts +220 -0
  105. package/src/protocols/types.ts +146 -0
  106. package/src/protocols/vnc/input.ts +13 -0
  107. package/src/protocols/vnc/novnc.d.ts +75 -0
  108. package/src/protocols/vnc/session.ts +367 -0
  109. package/src/shared/connection.ts +206 -0
  110. package/src/shared/device.ts +28 -0
  111. package/src/shared/hosts.ts +240 -0
  112. package/src/shared/icons.ts +49 -0
  113. package/src/shared/protocol.ts +55 -0
  114. package/src/shared/signals.ts +67 -0
@@ -0,0 +1,608 @@
1
+ import { MSG, EXTENDED_DATA_STDERR } from "./messages";
2
+ import { SshReader, SshWriter, decodeUtf8, encodeUtf8, toBase64 } from "./wire";
3
+ import { SshTransport } from "./transport";
4
+ const DEFAULT_PORT = 22;
5
+ const SERVICE = "ssh-connection";
6
+ const TERMINAL_TYPE = "xterm-256color";
7
+ const INITIAL_WINDOW = 2 * 1024 * 1024;
8
+ const MAX_PACKET = 32 * 1024;
9
+ const WINDOW_REFILL_THRESHOLD = INITIAL_WINDOW / 2;
10
+ const PTY_MODES = new Uint8Array([0]);
11
+ const COMMAND_START_MS = 2e4;
12
+ const COMMAND_IDLE_MS = 5 * 6e4;
13
+ const COMMAND_MAX_MS = 60 * 6e4;
14
+ const NO_BYTES = new Uint8Array(0);
15
+ function singleQuote(value) {
16
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
17
+ }
18
+ function shellFromPrompt(text) {
19
+ const tail = text.slice(-400);
20
+ if (/(?:^|\n)PS [A-Za-z]:\\[^\n]*>\s*$/.test(tail)) return "powershell";
21
+ if (/(?:^|\n)[^\n]*[A-Za-z]:\\[^\n]*>\s*$/.test(tail)) return "cmd";
22
+ return "";
23
+ }
24
+ const FRAMING_REJECTED = /is not recognized as an internal or external command|command not found|not recognized as the name of a cmdlet|CommandNotFoundException/i;
25
+ function frameCommand(family, command, id) {
26
+ if (family === "cmd") {
27
+ return `echo.&echo CCB${id}&${command}&call echo.CCE${id} %^ERRORLEVEL%\r`;
28
+ }
29
+ if (family === "powershell") {
30
+ const encoded = toBase64(encodeUtf8(command));
31
+ return `Write-Output ""; Write-Output ("CCB" + "${id}"); $__ccCode = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encoded}')); $global:LASTEXITCODE = $null; . ([ScriptBlock]::Create($__ccCode)); $__ccOk = $?; $__cc = if ($LASTEXITCODE -ne $null) { $LASTEXITCODE } elseif ($__ccOk) { 0 } else { 1 }; Write-Output ("CCE" + "${id} " + $__cc)\r`;
32
+ }
33
+ return `printf '\\n%s%s\\n' 'CCB' '${id}'; eval ${singleQuote(command)}; __cc=$?; printf '\\n%s%s %s\\n' 'CCE' '${id}' "$__cc"\r`;
34
+ }
35
+ function stripTerminalCodes(text) {
36
+ return text.replace(/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g, "").replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\u001b[@-Z\\-_]/g, "").replace(/\r\n/g, "\n").replace(/\r/g, "");
37
+ }
38
+ function filterDisplay(session, bytes) {
39
+ const filter = session.displayFilter;
40
+ if (!filter) return bytes;
41
+ filter.pending += decodeUtf8(bytes);
42
+ let shown = "";
43
+ if (filter.stage === "before") {
44
+ const at = filter.pending.indexOf(filter.begin);
45
+ if (at === -1) return NO_BYTES;
46
+ filter.pending = filter.pending.slice(at + filter.begin.length).replace(/^\r?\n/, "");
47
+ filter.stage = "inside";
48
+ }
49
+ if (filter.stage === "inside") {
50
+ const at = filter.pending.indexOf(filter.end);
51
+ if (at === -1) {
52
+ shown = filter.pending;
53
+ filter.pending = "";
54
+ return shown ? encodeUtf8(shown) : NO_BYTES;
55
+ }
56
+ shown = filter.pending.slice(0, at);
57
+ filter.pending = filter.pending.slice(at);
58
+ filter.stage = "closing";
59
+ }
60
+ const newline = filter.pending.indexOf("\n");
61
+ if (newline !== -1) {
62
+ shown += filter.pending.slice(newline + 1);
63
+ session.displayFilter = null;
64
+ }
65
+ return shown ? encodeUtf8(shown) : NO_BYTES;
66
+ }
67
+ function releaseDisplayFilter(session) {
68
+ const filter = session.displayFilter;
69
+ session.displayFilter = null;
70
+ if (!filter || filter.stage !== "before" || !filter.pending) return;
71
+ session.dispatchEvent(new CustomEvent("data", {
72
+ detail: { bytes: NO_BYTES, display: encodeUtf8(filter.pending), isStderr: false }
73
+ }));
74
+ }
75
+ class SshSession extends EventTarget {
76
+ /**
77
+ * @param {string} url gateway WebSocket URL
78
+ * @param {object} options
79
+ * @param {string} options.username
80
+ * @param {string} [options.password]
81
+ * @param {'powershell'} [options.preferredShell]
82
+ * @param {(info: object) => Promise<boolean>} [options.verifyHost]
83
+ * @param {(prompt: object) => Promise<string>} [options.requestInput]
84
+ */
85
+ constructor(url, options = {}) {
86
+ super();
87
+ this.options = options;
88
+ this.username = options.username || "";
89
+ this.password = options.password || "";
90
+ this.requestInput = options.requestInput || (async () => "");
91
+ this.columns = options.columns || 80;
92
+ this.rows = options.rows || 24;
93
+ this.channelId = 0;
94
+ this.remoteChannelId = null;
95
+ this.remoteWindow = 0;
96
+ this.remoteMaxPacket = MAX_PACKET;
97
+ this.localWindow = INITIAL_WINDOW;
98
+ this.pendingWrites = [];
99
+ this.shellOpen = false;
100
+ this.commandInFlight = false;
101
+ this.displayFilter = null;
102
+ this.readySignaled = false;
103
+ this.powerShellAttempted = false;
104
+ this.powerShellTimer = null;
105
+ this.shellFamily = "";
106
+ this.promptTail = "";
107
+ this.exitStatus = null;
108
+ this.authMethods = [];
109
+ this.closed = false;
110
+ this.waiters = /* @__PURE__ */ new Map();
111
+ this.transport = new SshTransport(url, {
112
+ verifyHost: options.verifyHost || (async () => true),
113
+ log: (step, detail) => console.info(`[SSH] ${step}`, detail)
114
+ });
115
+ this.transport.addEventListener("packet", (event) => {
116
+ this.#handlePacket(event.detail.type, event.detail.payload);
117
+ });
118
+ this.transport.addEventListener("greeting", (event) => {
119
+ for (const line of event.detail.lines) this.#emitBanner(`${line}\r
120
+ `);
121
+ });
122
+ this.transport.addEventListener("banner", (event) => this.#emitBanner(event.detail.text));
123
+ this.transport.addEventListener("keys", (event) => {
124
+ this.fingerprint = event.detail.fingerprint;
125
+ this.#status("secured", `Host key ${event.detail.fingerprint} verified.`);
126
+ this.dispatchEvent(new CustomEvent("secure", { detail: event.detail }));
127
+ if (event.detail.firstTime) void this.#authenticate();
128
+ });
129
+ this.transport.addEventListener("hostdisconnect", (event) => {
130
+ this.lastMessage = event.detail.reason;
131
+ });
132
+ this.transport.addEventListener("error", (event) => {
133
+ this.lastMessage = event.detail.message;
134
+ this.dispatchEvent(new CustomEvent("error", { detail: event.detail }));
135
+ });
136
+ this.transport.addEventListener("close", (event) => {
137
+ if (this.closed) return;
138
+ this.closed = true;
139
+ clearTimeout(this.powerShellTimer);
140
+ this.#abandonWaiters(event.detail.message || "The SSH connection closed.");
141
+ this.dispatchEvent(new CustomEvent("close", {
142
+ detail: {
143
+ ...event.detail,
144
+ message: event.detail.message || this.lastMessage || "",
145
+ exitStatus: this.exitStatus
146
+ }
147
+ }));
148
+ });
149
+ }
150
+ connect() {
151
+ this.#status("connecting", "Opening the SSH transport\u2026");
152
+ this.transport.connect();
153
+ }
154
+ disconnect() {
155
+ if (this.closed) return;
156
+ clearTimeout(this.powerShellTimer);
157
+ if (this.shellOpen && this.remoteChannelId !== null) {
158
+ this.transport.send(new SshWriter(8).u8(MSG.CHANNEL_CLOSE).u32(this.remoteChannelId).take());
159
+ }
160
+ this.transport.disconnect("Closed by the user");
161
+ }
162
+ /** Send keystrokes, or anything else the terminal produces, to the shell. */
163
+ write(data) {
164
+ const bytes = typeof data === "string" ? encodeUtf8(data) : data;
165
+ if (!bytes.length) return;
166
+ this.pendingWrites.push(bytes);
167
+ this.#flushWrites();
168
+ }
169
+ /**
170
+ * Run one command in this shell and return what it printed.
171
+ *
172
+ * Deliberately the *same* shell the user is looking at, not a second channel:
173
+ * a command the assistant runs inherits the working directory, the
174
+ * environment and the login the user already has, and it appears in their
175
+ * terminal as it happens. An agent acting on a machine invisibly is worse
176
+ * than an agent that is slower to read.
177
+ *
178
+ * The output is framed by markers rather than measured, because an
179
+ * interactive shell echoes its input and prints a prompt around it. The
180
+ * markers are assembled by the shell from two pieces, so the echoed command
181
+ * line -- which contains the pieces but not the joined marker -- never looks
182
+ * like the real boundary.
183
+ *
184
+ * A `command` event brackets the run so the terminal can label it, and the
185
+ * three deadlines above decide when a command is stuck rather than slow.
186
+ */
187
+ runCommand(command, {
188
+ startMs = COMMAND_START_MS,
189
+ idleMs = COMMAND_IDLE_MS,
190
+ maxMs = COMMAND_MAX_MS
191
+ } = {}) {
192
+ const text = String(command || "").trim();
193
+ if (!text) return Promise.reject(new Error("No command was given."));
194
+ if (!this.shellOpen || this.closed) {
195
+ return Promise.reject(new Error("The SSH shell is not open."));
196
+ }
197
+ if (this.readySignaled === false) {
198
+ return Promise.reject(new Error("The SSH shell is still starting."));
199
+ }
200
+ if (this.commandInFlight) {
201
+ return Promise.reject(new Error("Another command is still running in this shell."));
202
+ }
203
+ const id = Array.from(crypto.getRandomValues(new Uint8Array(6))).map((byte) => byte.toString(16).padStart(2, "0")).join("");
204
+ const begin = `CCB${id}`;
205
+ const end = `CCE${id}`;
206
+ const family = this.shellFamily || "posix";
207
+ this.commandInFlight = true;
208
+ const startedAt = Date.now();
209
+ this.displayFilter = { begin, end, stage: "before", pending: "" };
210
+ this.dispatchEvent(new CustomEvent("command", {
211
+ detail: { phase: "start", id, command: text }
212
+ }));
213
+ return new Promise((resolve, reject) => {
214
+ let transcript = "";
215
+ let started = false;
216
+ let startTimer = null;
217
+ let idleTimer = null;
218
+ let maxTimer = null;
219
+ const finish = (settle) => {
220
+ clearTimeout(startTimer);
221
+ clearTimeout(idleTimer);
222
+ clearTimeout(maxTimer);
223
+ this.removeEventListener("data", onData);
224
+ this.removeEventListener("close", onClose);
225
+ this.commandInFlight = false;
226
+ releaseDisplayFilter(this);
227
+ settle();
228
+ };
229
+ const done = (result) => finish(() => {
230
+ this.dispatchEvent(new CustomEvent("command", {
231
+ detail: {
232
+ phase: "end",
233
+ id,
234
+ command: text,
235
+ exitStatus: result.exitStatus,
236
+ timedOut: result.timedOut,
237
+ started: result.started !== false,
238
+ reason: result.reason,
239
+ durationMs: result.durationMs
240
+ }
241
+ }));
242
+ resolve(result);
243
+ });
244
+ const fail = (error) => finish(() => {
245
+ this.dispatchEvent(new CustomEvent("command", {
246
+ detail: {
247
+ phase: "end",
248
+ id,
249
+ command: text,
250
+ exitStatus: null,
251
+ timedOut: false,
252
+ started,
253
+ durationMs: Date.now() - startedAt,
254
+ error: error.message
255
+ }
256
+ }));
257
+ reject(error);
258
+ });
259
+ const partialOutput = () => {
260
+ const at = transcript.indexOf(begin);
261
+ return at === -1 ? "" : stripTerminalCodes(transcript.slice(at + begin.length)).replace(/^\r?\n/, "").replace(/\s+$/, "");
262
+ };
263
+ const giveUp = (reason) => {
264
+ this.write("");
265
+ done({
266
+ output: partialOutput(),
267
+ exitStatus: null,
268
+ timedOut: true,
269
+ started,
270
+ reason,
271
+ durationMs: Date.now() - startedAt
272
+ });
273
+ };
274
+ const onData = (event) => {
275
+ transcript += decodeUtf8(event.detail.bytes);
276
+ if (!started && transcript.indexOf(begin) !== -1) {
277
+ started = true;
278
+ clearTimeout(startTimer);
279
+ startTimer = null;
280
+ }
281
+ if (started) {
282
+ clearTimeout(idleTimer);
283
+ idleTimer = setTimeout(() => giveUp("idle"), idleMs);
284
+ }
285
+ const endAt = transcript.indexOf(`${end} `);
286
+ if (endAt === -1) {
287
+ if (!started && FRAMING_REJECTED.test(stripTerminalCodes(transcript))) {
288
+ const guessed = shellFromPrompt(stripTerminalCodes(transcript)) || "cmd";
289
+ const wrongGuess = family !== guessed;
290
+ this.shellFamily = guessed;
291
+ fail(new Error(
292
+ wrongGuess ? `This shell is ${guessed === "powershell" ? "PowerShell" : "Windows cmd"}, not a POSIX shell, and it rejected the wrapper used to capture output. The shell has been noted; run the command again and it will be framed correctly.` : `The shell rejected the wrapper used to capture output: ${stripTerminalCodes(transcript).split("\n").filter(Boolean).pop() || "no detail"}`
293
+ ));
294
+ }
295
+ return;
296
+ }
297
+ const tail = transcript.slice(endAt);
298
+ const status = parseInt(tail.slice(end.length + 1).trim(), 10);
299
+ const beginAt = transcript.indexOf(begin);
300
+ const body = beginAt === -1 ? transcript.slice(0, endAt) : transcript.slice(beginAt + begin.length, endAt);
301
+ done({
302
+ output: stripTerminalCodes(body).replace(/^\r?\n/, "").replace(/\s+$/, ""),
303
+ exitStatus: Number.isInteger(status) ? status : null,
304
+ timedOut: false,
305
+ started: true,
306
+ durationMs: Date.now() - startedAt
307
+ });
308
+ };
309
+ const onClose = () => fail(new Error("The SSH session closed while the command was running."));
310
+ this.addEventListener("data", onData);
311
+ this.addEventListener("close", onClose);
312
+ startTimer = setTimeout(() => giveUp("start"), startMs);
313
+ idleTimer = setTimeout(() => giveUp("idle"), idleMs);
314
+ maxTimer = setTimeout(() => giveUp("max"), maxMs);
315
+ this.write(frameCommand(family, text, id));
316
+ });
317
+ }
318
+ /** Tell the shell its window changed, so full-screen programs redraw. */
319
+ resize(columns, rows) {
320
+ const nextColumns = Math.max(1, Math.floor(columns));
321
+ const nextRows = Math.max(1, Math.floor(rows));
322
+ if (nextColumns === this.columns && nextRows === this.rows) return;
323
+ this.columns = nextColumns;
324
+ this.rows = nextRows;
325
+ if (!this.shellOpen || this.remoteChannelId === null) return;
326
+ this.transport.send(new SshWriter(64).u8(MSG.CHANNEL_REQUEST).u32(this.remoteChannelId).string("window-change").boolean(false).u32(nextColumns).u32(nextRows).u32(0).u32(0).take());
327
+ }
328
+ // --- plumbing ------------------------------------------------------------
329
+ #status(phase, message) {
330
+ this.dispatchEvent(new CustomEvent("status", { detail: { phase, message } }));
331
+ }
332
+ #emitBanner(text) {
333
+ if (text) this.dispatchEvent(new CustomEvent("banner", { detail: { text } }));
334
+ }
335
+ /** Wait for one of a few message types: the handshakes read linearly that way. */
336
+ #expect(...types) {
337
+ return new Promise((resolve, reject) => {
338
+ for (const type of types) this.waiters.set(type, { resolve, reject });
339
+ });
340
+ }
341
+ #settle(type, payload) {
342
+ const waiter = this.waiters.get(type);
343
+ if (!waiter) return false;
344
+ this.waiters.clear();
345
+ waiter.resolve({ type, payload });
346
+ return true;
347
+ }
348
+ /** A closed transport must not leave a handshake waiting forever. */
349
+ #abandonWaiters(message) {
350
+ if (this.waiters.size === 0) return;
351
+ const pending = [...new Set(this.waiters.values())];
352
+ this.waiters.clear();
353
+ for (const waiter of pending) waiter.reject(new Error(message));
354
+ }
355
+ #handlePacket(type, payload) {
356
+ if (this.#settle(type, payload)) return;
357
+ switch (type) {
358
+ case MSG.USERAUTH_BANNER:
359
+ this.#emitBanner(new SshReader(payload, 1).string().replaceAll("\n", "\r\n"));
360
+ return;
361
+ case MSG.CHANNEL_DATA: {
362
+ const reader = new SshReader(payload, 1);
363
+ reader.u32();
364
+ this.#consume(reader.stringBytes());
365
+ return;
366
+ }
367
+ case MSG.CHANNEL_EXTENDED_DATA: {
368
+ const reader = new SshReader(payload, 1);
369
+ reader.u32();
370
+ const dataType = reader.u32();
371
+ const bytes = reader.stringBytes();
372
+ this.#consume(bytes, dataType === EXTENDED_DATA_STDERR);
373
+ return;
374
+ }
375
+ case MSG.CHANNEL_WINDOW_ADJUST: {
376
+ const reader = new SshReader(payload, 1);
377
+ reader.u32();
378
+ this.remoteWindow += reader.u32();
379
+ this.#flushWrites();
380
+ return;
381
+ }
382
+ case MSG.CHANNEL_REQUEST: {
383
+ const reader = new SshReader(payload, 1);
384
+ reader.u32();
385
+ const request = reader.string();
386
+ const wantReply = reader.boolean();
387
+ if (request === "exit-status") this.exitStatus = reader.u32();
388
+ if (request === "exit-signal") this.exitSignal = reader.string();
389
+ if (wantReply && this.remoteChannelId !== null) {
390
+ this.transport.send(new SshWriter(8).u8(MSG.CHANNEL_SUCCESS).u32(this.remoteChannelId).take());
391
+ }
392
+ return;
393
+ }
394
+ case MSG.CHANNEL_EOF:
395
+ return;
396
+ case MSG.CHANNEL_CLOSE:
397
+ this.shellOpen = false;
398
+ this.#status("closed", "The remote shell ended.");
399
+ this.transport.disconnect("Shell closed");
400
+ return;
401
+ default:
402
+ return;
403
+ }
404
+ }
405
+ /**
406
+ * Watch the prompt for which shell this machine answers with, so the first
407
+ * command is framed correctly rather than discovering it by failing.
408
+ */
409
+ #noteShellFamily(bytes) {
410
+ this.promptTail = (this.promptTail + stripTerminalCodes(decodeUtf8(bytes))).slice(-400);
411
+ const family = shellFromPrompt(this.promptTail);
412
+ if (!family) return;
413
+ if (family === "cmd" && !this.powerShellAttempted) {
414
+ this.#startPowerShell();
415
+ return;
416
+ }
417
+ if (family === "cmd" && this.powerShellAttempted && !this.readySignaled) return;
418
+ this.shellFamily = family;
419
+ if (family === "powershell" && !this.readySignaled) {
420
+ this.#markReady("Connected \xB7 PowerShell");
421
+ }
422
+ }
423
+ /** Replace Windows OpenSSH's usual cmd.exe with an interactive PowerShell. */
424
+ #startPowerShell() {
425
+ if (this.powerShellAttempted || !this.shellOpen) return;
426
+ this.powerShellAttempted = true;
427
+ this.readySignaled = false;
428
+ this.shellFamily = "";
429
+ this.#status("opening", "Starting PowerShell\u2026");
430
+ this.write("powershell.exe -NoLogo\r");
431
+ this.powerShellTimer = setTimeout(() => {
432
+ if (this.readySignaled || this.closed) return;
433
+ this.shellFamily = "powershell";
434
+ this.#markReady("Connected \xB7 PowerShell");
435
+ }, 2e3);
436
+ }
437
+ #markReady(message = "Connected.") {
438
+ clearTimeout(this.powerShellTimer);
439
+ this.powerShellTimer = null;
440
+ this.readySignaled = true;
441
+ this.#status("ready", message);
442
+ this.dispatchEvent(new CustomEvent("ready", {
443
+ detail: { fingerprint: this.fingerprint, shellFamily: this.shellFamily || "posix" }
444
+ }));
445
+ }
446
+ /** Shell output, with the receive window topped up before it runs dry. */
447
+ #consume(bytes, isStderr = false) {
448
+ if (!bytes.length) return;
449
+ this.#noteShellFamily(bytes);
450
+ this.dispatchEvent(new CustomEvent("data", {
451
+ detail: { bytes, display: filterDisplay(this, bytes), isStderr }
452
+ }));
453
+ this.localWindow -= bytes.length;
454
+ if (this.localWindow > WINDOW_REFILL_THRESHOLD || this.remoteChannelId === null) return;
455
+ const increment = INITIAL_WINDOW - this.localWindow;
456
+ this.localWindow = INITIAL_WINDOW;
457
+ this.transport.send(new SshWriter(16).u8(MSG.CHANNEL_WINDOW_ADJUST).u32(this.remoteChannelId).u32(increment).take());
458
+ }
459
+ #flushWrites() {
460
+ if (!this.shellOpen || this.remoteChannelId === null) return;
461
+ while (this.pendingWrites.length > 0) {
462
+ const next = this.pendingWrites[0];
463
+ const room = Math.min(this.remoteWindow, this.remoteMaxPacket - 64);
464
+ if (room <= 0) return;
465
+ const chunk = next.length <= room ? next : next.subarray(0, room);
466
+ if (chunk.length === next.length) this.pendingWrites.shift();
467
+ else this.pendingWrites[0] = next.subarray(chunk.length);
468
+ this.remoteWindow -= chunk.length;
469
+ this.transport.send(new SshWriter(chunk.length + 16).u8(MSG.CHANNEL_DATA).u32(this.remoteChannelId).string(chunk).take());
470
+ }
471
+ }
472
+ // --- authentication ------------------------------------------------------
473
+ async #authenticate() {
474
+ try {
475
+ this.#status("authenticating", `Authenticating as ${this.username}\u2026`);
476
+ this.transport.send(new SshWriter(32).u8(MSG.SERVICE_REQUEST).string("ssh-userauth").take());
477
+ await this.#expect(MSG.SERVICE_ACCEPT);
478
+ if (await this.#tryNone()) return void await this.#openShell();
479
+ if (this.authMethods.includes("password") && this.password) {
480
+ if (await this.#tryPassword(this.password)) return void await this.#openShell();
481
+ }
482
+ if (this.authMethods.includes("keyboard-interactive")) {
483
+ if (await this.#tryKeyboardInteractive()) return void await this.#openShell();
484
+ if (this.authMethods.includes("keyboard-interactive")) {
485
+ if (await this.#tryKeyboardInteractive({ usePassword: false })) {
486
+ return void await this.#openShell();
487
+ }
488
+ }
489
+ }
490
+ if (this.authMethods.includes("password")) {
491
+ const typed = await this.requestInput({
492
+ prompt: `${this.username}@${this.options.hostLabel || "host"}'s password: `,
493
+ echo: false
494
+ });
495
+ if (typed && await this.#tryPassword(typed)) return void await this.#openShell();
496
+ }
497
+ throw new Error(this.authMethods.length ? `SSH: authentication failed. The host accepts ${this.authMethods.join(", ")}.` : "SSH: the host rejected the connection before any authentication method was offered.");
498
+ } catch (error) {
499
+ if (!this.closed) this.transport.fail(error);
500
+ }
501
+ }
502
+ async #tryNone() {
503
+ this.transport.send(new SshWriter(64).u8(MSG.USERAUTH_REQUEST).string(this.username).string(SERVICE).string("none").take());
504
+ return this.#authResult();
505
+ }
506
+ async #tryPassword(password) {
507
+ this.transport.send(new SshWriter(128).u8(MSG.USERAUTH_REQUEST).string(this.username).string(SERVICE).string("password").boolean(false).string(password).take());
508
+ return this.#authResult();
509
+ }
510
+ /**
511
+ * keyboard-interactive is how most Linux hosts actually ask for a password,
512
+ * and the only way a one-time code can be entered. Prompts are answered from
513
+ * the password when one is obviously being asked for, and by the user
514
+ * otherwise.
515
+ */
516
+ async #tryKeyboardInteractive({ usePassword = true } = {}) {
517
+ this.transport.send(new SshWriter(96).u8(MSG.USERAUTH_REQUEST).string(this.username).string(SERVICE).string("keyboard-interactive").string("").string("").take());
518
+ let usedPassword = !usePassword;
519
+ while (true) {
520
+ const { type, payload } = await this.#expect(
521
+ MSG.USERAUTH_SUCCESS,
522
+ MSG.USERAUTH_FAILURE,
523
+ MSG.USERAUTH_INFO_REQUEST
524
+ );
525
+ if (type === MSG.USERAUTH_SUCCESS) return true;
526
+ if (type === MSG.USERAUTH_FAILURE) {
527
+ this.#readFailure(payload);
528
+ return false;
529
+ }
530
+ const reader = new SshReader(payload, 1);
531
+ const name = reader.string();
532
+ const instruction = reader.string();
533
+ reader.string();
534
+ const promptCount = reader.u32();
535
+ if (name) this.#emitBanner(`${name.replaceAll("\n", "\r\n")}\r
536
+ `);
537
+ if (instruction) this.#emitBanner(`${instruction.replaceAll("\n", "\r\n")}\r
538
+ `);
539
+ const answers = [];
540
+ for (let index = 0; index < promptCount; index++) {
541
+ const prompt = reader.string();
542
+ const echo = reader.boolean();
543
+ const looksLikePassword = !echo && /pass(word|phrase)/i.test(prompt);
544
+ if (looksLikePassword && this.password && !usedPassword) {
545
+ usedPassword = true;
546
+ answers.push(this.password);
547
+ continue;
548
+ }
549
+ answers.push(await this.requestInput({ prompt, echo }));
550
+ }
551
+ const response = new SshWriter(256).u8(MSG.USERAUTH_INFO_RESPONSE).u32(answers.length);
552
+ for (const answer of answers) response.string(answer);
553
+ this.transport.send(response.take());
554
+ }
555
+ }
556
+ async #authResult() {
557
+ const { type, payload } = await this.#expect(MSG.USERAUTH_SUCCESS, MSG.USERAUTH_FAILURE);
558
+ if (type === MSG.USERAUTH_SUCCESS) return true;
559
+ this.#readFailure(payload);
560
+ return false;
561
+ }
562
+ #readFailure(payload) {
563
+ const reader = new SshReader(payload, 1);
564
+ this.authMethods = reader.nameList();
565
+ const partial = reader.boolean();
566
+ if (partial) this.#emitBanner("\r\nOne factor accepted; the host wants another.\r\n");
567
+ }
568
+ // --- the shell -----------------------------------------------------------
569
+ async #openShell() {
570
+ this.#status("opening", "Starting the remote shell\u2026");
571
+ this.transport.send(new SshWriter(64).u8(MSG.CHANNEL_OPEN).string("session").u32(this.channelId).u32(INITIAL_WINDOW).u32(MAX_PACKET).take());
572
+ const opened = await this.#expect(MSG.CHANNEL_OPEN_CONFIRMATION, MSG.CHANNEL_OPEN_FAILURE);
573
+ if (opened.type === MSG.CHANNEL_OPEN_FAILURE) {
574
+ const reader2 = new SshReader(opened.payload, 1);
575
+ reader2.u32();
576
+ reader2.u32();
577
+ throw new Error(`SSH: the host refused to open a session channel (${reader2.string()}).`);
578
+ }
579
+ const reader = new SshReader(opened.payload, 1);
580
+ reader.u32();
581
+ this.remoteChannelId = reader.u32();
582
+ this.remoteWindow = reader.u32();
583
+ this.remoteMaxPacket = Math.max(1024, Math.min(reader.u32(), MAX_PACKET));
584
+ this.transport.send(new SshWriter(128).u8(MSG.CHANNEL_REQUEST).u32(this.remoteChannelId).string("pty-req").boolean(true).string(TERMINAL_TYPE).u32(this.columns).u32(this.rows).u32(0).u32(0).string(PTY_MODES).take());
585
+ const pty = await this.#expect(MSG.CHANNEL_SUCCESS, MSG.CHANNEL_FAILURE);
586
+ if (pty.type === MSG.CHANNEL_FAILURE) {
587
+ throw new Error("SSH: the host refused a pseudo-terminal, so there is no interactive shell to show.");
588
+ }
589
+ this.transport.send(new SshWriter(32).u8(MSG.CHANNEL_REQUEST).u32(this.remoteChannelId).string("shell").boolean(true).take());
590
+ const shell = await this.#expect(MSG.CHANNEL_SUCCESS, MSG.CHANNEL_FAILURE);
591
+ if (shell.type === MSG.CHANNEL_FAILURE) {
592
+ throw new Error("SSH: the host refused to start a shell for this account.");
593
+ }
594
+ this.shellOpen = true;
595
+ this.#flushWrites();
596
+ if (this.options.preferredShell === "powershell") this.#startPowerShell();
597
+ else this.#markReady();
598
+ }
599
+ }
600
+ export {
601
+ COMMAND_IDLE_MS,
602
+ COMMAND_MAX_MS,
603
+ COMMAND_START_MS,
604
+ DEFAULT_PORT,
605
+ SshSession,
606
+ frameCommand
607
+ };
608
+ //# sourceMappingURL=session.js.map