Skip to content

Commit ef4b61f

Browse files
skulidropekclaude
andcommitted
fix(api): store SSH password in tunnel record, return stable password
Root cause: every POST /ssh-tunnel call regenerated a new password and set it on the container. Polling every 30s was invalidating the password shown in the panel while the old hostname stayed cached in the frontend. Fix: password is generated once per fresh tunnel start, stored in the SshTunnelRecord, and returned consistently while the tunnel is alive. A new password is only generated when the old tunnel actually dies. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 99ddb61 commit ef4b61f

3 files changed

Lines changed: 17 additions & 17 deletions

File tree

packages/api/src/http.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1261,18 +1261,12 @@ export const makeRouter = () => {
12611261
Effect.gen(function*(_) {
12621262
const { projectKey } = yield* _(projectKeyParams)
12631263
const project = yield* _(getProjectItemByKey(projectKey))
1264-
const sshPassword = generateSshPassword()
1265-
yield* _(
1266-
enableContainerPasswordAuth(project.containerName, sshPassword).pipe(
1267-
Effect.orElse(() => Effect.void)
1268-
)
1269-
)
1270-
const hostname = yield* _(
1271-
startSshProjectTunnel(projectKey, project.sshPort).pipe(
1272-
Effect.orElse(() => Effect.succeed(null))
1264+
const result = yield* _(
1265+
startSshProjectTunnel(projectKey, project.sshPort, project.containerName).pipe(
1266+
Effect.orElse(() => Effect.succeed({ hostname: null, sshPassword: "" }))
12731267
)
12741268
)
1275-
return yield* _(jsonResponse({ hostname, sshPassword }, 200))
1269+
return yield* _(jsonResponse(result, 200))
12761270
}).pipe(Effect.catchAll(errorResponse))
12771271
)
12781272
)

packages/api/src/services/ssh-project-tunnels.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ import { Duration, Effect, Fiber } from "effect"
1919
import { ApiInternalError } from "../api/errors.js"
2020
import { parseTryCloudflareUrl } from "./panel-cloudflare-tunnel-core.js"
2121
import { parseLinuxDefaultGatewayIp } from "./project-port-proxy-core.js"
22+
import { generateSshPassword, enableContainerPasswordAuth } from "./ssh-password-setup.js"
2223

2324
type SshTunnelRecord = {
2425
readonly homeDir: string
2526
process: ChildProcess | null
2627
processClosed: boolean
2728
hostname: string | null
29+
sshPassword: string
2830
stopping: boolean
2931
stopFiber: Fiber.RuntimeFiber<void> | null
3032
stdoutRemainder: string
@@ -193,18 +195,22 @@ const waitForHostname = (
193195
*/
194196
export const startSshProjectTunnel = (
195197
projectKey: string,
196-
sshPort: number
197-
): Effect.Effect<string | null, ApiInternalError> =>
198+
sshPort: number,
199+
containerName: string
200+
): Effect.Effect<{ hostname: string | null; sshPassword: string }, ApiInternalError> =>
198201
Effect.gen(function*(_) {
199202
const existing = projectTunnelMap.get(projectKey)
200203
if (existing !== undefined && !existing.stopping && !existing.processClosed && existing.hostname !== null) {
201-
return existing.hostname
204+
return { hostname: existing.hostname, sshPassword: existing.sshPassword }
202205
}
203206
if (existing !== undefined) {
204207
yield* _(stopRecord(existing).pipe(Effect.orElse(() => Effect.void)))
205208
projectTunnelMap.delete(projectKey)
206209
}
207210

211+
const sshPassword = generateSshPassword()
212+
yield* _(enableContainerPasswordAuth(containerName, sshPassword).pipe(Effect.orElse(() => Effect.void)))
213+
208214
const localhostHost = yield* _(defaultLocalhostHost())
209215
const sshUrl = `ssh://${localhostHost}:${sshPort}`
210216
const homeDir = sshTunnelHomeDir(randomUUID())
@@ -213,6 +219,7 @@ export const startSshProjectTunnel = (
213219
hostname: null,
214220
process: null,
215221
processClosed: false,
222+
sshPassword,
216223
stderrRemainder: "",
217224
stdoutRemainder: "",
218225
stopFiber: null,
@@ -239,7 +246,8 @@ export const startSshProjectTunnel = (
239246
})
240247
)
241248

242-
return yield* _(waitForHostname(record, startWaitAttempts))
249+
const hostname = yield* _(waitForHostname(record, startWaitAttempts))
250+
return { hostname, sshPassword }
243251
}).pipe(projectTunnelLock.withPermits(1))
244252

245253
/**

packages/app/src/web/app-ready-terminal-pane.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -409,9 +409,7 @@ export const TerminalPane = (props: TerminalPaneProps): JSX.Element => {
409409
onFailure: () => { setCfState({ tag: "failed" }) },
410410
onSuccess: ({ hostname, sshPassword }) => {
411411
if (hostname === null) { setCfState({ tag: "failed" }); return }
412-
setCfState((prev) =>
413-
prev.tag === "ready" && prev.hostname === hostname ? prev : { tag: "ready", hostname, sshPassword }
414-
)
412+
setCfState({ tag: "ready", hostname, sshPassword })
415413
}
416414
})
417415
)

0 commit comments

Comments
 (0)