diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 62d637670297..88b947d10dfc 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -70,6 +70,7 @@ export type Inputs = Integration.Inputs export type OAuthAuthorization = { readonly url: string readonly instructions: string + readonly expiresAt?: number } & ( | { readonly mode: "auto" @@ -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", diff --git a/packages/core/src/plugin/provider/xai.ts b/packages/core/src/plugin/provider/xai.ts index 76e7968ebb1b..617bb0f87181 100644 --- a/packages/core/src/plugin/provider/xai.ts +++ b/packages/core/src/plugin/provider/xai.ts @@ -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 diff --git a/packages/core/test/integration.test.ts b/packages/core/test/integration.test.ts index 42d1ab50d3ab..42934bd3e5e7 100644 --- a/packages/core/test/integration.test.ts +++ b/packages/core/test/integration.test.ts @@ -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" @@ -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( diff --git a/packages/plugin/src/v2/effect/integration.ts b/packages/plugin/src/v2/effect/integration.ts index 12bb71945ebb..2cbfe24612ac 100644 --- a/packages/plugin/src/v2/effect/integration.ts +++ b/packages/plugin/src/v2/effect/integration.ts @@ -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"