runAiinConnectCliFlow function

Future<AiinConnectResult?> runAiinConnectCliFlow({
  1. required void onStatus(
    1. String
    ),
  2. Future<bool> openBrowserFn(
    1. String
    ) = openBrowser,
  3. Client? client,
  4. String authBaseUrl = aiinAuthBaseUrl,
  5. Duration timeout = const Duration(minutes: 5),
  6. void onCallback()?,
  7. bool cancelWhenOpenSettles = false,
  8. String callbackHost = '127.0.0.1',
  9. Future<String?> interceptedCallback()?,
})

Runs the full AIIN connect flow for CLI/desktop hosts:

  1. binds the loopback callback server (ephemeral port),
  2. initiates the OAuth proxy flow for provider,
  3. opens the system browser at the sign-in URL (openBrowserFn),
  4. catches the redirect, exchanges the code for JWTs,
  5. registers an API key with the access JWT.

Returns null on timeout/cancel or a reported service error (status is printed through onStatus); throws AiinAuthException never — service failures surface through onStatus so callers can treat null as "not connected".

Implementation

Future<AiinConnectResult?> runAiinConnectCliFlow({
  required void Function(String) onStatus,
  Future<bool> Function(String) openBrowserFn = openBrowser,
  http.Client? client,
  String authBaseUrl = aiinAuthBaseUrl,
  Duration timeout = const Duration(minutes: 5),

  /// Called once the callback wait settles — when the callback lands OR
  /// the timeout/cancel path gives up — before the exchange/return. The
  /// mobile auth-session sheet dismisses itself here (a stale sheet
  /// closes too): the session intercepts the `http://` redirect
  /// (`callbackScheme: 'http'`, gh-1044 AC9) and hands the callback URL
  /// back through [interceptedCallback] instead of navigating it, so
  /// the sheet must be closed programmatically to hand the user back to
  /// the app. Optional — desktop callers skip it.
  void Function()? onCallback,

  /// Treats a successful open completion before any callback as a user
  /// cancel — throws [AiinSurfaceClosedException] — instead of waiting
  /// out [timeout]. For surfaces that resolve without a completion value
  /// (an external browser launch): when the launch future settles with no
  /// callback and no [interceptedCallback] channel, a user abandonment
  /// must short-circuit, not leave dead air. When [interceptedCallback]
  /// IS present the sheet's resolution rides it alone (its null IS the
  /// cancel) and this flag is ignored — the two signals come from the
  /// same resolution, and the open-settle path would only race the
  /// callback URL's delivery. Desktop callers leave this off (default
  /// false).
  bool cancelWhenOpenSettles = false,

  /// The host the callback URL advertises: the literal `127.0.0.1` on
  /// every surface (scheme interception ignores the host; the fallback
  /// leg needs an address that reaches the IPv4 bind without resolver
  /// ambiguity) — other values only for tests.
  String callbackHost = '127.0.0.1',

  /// The auth-session surface's completion value (gh-1044 AC9): resolves
  /// with the callback URL the native scheme interception caught
  /// (`callbackScheme: 'http'`), or null when the sheet closed without
  /// one (user cancel). When it delivers a URL the flow settles from it
  /// directly — completion no longer depends on the redirect loading the
  /// loopback server inside the sheet. The loopback server stays armed as
  /// the fallback leg (older surfaces still navigate the redirect for
  /// real); whichever leg lands first wins the race. This future is the
  /// sheet's single completion channel — the
  /// [cancelWhenOpenSettles] open-settle cancel does not apply while it
  /// exists.
  Future<String?> Function()? interceptedCallback,
}) async {
  final server = AiinCallbackServer();
  final redirectUri = await server.start(
    timeout: timeout,
    callbackHost: callbackHost,
  );
  try {
    // The hosted sign-in page: AIIN lists every provider, runs the whole
    // round-trip (silent for an existing session) and redirects back with
    // the code + our state.
    final state = aiinGenerateState();
    final loginUrl = buildAiinLoginUrl(
      redirectUri: redirectUri,
      state: state,
      // `desktop` is the client_type whose redirect shape is an arbitrary
      // localhost loopback URI — the mobile apps use the same shape. There
      // is no `mobile` value in AIIN's contract (the web build sends
      // `web`), so desktop parity is deliberate here.
      clientType: 'desktop',
      authBaseUrl: authBaseUrl,
    );
    onStatus('listening for the AIIN callback on $redirectUri');
    // Arm the callback wait and the browser surface CONCURRENTLY: the
    // mobile auth session resolves its open future only when the sheet
    // CLOSES, and the sheet is dismissed through [onCallback] — awaiting
    // the open first would deadlock the mobile flow. Open FAILURES (the
    // session cannot start) surface promptly through the race instead of
    // stalling until the callback timeout.
    final callbackFuture = server.waitForCallback();
    final opened = _openAiinBrowser(
      loginUrl.toString(),
      openBrowserFn,
      onStatus,
    );
    final intercepted = interceptedCallback?.call();
    AiinCallback? callback;
    try {
      final (won, source) = await _firstCallbackOrOpenError(
        callbackFuture,
        opened,
        cancelWhenOpenSettles: cancelWhenOpenSettles,
        intercepted: intercepted,
      );
      callback = won;
      // gh-1044 AC2: the winning leg is answerable from the log alone —
      // interception (the fixed path) vs a real loopback hit (the
      // fallback leg) discriminates F2 from F1 without a device debugger.
      // A null callback (timeout) keeps _settleAiinCallback's message.
      if (callback != null) {
        onStatus(switch (source) {
          _AiinCallbackSource.interceptedRedirect =>
            'AIIN redirect intercepted by the sign-in sheet (callback URL '
                'returned to the flow)',
          _AiinCallbackSource.loopbackServer =>
            'AIIN callback landed on the loopback server',
        });
      }
    } on AiinSurfaceClosedException {
      onStatus(
        'the sign-in sheet closed without completing the sign-in '
        '(no callback returned — user cancel)',
      );
      rethrow;
    }
    onCallback?.call();
    try {
      await opened;
    } on Object {
      // Late open failure after the callback won: the flow is settling
      // and the open surface is already gone (e.g. the native side fails
      // the pending session when the dismissal tears it down). Swallow —
      // the landed callback must always settle the flow.
    }
    return await _settleAiinCallback(
      callback,
      state,
      client: client,
      authBaseUrl: authBaseUrl,
      onStatus: onStatus,
    );
  } on AiinAuthException catch (error) {
    onStatus('AIIN sign-in failed: ${error.message}');
    return null;
  } finally {
    await server.close();
  }
}