Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 12
feat(app): per-container CF SSH tunnel for VS Code panel#434
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base:main
Are you sure you want to change the base?
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
421ddaa8550858781fe34e24edb026c967d49179e618aea4285ddedb6a49c5746f688099ddb61ef4b61f95d30e3d455535eb1e0cf02a5bd2658a11eef931d89132fdc0847b71File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Uh oh!
There was an error while loading. Please reload this page.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -809,3 +809,29 @@ export type ApiEvent = { | ||
| readonly at: string | ||
| readonly payload: unknown | ||
| } | ||
| export type ShareLinkInfo = { | ||
| readonly token: string | ||
| readonly projectKey: string | ||
| readonly projectDir: string | ||
| readonly displayName: string | ||
| readonly sshAlias: string | ||
| readonly sshConfigSnippet: string | ||
| readonly cfSshConfigSnippet: string | null | ||
| readonly vscodeUri: string | ||
| readonly cfVscodeUri: string | null | ||
| readonly workspacePath: string | ||
| readonly sshPassword: string | null | ||
| readonly createdAt: string | ||
| readonly expiresAt: string | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| export type CreateShareLinkRequest = { | ||
| readonly ttlMs?: number | undefined | ||
| } | ||
| export type CreateShareLinkResponse = { | ||
| readonly ok: true | ||
| readonly link: ShareLinkInfo | ||
| readonly url: string | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -38,6 +38,7 @@ import { | ||
| ProjectPortForwardRequestSchema, | ||
| ProjectPromptUpdateRequestSchema, | ||
| ProjectSkillUpdateRequestSchema, | ||
| CreateShareLinkRequestSchema, | ||
| StartProjectTerminalSessionRequestSchema, | ||
| StartPanelCloudflareTunnelRequestSchema, | ||
| StateCommitRequestSchema, | ||
| @@ -130,6 +131,24 @@ import { | ||
| startPanelCloudflareTunnel, | ||
| stopPanelCloudflareTunnel | ||
| } from "./services/panel-cloudflare-tunnel.js" | ||
| import { | ||
| createShareLink, | ||
| deleteShareLink, | ||
| listShareLinks, | ||
| resolveShareLink | ||
| } from "./services/project-share-links.js" | ||
| import { | ||
| getSshShareLinkTunnelHostname, | ||
| startSshShareLinkTunnel, | ||
| stopSshShareLinkTunnel | ||
| } from "./services/ssh-share-link-tunnels.js" | ||
| import { startSshProjectTunnel } from "./services/ssh-project-tunnels.js" | ||
| import { | ||
| disableContainerPasswordAuth, | ||
| enableContainerPasswordAuth, | ||
| generateSshPassword | ||
| } from "./services/ssh-password-setup.js" | ||
| import { buildShareLinkSshAccess } from "@effect-template/lib/usecases/ssh-access" | ||
| import { | ||
| deleteProjectDatabaseForward, | ||
| deleteProjectDatabaseProfile, | ||
| @@ -556,6 +575,13 @@ const skillScopeFromBody = (scope: string): ProjectSkillScope | null => | ||
| const readProjectPortForwardRequest = () => HttpServerRequest.schemaBodyJson(ProjectPortForwardRequestSchema) | ||
| const readStartPanelCloudflareTunnelRequest = () => | ||
| HttpServerRequest.schemaBodyJson(StartPanelCloudflareTunnelRequestSchema) | ||
| const readCreateShareLinkRequest = () => | ||
| HttpServerRequest.schemaBodyJson(CreateShareLinkRequestSchema) | ||
| const ShareLinkTokenParamsSchema = Schema.Struct({ token: Schema.String }) | ||
| const ShareLinkByProjectKeyParamsSchema = Schema.Struct({ projectKey: Schema.String, token: Schema.String }) | ||
| const shareLinkTokenParams = HttpRouter.schemaParams(ShareLinkTokenParamsSchema) | ||
| const shareLinkByProjectKeyParams = HttpRouter.schemaParams(ShareLinkByProjectKeyParamsSchema) | ||
| const readProjectDatabaseProfileRequest = () => HttpServerRequest.schemaBodyJson(ProjectDatabaseProfileRequestSchema) | ||
| const readStateInitRequest = () => HttpServerRequest.schemaBodyJson(StateInitRequestSchema) | ||
| const readStateCommitRequest = () => HttpServerRequest.schemaBodyJson(StateCommitRequestSchema) | ||
| @@ -1104,6 +1130,144 @@ export const makeRouter = () => { | ||
| Effect.flatMap((tunnel) => jsonResponse({ tunnel }, 200)), | ||
| Effect.catchAll(errorResponse) | ||
| ) | ||
| ), | ||
| HttpRouter.get( | ||
| "/share-links/:token", | ||
| Effect.gen(function*(_) { | ||
| const request = yield* _(HttpServerRequest.HttpServerRequest) | ||
| const { token } = yield* _(shareLinkTokenParams) | ||
| const projectsRoot = defaultProjectsRoot(process.cwd()) | ||
| const link = yield* _(resolveShareLink(projectsRoot, token)) | ||
| if (link === null) { | ||
| return yield* _(Effect.fail(new ApiNotFoundError({ message: `Share link not found or expired: ${token}` }))) | ||
| } | ||
| const project = yield* _(getProjectItemByKey(link.projectKey)) | ||
| const clientHost = new URL(request.url, "http://localhost").searchParams.get("host") | ||
| ?? resolvePortPublicHost(request) | ||
| ?? "localhost" | ||
| const sshCfHostname = getSshShareLinkTunnelHostname(link.token) | ||
| const sshAccess = buildShareLinkSshAccess({ | ||
| containerName: project.containerName, | ||
| sshUser: project.sshUser, | ||
| sshPort: project.sshPort, | ||
| sshKeyPath: null, | ||
| targetDir: project.targetDir, | ||
| clientHost, | ||
| sshCfHostname | ||
| }) | ||
| const shareLinkInfo = { | ||
| token: link.token, | ||
| projectKey: link.projectKey, | ||
| projectDir: link.projectDir, | ||
| displayName: project.displayName, | ||
| sshAlias: sshAccess.alias, | ||
| sshConfigSnippet: sshAccess.configSnippet, | ||
| cfSshConfigSnippet: sshAccess.cfConfigSnippet, | ||
| vscodeUri: sshAccess.vscodeUri, | ||
| cfVscodeUri: sshAccess.cfVscodeUri, | ||
| workspacePath: sshAccess.workspacePath, | ||
| sshPassword: link.sshPassword ?? null, | ||
| createdAt: link.createdAt, | ||
| expiresAt: link.expiresAt | ||
| } | ||
| return yield* _(jsonResponse({ link: shareLinkInfo }, 200)) | ||
| }).pipe(Effect.catchAll(errorResponse)) | ||
| ), | ||
| HttpRouter.post( | ||
| "/projects/by-key/:projectKey/share-links", | ||
| Effect.gen(function*(_) { | ||
| const request = yield* _(HttpServerRequest.HttpServerRequest) | ||
| const { projectKey } = yield* _(projectKeyParams) | ||
| const body = yield* _(readCreateShareLinkRequest()) | ||
| const project = yield* _(getProjectItemByKey(projectKey)) | ||
| const projectsRoot = defaultProjectsRoot(process.cwd()) | ||
| const sshPassword = generateSshPassword() | ||
| yield* _( | ||
| enableContainerPasswordAuth(project.containerName, sshPassword).pipe( | ||
| Effect.orElse(() => Effect.void) | ||
| ) | ||
| ) | ||
| const link = yield* _(createShareLink(projectsRoot, project.projectDir, projectKey, sshPassword, body.ttlMs)) | ||
| const clientHost = resolvePortPublicHost(request) ?? "localhost" | ||
| const sshCfHostname = yield* _( | ||
| startSshShareLinkTunnel(link.token, project.sshPort).pipe( | ||
| Effect.orElse(() => Effect.succeed(null)) | ||
| ) | ||
| ) | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| const sshAccess = buildShareLinkSshAccess({ | ||
| containerName: project.containerName, | ||
| sshUser: project.sshUser, | ||
| sshPort: project.sshPort, | ||
| sshKeyPath: null, | ||
| targetDir: project.targetDir, | ||
| clientHost, | ||
| sshCfHostname | ||
| }) | ||
| const shareLinkInfo = { | ||
| token: link.token, | ||
| projectKey: link.projectKey, | ||
| projectDir: link.projectDir, | ||
| displayName: project.displayName, | ||
| sshAlias: sshAccess.alias, | ||
| sshConfigSnippet: sshAccess.configSnippet, | ||
| cfSshConfigSnippet: sshAccess.cfConfigSnippet, | ||
| vscodeUri: sshAccess.vscodeUri, | ||
| cfVscodeUri: sshAccess.cfVscodeUri, | ||
| workspacePath: sshAccess.workspacePath, | ||
| sshPassword, | ||
| createdAt: link.createdAt, | ||
| expiresAt: link.expiresAt | ||
| } | ||
| const url = `${resolveRequestOrigin(request)}/ssh/${encodeURIComponent(projectKey)}?t=${link.token}` | ||
| return yield* _(jsonResponse({ ok: true, link: shareLinkInfo, url }, 201)) | ||
| }).pipe(Effect.catchAll(errorResponse)) | ||
| ), | ||
| HttpRouter.get( | ||
| "/projects/by-key/:projectKey/share-links", | ||
| projectKeyParams.pipe( | ||
| Effect.flatMap(({ projectKey }) => | ||
| Effect.gen(function*(_) { | ||
| const project = yield* _(getProjectItemByKey(projectKey)) | ||
| const projectsRoot = defaultProjectsRoot(process.cwd()) | ||
| const links = yield* _(listShareLinks(projectsRoot, project.projectDir)) | ||
| return { links } | ||
| }) | ||
| ), | ||
| Effect.flatMap((payload) => jsonResponse(payload, 200)), | ||
| Effect.catchAll(errorResponse) | ||
| ) | ||
| ), | ||
| HttpRouter.del( | ||
| "/projects/by-key/:projectKey/share-links/:token", | ||
| shareLinkByProjectKeyParams.pipe( | ||
| Effect.flatMap(({ projectKey, token }) => | ||
| Effect.gen(function*(_) { | ||
| const project = yield* _(getProjectItemByKey(projectKey)) | ||
| const projectsRoot = defaultProjectsRoot(process.cwd()) | ||
| yield* _(deleteShareLink(projectsRoot, project.projectDir, token)) | ||
| yield* _(stopSshShareLinkTunnel(token)) | ||
| const remaining = yield* _(listShareLinks(projectsRoot, project.projectDir)) | ||
| if (remaining.length === 0) { | ||
| yield* _(disableContainerPasswordAuth(project.containerName)) | ||
| } | ||
| }) | ||
| ), | ||
| Effect.flatMap(() => jsonResponse({ ok: true }, 200)), | ||
| Effect.catchAll(errorResponse) | ||
| ) | ||
| ), | ||
| HttpRouter.post( | ||
| "/projects/by-key/:projectKey/ssh-tunnel", | ||
| Effect.gen(function*(_) { | ||
| const { projectKey } = yield* _(projectKeyParams) | ||
| const project = yield* _(getProjectItemByKey(projectKey)) | ||
| const result = yield* _( | ||
| startSshProjectTunnel(projectKey, project.sshPort, project.containerName).pipe( | ||
| Effect.orElse(() => Effect.succeed({ hostname: null, sshPassword: "" })) | ||
| ) | ||
| ) | ||
| return yield* _(jsonResponse(result, 200)) | ||
| }).pipe(Effect.catchAll(errorResponse)) | ||
| ) | ||
| ) | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: ProverCoderAI/docker-git
Length of output: 50380
Даунгрейд
vitestдо^3.2.0нарушает peer dependency requirement@effect/vitestи создаёт критический риск.Версия
@effect/vitest@0.29.0(используется всеми пакетами, включая api) требуетvitest@4.1.0как peer dependency. Изменениеpackages/api/package.jsonнаvitest@^3.2.0конфликтует с этим требованием и приведёт к сбоям при запуске тестов (все test-файлы вpackages/api/tests/импортируют из@effect/vitest). Кроме того, это нарушает консистентность версий в монорепо, где все остальные пакеты используютvitest@^4.1.9.Либо вернуть
vitest@^4.1.9с объяснением почему это не удалось, либо обновить@effect/vitestна совместимую версию и задокументировать причину миграции.🤖 Prompt for AI Agents
Source: Coding guidelines