runAiinConnectCliFlow function
Runs the full AIIN connect flow for CLI/desktop hosts:
- binds the loopback callback server (ephemeral port),
- initiates the OAuth proxy flow for
provider, - opens the system browser at the sign-in URL (
openBrowserFn), - catches the redirect, exchanges the code for JWTs,
- 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 does not intercept the `http://localhost`
/// redirect (it loads the callback server for real), 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 auth-session surfaces that resolve only when the
/// sheet CLOSES (iOS `ASWebAuthenticationSession`): a user
/// swipe-dismissal must short-circuit to the caller's fallback, not
/// leave dead air. Desktop browser launches resolve immediately, so
/// they must leave this off (default false).
bool cancelWhenOpenSettles = false,
}) async {
final server = AiinCallbackServer();
final redirectUri = await server.start(timeout: timeout);
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 callback = await _firstCallbackOrOpenError(
callbackFuture,
opened,
cancelWhenOpenSettles: cancelWhenOpenSettles,
);
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();
}
}