const std = @import("std");const config_runtime = @import("../config/config_runtime.zig");const debug_trace = @import("../shared/debug_trace.zig");const host = @import("../hosts/host.zig");const runtime_profile = @import("../hosts/runtime_profile.zig");const io_mod = @import("../shared/io.zig");const credentials = @import("../auth/credentials.zig");const auth_runtime = @import("../auth/auth_runtime.zig");const login_flow = @import("../auth/login_flow.zig");const types = @import("../shared/types.zig"); fn oauthAuthEnabled(comptime App: type) bool { return runtime_profile.allows(App, .native_auth) or runtime_profile.allows(App, .js_host_auth);} pub fn Runtime(comptime App: type) type { return struct { fn ensurePromptCredential(app: *App) !bool { if (app.auth.credentialSource() != null) return true; const auth_view = app.auth.view(); if (auth_view.onboarding_skipped) { try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = credentials.missing_interactive_credential_message, }, true); } else if (!app.auth.pickerView().active) { try app.auth.refreshSourceInventory(app.alloc); app.auth.openOnboardingPicker(app.alloc); } app.shell.render_requests.request(.footer); return false; } pub fn runLoginCommand(app: *App) !void { if (comptime !oauthAuthEnabled(App)) { try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = "Set FX_API_KEY through createFxTerminal() to authenticate this WASM session.", }, true); return; } try beginSignIn(app, false); } pub fn runLogoutCommand(app: *App) !void { if (comptime !oauthAuthEnabled(App)) { try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = "Authentication is owned by the embedding SDK for this WASM session.", }, true); return; } try app.flushBeforeBlockingExternalWork(); const result = login_flow.logout(app.alloc, app.auth.oauthTransport()) catch |err| switch (err) { error.SessionDeleteFailed => { try writeAuthNotice(app, .{ .topic = "auth", .tone = .@"error", .body = "Could not complete fx logout. The current source is unchanged.", }); return; }, }; try applyLogoutResult(app, result); } pub fn openSetupHub(app: *App) !void { if (comptime !runtime_profile.allows(App, .native_auth)) { try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = "API key setup is unavailable in this WASM session.", }, true); return; } try app.auth.refreshSourceInventory(app.alloc); app.auth.openPicker(app.alloc); app.shell.render_requests.request(.footer); } fn applyLogoutResult(app: *App, result: login_flow.LogoutResult) !void { // Logging out is an explicit rejection of that credential, so a // remembered pointer to it would silently reactivate on next login. // A remembered source always wins resolution, so an active fx login // is the only way one can be remembered; clearing otherwise is a // no-op against a store that holds nothing. if (app.auth.credentialSource() == .fx_login) forgetCredentialSource(app); applyCredentialChange(app, try app.auth.reconcileAfterFxLoginLogout(app.alloc)); try writeAuthNotice(app, if (result.local_durability_failed) .{ .topic = "auth", .tone = .warning, .body = "Could not confirm durable fx logout. The active source was recalculated.", } else if (result.session_deleted) .{ .topic = "auth", .tone = .neutral, .body = "Signed out of fx.", } else .{ .topic = "auth", .tone = .neutral, .body = "No fx login session found.", }); if (result.remote_revocation_failed) { try writeAuthNotice(app, .{ .topic = "auth", .tone = .warning, .body = login_flow.remote_revocation_warning, }); } } pub fn applyPickerChoice(app: *App, choice: auth_runtime.Choice) !void { if (comptime !oauthAuthEnabled(App)) { try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = "Browser authentication is supplied by the embedding SDK.", }, true); return; } switch (choice) { .source => |source| try applySourceChoice(app, source), .action => |action| switch (action) { .login => try beginSignIn(app, true), .setup => { if (comptime !runtime_profile.allows(App, .native_auth)) { try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = "API key setup is unavailable in this WASM session.", }, true); return; } prepareApiKeyInputBoundary(app); app.auth.openApiKeyPickerFromRoot(app.alloc); }, .change_team => try beginTeamPicker(app), .switch_credential => app.auth.openSwitchCredentialPicker(app.alloc), .automatic => try applyAutomaticCredential(app), }, .team => |index| try applyTeamChoice(app, index), } } pub fn routeAuthPickerByte(app: *App, byte: u8) !bool { if (app.auth.signInEntryActive()) { switch (byte) { 3, 4 => _ = app.auth.popPickerStage(app.alloc), '\r', '\n' => try openSignInBrowser(app), else => {}, } app.shell.render_requests.request(.footer); return true; } if (app.auth.teamPickerActive()) { const consumed = switch (byte) { 8, 127 => app.auth.deleteTeamQueryByte(), else => try app.auth.appendTeamQueryByte(app.alloc, byte), }; if (consumed) { app.shell.render_requests.request(.footer); return true; } } if (!app.auth.apiKeyEntryActive()) return false; switch (byte) { 3, 4 => _ = app.auth.popPickerStage(app.alloc), '\r', '\n' => try submitApiKeyEntry(app), 8, 127 => _ = app.auth.deleteApiKeyByte(), else => _ = try app.auth.appendApiKeyByte(app.alloc, byte), } app.shell.render_requests.request(.footer); return true; } pub fn routeAuthPickerEscapeAction(app: *App, action: anytype) bool { if (!app.auth.signInEntryActive() and !app.auth.apiKeyEntryActive()) return false; return switch (action) { .escape, .remapped_byte => false, else => true, }; } pub fn collectSignInFacts(app: *App) !void { if (comptime !oauthAuthEnabled(App)) return; app.auth.pulseSignIn(app.alloc); switch (app.auth.pollSignInTransition(app.alloc)) { .none => {}, .cancelled => app.shell.render_requests.request(.footer), .failed => |err| { debug_trace.logf("auth", "login failed err={s}", .{@errorName(err)}); _ = app.auth.popPickerStage(app.alloc); try writeLoginError(app, err); }, .succeeded => |completed| { var selection = completed; defer selection.deinit(app.alloc); if (!try selectCredentialSource(app, .fx_login)) { _ = app.auth.popPickerStage(app.alloc); try writeAuthNotice(app, .{ .topic = "auth", .tone = .@"error", .body = "Signed in, but the fx login credential could not be loaded.", }); return; } rememberCredentialSource(app, .fx_login); if (selection.teams.items.len > 0) { app.auth.openTeamPicker(app.alloc, &selection); } else { _ = app.auth.popPickerStage(app.alloc); } try writeAuthNotice(app, .{ .topic = "auth", .tone = .neutral, .body = "Signed in to Vercel.", }); }, } } fn prepareApiKeyInputBoundary(app: *App) void { if (comptime @hasDecl(App, "prepareApiKeyInputBoundary")) { app.prepareApiKeyInputBoundary(); } } fn submitApiKeyEntry(app: *App) !void { if (app.auth.pickerView().api_key_mask_count == 0) return; switch (app.auth.beginApiKeySave(app.alloc)) { .started => app.shell.render_requests.request(.footer), .empty => {}, .busy => try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = "Still saving the previous API key. Nothing was stored for this one; try again in a moment.", }, true), } } /// Polled from the event loop so a save that blocks on a locked key store /// or a slow gateway never stalls rendering. pub fn collectApiKeySaveFacts(app: *App) !void { const result = app.auth.takeApiKeySaveResult(app.alloc) orelse return; try applyApiKeySaveResult(app, result); } fn applyApiKeySaveResult(app: *App, result: auth_runtime.ApiKeySaveResult) !void { switch (result) { .empty => return, .saved => |changed| { applyCredentialChange(app, changed); rememberCredentialSource(app, .stored_key); const body = try std.fmt.allocPrint( app.alloc, "Saved the API key to {s} and made it active.", .{credentials.stored_key_backend_label}, ); defer app.alloc.free(body); try app.writeDomainNotice(.{ .topic = "auth", .tone = .neutral, .body = body, }, true); }, .gateway_refused => try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = "The AI Gateway refused that API key. Nothing was stored.", }, true), .gateway_unavailable => try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = "Could not verify that API key with AI Gateway. Nothing was stored.", }, true), .store_failed => { const body = try std.fmt.allocPrint( app.alloc, "Could not save the API key to {s}. Nothing was stored.", .{credentials.stored_key_backend_label}, ); defer app.alloc.free(body); try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = body, }, true); }, .reload_failed => { const body = try std.fmt.allocPrint( app.alloc, "Saved the API key to {s}, but could not make it active.", .{credentials.stored_key_backend_label}, ); defer app.alloc.free(body); try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = body, }, true); }, } } /// Clearing the remembered choice must also re-resolve, otherwise the /// session would keep running on a source precedence no longer selects. fn applyAutomaticCredential(app: *App) !void { forgetCredentialSource(app); app.auth.closePicker(app.alloc); applyCredentialChange(app, try app.auth.reselectByPrecedence(app.alloc)); try app.writeDomainNotice(.{ .topic = "auth", .tone = .neutral, .body = "Using automatic credential precedence again.", }, true); } fn forgetCredentialSource(app: *App) void { var attempt = config_runtime.attemptUserPreferences( app.alloc, .{ .clear_credential_source = true }, ); defer attempt.deinit(app.alloc); switch (attempt) { .outcome => debug_trace.logf("auth", "credential choice cleared", .{}), .failure => |failure| debug_trace.logf( "auth", "credential choice not cleared err={s}", .{@errorName(failure.err)}, ), } } fn applySourceChoice(app: *App, source: credentials.Source) !void { const body = try std.fmt.allocPrint( app.alloc, "Switched credential to {s}.", .{credentials.sourceLabel(source)}, ); defer app.alloc.free(body); if (!try selectCredentialSource(app, source)) { try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = "That credential is no longer available. The current source is unchanged.", }, true); return; } rememberCredentialSource(app, source); try app.writeDomainNotice(.{ .topic = "auth", .tone = .neutral, .body = body, }, true); } /// An explicit source choice outlives the session. Failing to persist /// leaves the source active for this run rather than refusing a working /// credential the user already selected. fn rememberCredentialSource(app: *App, source: credentials.Source) void { if (comptime @hasDecl(App, "persistCredentialSourcePreference")) { app.persistCredentialSourcePreference(source); return; } var attempt = config_runtime.attemptUserPreferences( app.alloc, .{ .credential_source = source }, ); defer attempt.deinit(app.alloc); switch (attempt) { .outcome => debug_trace.logf("auth", "credential choice persisted source={t}", .{source}), .failure => |failure| debug_trace.logf( "auth", "credential choice not persisted source={t} err={s}", .{ source, @errorName(failure.err) }, ), } } fn beginTeamPicker(app: *App) !void { if (!app.auth.pickerView().fx_login_session_available) return; try app.flushBeforeBlockingExternalWork(); var selection = login_flow.loadTeamSelection(app.alloc, app.auth.oauthTransport()) catch |err| { debug_trace.logf("auth", "team picker load failed err={s}", .{@errorName(err)}); try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = switch (err) { error.NoSession => "The fx login session is no longer available. Sign in to change teams.", error.NoTeams => "No Vercel teams are available for this account.", else => "Could not load Vercel teams. The current team is unchanged.", }, }, true); return; }; defer selection.deinit(app.alloc); app.auth.openTeamPicker(app.alloc, &selection); app.shell.render_requests.request(.footer); } fn applyTeamChoice(app: *App, index: usize) !void { const selection = app.auth.teamSelection() orelse return; if (index >= selection.teams.items.len) return; const team = selection.teams.items[index]; const body = try std.fmt.allocPrint( app.alloc, "Changed Vercel team to {s} ({s}).", .{ team.name, team.slug }, ); defer app.alloc.free(body); var selected_team = selection.select(app.alloc, index) catch |err| { debug_trace.logf("auth", "team change failed err={s}", .{@errorName(err)}); app.auth.closePicker(app.alloc); try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = switch (err) { error.SessionChanged, error.NoSession => "The fx login session changed before the team could be saved.", else => "Could not change the Vercel team. The current team is unchanged.", }, }, true); return; }; defer selected_team.deinit(app.alloc); if (app.auth.credentialSource() == .fx_login) { applyCredentialChange(app, app.auth.adoptSelectedTeam(app.alloc, &selected_team)); } else if (!try selectCredentialSource(app, .fx_login)) { app.auth.closePicker(app.alloc); try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = "Changed the Vercel team, but the fx login credential could not be loaded.", }, true); return; } rememberCredentialSource(app, .fx_login); app.auth.closePicker(app.alloc); try app.writeDomainNotice(.{ .topic = "auth", .tone = .neutral, .body = body, }, true); } fn beginSignIn(app: *App, from_root: bool) !void { try app.flushBeforeBlockingExternalWork(); const started = if (from_root) app.auth.openSignInPickerFromRoot(app.alloc) else app.auth.openSignInPicker(app.alloc); if (started catch |err| { debug_trace.logf("auth", "login failed err={s}", .{@errorName(err)}); try writeLoginError(app, err); return; }) { app.shell.render_requests.request(.footer); // Open the browser as soon as the device code is ready instead of // waiting for Enter; Enter stays as a manual re-open, and // FX_NO_OPEN_BROWSER opts out for headless/SSH sessions. if (io_mod.getenv("FX_NO_OPEN_BROWSER") == null) try openSignInBrowser(app); } } fn openSignInBrowser(app: *App) !void { const url = (try app.auth.signInBrowserUrlAlloc(app.alloc)) orelse return; defer app.alloc.free(url); if (!try app.urlOpener().open(app.alloc, url)) { debug_trace.logf("auth", "login browser launcher failed", .{}); } } pub fn selectCredentialSource(app: *App, source: credentials.Source) !bool { const changed = (try app.auth.selectSource(app.alloc, source)) orelse return false; applyCredentialChange(app, changed); return true; } fn refreshFxLoginCredentialIfNeeded(app: *App) !void { if (!try app.auth.refreshFxLoginIfNeeded(app.alloc)) return; reconcileGatewayCredential(app); if (app.auth.modelCatalogAccess().authorizationCredential() == null) return; app.model_cache.reset();Lines 1–500 of 1217. Load more
const std = @import("std"); const config_runtime = @import("../config/config_runtime.zig"); const debug_trace = @import("../shared/debug_trace.zig"); const host = @import("../hosts/host.zig"); const runtime_profile = @import("../hosts/runtime_profile.zig"); const io_mod = @import("../shared/io.zig"); const credentials = @import("../auth/credentials.zig"); const auth_runtime = @import("../auth/auth_runtime.zig"); const login_flow = @import("../auth/login_flow.zig"); const types = @import("../shared/types.zig"); fn oauthAuthEnabled(comptime App: type) bool { return runtime_profile.allows(App, .native_auth) or runtime_profile.allows(App, .js_host_auth); } pub fn Runtime(comptime App: type) type { return struct { fn ensurePromptCredential(app: *App) !bool { if (app.auth.credentialSource() != null) return true; const auth_view = app.auth.view(); if (auth_view.onboarding_skipped) { try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = credentials.missing_interactive_credential_message, }, true); } else if (!app.auth.pickerView().active) { try app.auth.refreshSourceInventory(app.alloc); app.auth.openOnboardingPicker(app.alloc); } app.shell.render_requests.request(.footer); return false; } pub fn runLoginCommand(app: *App) !void { if (comptime !oauthAuthEnabled(App)) { try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = "Set FX_API_KEY through createFxTerminal() to authenticate this WASM session.", }, true); return; } try beginSignIn(app, false); } pub fn runLogoutCommand(app: *App) !void { if (comptime !oauthAuthEnabled(App)) { try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = "Authentication is owned by the embedding SDK for this WASM session.", }, true); return; } try app.flushBeforeBlockingExternalWork(); const result = login_flow.logout(app.alloc, app.auth.oauthTransport()) catch |err| switch (err) { error.SessionDeleteFailed => { try writeAuthNotice(app, .{ .topic = "auth", .tone = .@"error", .body = "Could not complete fx logout. The current source is unchanged.", }); return; }, }; try applyLogoutResult(app, result); } pub fn openSetupHub(app: *App) !void { if (comptime !runtime_profile.allows(App, .native_auth)) { try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = "API key setup is unavailable in this WASM session.", }, true); return; } try app.auth.refreshSourceInventory(app.alloc); app.auth.openPicker(app.alloc); app.shell.render_requests.request(.footer); } fn applyLogoutResult(app: *App, result: login_flow.LogoutResult) !void { // Logging out is an explicit rejection of that credential, so a // remembered pointer to it would silently reactivate on next login. // A remembered source always wins resolution, so an active fx login // is the only way one can be remembered; clearing otherwise is a // no-op against a store that holds nothing. if (app.auth.credentialSource() == .fx_login) forgetCredentialSource(app); applyCredentialChange(app, try app.auth.reconcileAfterFxLoginLogout(app.alloc)); try writeAuthNotice(app, if (result.local_durability_failed) .{ .topic = "auth", .tone = .warning, .body = "Could not confirm durable fx logout. The active source was recalculated.", } else if (result.session_deleted) .{ .topic = "auth", .tone = .neutral, .body = "Signed out of fx.", } else .{ .topic = "auth", .tone = .neutral, .body = "No fx login session found.", }); if (result.remote_revocation_failed) { try writeAuthNotice(app, .{ .topic = "auth", .tone = .warning, .body = login_flow.remote_revocation_warning, }); } } pub fn applyPickerChoice(app: *App, choice: auth_runtime.Choice) !void { if (comptime !oauthAuthEnabled(App)) { try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = "Browser authentication is supplied by the embedding SDK.", }, true); return; } switch (choice) { .source => |source| try applySourceChoice(app, source), .action => |action| switch (action) { .login => try beginSignIn(app, true), .setup => { if (comptime !runtime_profile.allows(App, .native_auth)) { try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = "API key setup is unavailable in this WASM session.", }, true); return; } prepareApiKeyInputBoundary(app); app.auth.openApiKeyPickerFromRoot(app.alloc); }, .change_team => try beginTeamPicker(app), .switch_credential => app.auth.openSwitchCredentialPicker(app.alloc), .automatic => try applyAutomaticCredential(app), }, .team => |index| try applyTeamChoice(app, index), } } pub fn routeAuthPickerByte(app: *App, byte: u8) !bool { if (app.auth.signInEntryActive()) { switch (byte) { 3, 4 => _ = app.auth.popPickerStage(app.alloc), '\r', '\n' => try openSignInBrowser(app), else => {}, } app.shell.render_requests.request(.footer); return true; } if (app.auth.teamPickerActive()) { const consumed = switch (byte) { 8, 127 => app.auth.deleteTeamQueryByte(), else => try app.auth.appendTeamQueryByte(app.alloc, byte), }; if (consumed) { app.shell.render_requests.request(.footer); return true; } } if (!app.auth.apiKeyEntryActive()) return false; switch (byte) { 3, 4 => _ = app.auth.popPickerStage(app.alloc), '\r', '\n' => try submitApiKeyEntry(app), 8, 127 => _ = app.auth.deleteApiKeyByte(), else => _ = try app.auth.appendApiKeyByte(app.alloc, byte), } app.shell.render_requests.request(.footer); return true; } pub fn routeAuthPickerEscapeAction(app: *App, action: anytype) bool { if (!app.auth.signInEntryActive() and !app.auth.apiKeyEntryActive()) return false; return switch (action) { .escape, .remapped_byte => false, else => true, }; } pub fn collectSignInFacts(app: *App) !void { if (comptime !oauthAuthEnabled(App)) return; app.auth.pulseSignIn(app.alloc); switch (app.auth.pollSignInTransition(app.alloc)) { .none => {}, .cancelled => app.shell.render_requests.request(.footer), .failed => |err| { debug_trace.logf("auth", "login failed err={s}", .{@errorName(err)}); _ = app.auth.popPickerStage(app.alloc); try writeLoginError(app, err); }, .succeeded => |completed| { var selection = completed; defer selection.deinit(app.alloc); if (!try selectCredentialSource(app, .fx_login)) { _ = app.auth.popPickerStage(app.alloc); try writeAuthNotice(app, .{ .topic = "auth", .tone = .@"error", .body = "Signed in, but the fx login credential could not be loaded.", }); return; } rememberCredentialSource(app, .fx_login); if (selection.teams.items.len > 0) { app.auth.openTeamPicker(app.alloc, &selection); } else { _ = app.auth.popPickerStage(app.alloc); } try writeAuthNotice(app, .{ .topic = "auth", .tone = .neutral, .body = "Signed in to Vercel.", }); }, } } fn prepareApiKeyInputBoundary(app: *App) void { if (comptime @hasDecl(App, "prepareApiKeyInputBoundary")) { app.prepareApiKeyInputBoundary(); } } fn submitApiKeyEntry(app: *App) !void { if (app.auth.pickerView().api_key_mask_count == 0) return; switch (app.auth.beginApiKeySave(app.alloc)) { .started => app.shell.render_requests.request(.footer), .empty => {}, .busy => try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = "Still saving the previous API key. Nothing was stored for this one; try again in a moment.", }, true), } } /// Polled from the event loop so a save that blocks on a locked key store /// or a slow gateway never stalls rendering. pub fn collectApiKeySaveFacts(app: *App) !void { const result = app.auth.takeApiKeySaveResult(app.alloc) orelse return; try applyApiKeySaveResult(app, result); } fn applyApiKeySaveResult(app: *App, result: auth_runtime.ApiKeySaveResult) !void { switch (result) { .empty => return, .saved => |changed| { applyCredentialChange(app, changed); rememberCredentialSource(app, .stored_key); const body = try std.fmt.allocPrint( app.alloc, "Saved the API key to {s} and made it active.", .{credentials.stored_key_backend_label}, ); defer app.alloc.free(body); try app.writeDomainNotice(.{ .topic = "auth", .tone = .neutral, .body = body, }, true); }, .gateway_refused => try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = "The AI Gateway refused that API key. Nothing was stored.", }, true), .gateway_unavailable => try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = "Could not verify that API key with AI Gateway. Nothing was stored.", }, true), .store_failed => { const body = try std.fmt.allocPrint( app.alloc, "Could not save the API key to {s}. Nothing was stored.", .{credentials.stored_key_backend_label}, ); defer app.alloc.free(body); try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = body, }, true); }, .reload_failed => { const body = try std.fmt.allocPrint( app.alloc, "Saved the API key to {s}, but could not make it active.", .{credentials.stored_key_backend_label}, ); defer app.alloc.free(body); try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = body, }, true); }, } } /// Clearing the remembered choice must also re-resolve, otherwise the /// session would keep running on a source precedence no longer selects. fn applyAutomaticCredential(app: *App) !void { forgetCredentialSource(app); app.auth.closePicker(app.alloc); applyCredentialChange(app, try app.auth.reselectByPrecedence(app.alloc)); try app.writeDomainNotice(.{ .topic = "auth", .tone = .neutral, .body = "Using automatic credential precedence again.", }, true); } fn forgetCredentialSource(app: *App) void { var attempt = config_runtime.attemptUserPreferences( app.alloc, .{ .clear_credential_source = true }, ); defer attempt.deinit(app.alloc); switch (attempt) { .outcome => debug_trace.logf("auth", "credential choice cleared", .{}), .failure => |failure| debug_trace.logf( "auth", "credential choice not cleared err={s}", .{@errorName(failure.err)}, ), } } fn applySourceChoice(app: *App, source: credentials.Source) !void { const body = try std.fmt.allocPrint( app.alloc, "Switched credential to {s}.", .{credentials.sourceLabel(source)}, ); defer app.alloc.free(body); if (!try selectCredentialSource(app, source)) { try app.writeDomainNotice(.{ .topic = "auth", .tone = .warning, .body = "That credential is no longer available. The current source is unchanged.", }, true); return; } rememberCredentialSource(app, source); try app.writeDomainNotice(.{ .topic = "auth", .tone = .neutral, .body = body, }, true); } /// An explicit source choice outlives the session. Failing to persist /// leaves the source active for this run rather than refusing a working /// credential the user already selected. fn rememberCredentialSource(app: *App, source: credentials.Source) void { if (comptime @hasDecl(App, "persistCredentialSourcePreference")) { app.persistCredentialSourcePreference(source); return; } var attempt = config_runtime.attemptUserPreferences( app.alloc, .{ .credential_source = source }, ); defer attempt.deinit(app.alloc); switch (attempt) { .outcome => debug_trace.logf("auth", "credential choice persisted source={t}", .{source}), .failure => |failure| debug_trace.logf( "auth", "credential choice not persisted source={t} err={s}", .{ source, @errorName(failure.err) }, ), } } fn beginTeamPicker(app: *App) !void { if (!app.auth.pickerView().fx_login_session_available) return; try app.flushBeforeBlockingExternalWork(); var selection = login_flow.loadTeamSelection(app.alloc, app.auth.oauthTransport()) catch |err| { debug_trace.logf("auth", "team picker load failed err={s}", .{@errorName(err)}); try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = switch (err) { error.NoSession => "The fx login session is no longer available. Sign in to change teams.", error.NoTeams => "No Vercel teams are available for this account.", else => "Could not load Vercel teams. The current team is unchanged.", }, }, true); return; }; defer selection.deinit(app.alloc); app.auth.openTeamPicker(app.alloc, &selection); app.shell.render_requests.request(.footer); } fn applyTeamChoice(app: *App, index: usize) !void { const selection = app.auth.teamSelection() orelse return; if (index >= selection.teams.items.len) return; const team = selection.teams.items[index]; const body = try std.fmt.allocPrint( app.alloc, "Changed Vercel team to {s} ({s}).", .{ team.name, team.slug }, ); defer app.alloc.free(body); var selected_team = selection.select(app.alloc, index) catch |err| { debug_trace.logf("auth", "team change failed err={s}", .{@errorName(err)}); app.auth.closePicker(app.alloc); try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = switch (err) { error.SessionChanged, error.NoSession => "The fx login session changed before the team could be saved.", else => "Could not change the Vercel team. The current team is unchanged.", }, }, true); return; }; defer selected_team.deinit(app.alloc); if (app.auth.credentialSource() == .fx_login) { applyCredentialChange(app, app.auth.adoptSelectedTeam(app.alloc, &selected_team)); } else if (!try selectCredentialSource(app, .fx_login)) { app.auth.closePicker(app.alloc); try app.writeDomainNotice(.{ .topic = "auth", .tone = .@"error", .body = "Changed the Vercel team, but the fx login credential could not be loaded.", }, true); return; } rememberCredentialSource(app, .fx_login); app.auth.closePicker(app.alloc); try app.writeDomainNotice(.{ .topic = "auth", .tone = .neutral, .body = body, }, true); } fn beginSignIn(app: *App, from_root: bool) !void { try app.flushBeforeBlockingExternalWork(); const started = if (from_root) app.auth.openSignInPickerFromRoot(app.alloc) else app.auth.openSignInPicker(app.alloc); if (started catch |err| { debug_trace.logf("auth", "login failed err={s}", .{@errorName(err)}); try writeLoginError(app, err); return; }) { app.shell.render_requests.request(.footer); // Open the browser as soon as the device code is ready instead of // waiting for Enter; Enter stays as a manual re-open, and // FX_NO_OPEN_BROWSER opts out for headless/SSH sessions. if (io_mod.getenv("FX_NO_OPEN_BROWSER") == null) try openSignInBrowser(app); } } fn openSignInBrowser(app: *App) !void { const url = (try app.auth.signInBrowserUrlAlloc(app.alloc)) orelse return; defer app.alloc.free(url); if (!try app.urlOpener().open(app.alloc, url)) { debug_trace.logf("auth", "login browser launcher failed", .{}); } } pub fn selectCredentialSource(app: *App, source: credentials.Source) !bool { const changed = (try app.auth.selectSource(app.alloc, source)) orelse return false; applyCredentialChange(app, changed); return true; } fn refreshFxLoginCredentialIfNeeded(app: *App) !void { if (!try app.auth.refreshFxLoginIfNeeded(app.alloc)) return; reconcileGatewayCredential(app); if (app.auth.modelCatalogAccess().authorizationCredential() == null) return; app.model_cache.reset();