Ver código fonte

fix(core): honor OAuth attempt expiration (#37557)

Aiden Cline 1 mês atrás
pai
commit
987242b3e8

+ 2 - 1
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",

+ 14 - 6
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

+ 36 - 1
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(

+ 1 - 0
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"