{target}. The password is passed
- to the local SSH process for this connection attempt and is not saved by T3 Code.
+ Enter the password or verification code required by {target}. Your response
+ is passed to the local SSH process for this connection attempt and is not saved by T3
+ Code.
{visibleResponseError}
) : (- Use SSH keys to avoid repeated password prompts on new SSH sessions. + SSH keys may replace a password, but your server can still require a verification + code.
)} diff --git a/packages/ssh/src/tunnel.test.ts b/packages/ssh/src/tunnel.test.ts index e2536ba92017..25d0a7c16e1c 100644 --- a/packages/ssh/src/tunnel.test.ts +++ b/packages/ssh/src/tunnel.test.ts @@ -386,6 +386,51 @@ describe("ssh tunnel scripts", () => { }).pipe(Effect.provide(processLayer)); }); + it.effect("prompts for a password or verification code after SSH authentication fails", () => { + const prompts: string[] = []; + const spawner = ChildProcessSpawner.make((command) => + Effect.succeed( + commandArgs(command).includes("-G") + ? makeSuccessfulProcess("") + : { + ...makeSuccessfulProcess(""), + exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(255)), + stderr: Stream.make( + new TextEncoder().encode("Permission denied (publickey,keyboard-interactive).\n"), + ), + }, + ), + ); + const layer = Layer.mergeAll( + NodeServices.layer, + Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner), + Layer.succeed(HttpClient.HttpClient, testHttpClient), + Layer.succeed(NetService.NetService, testNetService), + Layer.succeed(SshPasswordPrompt, { + isAvailable: true, + request: (request) => + Effect.sync(() => { + prompts.push(request.prompt); + return null; + }), + }), + SshEnvironmentManager.layer(), + ); + + return Effect.gen(function* () { + const manager = yield* SshEnvironmentManager; + yield* Effect.result( + manager.ensureEnvironment({ + alias: "devbox", + hostname: "devbox.example.com", + username: "julius", + port: 2222, + }), + ); + assert.deepEqual(prompts, ["Enter the SSH password or verification code for julius@devbox."]); + }).pipe(Effect.provide(layer), Effect.scoped); + }); + it.effect.each(["successful stop", "failed stop"] as const)( "closes the tunnel scope and starts fresh after a %s", (mode) => { diff --git a/packages/ssh/src/tunnel.ts b/packages/ssh/src/tunnel.ts index 9cb6b25e4121..021cf78f8206 100644 --- a/packages/ssh/src/tunnel.ts +++ b/packages/ssh/src/tunnel.ts @@ -1239,7 +1239,7 @@ const makeSshEnvironmentManager = Effect.fn("ssh/tunnel.SshEnvironmentManager.ma attempt, destination: target.alias.trim() || target.hostname.trim(), username: target.username, - prompt: `Enter the SSH password for ${hostSpec}.`, + prompt: `Enter the SSH password or verification code for ${hostSpec}.`, }); if (password === null) { yield* Effect.logWarning("ssh.auth.passwordPrompt.cancelled", {