Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/core/src/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export type Inputs = Integration.Inputs
export type OAuthAuthorization = {
readonly url: string
readonly instructions: string
readonly expiresAt?: number
} & (
| {
readonly mode: "auto"
Expand Down Expand Up @@ -560,7 +561,7 @@ const layer = Layer.effect(
)
const id = AttemptID.create()
const created = yield* Clock.currentTimeMillis
const time = { created, expires: created + attemptLifetime }
const time = { created, expires: authorization.expiresAt ?? created + attemptLifetime }
yield* SynchronizedRef.update(attempts, (current) =>
new Map(current).set(id, {
status: "pending",
Expand Down
20 changes: 14 additions & 6 deletions packages/core/src/plugin/provider/xai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,20 @@ const device = {
},
Device,
).pipe(
Effect.map((value) => ({
mode: "auto" as const,
url: value.verification_uri_complete ?? value.verification_uri,
instructions: `Open ${value.verification_uri} on any device and enter code: ${value.user_code}`,
callback: poll(value).pipe(Effect.flatMap((tokens) => credential(deviceMethodID, tokens))),
})),
Effect.flatMap((value) =>
Clock.currentTimeMillis.pipe(
Effect.map((created) => {
const lifetime = positiveSeconds(value.expires_in, 0)
return {
mode: "auto" as const,
url: value.verification_uri_complete ?? value.verification_uri,
instructions: `Open ${value.verification_uri} on any device and enter code: ${value.user_code}`,
...(lifetime ? { expiresAt: created + lifetime * 1000 } : {}),
callback: poll(value).pipe(Effect.flatMap((tokens) => credential(deviceMethodID, tokens))),
}
}),
),
),
),
refresh: (value) => refresh(deviceMethodID, Credential.OAuth.make({ ...value, methodID: deviceMethodID })),
} satisfies IntegrationOAuthMethodRegistration
Expand Down
37 changes: 36 additions & 1 deletion packages/core/test/integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect } from "bun:test"
import { Cause, Duration, Effect, Exit, Fiber, Layer, Scope, Stream } from "effect"
import { Cause, Clock, Duration, Effect, Exit, Fiber, Layer, Scope, Stream } from "effect"
import * as TestClock from "effect/testing/TestClock"
import { Credential } from "@opencode-ai/core/credential"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
Expand Down Expand Up @@ -414,6 +414,41 @@ describe("Integration", () => {
}),
)

it.effect("uses provider-defined OAuth attempt expirations", () =>
Effect.gen(function* () {
const integrations = yield* Integration.Service
const integrationID = Integration.ID.make("openai")
const created = yield* Clock.currentTimeMillis
const expirations = [
created + Duration.toMillis(Duration.minutes(5)),
created + Duration.toMillis(Duration.minutes(20)),
]

yield* Effect.forEach(expirations, (expiresAt, index) => {
const methodID = Integration.MethodID.make(`browser-${index}`)
return Effect.gen(function* () {
yield* integrations.transform((editor) =>
editor.method.update({
integrationID,
method: { id: methodID, type: "oauth", label: "Browser" },
authorize: () =>
Effect.succeed({
mode: "auto" as const,
url: "https://example.com/authorize",
instructions: "Sign in",
expiresAt,
callback: Effect.never,
}),
}),
)

const attempt = yield* integrations.oauth.connect({ integrationID, methodID, inputs: {} })
expect(attempt.time).toEqual({ created, expires: expiresAt })
})
})
}),
)

it.effect("projects credential and env connections", () => {
const integrationID = Integration.ID.make("acme")
return Effect.acquireUseRelease(
Expand Down
1 change: 1 addition & 0 deletions packages/plugin/src/v2/effect/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type { Transform } from "./registration.js"
export type IntegrationOAuthAuthorization = {
readonly url: string
readonly instructions: string
readonly expiresAt?: number
} & (
| {
readonly mode: "auto"
Expand Down
Loading