Skip to content

Commit bf533db

Browse files
committed
fix(api): route host cli through controller auth
1 parent 09b56e7 commit bf533db

25 files changed

+1793
-168
lines changed

packages/api/src/api/contracts.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,38 @@ export type ProjectDetails = ProjectSummary & {
2828
readonly clonedOnHostname?: string | undefined
2929
}
3030

31+
export type GithubAuthTokenStatus = {
32+
readonly key: string
33+
readonly label: string
34+
readonly status: "valid" | "invalid" | "unknown"
35+
readonly login: string | null
36+
}
37+
38+
export type GithubAuthStatus = {
39+
readonly summary: string
40+
readonly tokens: ReadonlyArray<GithubAuthTokenStatus>
41+
}
42+
43+
export type GithubAuthLoginRequest = {
44+
readonly label?: string | null | undefined
45+
readonly token?: string | null | undefined
46+
readonly scopes?: string | null | undefined
47+
}
48+
49+
export type GithubAuthLogoutRequest = {
50+
readonly label?: string | null | undefined
51+
}
52+
53+
export type ApplyAllRequest = {
54+
readonly activeOnly?: boolean | undefined
55+
}
56+
57+
export type ApiAuthRequired = {
58+
readonly provider: "github"
59+
readonly message: string
60+
readonly command: string
61+
}
62+
3163
export type CreateProjectRequest = {
3264
readonly repoUrl?: string | undefined
3365
readonly repoRef?: string | undefined
@@ -57,6 +89,7 @@ export type CreateProjectRequest = {
5789
readonly openSsh?: boolean | undefined
5890
readonly force?: boolean | undefined
5991
readonly forceEnv?: boolean | undefined
92+
readonly waitForClone?: boolean | undefined
6093
}
6194

6295
export type AgentEnvVar = {

packages/api/src/api/errors.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { Data } from "effect"
22

3+
export class ApiAuthRequiredError extends Data.TaggedError("ApiAuthRequiredError")<{
4+
readonly provider: "github"
5+
readonly message: string
6+
readonly command: string
7+
}> {}
8+
39
export class ApiBadRequestError extends Data.TaggedError("ApiBadRequestError")<{
410
readonly message: string
511
readonly details?: unknown
@@ -19,6 +25,7 @@ export class ApiInternalError extends Data.TaggedError("ApiInternalError")<{
1925
}> {}
2026

2127
export type ApiKnownError =
28+
| ApiAuthRequiredError
2229
| ApiBadRequestError
2330
| ApiNotFoundError
2431
| ApiConflictError

packages/api/src/api/schema.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as Schema from "effect/Schema"
22

33
const OptionalString = Schema.optional(Schema.String)
44
const OptionalBoolean = Schema.optional(Schema.Boolean)
5+
const OptionalNullableString = Schema.optional(Schema.NullOr(Schema.String))
56

67
export const CreateProjectRequestSchema = Schema.Struct({
78
repoUrl: OptionalString,
@@ -31,7 +32,22 @@ export const CreateProjectRequestSchema = Schema.Struct({
3132
up: OptionalBoolean,
3233
openSsh: OptionalBoolean,
3334
force: OptionalBoolean,
34-
forceEnv: OptionalBoolean
35+
forceEnv: OptionalBoolean,
36+
waitForClone: OptionalBoolean
37+
})
38+
39+
export const GithubAuthLoginRequestSchema = Schema.Struct({
40+
label: OptionalNullableString,
41+
token: OptionalNullableString,
42+
scopes: OptionalNullableString
43+
})
44+
45+
export const GithubAuthLogoutRequestSchema = Schema.Struct({
46+
label: OptionalNullableString
47+
})
48+
49+
export const ApplyAllRequestSchema = Schema.Struct({
50+
activeOnly: OptionalBoolean
3551
})
3652

3753
export const AgentProviderSchema = Schema.Literal("codex", "opencode", "claude", "custom")
@@ -84,5 +100,8 @@ export const AgentLogLineSchema = Schema.Struct({
84100
})
85101

86102
export type CreateProjectRequestInput = Schema.Schema.Type<typeof CreateProjectRequestSchema>
103+
export type GithubAuthLoginRequestInput = Schema.Schema.Type<typeof GithubAuthLoginRequestSchema>
104+
export type GithubAuthLogoutRequestInput = Schema.Schema.Type<typeof GithubAuthLogoutRequestSchema>
105+
export type ApplyAllRequestInput = Schema.Schema.Type<typeof ApplyAllRequestSchema>
87106
export type CreateAgentRequestInput = Schema.Schema.Type<typeof CreateAgentRequestSchema>
88107
export type CreateFollowRequestInput = Schema.Schema.Type<typeof CreateFollowRequestSchema>

packages/api/src/http.ts

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import * as HttpServerError from "@effect/platform/HttpServerError"
99
import * as ParseResult from "effect/ParseResult"
1010
import * as Schema from "effect/Schema"
1111

12-
import { ApiBadRequestError, ApiConflictError, ApiInternalError, ApiNotFoundError, describeUnknown } from "./api/errors.js"
13-
import { CreateAgentRequestSchema, CreateFollowRequestSchema, CreateProjectRequestSchema } from "./api/schema.js"
12+
import { ApiAuthRequiredError, ApiBadRequestError, ApiConflictError, ApiInternalError, ApiNotFoundError, describeUnknown } from "./api/errors.js"
13+
import { ApplyAllRequestSchema, CreateAgentRequestSchema, CreateFollowRequestSchema, CreateProjectRequestSchema, GithubAuthLoginRequestSchema, GithubAuthLogoutRequestSchema } from "./api/schema.js"
1414
import { uiHtml, uiScript, uiStyles } from "./ui.js"
15+
import { loginGithubAuth, logoutGithubAuth, readGithubAuthStatus } from "./services/auth.js"
1516
import { getAgent, getAgentAttachInfo, listAgents, readAgentLogs, startAgent, stopAgent } from "./services/agents.js"
1617
import { latestProjectCursor, listProjectEventsSince } from "./services/events.js"
1718
import {
@@ -27,8 +28,10 @@ import {
2728
makeFederationOutboxCollection
2829
} from "./services/federation.js"
2930
import {
31+
applyAllProjects,
3032
createProjectFromRequest,
3133
deleteProjectById,
34+
downAllProjects,
3235
downProject,
3336
getProject,
3437
listProjects,
@@ -48,6 +51,7 @@ const AgentParamsSchema = Schema.Struct({
4851
})
4952

5053
type ApiError =
54+
| ApiAuthRequiredError
5155
| ApiBadRequestError
5256
| ApiNotFoundError
5357
| ApiConflictError
@@ -93,6 +97,20 @@ const errorResponse = (error: ApiError | unknown) => {
9397
return jsonResponse({ error: { type: error._tag, message: error.message, details: error.details } }, 400)
9498
}
9599

100+
if (error instanceof ApiAuthRequiredError) {
101+
return jsonResponse(
102+
{
103+
error: {
104+
type: error._tag,
105+
message: error.message,
106+
provider: error.provider,
107+
command: error.command
108+
}
109+
},
110+
401
111+
)
112+
}
113+
96114
if (error instanceof ApiNotFoundError) {
97115
return jsonResponse({ error: { type: error._tag, message: error.message } }, 404)
98116
}
@@ -121,6 +139,9 @@ const agentParams = HttpRouter.schemaParams(AgentParamsSchema)
121139

122140
const readCreateProjectRequest = () => HttpServerRequest.schemaBodyJson(CreateProjectRequestSchema)
123141
const readCreateFollowRequest = () => HttpServerRequest.schemaBodyJson(CreateFollowRequestSchema)
142+
const readGithubAuthLoginRequest = () => HttpServerRequest.schemaBodyJson(GithubAuthLoginRequestSchema)
143+
const readGithubAuthLogoutRequest = () => HttpServerRequest.schemaBodyJson(GithubAuthLogoutRequestSchema)
144+
const readApplyAllRequest = () => HttpServerRequest.schemaBodyJson(ApplyAllRequestSchema)
124145
const readInboxPayload = () => HttpServerRequest.schemaBodyJson(Schema.Unknown)
125146

126147
const configuredFederationPublicOrigin =
@@ -184,6 +205,29 @@ export const makeRouter = () => {
184205
HttpRouter.get("/ui/styles.css", textResponse(uiStyles, "text/css; charset=utf-8", 200)),
185206
HttpRouter.get("/ui/app.js", textResponse(uiScript, "application/javascript; charset=utf-8", 200)),
186207
HttpRouter.get("/health", jsonResponse({ ok: true }, 200)),
208+
HttpRouter.get(
209+
"/auth/github/status",
210+
Effect.gen(function*(_) {
211+
const status = yield* _(readGithubAuthStatus())
212+
return yield* _(jsonResponse({ status }, 200))
213+
}).pipe(Effect.catchAll(errorResponse))
214+
),
215+
HttpRouter.post(
216+
"/auth/github/login",
217+
Effect.gen(function*(_) {
218+
const request = yield* _(readGithubAuthLoginRequest())
219+
const status = yield* _(loginGithubAuth(request))
220+
return yield* _(jsonResponse({ ok: true, status }, 201))
221+
}).pipe(Effect.catchAll(errorResponse))
222+
),
223+
HttpRouter.post(
224+
"/auth/github/logout",
225+
Effect.gen(function*(_) {
226+
const request = yield* _(readGithubAuthLogoutRequest())
227+
const status = yield* _(logoutGithubAuth(request))
228+
return yield* _(jsonResponse({ ok: true, status }, 200))
229+
}).pipe(Effect.catchAll(errorResponse))
230+
),
187231
HttpRouter.get(
188232
"/federation/issues",
189233
Effect.sync(() => ({ issues: listFederationIssues() })).pipe(
@@ -274,6 +318,21 @@ export const makeRouter = () => {
274318
return yield* _(jsonResponse({ project }, 201))
275319
}).pipe(Effect.catchAll(errorResponse))
276320
),
321+
HttpRouter.post(
322+
"/projects/apply-all",
323+
Effect.gen(function*(_) {
324+
const request = yield* _(readApplyAllRequest())
325+
yield* _(applyAllProjects(request.activeOnly ?? false))
326+
return yield* _(jsonResponse({ ok: true }, 200))
327+
}).pipe(Effect.catchAll(errorResponse))
328+
),
329+
HttpRouter.post(
330+
"/projects/down-all",
331+
downAllProjects().pipe(
332+
Effect.flatMap(() => jsonResponse({ ok: true }, 200)),
333+
Effect.catchAll(errorResponse)
334+
)
335+
),
277336
HttpRouter.get(
278337
"/projects/:projectId",
279338
projectParams.pipe(

0 commit comments

Comments
 (0)