refactor(api): scope oauth operations by integration

This commit is contained in:
Dax Raad 2026-07-15 18:20:19 -04:00
commit a772767e9b
19 changed files with 404 additions and 303 deletions

View file

@ -181,14 +181,14 @@ describe("Integration", () => {
}),
)
const attempt = yield* integrations.connection.oauth({
const attempt = yield* integrations.oauth.connect({
integrationID,
methodID,
inputs: {},
label: "Personal",
})
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.objectContaining({
@ -230,12 +230,17 @@ describe("Integration", () => {
}),
)
const attempt = yield* integrations.connection.oauth({ integrationID, methodID, inputs: {} })
expect(yield* integrations.attempt.complete({ attemptID: attempt.attemptID }).pipe(Effect.flip)).toBeInstanceOf(
Integration.CodeRequiredError,
)
const attempt = yield* integrations.oauth.connect({ integrationID, methodID, inputs: {} })
expect(
yield* integrations.oauth.complete({ integrationID, attemptID: attempt.attemptID }).pipe(Effect.flip),
).toBeInstanceOf(Integration.CodeRequiredError)
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(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
expect(yield* integrations.attempt.status(attempt.attemptID)).toEqual({
expect(yield* integrations.oauth.status({ integrationID, attemptID: attempt.attemptID })).toEqual({
status: "complete",
time: attempt.time,
})
@ -301,12 +306,12 @@ describe("Integration", () => {
}),
)
const attempt = yield* integrations.connection.oauth({ integrationID, methodID, inputs: {} })
const exit = yield* integrations.attempt
.complete({ attemptID: attempt.attemptID, code: "1234" })
const attempt = yield* integrations.oauth.connect({ integrationID, methodID, inputs: {} })
const exit = yield* integrations.oauth
.complete({ integrationID, attemptID: attempt.attemptID, code: "1234" })
.pipe(Effect.exit)
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",
message: "credential persistence failed",
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)))
yield* TestClock.adjust(Duration.minutes(10))
yield* Effect.yieldNow
expect(yield* integrations.attempt.status(attempt.attemptID)).toEqual({
expect(yield* integrations.oauth.status({ integrationID, attemptID: attempt.attemptID })).toEqual({
status: "expired",
time: attempt.time,
})

View file

@ -187,11 +187,11 @@ function resourceMcpLayer(url: string, onFormCreated?: (form: Form.Info) => Effe
active: unusedIntegration,
resolve: unusedIntegration,
key: unusedIntegration,
oauth: unusedIntegration,
update: unusedIntegration,
remove: unusedIntegration,
},
attempt: {
oauth: {
connect: unusedIntegration,
status: unusedIntegration,
complete: unusedIntegration,
cancel: unusedIntegration,

View file

@ -48,12 +48,12 @@ export function host(overrides: Overrides = {}): PluginContext {
get: () => Effect.die("unused integration.get"),
connect: {
key: () => Effect.die("unused integration.connect.key"),
oauth: () => Effect.die("unused integration.connect.oauth"),
},
attempt: {
status: () => Effect.die("unused integration.attempt.status"),
complete: () => Effect.die("unused integration.attempt.complete"),
cancel: () => Effect.die("unused integration.attempt.cancel"),
oauth: {
connect: () => Effect.die("unused integration.oauth.connect"),
status: () => Effect.die("unused integration.oauth.status"),
complete: () => Effect.die("unused integration.oauth.complete"),
cancel: () => Effect.die("unused integration.oauth.cancel"),
},
transform: () => Effect.die("unused integration.transform"),
reload: () => Effect.die("unused integration.reload"),
@ -193,12 +193,12 @@ export function integrationHost(integration: Integration.Interface): PluginConte
get: () => Effect.die("unused integration.get"),
connect: {
key: () => Effect.die("unused integration.connect.key"),
oauth: () => Effect.die("unused integration.connect.oauth"),
},
attempt: {
status: () => Effect.die("unused integration.attempt.status"),
complete: () => Effect.die("unused integration.attempt.complete"),
cancel: () => Effect.die("unused integration.attempt.cancel"),
oauth: {
connect: () => Effect.die("unused integration.oauth.connect"),
status: () => Effect.die("unused integration.oauth.status"),
complete: () => Effect.die("unused integration.oauth.complete"),
cancel: () => Effect.die("unused integration.oauth.cancel"),
},
reload: integration.reload,
connection: {

View file

@ -124,13 +124,17 @@ describe("OpencodePlugin", () => {
Effect.gen(function* () {
yield* addPlugin()
const integrations = yield* Integration.Service
const attempt = yield* integrations.connection.oauth({
integrationID: Integration.ID.make("opencode"),
const integrationID = Integration.ID.make("opencode")
const attempt = yield* integrations.oauth.connect({
integrationID,
methodID: Integration.MethodID.make("device"),
inputs: { server: `${server.url.origin}/console///?ignored=true#ignored` },
})
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/token")
@ -147,8 +151,8 @@ describe("OpencodePlugin", () => {
it.effect("rejects non-HTTP OpenCode servers", () =>
Effect.gen(function* () {
yield* addPlugin()
const error = yield* (yield* Integration.Service).connection
.oauth({
const error = yield* (yield* Integration.Service).oauth
.connect({
integrationID: Integration.ID.make("opencode"),
methodID: Integration.MethodID.make("device"),
inputs: { server: "ftp://console.example.com" },