| 1 | import { |
| 2 | ProviderSetupError, |
| 3 | type ProviderAuthState, |
| 4 | type ProviderInstanceId, |
| 5 | } from "@t3tools/contracts"; |
| 6 | import * as Cause from "effect/Cause"; |
| 7 | import * as Clock from "effect/Clock"; |
| 8 | import * as Crypto from "effect/Crypto"; |
| 9 | import * as DateTime from "effect/DateTime"; |
| 10 | import * as Deferred from "effect/Deferred"; |
| 11 | import * as Effect from "effect/Effect"; |
| 12 | import * as Exit from "effect/Exit"; |
| 13 | import * as Fiber from "effect/Fiber"; |
| 14 | import * as Option from "effect/Option"; |
| 15 | import * as Schema from "effect/Schema"; |
| 16 | import * as Scope from "effect/Scope"; |
| 17 | import * as Semaphore from "effect/Semaphore"; |
| 18 | import * as Stream from "effect/Stream"; |
| 19 | import * as SubscriptionRef from "effect/SubscriptionRef"; |
| 20 | import * as AcpErrors from "effect-acp/errors"; |
| 21 | |
| 22 | import type * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime"; |
| 23 | import { |
| 24 | parseAntigravityAuthorizationUrl, |
| 25 | type AntigravityAuthorizationUrl, |
| 26 | } from "./antigravityAuthSupport.ts"; |
| 27 | import { |
| 28 | forwardAntigravityCallback, |
| 29 | validateAntigravityCallbackUrl, |
| 30 | } from "./antigravityCallback.ts"; |
| 31 | import type { ProviderAuthController } from "./ProviderAuthService.ts"; |
| 32 | |
| 33 | const AUTH_TIMEOUT_MS = 300_000; |
| 34 | const FORWARDING_FAILED_MESSAGE = "Could not deliver the sign-in response. Start sign-in again."; |
| 35 | const isSetupError = Schema.is(ProviderSetupError); |
| 36 | const isAcpRequestError = Schema.is(AcpErrors.AcpRequestError); |
| 37 | |
| 38 | interface AuthSnapshot { |
| 39 | readonly ownerSessionId: string | null; |
| 40 | readonly state: ProviderAuthState; |
| 41 | } |
| 42 | |
| 43 | interface AuthFlow { |
| 44 | readonly id: string; |
| 45 | readonly ownerSessionId: string; |
| 46 | readonly expiresAtMillis: number; |
| 47 | state: ProviderAuthState; |
| 48 | pending: AntigravityAuthorizationUrl | undefined; |
| 49 | callbackSent: boolean; |
| 50 | fiber: Fiber.Fiber<void> | undefined; |
| 51 | forwarding: Fiber.Fiber<void, ProviderSetupError> | undefined; |
| 52 | } |
| 53 | |
| 54 | interface OwnedProcess { |
| 55 | readonly stop: Effect.Effect<void>; |
| 56 | startup: Fiber.Fiber<unknown, unknown> | undefined; |
| 57 | } |
| 58 | |
| 59 | export interface AntigravityAuth { |
| 60 | readonly controller: ProviderAuthController; |
| 61 | /** Tracks startup and the process scope so sign-out cannot leave cached credentials in memory. */ |
| 62 | readonly withProcess: <A, E, R>( |
| 63 | stop: Effect.Effect<void>, |
| 64 | task: Effect.Effect<A, E, R>, |
| 65 | ) => Effect.Effect<A, E | ProviderSetupError, R | Scope.Scope>; |
| 66 | } |
| 67 | |
| 68 | export type AntigravityAuthRuntime = Pick< |
| 69 | AcpSessionRuntime.AcpSessionRuntime["Service"], |
| 70 | "initialize" | "start" | "request" |
| 71 | >; |
| 72 | |
| 73 | export interface AntigravityAuthOptions< |
| 74 | Runtime extends AntigravityAuthRuntime = AcpSessionRuntime.AcpSessionRuntime["Service"], |
| 75 | > { |
| 76 | readonly instanceId: ProviderInstanceId; |
| 77 | readonly makeRuntime: (input: { |
| 78 | readonly onAuthorizationUrl?: (url: string) => Effect.Effect<void, AcpErrors.AcpError>; |
| 79 | }) => Effect.Effect<Runtime, AcpErrors.AcpError | ProviderSetupError, Scope.Scope>; |
| 80 | readonly onAuthenticated: ( |
| 81 | result: AcpSessionRuntime.AcpSessionRuntimeStartResult, |
| 82 | runtime: Runtime, |
| 83 | ) => Effect.Effect<void>; |
| 84 | readonly onSignedOut: Effect.Effect<void>; |
| 85 | readonly forwardCallback?: (callback: URL) => Effect.Effect<void, ProviderSetupError>; |
| 86 | /** False for API key methods, which authenticate without a Google sign-in page. */ |
| 87 | readonly usesBrowser?: boolean; |
| 88 | } |
| 89 | |
| 90 | function visibleSnapshot(snapshot: AuthSnapshot, ownerSessionId: string): ProviderAuthState { |
| 91 | if (snapshot.ownerSessionId === null || snapshot.ownerSessionId === ownerSessionId) { |
| 92 | return snapshot.state; |
| 93 | } |
| 94 | const busy = ["starting", "waiting", "verifying"].includes(snapshot.state.phase); |
| 95 | return { |
| 96 | ...snapshot.state, |
| 97 | flowId: null, |
| 98 | authorizationUrl: null, |
| 99 | interaction: null, |
| 100 | expiresAt: null, |
| 101 | ...(busy ? { message: "Sign-in is in progress in another client." } : {}), |
| 102 | }; |
| 103 | } |
| 104 | |
| 105 | function safeAuthFailure(cause: Cause.Cause<unknown>, usesBrowser: boolean): string { |
| 106 | const error = Cause.findErrorOption(cause); |
| 107 | if (Option.isSome(error)) { |
| 108 | if (isSetupError(error.value)) { |
| 109 | return error.value.detail; |
| 110 | } |
| 111 | if (isAcpRequestError(error.value)) { |
| 112 | if (error.value.errorMessage.includes("SUBSCRIPTION_REQUIRED")) { |
| 113 | return "Google requires an eligible Antigravity subscription for this account."; |
| 114 | } |
| 115 | if (/access_denied|denied access|cancelled/i.test(error.value.errorMessage)) { |
| 116 | return "Google sign-in was not approved. Start sign-in again."; |
| 117 | } |
| 118 | if (error.value.method === "session/new" && error.value.code === -32603) { |
| 119 | return "Antigravity authenticated, but could not initialize a session or load models."; |
| 120 | } |
| 121 | if (!usesBrowser && error.value.code === -32602) { |
| 122 | return "Antigravity rejected the configured credentials. Check the provider settings."; |
| 123 | } |
| 124 | } |
| 125 | } |
| 126 | return usesBrowser |
| 127 | ? "Google sign-in failed. Start sign-in again." |
| 128 | : "Antigravity could not authenticate with the configured credentials."; |
| 129 | } |
| 130 | |
| 131 | /** Owns one instance's explicit sign-in and all process admission around sign-out. */ |
| 132 | export const makeAntigravityAuth = Effect.fn("makeAntigravityAuth")(function* < |
| 133 | Runtime extends AntigravityAuthRuntime, |
| 134 | >( |
| 135 | options: AntigravityAuthOptions<Runtime>, |
| 136 | ): Effect.fn.Return<AntigravityAuth, never, Crypto.Crypto | Scope.Scope> { |
| 137 | const crypto = yield* Crypto.Crypto; |
| 138 | const instanceScope = yield* Scope.Scope; |
| 139 | const usesBrowser = options.usesBrowser ?? true; |
| 140 | const lock = yield* Semaphore.make(1); |
| 141 | const closed = yield* Deferred.make<void>(); |
| 142 | const emptyState: ProviderAuthState = { |
| 143 | instanceId: options.instanceId, |
| 144 | phase: "idle", |
| 145 | flowId: null, |
| 146 | authorizationUrl: null, |
| 147 | expiresAt: null, |
| 148 | message: null, |
| 149 | }; |
| 150 | const snapshot = yield* SubscriptionRef.make<AuthSnapshot>({ |
| 151 | ownerSessionId: null, |
| 152 | state: emptyState, |
| 153 | }); |
| 154 | const processes = new Set<OwnedProcess>(); |
| 155 | let activeFlow: AuthFlow | undefined; |
| 156 | let operation: "idle" | "auth" | "logout" | "cancel" | "closed" = "idle"; |
| 157 | |
| 158 | const setupError = (name: string, detail: string) => |
| 159 | new ProviderSetupError({ instanceId: options.instanceId, operation: name, detail }); |
| 160 | const currentState = (ownerSessionId: string) => |
| 161 | SubscriptionRef.get(snapshot).pipe( |
| 162 | Effect.map((value) => visibleSnapshot(value, ownerSessionId)), |
| 163 | ); |
| 164 | const publishFlow = (flow: AuthFlow, state: ProviderAuthState) => { |
| 165 | flow.state = state; |
| 166 | return SubscriptionRef.set(snapshot, { ownerSessionId: flow.ownerSessionId, state }); |
| 167 | }; |
| 168 | const stopOwnedProcesses = Effect.suspend(() => |
| 169 | Effect.forEach( |
| 170 | Array.from(processes), |
| 171 | (owned) => |
| 172 | Effect.gen(function* () { |
| 173 | if (owned.startup) { |
| 174 | yield* Fiber.interrupt(owned.startup); |
| 175 | } |
| 176 | yield* owned.stop; |
| 177 | }), |
| 178 | { discard: true, concurrency: "unbounded" }, |
| 179 | ), |
| 180 | ); |
| 181 | |
| 182 | const withProcess: AntigravityAuth["withProcess"] = (stop, task) => |
| 183 | Effect.uninterruptibleMask((restore) => |
| 184 | Effect.gen(function* () { |
| 185 | const scope = yield* Scope.Scope; |
| 186 | const owned: OwnedProcess = { stop, startup: undefined }; |
| 187 | const fiber = yield* lock.withPermits(1)( |
| 188 | Effect.gen(function* () { |
| 189 | if (operation !== "idle") { |
| 190 | return yield* setupError( |
| 191 | "startProcess", |
| 192 | "Antigravity sign-in or sign-out is in progress. Try again after it finishes.", |
| 193 | ); |
| 194 | } |
| 195 | processes.add(owned); |
| 196 | yield* Scope.addFinalizer( |
| 197 | scope, |
| 198 | Effect.sync(() => { |
| 199 | processes.delete(owned); |
| 200 | }), |
| 201 | ); |
| 202 | const child = yield* restore(task).pipe(Effect.forkIn(scope)); |
| 203 | owned.startup = child; |
| 204 | return child; |
| 205 | }), |
| 206 | ); |
| 207 | // Propagate interruption after the exit wait so concurrent stop waiters stay attached. |
| 208 | return yield* restore(Fiber.await(fiber)).pipe( |
| 209 | Effect.flatMap((result) => result), |
| 210 | Effect.ensuring(Fiber.interrupt(fiber)), |
| 211 | Effect.ensuring( |
| 212 | Effect.sync(() => { |
| 213 | owned.startup = undefined; |
| 214 | }), |
| 215 | ), |
| 216 | ); |
| 217 | }), |
| 218 | ); |
| 219 | |
| 220 | const finishFlow = (flow: AuthFlow, result: Exit.Exit<void, unknown>) => |
| 221 | lock.withPermits(1)( |
| 222 | Effect.gen(function* () { |
| 223 | if (activeFlow !== flow) return; |
| 224 | activeFlow = undefined; |
| 225 | operation = "idle"; |
| 226 | flow.pending = undefined; |
| 227 | yield* publishFlow(flow, { |
| 228 | ...flow.state, |
| 229 | phase: Exit.isSuccess(result) ? "succeeded" : "failed", |
| 230 | authorizationUrl: null, |
| 231 | expiresAt: null, |
| 232 | message: Exit.isSuccess(result) |
| 233 | ? usesBrowser |
| 234 | ? "Signed in with Google." |
| 235 | : "Connected to Antigravity." |
| 236 | : safeAuthFailure(result.cause, usesBrowser), |
| 237 | }); |
| 238 | }), |
| 239 | ); |
| 240 | |
| 241 | const receiveAuthorizationUrl = (flow: AuthFlow, url: string) => |
| 242 | parseAntigravityAuthorizationUrl(url).pipe( |
| 243 | Effect.flatMap((authorization) => |
| 244 | lock.withPermits(1)( |
| 245 | Effect.gen(function* () { |
| 246 | if (activeFlow !== flow || operation !== "auth") return; |
| 247 | if (flow.pending) { |
| 248 | if (flow.pending.authorizationUrl === authorization.authorizationUrl) return; |
| 249 | return yield* new AcpErrors.AcpTransportError({ |
| 250 | detail: "Antigravity started more than one Google sign-in request.", |
| 251 | cause: undefined, |
| 252 | }); |
| 253 | } |
| 254 | flow.pending = authorization; |
| 255 | yield* publishFlow(flow, { |
| 256 | ...flow.state, |
| 257 | phase: "waiting", |
| 258 | authorizationUrl: authorization.authorizationUrl, |
| 259 | message: |
| 260 | "Open the Google sign-in link. If you are remote, paste the redirect URL here.", |
| 261 | }); |
| 262 | }), |
| 263 | ), |
| 264 | ), |
| 265 | ); |
| 266 | |
| 267 | const runSignIn = (flow: AuthFlow, stopSessions: Effect.Effect<void, ProviderSetupError>) => |
| 268 | Effect.gen(function* () { |
| 269 | yield* stopSessions.pipe(Effect.ensuring(stopOwnedProcesses)); |
| 270 | const runtime = yield* options.makeRuntime({ |
| 271 | onAuthorizationUrl: (url) => receiveAuthorizationUrl(flow, url), |
| 272 | }); |
| 273 | const started = yield* runtime.start(); |
| 274 | yield* lock.withPermits(1)( |
| 275 | Effect.gen(function* () { |
| 276 | if (activeFlow !== flow) return; |
| 277 | flow.pending = undefined; |
| 278 | yield* publishFlow(flow, { |
| 279 | ...flow.state, |
| 280 | phase: "verifying", |
| 281 | authorizationUrl: null, |
| 282 | message: "Checking Antigravity access and models.", |
| 283 | }); |
| 284 | }), |
| 285 | ); |
| 286 | yield* options.onAuthenticated(started, runtime); |
| 287 | }).pipe( |
| 288 | Effect.scoped, |
| 289 | Effect.timeoutOrElse({ |
| 290 | duration: AUTH_TIMEOUT_MS, |
| 291 | orElse: () => |
| 292 | Effect.fail(setupError("start", "Google sign-in expired. Start sign-in again.")), |
| 293 | }), |
| 294 | Effect.exit, |
| 295 | Effect.flatMap((result) => finishFlow(flow, result)), |
| 296 | ); |
| 297 | |
| 298 | const stopFlow = (flow: AuthFlow, phase: "cancelled" | "failed", message: string) => |
| 299 | Effect.uninterruptible( |
| 300 | Effect.gen(function* () { |
| 301 | const detached = yield* lock.withPermits(1)( |
| 302 | Effect.gen(function* () { |
| 303 | if (activeFlow !== flow) return false; |
| 304 | activeFlow = undefined; |
| 305 | operation = "cancel"; |
| 306 | flow.pending = undefined; |
| 307 | yield* publishFlow(flow, { |
| 308 | ...flow.state, |
| 309 | phase, |
| 310 | authorizationUrl: null, |
| 311 | expiresAt: null, |
| 312 | message, |
| 313 | }); |
| 314 | return true; |
| 315 | }), |
| 316 | ); |
| 317 | if (!detached) return; |
| 318 | if (flow.forwarding) yield* Fiber.interrupt(flow.forwarding); |
| 319 | if (flow.fiber) yield* Fiber.interrupt(flow.fiber); |
| 320 | yield* lock.withPermits(1)( |
| 321 | Effect.sync(() => { |
| 322 | if (operation === "cancel") operation = "idle"; |
| 323 | }), |
| 324 | ); |
| 325 | }), |
| 326 | ); |
| 327 | |
| 328 | const requireFlow = (ownerSessionId: string, flowId: string, name: string) => |
| 329 | Effect.gen(function* () { |
| 330 | const flow = activeFlow; |
| 331 | if (!flow || flow.id !== flowId || flow.ownerSessionId !== ownerSessionId) { |
| 332 | return yield* setupError(name, "This sign-in is no longer active in this client."); |
| 333 | } |
| 334 | const now = yield* Clock.currentTimeMillis; |
| 335 | if (now >= flow.expiresAtMillis) { |
| 336 | return yield* setupError(name, "Google sign-in expired. Start sign-in again."); |
| 337 | } |
| 338 | return flow; |
| 339 | }); |
| 340 | |
| 341 | const controller: ProviderAuthController = { |
| 342 | start: (ownerSessionId, stopSessions = Effect.void) => |
| 343 | lock.withPermits(1)( |
| 344 | Effect.uninterruptible( |
| 345 | Effect.gen(function* () { |
| 346 | if (activeFlow?.ownerSessionId === ownerSessionId && operation === "auth") { |
| 347 | return activeFlow.state; |
| 348 | } |
| 349 | if (operation !== "idle") { |
| 350 | return yield* setupError("start", "Antigravity setup is already in progress."); |
| 351 | } |
| 352 | const flowId = yield* crypto |
| 353 | .randomUUIDv4() |
| 354 | .pipe( |
| 355 | Effect.mapError(() => |
| 356 | setupError("start", "Could not start Google sign-in. Try again."), |
| 357 | ), |
| 358 | ); |
| 359 | const expiresAtMillis = (yield* Clock.currentTimeMillis) + AUTH_TIMEOUT_MS; |
| 360 | const state: ProviderAuthState = { |
| 361 | ...emptyState, |
| 362 | phase: "starting", |
| 363 | flowId, |
| 364 | expiresAt: DateTime.formatIso(DateTime.makeUnsafe(expiresAtMillis)), |
| 365 | message: usesBrowser ? "Starting Google sign-in." : "Checking credentials.", |
| 366 | }; |
| 367 | const flow: AuthFlow = { |
| 368 | id: flowId, |
| 369 | ownerSessionId, |
| 370 | expiresAtMillis, |
| 371 | state, |
| 372 | pending: undefined, |
| 373 | callbackSent: false, |
| 374 | fiber: undefined, |
| 375 | forwarding: undefined, |
| 376 | }; |
| 377 | activeFlow = flow; |
| 378 | operation = "auth"; |
| 379 | yield* publishFlow(flow, state); |
| 380 | flow.fiber = yield* runSignIn(flow, stopSessions).pipe( |
| 381 | Effect.interruptible, |
| 382 | Effect.forkIn(instanceScope), |
| 383 | ); |
| 384 | return state; |
| 385 | }), |
| 386 | ), |
| 387 | ), |
| 388 | complete: Effect.fn("AntigravityAuth.complete")(function* (ownerSessionId, input) { |
| 389 | const pending = yield* lock.withPermits(1)( |
| 390 | Effect.gen(function* () { |
| 391 | const flow = yield* requireFlow(ownerSessionId, input.flowId, "complete"); |
| 392 | if (!flow.pending || flow.callbackSent) { |
| 393 | return yield* setupError( |
| 394 | "complete", |
| 395 | flow.callbackSent |
| 396 | ? "The sign-in response was already sent. Wait for Google to finish." |
| 397 | : "Wait for the Google sign-in link before you send a redirect URL.", |
| 398 | ); |
| 399 | } |
| 400 | const callback = yield* validateAntigravityCallbackUrl( |
| 401 | options.instanceId, |
| 402 | flow.pending, |
| 403 | input.callbackUrl, |
| 404 | ); |
| 405 | flow.callbackSent = true; |
| 406 | yield* publishFlow(flow, { |
| 407 | ...flow.state, |
| 408 | phase: "verifying", |
| 409 | authorizationUrl: null, |
| 410 | message: "Waiting for Google to finish sign-in.", |
| 411 | }); |
| 412 | // The instance owns delivery and its failure handling. The RPC that |
| 413 | // sent the callback may disconnect before Google answers, and the |
| 414 | // flow must still settle instead of sitting at "verifying" until |
| 415 | // the deadline. |
| 416 | const forwarding = yield* ( |
| 417 | options.forwardCallback?.(callback) ?? |
| 418 | forwardAntigravityCallback(options.instanceId, callback) |
| 419 | ).pipe( |
| 420 | // stopFlow interrupts this fiber, so it runs from a sibling fiber. |
| 421 | Effect.tapError(() => |
| 422 | stopFlow(flow, "failed", FORWARDING_FAILED_MESSAGE).pipe( |
| 423 | Effect.forkIn(instanceScope), |
| 424 | ), |
| 425 | ), |
| 426 | Effect.interruptible, |
| 427 | Effect.forkIn(instanceScope), |
| 428 | ); |
| 429 | flow.forwarding = forwarding; |
| 430 | return { flow, forwarding }; |
| 431 | }), |
| 432 | ); |
| 433 | const forwarded = yield* Fiber.await(pending.forwarding); |
| 434 | if (Exit.isFailure(forwarded)) { |
| 435 | return yield* setupError("complete", FORWARDING_FAILED_MESSAGE); |
| 436 | } |
| 437 | return pending.flow.state; |
| 438 | }), |
| 439 | cancel: Effect.fn("AntigravityAuth.cancel")(function* (ownerSessionId, flowId) { |
| 440 | const flow = yield* lock.withPermits(1)(requireFlow(ownerSessionId, flowId, "cancel")); |
| 441 | yield* stopFlow(flow, "cancelled", "Google sign-in was cancelled."); |
| 442 | return flow.state; |
| 443 | }), |
| 444 | logout: Effect.fn("AntigravityAuth.logout")(function* (stopSessions) { |
| 445 | const task = Effect.uninterruptibleMask((restore) => |
| 446 | Effect.gen(function* () { |
| 447 | const flow = yield* lock.withPermits(1)( |
| 448 | Effect.gen(function* () { |
| 449 | if (operation !== "idle" && operation !== "auth") { |
| 450 | return yield* setupError("logout", "Antigravity setup is already stopping."); |
| 451 | } |
| 452 | operation = "logout"; |
| 453 | const currentFlow = activeFlow; |
| 454 | activeFlow = undefined; |
| 455 | if (currentFlow) { |
| 456 | currentFlow.pending = undefined; |
| 457 | yield* publishFlow(currentFlow, { |
| 458 | ...currentFlow.state, |
| 459 | phase: "cancelled", |
| 460 | authorizationUrl: null, |
| 461 | expiresAt: null, |
| 462 | message: "Google sign-in was cancelled by sign-out.", |
| 463 | }); |
| 464 | } |
| 465 | return currentFlow; |
| 466 | }), |
| 467 | ); |
| 468 | const stopRemaining = Effect.gen(function* () { |
| 469 | if (flow?.forwarding) yield* Fiber.interrupt(flow.forwarding); |
| 470 | if (flow?.fiber) yield* Fiber.interrupt(flow.fiber); |
| 471 | yield* stopOwnedProcesses; |
| 472 | }); |
| 473 | const result = yield* restore( |
| 474 | Effect.gen(function* () { |
| 475 | yield* stopSessions.pipe(Effect.ensuring(stopRemaining)); |
| 476 | const runtime = yield* options.makeRuntime({}); |
| 477 | const initialized = yield* runtime.initialize(); |
| 478 | if (!initialized.agentCapabilities?.auth?.logout) { |
| 479 | return yield* setupError( |
| 480 | "logout", |
| 481 | "This Antigravity version does not support sign-out. Update the provider.", |
| 482 | ); |
| 483 | } |
| 484 | yield* runtime.request("logout", {}); |
| 485 | yield* options.onSignedOut; |
| 486 | }).pipe( |
| 487 | Effect.scoped, |
| 488 | Effect.timeoutOrElse({ |
| 489 | duration: "90 seconds", |
| 490 | orElse: () => Effect.fail(setupError("logout", "Antigravity sign-out timed out.")), |
| 491 | }), |
| 492 | ), |
| 493 | ).pipe(Effect.exit); |
| 494 | yield* lock.withPermits(1)( |
| 495 | Effect.gen(function* () { |
| 496 | operation = "idle"; |
| 497 | yield* SubscriptionRef.set(snapshot, { |
| 498 | ownerSessionId: null, |
| 499 | state: { |
| 500 | ...emptyState, |
| 501 | phase: Exit.isSuccess(result) ? "idle" : "failed", |
| 502 | message: Exit.isSuccess(result) |
| 503 | ? "Signed out of Google." |
| 504 | : "Antigravity sign-out failed. Try again.", |
| 505 | }, |
| 506 | }); |
| 507 | }), |
| 508 | ); |
| 509 | if (Exit.isFailure(result)) { |
| 510 | const failure = Cause.findErrorOption(result.cause); |
| 511 | return yield* Option.isSome(failure) && isSetupError(failure.value) |
| 512 | ? failure.value |
| 513 | : setupError("logout", "Antigravity sign-out failed. Try again."); |
| 514 | } |
| 515 | return yield* currentState(""); |
| 516 | }), |
| 517 | ); |
| 518 | const worker = yield* task.pipe(Effect.forkIn(instanceScope)); |
| 519 | return yield* Fiber.await(worker).pipe(Effect.flatMap((result) => result)); |
| 520 | }), |
| 521 | subscribe: (ownerSessionId) => |
| 522 | SubscriptionRef.changes(snapshot).pipe( |
| 523 | Stream.map((value) => { |
| 524 | const state = visibleSnapshot(value, ownerSessionId); |
| 525 | return { |
| 526 | ...state, |
| 527 | credentialOwner: "provider" as const, |
| 528 | interaction: |
| 529 | state.phase === "waiting" && state.authorizationUrl && state.flowId |
| 530 | ? { |
| 531 | type: "browser" as const, |
| 532 | id: state.flowId, |
| 533 | url: state.authorizationUrl, |
| 534 | requiresConsent: false, |
| 535 | acceptsCallback: true, |
| 536 | } |
| 537 | : null, |
| 538 | }; |
| 539 | }), |
| 540 | Stream.interruptWhen(Deferred.await(closed)), |
| 541 | ), |
| 542 | isLogoutPrompt: (text, hasAttachments) => !hasAttachments && text.trim() === "/logout", |
| 543 | }; |
| 544 | |
| 545 | yield* Effect.addFinalizer(() => |
| 546 | Effect.gen(function* () { |
| 547 | operation = "closed"; |
| 548 | const flow = activeFlow; |
| 549 | activeFlow = undefined; |
| 550 | if (flow) { |
| 551 | flow.pending = undefined; |
| 552 | if (flow.forwarding) yield* Fiber.interrupt(flow.forwarding); |
| 553 | if (flow.fiber) yield* Fiber.interrupt(flow.fiber); |
| 554 | } |
| 555 | yield* stopOwnedProcesses; |
| 556 | yield* Deferred.succeed(closed, undefined); |
| 557 | }), |
| 558 | ); |
| 559 | |
| 560 | return { controller, withProcess }; |
| 561 | }); |