refactor(api): scope oauth operations by integration

This commit is contained in:
Dax Raad
2026-07-15 19:08:56 -04:00
parent fd19fa89c4
commit a772767e9b
19 changed files with 404 additions and 303 deletions
@@ -46,7 +46,7 @@ const login = Effect.fn("cli.console.login.run")(function* (timeline: TimelineHo
yield* request(() => timeline.pending("Starting authorization...")) yield* request(() => timeline.pending("Starting authorization..."))
const started = yield* request((signal) => const started = yield* request((signal) =>
client.integration.connect.oauth( client.integration.oauth.connect(
{ {
integrationID, integrationID,
methodID: method.id, methodID: method.id,
@@ -59,8 +59,8 @@ const login = Effect.fn("cli.console.login.run")(function* (timeline: TimelineHo
const attempt = started.data const attempt = started.data
yield* Effect.addFinalizer(() => yield* Effect.addFinalizer(() =>
request(() => request(() =>
client.integration.attempt.cancel( client.integration.oauth.cancel(
{ attemptID: attempt.attemptID, location }, { integrationID, attemptID: attempt.attemptID, location },
{ signal: AbortSignal.timeout(5_000) }, { signal: AbortSignal.timeout(5_000) },
), ),
).pipe(Effect.ignore), ).pipe(Effect.ignore),
@@ -75,7 +75,7 @@ const login = Effect.fn("cli.console.login.run")(function* (timeline: TimelineHo
}).pipe(Effect.ignore) }).pipe(Effect.ignore)
yield* request(() => timeline.pending("Waiting for authorization...")) yield* request(() => timeline.pending("Waiting for authorization..."))
const status = yield* waitForConsoleLogin(client, attempt.attemptID) const status = yield* waitForConsoleLogin(client, integrationID, attempt.attemptID)
if (status.status === "failed") yield* Effect.fail(new Error(status.message)) if (status.status === "failed") yield* Effect.fail(new Error(status.message))
if (status.status === "expired") yield* Effect.fail(new Error("Device code expired")) if (status.status === "expired") yield* Effect.fail(new Error("Device code expired"))
@@ -85,11 +85,12 @@ const login = Effect.fn("cli.console.login.run")(function* (timeline: TimelineHo
const waitForConsoleLogin = Effect.fn("cli.console.login.wait")(function* ( const waitForConsoleLogin = Effect.fn("cli.console.login.wait")(function* (
client: OpenCodeClient, client: OpenCodeClient,
integrationID: string,
attemptID: string, attemptID: string,
) { ) {
while (true) { while (true) {
const response = yield* request((signal) => const response = yield* request((signal) =>
client.integration.attempt.status({ attemptID, location }, { signal }), client.integration.oauth.status({ integrationID, attemptID, location }, { signal }),
) )
if (response.data.status !== "pending") return response.data if (response.data.status !== "pending") return response.data
yield* Effect.sleep(500) yield* Effect.sleep(500)
@@ -32,7 +32,7 @@ export default Runtime.handler(
return yield* Effect.fail(new Error(`MCP server "${input.name}" is not an OAuth-capable remote server`)) return yield* Effect.fail(new Error(`MCP server "${input.name}" is not an OAuth-capable remote server`))
const started = yield* Effect.promise(() => const started = yield* Effect.promise(() =>
client.integration.connect.oauth({ integrationID: integration.id, methodID: method.id, inputs: {}, location }), client.integration.oauth.connect({ integrationID: integration.id, methodID: method.id, inputs: {}, location }),
) )
const attempt = started.data const attempt = started.data
if (attempt.mode === "code") if (attempt.mode === "code")
@@ -40,7 +40,7 @@ export default Runtime.handler(
process.stdout.write(attempt.instructions + EOL + attempt.url + EOL) process.stdout.write(attempt.instructions + EOL + attempt.url + EOL)
const result = yield* poll(client, attempt.attemptID) const result = yield* poll(client, integration.id, attempt.attemptID)
if (result.status === "complete") { if (result.status === "complete") {
process.stdout.write(`Authenticated with ${input.name}` + EOL) process.stdout.write(`Authenticated with ${input.name}` + EOL)
return return
@@ -52,15 +52,16 @@ export default Runtime.handler(
const poll = ( const poll = (
client: OpenCodeClient, client: OpenCodeClient,
integrationID: string,
attemptID: string, attemptID: string,
): Effect.Effect<Exclude<IntegrationAttemptStatus, { status: "pending" }>> => ): Effect.Effect<Exclude<IntegrationAttemptStatus, { status: "pending" }>> =>
Effect.gen(function* () { Effect.gen(function* () {
const status = yield* Effect.promise(() => client.integration.attempt.status({ attemptID, location })).pipe( const status = yield* Effect.promise(() =>
Effect.map((result) => result.data), client.integration.oauth.status({ integrationID, attemptID, location }),
) ).pipe(Effect.map((result) => result.data))
if (status.status === "pending") { if (status.status === "pending") {
yield* Effect.sleep("1 second") yield* Effect.sleep("1 second")
return yield* poll(client, attemptID) return yield* poll(client, integrationID, attemptID)
} }
return status return status
}) })
+21 -22
View File
@@ -419,7 +419,7 @@ export type IntegrationConnectKeyOperation<E = never> = (
input: Endpoint10_2Input, input: Endpoint10_2Input,
) => Effect.Effect<Endpoint10_2Output, E> ) => Effect.Effect<Endpoint10_2Output, E>
type Endpoint10_3Request = Parameters<RawClient["server.integration"]["integration.connect.oauth"]>[0] type Endpoint10_3Request = Parameters<RawClient["server.integration"]["integration.oauth.connect"]>[0]
export type Endpoint10_3Input = { export type Endpoint10_3Input = {
readonly integrationID: Endpoint10_3Request["params"]["integrationID"] readonly integrationID: Endpoint10_3Request["params"]["integrationID"]
readonly location?: Endpoint10_3Request["query"]["location"] readonly location?: Endpoint10_3Request["query"]["location"]
@@ -427,55 +427,54 @@ export type Endpoint10_3Input = {
readonly inputs: Endpoint10_3Request["payload"]["inputs"] readonly inputs: Endpoint10_3Request["payload"]["inputs"]
readonly label?: Endpoint10_3Request["payload"]["label"] readonly label?: Endpoint10_3Request["payload"]["label"]
} }
export type Endpoint10_3Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.connect.oauth"]>> export type Endpoint10_3Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.connect"]>>
export type IntegrationConnectOauthOperation<E = never> = ( export type IntegrationOauthConnectOperation<E = never> = (
input: Endpoint10_3Input, input: Endpoint10_3Input,
) => Effect.Effect<Endpoint10_3Output, E> ) => Effect.Effect<Endpoint10_3Output, E>
type Endpoint10_4Request = Parameters<RawClient["server.integration"]["integration.attempt.status"]>[0] type Endpoint10_4Request = Parameters<RawClient["server.integration"]["integration.oauth.status"]>[0]
export type Endpoint10_4Input = { export type Endpoint10_4Input = {
readonly integrationID: Endpoint10_4Request["params"]["integrationID"]
readonly attemptID: Endpoint10_4Request["params"]["attemptID"] readonly attemptID: Endpoint10_4Request["params"]["attemptID"]
readonly location?: Endpoint10_4Request["query"]["location"] readonly location?: Endpoint10_4Request["query"]["location"]
} }
export type Endpoint10_4Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.attempt.status"]>> export type Endpoint10_4Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.status"]>>
export type IntegrationAttemptStatusOperation<E = never> = ( export type IntegrationOauthStatusOperation<E = never> = (
input: Endpoint10_4Input, input: Endpoint10_4Input,
) => Effect.Effect<Endpoint10_4Output, E> ) => Effect.Effect<Endpoint10_4Output, E>
type Endpoint10_5Request = Parameters<RawClient["server.integration"]["integration.attempt.complete"]>[0] type Endpoint10_5Request = Parameters<RawClient["server.integration"]["integration.oauth.complete"]>[0]
export type Endpoint10_5Input = { export type Endpoint10_5Input = {
readonly integrationID: Endpoint10_5Request["params"]["integrationID"]
readonly attemptID: Endpoint10_5Request["params"]["attemptID"] readonly attemptID: Endpoint10_5Request["params"]["attemptID"]
readonly location?: Endpoint10_5Request["query"]["location"] readonly location?: Endpoint10_5Request["query"]["location"]
readonly code?: Endpoint10_5Request["payload"]["code"] readonly code?: Endpoint10_5Request["payload"]["code"]
} }
export type Endpoint10_5Output = EffectValue< export type Endpoint10_5Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.complete"]>>
ReturnType<RawClient["server.integration"]["integration.attempt.complete"]> export type IntegrationOauthCompleteOperation<E = never> = (
>
export type IntegrationAttemptCompleteOperation<E = never> = (
input: Endpoint10_5Input, input: Endpoint10_5Input,
) => Effect.Effect<Endpoint10_5Output, E> ) => Effect.Effect<Endpoint10_5Output, E>
type Endpoint10_6Request = Parameters<RawClient["server.integration"]["integration.attempt.cancel"]>[0] type Endpoint10_6Request = Parameters<RawClient["server.integration"]["integration.oauth.cancel"]>[0]
export type Endpoint10_6Input = { export type Endpoint10_6Input = {
readonly integrationID: Endpoint10_6Request["params"]["integrationID"]
readonly attemptID: Endpoint10_6Request["params"]["attemptID"] readonly attemptID: Endpoint10_6Request["params"]["attemptID"]
readonly location?: Endpoint10_6Request["query"]["location"] readonly location?: Endpoint10_6Request["query"]["location"]
} }
export type Endpoint10_6Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.attempt.cancel"]>> export type Endpoint10_6Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.cancel"]>>
export type IntegrationAttemptCancelOperation<E = never> = ( export type IntegrationOauthCancelOperation<E = never> = (
input: Endpoint10_6Input, input: Endpoint10_6Input,
) => Effect.Effect<Endpoint10_6Output, E> ) => Effect.Effect<Endpoint10_6Output, E>
export interface IntegrationApi<E = never> { export interface IntegrationApi<E = never> {
readonly list: IntegrationListOperation<E> readonly list: IntegrationListOperation<E>
readonly get: IntegrationGetOperation<E> readonly get: IntegrationGetOperation<E>
readonly connect: { readonly connect: { readonly key: IntegrationConnectKeyOperation<E> }
readonly key: IntegrationConnectKeyOperation<E> readonly oauth: {
readonly oauth: IntegrationConnectOauthOperation<E> readonly connect: IntegrationOauthConnectOperation<E>
} readonly status: IntegrationOauthStatusOperation<E>
readonly attempt: { readonly complete: IntegrationOauthCompleteOperation<E>
readonly status: IntegrationAttemptStatusOperation<E> readonly cancel: IntegrationOauthCancelOperation<E>
readonly complete: IntegrationAttemptCompleteOperation<E>
readonly cancel: IntegrationAttemptCancelOperation<E>
} }
} }
+21 -13
View File
@@ -518,7 +518,7 @@ const Endpoint10_2 = (raw: RawClient["server.integration"]) => (input: Endpoint1
payload: { key: input["key"], label: input["label"] }, payload: { key: input["key"], label: input["label"] },
}).pipe(Effect.mapError(mapClientError)) }).pipe(Effect.mapError(mapClientError))
type Endpoint10_3Request = Parameters<RawClient["server.integration"]["integration.connect.oauth"]>[0] type Endpoint10_3Request = Parameters<RawClient["server.integration"]["integration.oauth.connect"]>[0]
type Endpoint10_3Input = { type Endpoint10_3Input = {
readonly integrationID: Endpoint10_3Request["params"]["integrationID"] readonly integrationID: Endpoint10_3Request["params"]["integrationID"]
readonly location?: Endpoint10_3Request["query"]["location"] readonly location?: Endpoint10_3Request["query"]["location"]
@@ -527,52 +527,60 @@ type Endpoint10_3Input = {
readonly label?: Endpoint10_3Request["payload"]["label"] readonly label?: Endpoint10_3Request["payload"]["label"]
} }
const Endpoint10_3 = (raw: RawClient["server.integration"]) => (input: Endpoint10_3Input) => const Endpoint10_3 = (raw: RawClient["server.integration"]) => (input: Endpoint10_3Input) =>
raw["integration.connect.oauth"]({ raw["integration.oauth.connect"]({
params: { integrationID: input["integrationID"] }, params: { integrationID: input["integrationID"] },
query: { location: input["location"] }, query: { location: input["location"] },
payload: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] }, payload: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] },
}).pipe(Effect.mapError(mapClientError)) }).pipe(Effect.mapError(mapClientError))
type Endpoint10_4Request = Parameters<RawClient["server.integration"]["integration.attempt.status"]>[0] type Endpoint10_4Request = Parameters<RawClient["server.integration"]["integration.oauth.status"]>[0]
type Endpoint10_4Input = { type Endpoint10_4Input = {
readonly integrationID: Endpoint10_4Request["params"]["integrationID"]
readonly attemptID: Endpoint10_4Request["params"]["attemptID"] readonly attemptID: Endpoint10_4Request["params"]["attemptID"]
readonly location?: Endpoint10_4Request["query"]["location"] readonly location?: Endpoint10_4Request["query"]["location"]
} }
const Endpoint10_4 = (raw: RawClient["server.integration"]) => (input: Endpoint10_4Input) => const Endpoint10_4 = (raw: RawClient["server.integration"]) => (input: Endpoint10_4Input) =>
raw["integration.attempt.status"]({ raw["integration.oauth.status"]({
params: { attemptID: input["attemptID"] }, params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
query: { location: input["location"] }, query: { location: input["location"] },
}).pipe(Effect.mapError(mapClientError)) }).pipe(Effect.mapError(mapClientError))
type Endpoint10_5Request = Parameters<RawClient["server.integration"]["integration.attempt.complete"]>[0] type Endpoint10_5Request = Parameters<RawClient["server.integration"]["integration.oauth.complete"]>[0]
type Endpoint10_5Input = { type Endpoint10_5Input = {
readonly integrationID: Endpoint10_5Request["params"]["integrationID"]
readonly attemptID: Endpoint10_5Request["params"]["attemptID"] readonly attemptID: Endpoint10_5Request["params"]["attemptID"]
readonly location?: Endpoint10_5Request["query"]["location"] readonly location?: Endpoint10_5Request["query"]["location"]
readonly code?: Endpoint10_5Request["payload"]["code"] readonly code?: Endpoint10_5Request["payload"]["code"]
} }
const Endpoint10_5 = (raw: RawClient["server.integration"]) => (input: Endpoint10_5Input) => const Endpoint10_5 = (raw: RawClient["server.integration"]) => (input: Endpoint10_5Input) =>
raw["integration.attempt.complete"]({ raw["integration.oauth.complete"]({
params: { attemptID: input["attemptID"] }, params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
query: { location: input["location"] }, query: { location: input["location"] },
payload: { code: input["code"] }, payload: { code: input["code"] },
}).pipe(Effect.mapError(mapClientError)) }).pipe(Effect.mapError(mapClientError))
type Endpoint10_6Request = Parameters<RawClient["server.integration"]["integration.attempt.cancel"]>[0] type Endpoint10_6Request = Parameters<RawClient["server.integration"]["integration.oauth.cancel"]>[0]
type Endpoint10_6Input = { type Endpoint10_6Input = {
readonly integrationID: Endpoint10_6Request["params"]["integrationID"]
readonly attemptID: Endpoint10_6Request["params"]["attemptID"] readonly attemptID: Endpoint10_6Request["params"]["attemptID"]
readonly location?: Endpoint10_6Request["query"]["location"] readonly location?: Endpoint10_6Request["query"]["location"]
} }
const Endpoint10_6 = (raw: RawClient["server.integration"]) => (input: Endpoint10_6Input) => const Endpoint10_6 = (raw: RawClient["server.integration"]) => (input: Endpoint10_6Input) =>
raw["integration.attempt.cancel"]({ raw["integration.oauth.cancel"]({
params: { attemptID: input["attemptID"] }, params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
query: { location: input["location"] }, query: { location: input["location"] },
}).pipe(Effect.mapError(mapClientError)) }).pipe(Effect.mapError(mapClientError))
const adaptGroup10 = (raw: RawClient["server.integration"]) => ({ const adaptGroup10 = (raw: RawClient["server.integration"]) => ({
list: Endpoint10_0(raw), list: Endpoint10_0(raw),
get: Endpoint10_1(raw), get: Endpoint10_1(raw),
connect: { key: Endpoint10_2(raw), oauth: Endpoint10_3(raw) }, connect: { key: Endpoint10_2(raw) },
attempt: { status: Endpoint10_4(raw), complete: Endpoint10_5(raw), cancel: Endpoint10_6(raw) }, oauth: {
connect: Endpoint10_3(raw),
status: Endpoint10_4(raw),
complete: Endpoint10_5(raw),
cancel: Endpoint10_6(raw),
},
}) })
type Endpoint11_0Request = Parameters<RawClient["server.mcp"]["mcp.list"]>[0] type Endpoint11_0Request = Parameters<RawClient["server.mcp"]["mcp.list"]>[0]
+21 -21
View File
@@ -84,14 +84,14 @@ import type {
IntegrationGetOutput, IntegrationGetOutput,
IntegrationConnectKeyInput, IntegrationConnectKeyInput,
IntegrationConnectKeyOutput, IntegrationConnectKeyOutput,
IntegrationConnectOauthInput, IntegrationOauthConnectInput,
IntegrationConnectOauthOutput, IntegrationOauthConnectOutput,
IntegrationAttemptStatusInput, IntegrationOauthStatusInput,
IntegrationAttemptStatusOutput, IntegrationOauthStatusOutput,
IntegrationAttemptCompleteInput, IntegrationOauthCompleteInput,
IntegrationAttemptCompleteOutput, IntegrationOauthCompleteOutput,
IntegrationAttemptCancelInput, IntegrationOauthCancelInput,
IntegrationAttemptCancelOutput, IntegrationOauthCancelOutput,
McpListInput, McpListInput,
McpListOutput, McpListOutput,
McpResourceCatalogInput, McpResourceCatalogInput,
@@ -901,8 +901,10 @@ export function make(options: ClientOptions) {
}, },
requestOptions, requestOptions,
), ),
oauth: (input: IntegrationConnectOauthInput, requestOptions?: RequestOptions) => },
request<IntegrationConnectOauthOutput>( oauth: {
connect: (input: IntegrationOauthConnectInput, requestOptions?: RequestOptions) =>
request<IntegrationOauthConnectOutput>(
{ {
method: "POST", method: "POST",
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth`, path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth`,
@@ -914,13 +916,11 @@ export function make(options: ClientOptions) {
}, },
requestOptions, requestOptions,
), ),
}, status: (input: IntegrationOauthStatusInput, requestOptions?: RequestOptions) =>
attempt: { request<IntegrationOauthStatusOutput>(
status: (input: IntegrationAttemptStatusInput, requestOptions?: RequestOptions) =>
request<IntegrationAttemptStatusOutput>(
{ {
method: "GET", method: "GET",
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`, path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth/${encodeURIComponent(input.attemptID)}`,
query: { location: input["location"] }, query: { location: input["location"] },
successStatus: 200, successStatus: 200,
declaredStatuses: [401, 400], declaredStatuses: [401, 400],
@@ -928,11 +928,11 @@ export function make(options: ClientOptions) {
}, },
requestOptions, requestOptions,
), ),
complete: (input: IntegrationAttemptCompleteInput, requestOptions?: RequestOptions) => complete: (input: IntegrationOauthCompleteInput, requestOptions?: RequestOptions) =>
request<IntegrationAttemptCompleteOutput>( request<IntegrationOauthCompleteOutput>(
{ {
method: "POST", method: "POST",
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}/complete`, path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth/${encodeURIComponent(input.attemptID)}/complete`,
query: { location: input["location"] }, query: { location: input["location"] },
body: { code: input["code"] }, body: { code: input["code"] },
successStatus: 204, successStatus: 204,
@@ -941,11 +941,11 @@ export function make(options: ClientOptions) {
}, },
requestOptions, requestOptions,
), ),
cancel: (input: IntegrationAttemptCancelInput, requestOptions?: RequestOptions) => cancel: (input: IntegrationOauthCancelInput, requestOptions?: RequestOptions) =>
request<IntegrationAttemptCancelOutput>( request<IntegrationOauthCancelOutput>(
{ {
method: "DELETE", method: "DELETE",
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`, path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth/${encodeURIComponent(input.attemptID)}`,
query: { location: input["location"] }, query: { location: input["location"] },
successStatus: 204, successStatus: 204,
declaredStatuses: [401, 400], declaredStatuses: [401, 400],
+14 -11
View File
@@ -3284,7 +3284,7 @@ export type IntegrationConnectKeyInput = {
export type IntegrationConnectKeyOutput = void export type IntegrationConnectKeyOutput = void
export type IntegrationConnectOauthInput = { export type IntegrationOauthConnectInput = {
readonly integrationID: { readonly integrationID: string }["integrationID"] readonly integrationID: { readonly integrationID: string }["integrationID"]
readonly location?: { readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
@@ -3306,7 +3306,7 @@ export type IntegrationConnectOauthInput = {
}["label"] }["label"]
} }
export type IntegrationConnectOauthOutput = { export type IntegrationOauthConnectOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: { data: {
attemptID: string attemptID: string
@@ -3317,36 +3317,39 @@ export type IntegrationConnectOauthOutput = {
} }
} }
export type IntegrationAttemptStatusInput = { export type IntegrationOauthStatusInput = {
readonly attemptID: { readonly attemptID: string }["attemptID"] readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"]
readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"]
readonly location?: { readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"] }["location"]
} }
export type IntegrationAttemptStatusOutput = { export type IntegrationOauthStatusOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: IntegrationAttemptStatus data: IntegrationAttemptStatus
} }
export type IntegrationAttemptCompleteInput = { export type IntegrationOauthCompleteInput = {
readonly attemptID: { readonly attemptID: string }["attemptID"] readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"]
readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"]
readonly location?: { readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"] }["location"]
readonly code?: { readonly code?: string | undefined }["code"] readonly code?: { readonly code?: string | undefined }["code"]
} }
export type IntegrationAttemptCompleteOutput = void export type IntegrationOauthCompleteOutput = void
export type IntegrationAttemptCancelInput = { export type IntegrationOauthCancelInput = {
readonly attemptID: { readonly attemptID: string }["attemptID"] readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"]
readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"]
readonly location?: { readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"] }["location"]
} }
export type IntegrationAttemptCancelOutput = void export type IntegrationOauthCancelOutput = void
export type McpListInput = { export type McpListInput = {
readonly location?: { readonly location?: {
+3 -3
View File
@@ -36,9 +36,9 @@ test("exposes every standard HTTP API group", () => {
expect(Object.keys(client.debug)).toEqual(["location"]) expect(Object.keys(client.debug)).toEqual(["location"])
expect(Object.keys(client.debug.location)).toEqual(["list", "evict"]) expect(Object.keys(client.debug.location)).toEqual(["list", "evict"])
expect(Object.keys(client.message)).toEqual(["list"]) expect(Object.keys(client.message)).toEqual(["list"])
expect(Object.keys(client.integration)).toEqual(["list", "get", "connect", "attempt"]) expect(Object.keys(client.integration)).toEqual(["list", "get", "connect", "oauth"])
expect(Object.keys(client.integration.connect)).toEqual(["key", "oauth"]) expect(Object.keys(client.integration.connect)).toEqual(["key"])
expect(Object.keys(client.integration.attempt)).toEqual(["status", "complete", "cancel"]) expect(Object.keys(client.integration.oauth)).toEqual(["connect", "status", "complete", "cancel"])
expect(Object.keys(client.file)).toEqual(["read", "list", "find"]) expect(Object.keys(client.file)).toEqual(["read", "list", "find"])
expect(Object.keys(client.vcs)).toEqual(["status", "diff"]) expect(Object.keys(client.vcs)).toEqual(["status", "diff"])
expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove"]) expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove"])
+94 -73
View File
@@ -159,17 +159,6 @@ export interface Interface extends State.Transformable<Draft> {
/** User-facing label for the stored credential. */ /** User-facing label for the stored credential. */
readonly label?: string readonly label?: string
}) => Effect.Effect<void, AuthorizationError> }) => Effect.Effect<void, AuthorizationError>
/** Starts a stateful OAuth attempt. */
readonly oauth: (input: {
/** Integration being authenticated. */
readonly integrationID: ID
/** OAuth method selected by the caller. */
readonly methodID: MethodID
/** Answers to the method's optional prompts. */
readonly inputs: Inputs
/** User-facing label for the credential created on completion. */
readonly label?: string
}) => Effect.Effect<Attempt, AuthorizationError>
/** Updates a stored credential exposed as a connection. */ /** Updates a stored credential exposed as a connection. */
readonly update: ( readonly update: (
credentialID: Credential.ID, credentialID: Credential.ID,
@@ -178,18 +167,27 @@ export interface Interface extends State.Transformable<Draft> {
/** Removes a stored credential connection. */ /** Removes a stored credential connection. */
readonly remove: (credentialID: Credential.ID) => Effect.Effect<void> readonly remove: (credentialID: Credential.ID) => Effect.Effect<void>
} }
readonly attempt: { readonly oauth: {
/** Starts a stateful OAuth attempt. */
readonly connect: (input: {
readonly integrationID: ID
readonly methodID: MethodID
readonly inputs: Inputs
readonly label?: string
}) => Effect.Effect<Attempt, AuthorizationError>
/** Returns the current state of an OAuth attempt. */ /** Returns the current state of an OAuth attempt. */
readonly status: (attemptID: AttemptID) => Effect.Effect<AttemptStatus> readonly status: (input: {
readonly integrationID: ID
readonly attemptID: AttemptID
}) => Effect.Effect<AttemptStatus>
/** Completes the attempt and stores its credential. */ /** Completes the attempt and stores its credential. */
readonly complete: (input: { readonly complete: (input: {
/** Opaque handle returned by `oauth`. */ readonly integrationID: ID
readonly attemptID: AttemptID readonly attemptID: AttemptID
/** Authorization code required by attempts in code mode. */
readonly code?: string readonly code?: string
}) => Effect.Effect<void, CodeRequiredError | AuthorizationError> }) => Effect.Effect<void, CodeRequiredError | AuthorizationError>
/** Cancels an attempt and releases its resources. */ /** Cancels an attempt and releases its resources. */
readonly cancel: (attemptID: AttemptID) => Effect.Effect<void> readonly cancel: (input: { readonly integrationID: ID; readonly attemptID: AttemptID }) => Effect.Effect<void>
} }
} }
@@ -213,6 +211,7 @@ type PendingAttempt = {
} }
type TerminalAttempt = { type TerminalAttempt = {
status: "complete" | "failed" | "expired" status: "complete" | "failed" | "expired"
integrationID: ID
message?: string message?: string
removeAt: number removeAt: number
time: AttemptTime time: AttemptTime
@@ -332,6 +331,7 @@ const layer = Layer.effect(
? { ...match, persisting: true } ? { ...match, persisting: true }
: { : {
status: "failed" as const, status: "failed" as const,
integrationID: match.integrationID,
message: message(exit.cause), message: message(exit.cause),
time: match.time, time: match.time,
removeAt: now + terminalRetention, removeAt: now + terminalRetention,
@@ -362,13 +362,19 @@ const layer = Layer.effect(
) )
const settledAt = yield* Clock.currentTimeMillis const settledAt = yield* Clock.currentTimeMillis
const terminal: TerminalAttempt = Exit.isSuccess(persistence) const terminal: TerminalAttempt = Exit.isSuccess(persistence)
? { status: "complete", time: attempt.time, removeAt: settledAt + terminalRetention } ? {
status: "complete",
integrationID: attempt.integrationID,
time: attempt.time,
removeAt: settledAt + terminalRetention,
}
: { : {
status: "failed", status: "failed",
message: message(persistence.cause), integrationID: attempt.integrationID,
time: attempt.time, message: message(persistence.cause),
removeAt: settledAt + terminalRetention, time: attempt.time,
} removeAt: settledAt + terminalRetention,
}
// Persisting attempts cannot be cancelled, expired, or claimed again. // Persisting attempts cannot be cancelled, expired, or claimed again.
yield* SynchronizedRef.update(attempts, (current) => new Map(current).set(attemptID, terminal)) yield* SynchronizedRef.update(attempts, (current) => new Map(current).set(attemptID, terminal))
if (Exit.isFailure(persistence)) yield* Effect.failCause(persistence.cause) if (Exit.isFailure(persistence)) yield* Effect.failCause(persistence.cause)
@@ -387,7 +393,12 @@ const layer = Layer.effect(
for (const [id, attempt] of current) { for (const [id, attempt] of current) {
if (attempt.status === "pending" && !attempt.persisting && attempt.time.expires <= now) { if (attempt.status === "pending" && !attempt.persisting && attempt.time.expires <= now) {
scopes.push(attempt.scope) scopes.push(attempt.scope)
next.set(id, { status: "expired", time: attempt.time, removeAt: now + terminalRetention }) next.set(id, {
status: "expired",
integrationID: attempt.integrationID,
time: attempt.time,
removeAt: now + terminalRetention,
})
continue continue
} }
if (attempt.status !== "pending" && attempt.removeAt <= now) next.delete(id) if (attempt.status !== "pending" && attempt.removeAt <= now) next.delete(id)
@@ -399,6 +410,53 @@ const layer = Layer.effect(
yield* scrub().pipe(Effect.repeat(Schedule.spaced(scrubInterval)), Effect.forkIn(scope)) yield* scrub().pipe(Effect.repeat(Schedule.spaced(scrubInterval)), Effect.forkIn(scope))
const connectOAuth = Effect.fn("Integration.oauth.connect")(function* (input: {
readonly integrationID: ID
readonly methodID: MethodID
readonly inputs: Inputs
readonly label?: string
}) {
const method = state.get().integrations.get(input.integrationID)?.implementations.get(input.methodID)
if (!method) {
return yield* Effect.die(new Error(`OAuth method not found: ${input.integrationID}/${input.methodID}`))
}
const attemptScope = yield* Scope.fork(scope)
const authorization = yield* authorize(method.authorize(input.inputs)).pipe(
Scope.provide(attemptScope),
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(attemptScope, exit) : Effect.void)),
)
const id = AttemptID.create()
const created = yield* Clock.currentTimeMillis
const time = { created, expires: created + attemptLifetime }
yield* SynchronizedRef.update(attempts, (current) =>
new Map(current).set(id, {
status: "pending",
completing: authorization.mode === "auto",
persisting: false,
authorization,
integrationID: input.integrationID,
methodID: input.methodID,
label: input.label,
scope: attemptScope,
time,
}),
)
if (authorization.mode === "auto") {
yield* authorization.callback.pipe(
Effect.exit,
Effect.flatMap((exit) => settle(id, exit)),
Effect.forkIn(attemptScope, { startImmediately: true }),
)
}
return new Attempt({
attemptID: id,
url: authorization.url,
instructions: authorization.instructions,
mode: authorization.mode,
time,
})
})
return Service.of({ return Service.of({
transform: state.transform, transform: state.transform,
reload: state.reload, reload: state.reload,
@@ -451,47 +509,6 @@ const layer = Layer.effect(
yield* events.publish(Event.ConnectionUpdated, { integrationID: input.integrationID }) yield* events.publish(Event.ConnectionUpdated, { integrationID: input.integrationID })
yield* events.publish(Event.Updated, {}) yield* events.publish(Event.Updated, {})
}), }),
oauth: Effect.fn("Integration.connection.oauth")(function* (input) {
const method = state.get().integrations.get(input.integrationID)?.implementations.get(input.methodID)
if (!method) {
return yield* Effect.die(new Error(`OAuth method not found: ${input.integrationID}/${input.methodID}`))
}
const attemptScope = yield* Scope.fork(scope)
const authorization = yield* authorize(method.authorize(input.inputs)).pipe(
Scope.provide(attemptScope),
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(attemptScope, exit) : Effect.void)),
)
const id = AttemptID.create()
const created = yield* Clock.currentTimeMillis
const time = { created, expires: created + attemptLifetime }
yield* SynchronizedRef.update(attempts, (current) =>
new Map(current).set(id, {
status: "pending",
completing: authorization.mode === "auto",
persisting: false,
authorization,
integrationID: input.integrationID,
methodID: input.methodID,
label: input.label,
scope: attemptScope,
time,
}),
)
if (authorization.mode === "auto") {
yield* authorization.callback.pipe(
Effect.exit,
Effect.flatMap((exit) => settle(id, exit)),
Effect.forkIn(attemptScope, { startImmediately: true }),
)
}
return new Attempt({
attemptID: id,
url: authorization.url,
instructions: authorization.instructions,
mode: authorization.mode,
time,
})
}),
update: Effect.fn("Integration.connection.update")(function* (credentialID, updates) { update: Effect.fn("Integration.connection.update")(function* (credentialID, updates) {
const credential = yield* credentials.get(credentialID) const credential = yield* credentials.get(credentialID)
yield* credentials.update(credentialID, updates) yield* credentials.update(credentialID, updates)
@@ -509,19 +526,22 @@ const layer = Layer.effect(
yield* events.publish(Event.Updated, {}) yield* events.publish(Event.Updated, {})
}), }),
}, },
attempt: { oauth: {
status: Effect.fn("Integration.attempt.status")(function* (attemptID) { connect: connectOAuth,
const attempt = (yield* SynchronizedRef.get(attempts)).get(attemptID) status: Effect.fn("Integration.oauth.status")(function* (input) {
if (!attempt) return yield* Effect.die(new Error(`OAuth attempt not found: ${attemptID}`)) const attempt = (yield* SynchronizedRef.get(attempts)).get(input.attemptID)
if (!attempt || attempt.integrationID !== input.integrationID)
return yield* Effect.die(new Error(`OAuth attempt not found: ${input.attemptID}`))
if (attempt.status === "failed") { if (attempt.status === "failed") {
return { status: attempt.status, message: attempt.message ?? "Authorization failed", time: attempt.time } return { status: attempt.status, message: attempt.message ?? "Authorization failed", time: attempt.time }
} }
return { status: attempt.status, time: attempt.time } return { status: attempt.status, time: attempt.time }
}), }),
complete: Effect.fn("Integration.attempt.complete")(function* (input) { complete: Effect.fn("Integration.oauth.complete")(function* (input) {
const attempt = yield* SynchronizedRef.modify(attempts, (current) => { const attempt = yield* SynchronizedRef.modify(attempts, (current) => {
const match = current.get(input.attemptID) const match = current.get(input.attemptID)
if (!match || match.status !== "pending" || match.completing) return [match, current] if (!match || match.integrationID !== input.integrationID) return [undefined, current]
if (match.status !== "pending" || match.completing) return [match, current]
if (match.authorization.mode === "code" && input.code === undefined) return [match, current] if (match.authorization.mode === "code" && input.code === undefined) return [match, current]
return [match, new Map(current).set(input.attemptID, { ...match, completing: true })] return [match, new Map(current).set(input.attemptID, { ...match, completing: true })]
}) })
@@ -540,12 +560,13 @@ const layer = Layer.effect(
yield* settle(input.attemptID, exit) yield* settle(input.attemptID, exit)
if (Exit.isFailure(exit)) return yield* exit if (Exit.isFailure(exit)) return yield* exit
}), }),
cancel: Effect.fn("Integration.attempt.cancel")(function* (attemptID) { cancel: Effect.fn("Integration.oauth.cancel")(function* (input) {
const attempt = yield* SynchronizedRef.modify(attempts, (current) => { const attempt = yield* SynchronizedRef.modify(attempts, (current) => {
const match = current.get(attemptID) const match = current.get(input.attemptID)
if (!match || match.status !== "pending" || match.persisting) return [undefined, current] if (!match || match.integrationID !== input.integrationID || match.status !== "pending" || match.persisting)
return [undefined, current]
const next = new Map(current) const next = new Map(current)
next.delete(attemptID) next.delete(input.attemptID)
return [match, next] return [match, next]
}) })
if (attempt) yield* Scope.close(attempt.scope, Exit.void) if (attempt) yield* Scope.close(attempt.scope, Exit.void)
+21 -7
View File
@@ -173,21 +173,35 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
key: input.key, key: input.key,
label: input.label, label: input.label,
}), }),
oauth: (input) => },
oauth: {
connect: (input) =>
response( response(
integration.connection.oauth({ integration.oauth.connect({
integrationID: Integration.ID.make(input.integrationID), integrationID: Integration.ID.make(input.integrationID),
methodID: Integration.MethodID.make(input.methodID), methodID: Integration.MethodID.make(input.methodID),
inputs: input.inputs, inputs: input.inputs,
label: input.label, label: input.label,
}), }),
), ),
}, status: (input) =>
attempt: { response(
status: (input) => response(integration.attempt.status(Integration.AttemptID.make(input.attemptID))), integration.oauth.status({
integrationID: Integration.ID.make(input.integrationID),
attemptID: Integration.AttemptID.make(input.attemptID),
}),
),
complete: (input) => complete: (input) =>
integration.attempt.complete({ attemptID: Integration.AttemptID.make(input.attemptID), code: input.code }), integration.oauth.complete({
cancel: (input) => integration.attempt.cancel(Integration.AttemptID.make(input.attemptID)), integrationID: Integration.ID.make(input.integrationID),
attemptID: Integration.AttemptID.make(input.attemptID),
code: input.code,
}),
cancel: (input) =>
integration.oauth.cancel({
integrationID: Integration.ID.make(input.integrationID),
attemptID: Integration.AttemptID.make(input.attemptID),
}),
}, },
reload: integration.reload, reload: integration.reload,
connection: { connection: {
+15 -9
View File
@@ -70,7 +70,8 @@ export function fromPromise(plugin: Plugin) {
catalog: { catalog: {
provider: { provider: {
list: (input) => run(host.catalog.provider.list(input)), list: (input) => run(host.catalog.provider.list(input)),
get: (input) => run(host.catalog.provider.get({ ...input, providerID: Provider.ID.make(input.providerID) })), get: (input) =>
run(host.catalog.provider.get({ ...input, providerID: Provider.ID.make(input.providerID) })),
}, },
model: { model: {
list: (input) => run(host.catalog.model.list(input)), list: (input) => run(host.catalog.model.list(input)),
@@ -96,35 +97,40 @@ export function fromPromise(plugin: Plugin) {
), ),
connect: { connect: {
key: (input) => key: (input) =>
run(host.integration.connect.key({ ...input, integrationID: Integration.ID.make(input.integrationID) })),
oauth: (input) =>
run( run(
host.integration.connect.oauth({ host.integration.connect.key({ ...input, integrationID: Integration.ID.make(input.integrationID) }),
),
},
oauth: {
connect: (input) =>
run(
host.integration.oauth.connect({
...input, ...input,
integrationID: Integration.ID.make(input.integrationID), integrationID: Integration.ID.make(input.integrationID),
methodID: Integration.MethodID.make(input.methodID), methodID: Integration.MethodID.make(input.methodID),
}), }),
), ),
},
attempt: {
status: (input) => status: (input) =>
run( run(
host.integration.attempt.status({ host.integration.oauth.status({
...input, ...input,
integrationID: Integration.ID.make(input.integrationID),
attemptID: Integration.AttemptID.make(input.attemptID), attemptID: Integration.AttemptID.make(input.attemptID),
}), }),
), ),
complete: (input) => complete: (input) =>
run( run(
host.integration.attempt.complete({ host.integration.oauth.complete({
...input, ...input,
integrationID: Integration.ID.make(input.integrationID),
attemptID: Integration.AttemptID.make(input.attemptID), attemptID: Integration.AttemptID.make(input.attemptID),
}), }),
), ),
cancel: (input) => cancel: (input) =>
run( run(
host.integration.attempt.cancel({ host.integration.oauth.cancel({
...input, ...input,
integrationID: Integration.ID.make(input.integrationID),
attemptID: Integration.AttemptID.make(input.attemptID), attemptID: Integration.AttemptID.make(input.attemptID),
}), }),
), ),
+20 -15
View File
@@ -181,14 +181,14 @@ describe("Integration", () => {
}), }),
) )
const attempt = yield* integrations.connection.oauth({ const attempt = yield* integrations.oauth.connect({
integrationID, integrationID,
methodID, methodID,
inputs: {}, inputs: {},
label: "Personal", label: "Personal",
}) })
expect(attempt.mode).toBe("code") expect(attempt.mode).toBe("code")
yield* integrations.attempt.complete({ attemptID: attempt.attemptID, code: "1234" }) yield* integrations.oauth.complete({ integrationID, attemptID: attempt.attemptID, code: "1234" })
expect((yield* credentials.list(integrationID))[0]).toEqual( expect((yield* credentials.list(integrationID))[0]).toEqual(
expect.objectContaining({ expect.objectContaining({
@@ -230,12 +230,17 @@ describe("Integration", () => {
}), }),
) )
const attempt = yield* integrations.connection.oauth({ integrationID, methodID, inputs: {} }) const attempt = yield* integrations.oauth.connect({ integrationID, methodID, inputs: {} })
expect(yield* integrations.attempt.complete({ attemptID: attempt.attemptID }).pipe(Effect.flip)).toBeInstanceOf( expect(
Integration.CodeRequiredError, yield* integrations.oauth.complete({ integrationID, attemptID: attempt.attemptID }).pipe(Effect.flip),
) ).toBeInstanceOf(Integration.CodeRequiredError)
expect(closed).toBe(false) expect(closed).toBe(false)
yield* integrations.attempt.cancel(attempt.attemptID) yield* integrations.oauth.cancel({
integrationID: Integration.ID.make("other"),
attemptID: attempt.attemptID,
})
expect(closed).toBe(false)
yield* integrations.oauth.cancel({ integrationID, attemptID: attempt.attemptID })
expect(closed).toBe(true) expect(closed).toBe(true)
expect(yield* credentials.list(integrationID)).toEqual([]) expect(yield* credentials.list(integrationID)).toEqual([])
}), }),
@@ -263,9 +268,9 @@ describe("Integration", () => {
}), }),
) )
const attempt = yield* integrations.connection.oauth({ integrationID, methodID, inputs: {} }) const attempt = yield* integrations.oauth.connect({ integrationID, methodID, inputs: {} })
yield* Effect.yieldNow yield* Effect.yieldNow
expect(yield* integrations.attempt.status(attempt.attemptID)).toEqual({ expect(yield* integrations.oauth.status({ integrationID, attemptID: attempt.attemptID })).toEqual({
status: "complete", status: "complete",
time: attempt.time, time: attempt.time,
}) })
@@ -301,12 +306,12 @@ describe("Integration", () => {
}), }),
) )
const attempt = yield* integrations.connection.oauth({ integrationID, methodID, inputs: {} }) const attempt = yield* integrations.oauth.connect({ integrationID, methodID, inputs: {} })
const exit = yield* integrations.attempt const exit = yield* integrations.oauth
.complete({ attemptID: attempt.attemptID, code: "1234" }) .complete({ integrationID, attemptID: attempt.attemptID, code: "1234" })
.pipe(Effect.exit) .pipe(Effect.exit)
expect(Exit.isFailure(exit) && Cause.hasDies(exit.cause)).toBe(true) expect(Exit.isFailure(exit) && Cause.hasDies(exit.cause)).toBe(true)
expect(yield* integrations.attempt.status(attempt.attemptID)).toEqual({ expect(yield* integrations.oauth.status({ integrationID, attemptID: attempt.attemptID })).toEqual({
status: "failed", status: "failed",
message: "credential persistence failed", message: "credential persistence failed",
time: attempt.time, time: attempt.time,
@@ -337,11 +342,11 @@ describe("Integration", () => {
}), }),
) )
const attempt = yield* integrations.connection.oauth({ integrationID, methodID, inputs: {} }) const attempt = yield* integrations.oauth.connect({ integrationID, methodID, inputs: {} })
expect(attempt.time.expires - attempt.time.created).toBe(Duration.toMillis(Duration.minutes(10))) expect(attempt.time.expires - attempt.time.created).toBe(Duration.toMillis(Duration.minutes(10)))
yield* TestClock.adjust(Duration.minutes(10)) yield* TestClock.adjust(Duration.minutes(10))
yield* Effect.yieldNow yield* Effect.yieldNow
expect(yield* integrations.attempt.status(attempt.attemptID)).toEqual({ expect(yield* integrations.oauth.status({ integrationID, attemptID: attempt.attemptID })).toEqual({
status: "expired", status: "expired",
time: attempt.time, time: attempt.time,
}) })
+2 -2
View File
@@ -187,11 +187,11 @@ function resourceMcpLayer(url: string, onFormCreated?: (form: Form.Info) => Effe
active: unusedIntegration, active: unusedIntegration,
resolve: unusedIntegration, resolve: unusedIntegration,
key: unusedIntegration, key: unusedIntegration,
oauth: unusedIntegration,
update: unusedIntegration, update: unusedIntegration,
remove: unusedIntegration, remove: unusedIntegration,
}, },
attempt: { oauth: {
connect: unusedIntegration,
status: unusedIntegration, status: unusedIntegration,
complete: unusedIntegration, complete: unusedIntegration,
cancel: unusedIntegration, cancel: unusedIntegration,
+10 -10
View File
@@ -48,12 +48,12 @@ export function host(overrides: Overrides = {}): PluginContext {
get: () => Effect.die("unused integration.get"), get: () => Effect.die("unused integration.get"),
connect: { connect: {
key: () => Effect.die("unused integration.connect.key"), key: () => Effect.die("unused integration.connect.key"),
oauth: () => Effect.die("unused integration.connect.oauth"),
}, },
attempt: { oauth: {
status: () => Effect.die("unused integration.attempt.status"), connect: () => Effect.die("unused integration.oauth.connect"),
complete: () => Effect.die("unused integration.attempt.complete"), status: () => Effect.die("unused integration.oauth.status"),
cancel: () => Effect.die("unused integration.attempt.cancel"), complete: () => Effect.die("unused integration.oauth.complete"),
cancel: () => Effect.die("unused integration.oauth.cancel"),
}, },
transform: () => Effect.die("unused integration.transform"), transform: () => Effect.die("unused integration.transform"),
reload: () => Effect.die("unused integration.reload"), reload: () => Effect.die("unused integration.reload"),
@@ -193,12 +193,12 @@ export function integrationHost(integration: Integration.Interface): PluginConte
get: () => Effect.die("unused integration.get"), get: () => Effect.die("unused integration.get"),
connect: { connect: {
key: () => Effect.die("unused integration.connect.key"), key: () => Effect.die("unused integration.connect.key"),
oauth: () => Effect.die("unused integration.connect.oauth"),
}, },
attempt: { oauth: {
status: () => Effect.die("unused integration.attempt.status"), connect: () => Effect.die("unused integration.oauth.connect"),
complete: () => Effect.die("unused integration.attempt.complete"), status: () => Effect.die("unused integration.oauth.status"),
cancel: () => Effect.die("unused integration.attempt.cancel"), complete: () => Effect.die("unused integration.oauth.complete"),
cancel: () => Effect.die("unused integration.oauth.cancel"),
}, },
reload: integration.reload, reload: integration.reload,
connection: { connection: {
@@ -124,13 +124,17 @@ describe("OpencodePlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
yield* addPlugin() yield* addPlugin()
const integrations = yield* Integration.Service const integrations = yield* Integration.Service
const attempt = yield* integrations.connection.oauth({ const integrationID = Integration.ID.make("opencode")
integrationID: Integration.ID.make("opencode"), const attempt = yield* integrations.oauth.connect({
integrationID,
methodID: Integration.MethodID.make("device"), methodID: Integration.MethodID.make("device"),
inputs: { server: `${server.url.origin}/console///?ignored=true#ignored` }, inputs: { server: `${server.url.origin}/console///?ignored=true#ignored` },
}) })
expect(attempt.url).toBe(`${server.url.origin}/verify`) expect(attempt.url).toBe(`${server.url.origin}/verify`)
yield* eventually(integrations.attempt.status(attempt.attemptID), (status) => status.status === "complete") yield* eventually(
integrations.oauth.status({ integrationID, attemptID: attempt.attemptID }),
(status) => status.status === "complete",
)
expect(requests).toContain("POST /console/auth/device/code") expect(requests).toContain("POST /console/auth/device/code")
expect(requests).toContain("POST /console/auth/device/token") expect(requests).toContain("POST /console/auth/device/token")
@@ -147,8 +151,8 @@ describe("OpencodePlugin", () => {
it.effect("rejects non-HTTP OpenCode servers", () => it.effect("rejects non-HTTP OpenCode servers", () =>
Effect.gen(function* () { Effect.gen(function* () {
yield* addPlugin() yield* addPlugin()
const error = yield* (yield* Integration.Service).connection const error = yield* (yield* Integration.Service).oauth
.oauth({ .connect({
integrationID: Integration.ID.make("opencode"), integrationID: Integration.ID.make("opencode"),
methodID: Integration.MethodID.make("device"), methodID: Integration.MethodID.make("device"),
inputs: { server: "ftp://console.example.com" }, inputs: { server: "ftp://console.example.com" },
+20 -16
View File
@@ -58,7 +58,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
), ),
) )
.add( .add(
HttpApiEndpoint.post("integration.connect.oauth", "/api/integration/:integrationID/connect/oauth", { HttpApiEndpoint.post("integration.oauth.connect", "/api/integration/:integrationID/connect/oauth", {
params: { integrationID: Integration.ID }, params: { integrationID: Integration.ID },
query: LocationQuery, query: LocationQuery,
payload: Schema.Struct({ payload: Schema.Struct({
@@ -72,54 +72,58 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
.annotateMerge(locationQueryOpenApi) .annotateMerge(locationQueryOpenApi)
.annotateMerge( .annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
identifier: "v2.integration.connect.oauth", identifier: "v2.integration.oauth.connect",
summary: "Begin OAuth connection", summary: "Begin OAuth connection",
description: "Start an OAuth attempt and return the authorization details.", description: "Start an OAuth attempt and return the authorization details.",
}), }),
), ),
) )
.add( .add(
HttpApiEndpoint.get("integration.attempt.status", "/api/integration/attempt/:attemptID", { HttpApiEndpoint.get("integration.oauth.status", "/api/integration/:integrationID/connect/oauth/:attemptID", {
params: { attemptID: Integration.AttemptID }, params: { integrationID: Integration.ID, attemptID: Integration.AttemptID },
query: LocationQuery, query: LocationQuery,
success: Location.response(Integration.AttemptStatus), success: Location.response(Integration.AttemptStatus),
}) })
.annotateMerge(locationQueryOpenApi) .annotateMerge(locationQueryOpenApi)
.annotateMerge( .annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
identifier: "v2.integration.attempt.status", identifier: "v2.integration.oauth.status",
summary: "Get OAuth attempt status", summary: "Get OAuth attempt status",
description: "Poll the current status of an OAuth attempt.", description: "Poll the current status of an OAuth attempt.",
}), }),
), ),
) )
.add( .add(
HttpApiEndpoint.post("integration.attempt.complete", "/api/integration/attempt/:attemptID/complete", { HttpApiEndpoint.post(
params: { attemptID: Integration.AttemptID }, "integration.oauth.complete",
query: LocationQuery, "/api/integration/:integrationID/connect/oauth/:attemptID/complete",
payload: Schema.Struct({ code: Schema.optional(Schema.String) }), {
success: HttpApiSchema.NoContent, params: { integrationID: Integration.ID, attemptID: Integration.AttemptID },
error: InvalidRequestError, query: LocationQuery,
}) payload: Schema.Struct({ code: Schema.optional(Schema.String) }),
success: HttpApiSchema.NoContent,
error: InvalidRequestError,
},
)
.annotateMerge(locationQueryOpenApi) .annotateMerge(locationQueryOpenApi)
.annotateMerge( .annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
identifier: "v2.integration.attempt.complete", identifier: "v2.integration.oauth.complete",
summary: "Complete OAuth connection", summary: "Complete OAuth connection",
description: "Complete a code-based OAuth attempt and store the resulting credential.", description: "Complete a code-based OAuth attempt and store the resulting credential.",
}), }),
), ),
) )
.add( .add(
HttpApiEndpoint.delete("integration.attempt.cancel", "/api/integration/attempt/:attemptID", { HttpApiEndpoint.delete("integration.oauth.cancel", "/api/integration/:integrationID/connect/oauth/:attemptID", {
params: { attemptID: Integration.AttemptID }, params: { integrationID: Integration.ID, attemptID: Integration.AttemptID },
query: LocationQuery, query: LocationQuery,
success: HttpApiSchema.NoContent, success: HttpApiSchema.NoContent,
}) })
.annotateMerge(locationQueryOpenApi) .annotateMerge(locationQueryOpenApi)
.annotateMerge( .annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
identifier: "v2.integration.attempt.cancel", identifier: "v2.integration.oauth.cancel",
summary: "Cancel OAuth connection", summary: "Cancel OAuth connection",
description: "Cancel an OAuth attempt and release its resources.", description: "Cancel an OAuth attempt and release its resources.",
}), }),
+31 -25
View File
@@ -294,20 +294,20 @@ import type {
V2HealthGetResponses, V2HealthGetResponses,
V2HealthStopErrors, V2HealthStopErrors,
V2HealthStopResponses, V2HealthStopResponses,
V2IntegrationAttemptCancelErrors,
V2IntegrationAttemptCancelResponses,
V2IntegrationAttemptCompleteErrors,
V2IntegrationAttemptCompleteResponses,
V2IntegrationAttemptStatusErrors,
V2IntegrationAttemptStatusResponses,
V2IntegrationConnectKeyErrors, V2IntegrationConnectKeyErrors,
V2IntegrationConnectKeyResponses, V2IntegrationConnectKeyResponses,
V2IntegrationConnectOauthErrors,
V2IntegrationConnectOauthResponses,
V2IntegrationGetErrors, V2IntegrationGetErrors,
V2IntegrationGetResponses, V2IntegrationGetResponses,
V2IntegrationListErrors, V2IntegrationListErrors,
V2IntegrationListResponses, V2IntegrationListResponses,
V2IntegrationOauthCancelErrors,
V2IntegrationOauthCancelResponses,
V2IntegrationOauthCompleteErrors,
V2IntegrationOauthCompleteResponses,
V2IntegrationOauthConnectErrors,
V2IntegrationOauthConnectResponses,
V2IntegrationOauthStatusErrors,
V2IntegrationOauthStatusResponses,
V2LocationGetErrors, V2LocationGetErrors,
V2LocationGetResponses, V2LocationGetResponses,
V2McpListErrors, V2McpListErrors,
@@ -6827,13 +6827,15 @@ export class Connect extends HeyApiClient {
}, },
}) })
} }
}
export class Oauth2 extends HeyApiClient {
/** /**
* Begin OAuth connection * Begin OAuth connection
* *
* Start an OAuth attempt and return the authorization details. * Start an OAuth attempt and return the authorization details.
*/ */
public oauth<ThrowOnError extends boolean = false>( public connect<ThrowOnError extends boolean = false>(
parameters: { parameters: {
integrationID: string integrationID: string
location?: { location?: {
@@ -6863,8 +6865,8 @@ export class Connect extends HeyApiClient {
], ],
) )
return (options?.client ?? this.client).post< return (options?.client ?? this.client).post<
V2IntegrationConnectOauthResponses, V2IntegrationOauthConnectResponses,
V2IntegrationConnectOauthErrors, V2IntegrationOauthConnectErrors,
ThrowOnError ThrowOnError
>({ >({
url: "/api/integration/{integrationID}/connect/oauth", url: "/api/integration/{integrationID}/connect/oauth",
@@ -6877,9 +6879,7 @@ export class Connect extends HeyApiClient {
}, },
}) })
} }
}
export class Attempt extends HeyApiClient {
/** /**
* Cancel OAuth connection * Cancel OAuth connection
* *
@@ -6887,6 +6887,7 @@ export class Attempt extends HeyApiClient {
*/ */
public cancel<ThrowOnError extends boolean = false>( public cancel<ThrowOnError extends boolean = false>(
parameters: { parameters: {
integrationID: string
attemptID: string attemptID: string
location?: { location?: {
directory?: string | null directory?: string | null
@@ -6900,6 +6901,7 @@ export class Attempt extends HeyApiClient {
[ [
{ {
args: [ args: [
{ in: "path", key: "integrationID" },
{ in: "path", key: "attemptID" }, { in: "path", key: "attemptID" },
{ in: "query", key: "location" }, { in: "query", key: "location" },
], ],
@@ -6907,11 +6909,11 @@ export class Attempt extends HeyApiClient {
], ],
) )
return (options?.client ?? this.client).delete< return (options?.client ?? this.client).delete<
V2IntegrationAttemptCancelResponses, V2IntegrationOauthCancelResponses,
V2IntegrationAttemptCancelErrors, V2IntegrationOauthCancelErrors,
ThrowOnError ThrowOnError
>({ >({
url: "/api/integration/attempt/{attemptID}", url: "/api/integration/{integrationID}/connect/oauth/{attemptID}",
...options, ...options,
...params, ...params,
}) })
@@ -6924,6 +6926,7 @@ export class Attempt extends HeyApiClient {
*/ */
public status<ThrowOnError extends boolean = false>( public status<ThrowOnError extends boolean = false>(
parameters: { parameters: {
integrationID: string
attemptID: string attemptID: string
location?: { location?: {
directory?: string | null directory?: string | null
@@ -6937,6 +6940,7 @@ export class Attempt extends HeyApiClient {
[ [
{ {
args: [ args: [
{ in: "path", key: "integrationID" },
{ in: "path", key: "attemptID" }, { in: "path", key: "attemptID" },
{ in: "query", key: "location" }, { in: "query", key: "location" },
], ],
@@ -6944,11 +6948,11 @@ export class Attempt extends HeyApiClient {
], ],
) )
return (options?.client ?? this.client).get< return (options?.client ?? this.client).get<
V2IntegrationAttemptStatusResponses, V2IntegrationOauthStatusResponses,
V2IntegrationAttemptStatusErrors, V2IntegrationOauthStatusErrors,
ThrowOnError ThrowOnError
>({ >({
url: "/api/integration/attempt/{attemptID}", url: "/api/integration/{integrationID}/connect/oauth/{attemptID}",
...options, ...options,
...params, ...params,
}) })
@@ -6961,6 +6965,7 @@ export class Attempt extends HeyApiClient {
*/ */
public complete<ThrowOnError extends boolean = false>( public complete<ThrowOnError extends boolean = false>(
parameters: { parameters: {
integrationID: string
attemptID: string attemptID: string
location?: { location?: {
directory?: string | null directory?: string | null
@@ -6975,6 +6980,7 @@ export class Attempt extends HeyApiClient {
[ [
{ {
args: [ args: [
{ in: "path", key: "integrationID" },
{ in: "path", key: "attemptID" }, { in: "path", key: "attemptID" },
{ in: "query", key: "location" }, { in: "query", key: "location" },
{ in: "body", key: "code" }, { in: "body", key: "code" },
@@ -6983,11 +6989,11 @@ export class Attempt extends HeyApiClient {
], ],
) )
return (options?.client ?? this.client).post< return (options?.client ?? this.client).post<
V2IntegrationAttemptCompleteResponses, V2IntegrationOauthCompleteResponses,
V2IntegrationAttemptCompleteErrors, V2IntegrationOauthCompleteErrors,
ThrowOnError ThrowOnError
>({ >({
url: "/api/integration/attempt/{attemptID}/complete", url: "/api/integration/{integrationID}/connect/oauth/{attemptID}/complete",
...options, ...options,
...params, ...params,
headers: { headers: {
@@ -7060,9 +7066,9 @@ export class Integration extends HeyApiClient {
return (this._connect ??= new Connect({ client: this.client })) return (this._connect ??= new Connect({ client: this.client }))
} }
private _attempt?: Attempt private _oauth?: Oauth2
get attempt(): Attempt { get oauth(): Oauth2 {
return (this._attempt ??= new Attempt({ client: this.client })) return (this._oauth ??= new Oauth2({ client: this.client }))
} }
} }
+30 -28
View File
@@ -16846,7 +16846,7 @@ export type V2IntegrationConnectKeyResponses = {
export type V2IntegrationConnectKeyResponse = V2IntegrationConnectKeyResponses[keyof V2IntegrationConnectKeyResponses] export type V2IntegrationConnectKeyResponse = V2IntegrationConnectKeyResponses[keyof V2IntegrationConnectKeyResponses]
export type V2IntegrationConnectOauthData = { export type V2IntegrationOauthConnectData = {
body: { body: {
methodID: string methodID: string
inputs: { inputs: {
@@ -16866,7 +16866,7 @@ export type V2IntegrationConnectOauthData = {
url: "/api/integration/{integrationID}/connect/oauth" url: "/api/integration/{integrationID}/connect/oauth"
} }
export type V2IntegrationConnectOauthErrors = { export type V2IntegrationOauthConnectErrors = {
/** /**
* InvalidRequestError * InvalidRequestError
*/ */
@@ -16877,9 +16877,9 @@ export type V2IntegrationConnectOauthErrors = {
401: UnauthorizedError 401: UnauthorizedError
} }
export type V2IntegrationConnectOauthError = V2IntegrationConnectOauthErrors[keyof V2IntegrationConnectOauthErrors] export type V2IntegrationOauthConnectError = V2IntegrationOauthConnectErrors[keyof V2IntegrationOauthConnectErrors]
export type V2IntegrationConnectOauthResponses = { export type V2IntegrationOauthConnectResponses = {
/** /**
* Success * Success
*/ */
@@ -16889,12 +16889,13 @@ export type V2IntegrationConnectOauthResponses = {
} }
} }
export type V2IntegrationConnectOauthResponse = export type V2IntegrationOauthConnectResponse =
V2IntegrationConnectOauthResponses[keyof V2IntegrationConnectOauthResponses] V2IntegrationOauthConnectResponses[keyof V2IntegrationOauthConnectResponses]
export type V2IntegrationAttemptCancelData = { export type V2IntegrationOauthCancelData = {
body?: never body?: never
path: { path: {
integrationID: string
attemptID: string attemptID: string
} }
query?: { query?: {
@@ -16903,10 +16904,10 @@ export type V2IntegrationAttemptCancelData = {
workspace?: string | null workspace?: string | null
} | null } | null
} }
url: "/api/integration/attempt/{attemptID}" url: "/api/integration/{integrationID}/connect/oauth/{attemptID}"
} }
export type V2IntegrationAttemptCancelErrors = { export type V2IntegrationOauthCancelErrors = {
/** /**
* InvalidRequestError * InvalidRequestError
*/ */
@@ -16917,21 +16918,22 @@ export type V2IntegrationAttemptCancelErrors = {
401: UnauthorizedError 401: UnauthorizedError
} }
export type V2IntegrationAttemptCancelError = V2IntegrationAttemptCancelErrors[keyof V2IntegrationAttemptCancelErrors] export type V2IntegrationOauthCancelError = V2IntegrationOauthCancelErrors[keyof V2IntegrationOauthCancelErrors]
export type V2IntegrationAttemptCancelResponses = { export type V2IntegrationOauthCancelResponses = {
/** /**
* <No Content> * <No Content>
*/ */
204: void 204: void
} }
export type V2IntegrationAttemptCancelResponse = export type V2IntegrationOauthCancelResponse =
V2IntegrationAttemptCancelResponses[keyof V2IntegrationAttemptCancelResponses] V2IntegrationOauthCancelResponses[keyof V2IntegrationOauthCancelResponses]
export type V2IntegrationAttemptStatusData = { export type V2IntegrationOauthStatusData = {
body?: never body?: never
path: { path: {
integrationID: string
attemptID: string attemptID: string
} }
query?: { query?: {
@@ -16940,10 +16942,10 @@ export type V2IntegrationAttemptStatusData = {
workspace?: string | null workspace?: string | null
} | null } | null
} }
url: "/api/integration/attempt/{attemptID}" url: "/api/integration/{integrationID}/connect/oauth/{attemptID}"
} }
export type V2IntegrationAttemptStatusErrors = { export type V2IntegrationOauthStatusErrors = {
/** /**
* InvalidRequestError * InvalidRequestError
*/ */
@@ -16954,9 +16956,9 @@ export type V2IntegrationAttemptStatusErrors = {
401: UnauthorizedError 401: UnauthorizedError
} }
export type V2IntegrationAttemptStatusError = V2IntegrationAttemptStatusErrors[keyof V2IntegrationAttemptStatusErrors] export type V2IntegrationOauthStatusError = V2IntegrationOauthStatusErrors[keyof V2IntegrationOauthStatusErrors]
export type V2IntegrationAttemptStatusResponses = { export type V2IntegrationOauthStatusResponses = {
/** /**
* Success * Success
*/ */
@@ -16966,14 +16968,15 @@ export type V2IntegrationAttemptStatusResponses = {
} }
} }
export type V2IntegrationAttemptStatusResponse = export type V2IntegrationOauthStatusResponse =
V2IntegrationAttemptStatusResponses[keyof V2IntegrationAttemptStatusResponses] V2IntegrationOauthStatusResponses[keyof V2IntegrationOauthStatusResponses]
export type V2IntegrationAttemptCompleteData = { export type V2IntegrationOauthCompleteData = {
body: { body: {
code?: string | null code?: string | null
} }
path: { path: {
integrationID: string
attemptID: string attemptID: string
} }
query?: { query?: {
@@ -16982,10 +16985,10 @@ export type V2IntegrationAttemptCompleteData = {
workspace?: string | null workspace?: string | null
} | null } | null
} }
url: "/api/integration/attempt/{attemptID}/complete" url: "/api/integration/{integrationID}/connect/oauth/{attemptID}/complete"
} }
export type V2IntegrationAttemptCompleteErrors = { export type V2IntegrationOauthCompleteErrors = {
/** /**
* InvalidRequestError * InvalidRequestError
*/ */
@@ -16996,18 +16999,17 @@ export type V2IntegrationAttemptCompleteErrors = {
401: UnauthorizedError 401: UnauthorizedError
} }
export type V2IntegrationAttemptCompleteError = export type V2IntegrationOauthCompleteError = V2IntegrationOauthCompleteErrors[keyof V2IntegrationOauthCompleteErrors]
V2IntegrationAttemptCompleteErrors[keyof V2IntegrationAttemptCompleteErrors]
export type V2IntegrationAttemptCompleteResponses = { export type V2IntegrationOauthCompleteResponses = {
/** /**
* <No Content> * <No Content>
*/ */
204: void 204: void
} }
export type V2IntegrationAttemptCompleteResponse = export type V2IntegrationOauthCompleteResponse =
V2IntegrationAttemptCompleteResponses[keyof V2IntegrationAttemptCompleteResponses] V2IntegrationOauthCompleteResponses[keyof V2IntegrationOauthCompleteResponses]
export type V2McpListData = { export type V2McpListData = {
body?: never body?: never
+36 -22
View File
@@ -48,12 +48,12 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
}), }),
) )
.handle( .handle(
"integration.connect.oauth", "integration.oauth.connect",
Effect.fn(function* (ctx) { Effect.fn(function* (ctx) {
const service = yield* Integration.Service const service = yield* Integration.Service
return yield* response( return yield* response(
authorize( authorize(
service.connection.oauth({ service.oauth.connect({
integrationID: ctx.params.integrationID, integrationID: ctx.params.integrationID,
methodID: ctx.payload.methodID, methodID: ctx.payload.methodID,
inputs: ctx.payload.inputs, inputs: ctx.payload.inputs,
@@ -64,39 +64,53 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
}), }),
) )
.handle( .handle(
"integration.attempt.status", "integration.oauth.status",
Effect.fn(function* (ctx) { Effect.fn(function* (ctx) {
const service = yield* Integration.Service const service = yield* Integration.Service
return yield* response(service.attempt.status(ctx.params.attemptID)) return yield* response(
service.oauth.status({
integrationID: ctx.params.integrationID,
attemptID: ctx.params.attemptID,
}),
)
}), }),
) )
.handle( .handle(
"integration.attempt.complete", "integration.oauth.complete",
Effect.fn(function* (ctx) { Effect.fn(function* (ctx) {
const service = yield* Integration.Service const service = yield* Integration.Service
yield* service.attempt.complete({ attemptID: ctx.params.attemptID, code: ctx.payload.code }).pipe( yield* service.oauth
Effect.mapError( .complete({
(error) => integrationID: ctx.params.integrationID,
new InvalidRequestError({ attemptID: ctx.params.attemptID,
message: code: ctx.payload.code,
error._tag === "Integration.CodeRequired" })
? "Authorization code is required" .pipe(
: "Authentication failed", Effect.mapError(
kind: (error) =>
error._tag === "Integration.CodeRequired" new InvalidRequestError({
? "integration_code_required" message:
: "integration_authorization", error._tag === "Integration.CodeRequired"
}), ? "Authorization code is required"
), : "Authentication failed",
) kind:
error._tag === "Integration.CodeRequired"
? "integration_code_required"
: "integration_authorization",
}),
),
)
return HttpApiSchema.NoContent.make() return HttpApiSchema.NoContent.make()
}), }),
) )
.handle( .handle(
"integration.attempt.cancel", "integration.oauth.cancel",
Effect.fn(function* (ctx) { Effect.fn(function* (ctx) {
const service = yield* Integration.Service const service = yield* Integration.Service
yield* service.attempt.cancel(ctx.params.attemptID) yield* service.oauth.cancel({
integrationID: ctx.params.integrationID,
attemptID: ctx.params.attemptID,
})
return HttpApiSchema.NoContent.make() return HttpApiSchema.NoContent.make()
}), }),
) )
@@ -1,8 +1,8 @@
import { TextAttributes } from "@opentui/core" import { TextAttributes } from "@opentui/core"
import type { import type {
ConnectionInfo, ConnectionInfo,
IntegrationConnectOauthOutput,
IntegrationInfo, IntegrationInfo,
IntegrationOauthConnectOutput,
IntegrationOAuthMethod, IntegrationOAuthMethod,
} from "@opencode-ai/client" } from "@opencode-ai/client"
import { createMemo, createSignal, onCleanup, onMount, Show } from "solid-js" import { createMemo, createSignal, onCleanup, onMount, Show } from "solid-js"
@@ -27,7 +27,7 @@ const INTEGRATION_PRIORITY: Record<string, number> = {
} }
type ConnectMethod = Exclude<IntegrationInfo["methods"][number], { type: "env" }> type ConnectMethod = Exclude<IntegrationInfo["methods"][number], { type: "env" }>
type IntegrationAttempt = IntegrationConnectOauthOutput["data"] type IntegrationAttempt = IntegrationOauthConnectOutput["data"]
type OnIntegrationConnected = (providerID?: string) => void type OnIntegrationConnected = (providerID?: string) => void
export function integrationOptions(list: IntegrationInfo[]) { export function integrationOptions(list: IntegrationInfo[]) {
@@ -227,8 +227,8 @@ function OAuthStarting(props: {
const toast = useToast() const toast = useToast()
onMount(() => { onMount(() => {
void client.api.integration.connect void client.api.integration.oauth
.oauth({ .connect({
integrationID: props.integration.id, integrationID: props.integration.id,
location: location(data), location: location(data),
methodID: props.method.id, methodID: props.method.id,
@@ -297,8 +297,8 @@ function OAuthAuto(props: {
})) }))
const poll = () => { const poll = () => {
void client.api.integration.attempt void client.api.integration.oauth
.status({ attemptID: props.attempt.attemptID, location: location(data) }) .status({ integrationID: props.integration.id, attemptID: props.attempt.attemptID, location: location(data) })
.then((result) => { .then((result) => {
const status = result.data const status = result.data
if (status.status === "pending") { if (status.status === "pending") {
@@ -324,7 +324,11 @@ function OAuthAuto(props: {
onCleanup(() => { onCleanup(() => {
if (timer) clearTimeout(timer) if (timer) clearTimeout(timer)
if (settled) return if (settled) return
void client.api.integration.attempt.cancel({ attemptID: props.attempt.attemptID, location: location(data) }) void client.api.integration.oauth.cancel({
integrationID: props.integration.id,
attemptID: props.attempt.attemptID,
location: location(data),
})
}) })
return ( return (
@@ -354,7 +358,11 @@ function OAuthCode(props: {
onCleanup(() => { onCleanup(() => {
if (settled) return if (settled) return
void client.api.integration.attempt.cancel({ attemptID: props.attempt.attemptID, location: location(data) }) void client.api.integration.oauth.cancel({
integrationID: props.integration.id,
attemptID: props.attempt.attemptID,
location: location(data),
})
}) })
return ( return (
@@ -363,8 +371,13 @@ function OAuthCode(props: {
placeholder="Authorization code" placeholder="Authorization code"
onConfirm={(code) => { onConfirm={(code) => {
if (!code) return if (!code) return
void client.api.integration.attempt void client.api.integration.oauth
.complete({ attemptID: props.attempt.attemptID, location: location(data), code }) .complete({
integrationID: props.integration.id,
attemptID: props.attempt.attemptID,
location: location(data),
code,
})
.then(() => { .then(() => {
settled = true settled = true
return connected(props.integration, data, dialog, toast, props.onConnected) return connected(props.integration, data, dialog, toast, props.onConnected)