runAiinConnectCliFlow function
Future<AiinConnectResult?>
runAiinConnectCliFlow({
- required void onStatus(),
- Future<
bool> openBrowserFn() = openBrowser, - Client? client,
- String authBaseUrl = aiinAuthBaseUrl,
- Duration timeout = const Duration(minutes: 5),
- void onCallback()?,
- bool cancelWhenOpenSettles = false,
- String callbackHost = '127.0.0.1',
- Future<
String?> interceptedCallback()?,
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 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();
}
}