Skip to content

Commit b1ac5c2

Browse files
authored
Merge pull request #223 from ProverCoderAI/codex/ci-e2e-ssh-checks
feat(web): add browser launch and database access
2 parents a9430db + aeb7a2c commit b1ac5c2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+3255
-46
lines changed

packages/api/src/api/contracts.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,60 @@ export type ProjectBrowserSession = {
7070
readonly cdpUrl: string
7171
}
7272

73+
export type ProjectDatabaseEngine = "postgres" | "mysql" | "mariadb"
74+
75+
export type ProjectDatabaseProfile = {
76+
readonly createdAt: string
77+
readonly database: string
78+
readonly engine: ProjectDatabaseEngine
79+
readonly host: string
80+
readonly id: string
81+
readonly label: string
82+
readonly maskedConnectionString: string
83+
readonly port: number
84+
readonly updatedAt: string
85+
readonly user: string
86+
}
87+
88+
export type ProjectDatabaseProfileRequest = {
89+
readonly connectionString: string
90+
readonly label?: string | null | undefined
91+
}
92+
93+
export type ProjectDatabaseSessionStatus = "running" | "stopped" | "missing" | "unknown"
94+
95+
export type ProjectDatabaseSession = {
96+
readonly configHash: string
97+
readonly containerName: string
98+
readonly editorPath: string
99+
readonly editorUrl: string
100+
readonly projectId: string
101+
readonly projectKey: string
102+
readonly status: ProjectDatabaseSessionStatus
103+
}
104+
105+
export type ProjectDatabaseForwardStatus = "running" | "stopped" | "unknown"
106+
107+
export type ProjectDatabaseForward = {
108+
readonly bindHost: string
109+
readonly containerName: string
110+
readonly createdAt: string | null
111+
readonly database: string
112+
readonly engine: ProjectDatabaseEngine
113+
readonly externalConnectionString: string
114+
readonly hostPort: number
115+
readonly id: string
116+
readonly maskedExternalConnectionString: string
117+
readonly profileId: string
118+
readonly profileLabel: string
119+
readonly projectId: string
120+
readonly projectKey: string
121+
readonly publicHost: string
122+
readonly status: ProjectDatabaseForwardStatus
123+
readonly targetHost: string
124+
readonly targetPort: number
125+
}
126+
73127
export type GithubAuthTokenStatus = {
74128
readonly key: string
75129
readonly label: string

packages/api/src/api/schema.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,60 @@ export const ProjectBrowserSessionSchema = Schema.Struct({
142142
status: ProjectBrowserStatusSchema
143143
})
144144

145+
export const ProjectDatabaseEngineSchema = Schema.Literal("postgres", "mysql", "mariadb")
146+
147+
export const ProjectDatabaseProfileSchema = Schema.Struct({
148+
createdAt: Schema.String,
149+
database: Schema.String,
150+
engine: ProjectDatabaseEngineSchema,
151+
host: Schema.String,
152+
id: Schema.String,
153+
label: Schema.String,
154+
maskedConnectionString: Schema.String,
155+
port: Schema.Number,
156+
updatedAt: Schema.String,
157+
user: Schema.String
158+
})
159+
160+
export const ProjectDatabaseProfileRequestSchema = Schema.Struct({
161+
connectionString: Schema.String,
162+
label: OptionalNullableString
163+
})
164+
165+
export const ProjectDatabaseSessionStatusSchema = Schema.Literal("running", "stopped", "missing", "unknown")
166+
167+
export const ProjectDatabaseSessionSchema = Schema.Struct({
168+
configHash: Schema.String,
169+
containerName: Schema.String,
170+
editorPath: Schema.String,
171+
editorUrl: Schema.String,
172+
projectId: Schema.String,
173+
projectKey: Schema.String,
174+
status: ProjectDatabaseSessionStatusSchema
175+
})
176+
177+
export const ProjectDatabaseForwardStatusSchema = Schema.Literal("running", "stopped", "unknown")
178+
179+
export const ProjectDatabaseForwardSchema = Schema.Struct({
180+
bindHost: Schema.String,
181+
containerName: Schema.String,
182+
createdAt: Schema.NullOr(Schema.String),
183+
database: Schema.String,
184+
engine: ProjectDatabaseEngineSchema,
185+
externalConnectionString: Schema.String,
186+
hostPort: Schema.Number,
187+
id: Schema.String,
188+
maskedExternalConnectionString: Schema.String,
189+
profileId: Schema.String,
190+
profileLabel: Schema.String,
191+
projectId: Schema.String,
192+
projectKey: Schema.String,
193+
publicHost: Schema.String,
194+
status: ProjectDatabaseForwardStatusSchema,
195+
targetHost: Schema.String,
196+
targetPort: Schema.Number
197+
})
198+
145199
export const AgentProviderSchema = Schema.Literal("codex", "opencode", "claude", "custom")
146200

147201
export const AgentEnvVarSchema = Schema.Struct({
@@ -220,5 +274,6 @@ export type StateSyncRequestInput = Schema.Schema.Type<typeof StateSyncRequestSc
220274
export type ApplyAllRequestInput = Schema.Schema.Type<typeof ApplyAllRequestSchema>
221275
export type UpProjectRequestInput = Schema.Schema.Type<typeof UpProjectRequestSchema>
222276
export type ProjectPortForwardRequestInput = Schema.Schema.Type<typeof ProjectPortForwardRequestSchema>
277+
export type ProjectDatabaseProfileRequestInput = Schema.Schema.Type<typeof ProjectDatabaseProfileRequestSchema>
223278
export type CreateAgentRequestInput = Schema.Schema.Type<typeof CreateAgentRequestSchema>
224279
export type CreateFollowRequestInput = Schema.Schema.Type<typeof CreateFollowRequestSchema>

packages/api/src/http.ts

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
CreateProjectRequestSchema,
2424
GithubAuthLoginRequestSchema,
2525
GithubAuthLogoutRequestSchema,
26+
ProjectDatabaseProfileRequestSchema,
2627
ProjectAuthRequestSchema,
2728
ProjectPortForwardRequestSchema,
2829
StateCommitRequestSchema,
@@ -76,6 +77,22 @@ import {
7677
import { readProjectAuthSnapshot, runProjectAuthFlow } from "./services/project-auth.js"
7778
import { readProjectBrowserSession, proxyProjectBrowser } from "./services/project-browser.js"
7879
import { parseProjectBrowserProxyPath } from "./services/project-browser-core.js"
80+
import {
81+
deleteProjectDatabaseForward,
82+
deleteProjectDatabaseProfile,
83+
exposeProjectDatabaseProfile,
84+
listProjectDatabaseForwards,
85+
listProjectDatabaseProfiles,
86+
openProjectDatabaseEditor,
87+
proxyProjectDatabase,
88+
readProjectDatabaseSession,
89+
restartProjectDatabaseEditor,
90+
saveProjectDatabaseProfile
91+
} from "./services/project-databases.js"
92+
import {
93+
parseProjectDatabaseProxyPath,
94+
parseProjectDatabaseStatefulProxyPath
95+
} from "./services/project-databases-core.js"
7996
import {
8097
createProjectPortForward,
8198
deleteProjectPortForward,
@@ -103,6 +120,11 @@ const ProjectPortForwardParamsSchema = Schema.Struct({
103120
targetPort: Schema.String
104121
})
105122

123+
const ProjectDatabaseProfileParamsSchema = Schema.Struct({
124+
projectId: Schema.String,
125+
profileId: Schema.String
126+
})
127+
106128
const AgentParamsSchema = Schema.Struct({
107129
projectId: Schema.String,
108130
agentId: Schema.String
@@ -264,6 +286,7 @@ const errorResponse = (error: ApiError | unknown) => {
264286

265287
const projectParams = HttpRouter.schemaParams(ProjectParamsSchema)
266288
const projectPortForwardParams = HttpRouter.schemaParams(ProjectPortForwardParamsSchema)
289+
const projectDatabaseProfileParams = HttpRouter.schemaParams(ProjectDatabaseProfileParamsSchema)
267290
const agentParams = HttpRouter.schemaParams(AgentParamsSchema)
268291
const terminalSessionParams = HttpRouter.schemaParams(TerminalSessionParamsSchema)
269292
const authTerminalSessionParams = HttpRouter.schemaParams(AuthTerminalSessionParamsSchema)
@@ -279,6 +302,7 @@ const readCodexAuthLoginRequest = () => HttpServerRequest.schemaBodyJson(CodexAu
279302
const readCodexAuthLogoutRequest = () => HttpServerRequest.schemaBodyJson(CodexAuthLogoutRequestSchema)
280303
const readProjectAuthRequest = () => HttpServerRequest.schemaBodyJson(ProjectAuthRequestSchema)
281304
const readProjectPortForwardRequest = () => HttpServerRequest.schemaBodyJson(ProjectPortForwardRequestSchema)
305+
const readProjectDatabaseProfileRequest = () => HttpServerRequest.schemaBodyJson(ProjectDatabaseProfileRequestSchema)
282306
const readStateInitRequest = () => HttpServerRequest.schemaBodyJson(StateInitRequestSchema)
283307
const readStateCommitRequest = () => HttpServerRequest.schemaBodyJson(StateCommitRequestSchema)
284308
const readStateSyncRequest = () => HttpServerRequest.schemaBodyJson(StateSyncRequestSchema)
@@ -367,8 +391,20 @@ const projectProxyResponse = Effect.gen(function*(_) {
367391
if (browserTarget !== null) {
368392
return yield* _(proxyProjectBrowser(request, browserTarget, resolveRequestOrigin(request)))
369393
}
394+
const databaseTarget = parseProjectDatabaseProxyPath(pathname)
395+
if (databaseTarget !== null) {
396+
return yield* _(proxyProjectDatabase(request, databaseTarget))
397+
}
370398
const target = parseProjectPortProxyPath(pathname)
371399
if (target === null) {
400+
const statefulDatabaseTarget = parseProjectDatabaseStatefulProxyPath(
401+
pathname,
402+
readHeader(request, "referer"),
403+
readHeader(request, "cookie")
404+
)
405+
if (statefulDatabaseTarget !== null) {
406+
return yield* _(proxyProjectDatabase(request, statefulDatabaseTarget))
407+
}
372408
return yield* _(Effect.fail(new ApiNotFoundError({ message: `Route not found: ${pathname}` })))
373409
}
374410
return yield* _(proxyProjectPortForward(request, target))
@@ -740,7 +776,91 @@ export const makeRouter = () => {
740776
const browser = yield* _(readProjectBrowserSession(projectId, resolveRequestOrigin(request)))
741777
return yield* _(jsonResponse({ browser }, 200))
742778
}).pipe(Effect.catchAll(errorResponse))
779+
)
780+
)
781+
782+
const withProjectDatabases = withProjects.pipe(
783+
HttpRouter.get(
784+
"/projects/:projectId/databases/profiles",
785+
projectParams.pipe(
786+
Effect.flatMap(({ projectId }) => listProjectDatabaseProfiles(projectId)),
787+
Effect.flatMap((profiles) => jsonResponse({ profiles }, 200)),
788+
Effect.catchAll(errorResponse)
789+
)
743790
),
791+
HttpRouter.get(
792+
"/projects/:projectId/databases/forwards",
793+
projectParams.pipe(
794+
Effect.flatMap(({ projectId }) => listProjectDatabaseForwards(projectId)),
795+
Effect.flatMap((forwards) => jsonResponse({ forwards }, 200)),
796+
Effect.catchAll(errorResponse)
797+
)
798+
),
799+
HttpRouter.post(
800+
"/projects/:projectId/databases/profiles",
801+
Effect.gen(function*(_) {
802+
const { projectId } = yield* _(projectParams)
803+
const request = yield* _(readProjectDatabaseProfileRequest())
804+
const profile = yield* _(saveProjectDatabaseProfile(projectId, request))
805+
return yield* _(jsonResponse({ profile }, 201))
806+
}).pipe(Effect.catchAll(errorResponse))
807+
),
808+
HttpRouter.del(
809+
"/projects/:projectId/databases/profiles/:profileId",
810+
projectDatabaseProfileParams.pipe(
811+
Effect.flatMap(({ projectId, profileId }) => deleteProjectDatabaseProfile(projectId, profileId)),
812+
Effect.flatMap(() => jsonResponse({ ok: true }, 200)),
813+
Effect.catchAll(errorResponse)
814+
)
815+
),
816+
HttpRouter.post(
817+
"/projects/:projectId/databases/profiles/:profileId/expose",
818+
Effect.gen(function*(_) {
819+
const { projectId, profileId } = yield* _(projectDatabaseProfileParams)
820+
const serverRequest = yield* _(HttpServerRequest.HttpServerRequest)
821+
const forward = yield* _(exposeProjectDatabaseProfile(
822+
projectId,
823+
profileId,
824+
resolvePortPublicHost(serverRequest)
825+
))
826+
return yield* _(jsonResponse({ forward }, 201))
827+
}).pipe(Effect.catchAll(errorResponse))
828+
),
829+
HttpRouter.del(
830+
"/projects/:projectId/databases/profiles/:profileId/expose",
831+
projectDatabaseProfileParams.pipe(
832+
Effect.flatMap(({ projectId, profileId }) => deleteProjectDatabaseForward(projectId, profileId)),
833+
Effect.flatMap(() => jsonResponse({ ok: true }, 200)),
834+
Effect.catchAll(errorResponse)
835+
)
836+
),
837+
HttpRouter.get(
838+
"/projects/:projectId/databases/session",
839+
projectParams.pipe(
840+
Effect.flatMap(({ projectId }) => readProjectDatabaseSession(projectId)),
841+
Effect.flatMap((session) => jsonResponse({ session }, 200)),
842+
Effect.catchAll(errorResponse)
843+
)
844+
),
845+
HttpRouter.post(
846+
"/projects/:projectId/databases/open",
847+
projectParams.pipe(
848+
Effect.flatMap(({ projectId }) => openProjectDatabaseEditor(projectId)),
849+
Effect.flatMap((session) => jsonResponse({ session }, 200)),
850+
Effect.catchAll(errorResponse)
851+
)
852+
),
853+
HttpRouter.post(
854+
"/projects/:projectId/databases/restart",
855+
projectParams.pipe(
856+
Effect.flatMap(({ projectId }) => restartProjectDatabaseEditor(projectId)),
857+
Effect.flatMap((session) => jsonResponse({ session }, 200)),
858+
Effect.catchAll(errorResponse)
859+
)
860+
)
861+
)
862+
863+
const withProjectLifecycle = withProjectDatabases.pipe(
744864
HttpRouter.del(
745865
"/projects/:projectId",
746866
projectParams.pipe(
@@ -816,7 +936,7 @@ export const makeRouter = () => {
816936
)
817937
)
818938

819-
const withAgents = withProjects.pipe(
939+
const withAgents = withProjectLifecycle.pipe(
820940
HttpRouter.post(
821941
"/projects/:projectId/agents",
822942
Effect.gen(function*(_) {

packages/api/src/program.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { initializeAgentState } from "./services/agents.js"
88
import { attachAuthTerminalWebSocketServer } from "./services/auth-terminal-sessions.js"
99
import { startOutboxPolling } from "./services/federation.js"
1010
import { attachProjectBrowserWebSocketServer } from "./services/project-browser.js"
11+
import { attachProjectDatabaseWebSocketServer } from "./services/project-databases.js"
1112
import { attachTerminalWebSocketServer } from "./services/terminal-sessions.js"
1213

1314
const resolvePort = (env: Record<string, string | undefined>): number => {
@@ -48,6 +49,7 @@ export const program = (() => {
4849
attachAuthTerminalWebSocketServer(server)
4950
attachTerminalWebSocketServer(server)
5051
attachProjectBrowserWebSocketServer(server)
52+
attachProjectDatabaseWebSocketServer(server)
5153
const serverLayer = NodeHttpServer.layer(() => server, { port })
5254

5355
const pollingInterval = parseInt(process.env["DOCKER_GIT_OUTBOX_POLLING_INTERVAL_MS"] ?? "5000", 10)

0 commit comments

Comments
 (0)