const std = @import("std");const api_key_validator = @import("api_key_validator.zig");const credentials = @import("credentials.zig");const host = @import("../hosts/host.zig");const login_flow = @import("login_flow.zig");const oauth_transport = @import("oauth_transport.zig");const secret = @import("secret.zig");const debug_trace = @import("../shared/debug_trace.zig");const io_mod = @import("../shared/io.zig");const text_utils = @import("../shared/text_utils.zig"); const Allocator = std.mem.Allocator; pub const SourceSet = std.EnumSet(credentials.Source); pub const CredentialRefreshMode = enum { if_needed, force,}; const credential_source_order = [_]credentials.Source{ .vercel_oidc_token, .ai_gateway_api_key, .fx_login, .stored_key,}; const SourceProbeFn = *const fn (?*anyopaque, Allocator, credentials.Source) anyerror!bool;const CredentialLoaderFn = *const fn (?*anyopaque, Allocator, credentials.Source) anyerror!?credentials.Credential;const StoredKeyStoreFn = *const fn (?*anyopaque, Allocator, []const u8) anyerror!void; const max_api_key_entry_bytes: usize = 8 * 1024;const max_api_key_mask_glyphs: usize = 32;const max_team_query_bytes: usize = 256; fn sourceLabelOrMissing(source: ?credentials.Source) []const u8 { return credentials.sourceLabel(source orelse return "missing");} pub const FailureReason = enum { credential_refresh_failed, http_unauthorized,}; pub const FailureSnapshot = struct { source: credentials.Source, reason: FailureReason, http_status: ?std.http.Status = null, pub fn fromHttp(status: std.http.Status, source: ?credentials.Source) ?FailureSnapshot { if (status != .unauthorized) return null; return .{ .source = source orelse return null, .reason = .http_unauthorized, .http_status = status, }; } /// Returns owned, detail-free text. The caller owns the returned slice. pub fn renderText(self: FailureSnapshot, alloc: Allocator) ![]u8 { var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try out.writer.print("{s} {s}", .{ credentials.sourceLabel(self.source), switch (self.reason) { .credential_refresh_failed => "credential refresh failed", .http_unauthorized => "authentication failed", }, }); if (self.http_status) |status| { try out.writer.print(" · HTTP {d}", .{@intFromEnum(status)}); } return try out.toOwnedSlice(); } /// Returns owned JSON containing only the shared auth-failure facts. pub fn renderJson(self: FailureSnapshot, alloc: Allocator) ![]u8 { var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try self.writeJson(&out.writer); return try out.toOwnedSlice(); } pub fn writeJson(self: FailureSnapshot, writer: *std.Io.Writer) !void { try writer.writeAll("{\"source\":"); try std.json.Stringify.value(credentials.sourceLabel(self.source), .{}, writer); try writer.writeAll(",\"reason\":"); try std.json.Stringify.value(@tagName(self.reason), .{}, writer); if (self.http_status) |status| { try writer.print(",\"http_status\":{d}", .{@intFromEnum(status)}); } try writer.writeByte('}'); }}; /// Returns an owned token when the selected fx-login credential can be/// refreshed. The caller must release it with `secret.zeroAndFree`.pub fn refreshFxLoginToken( transport: oauth_transport.Provider, alloc: Allocator, source: credentials.Source, mode: CredentialRefreshMode,) !?[]u8 { if (source != .fx_login) return null; var credential = switch (mode) { .if_needed => (try credentials.loadFxLoginCredential(alloc, transport)) orelse return null, .force => (try credentials.refreshFxLoginCredential(alloc, transport)) orelse return null, }; defer credential.deinit(alloc); const token = credential.token; credential.token = &.{}; return token;} pub const AcquisitionAction = enum { login, setup, change_team, switch_credential, /// Clears a remembered choice so resolution returns to plain precedence. /// Without it the only way back would be editing settings.json by hand. automatic,}; pub const PickerStage = enum { root, sign_in, api_key, change_team, switch_credential,}; pub const ApiKeySaveStart = enum { started, /// Nothing typed, so Enter is a no-op the user already understands. empty, /// A previous save is still in flight. The entered key is discarded rather /// than queued, so the caller must say so instead of failing silently. busy,}; pub const ApiKeySaveResult = union(enum) { empty, saved: bool, gateway_refused, gateway_unavailable, store_failed, reload_failed,}; /// The save does a gateway round trip and a key-store write, either of which can/// take seconds. It runs on a worker so the event loop keeps drawing; the worker/// performs I/O only and hands the loaded credential back for the main thread to/// adopt, keeping `selected_credential` single-threaded.pub const ApiKeySaveOutcome = union(enum) { gateway_refused, gateway_unavailable, store_failed, reload_failed, loaded: credentials.Credential, pub fn deinit(self: *ApiKeySaveOutcome, alloc: Allocator) void { switch (self.*) { .loaded => |*credential| credential.deinit(alloc), else => {}, } self.* = .reload_failed; }}; const ApiKeySaveDeps = struct { ctx: ?*anyopaque = null, validator: api_key_validator.Provider = api_key_validator.unavailable_provider, store: StoredKeyStoreFn = storeUnavailableSecret, loader: CredentialLoaderFn = loadCredentialSource,}; /// The whole save sequence with no runtime state, so outcome behaviour can be/// tested synchronously while the worker owns only threading.fn performApiKeySave(alloc: Allocator, key: []const u8, deps: ApiKeySaveDeps) ApiKeySaveOutcome { switch (deps.validator.validate(alloc, key)) { .accepted => {}, .refused => return .gateway_refused, .unavailable => return .gateway_unavailable, } deps.store(deps.ctx, alloc, key) catch |err| { debug_trace.logf("auth", "api key save failed step=store err={s}", .{@errorName(err)}); return .store_failed; }; const loaded = deps.loader(deps.ctx, alloc, .stored_key) catch |err| { debug_trace.logf("auth", "api key save failed step=reload err={s}", .{@errorName(err)}); return .reload_failed; }; const credential = loaded orelse return .reload_failed; if (credential.source != .stored_key) { var wrong = credential; wrong.deinit(alloc); return .reload_failed; } return .{ .loaded = credential };} const ApiKeySaveRuntime = struct { const Self = @This(); mutex: std.Io.Mutex = .init, thread: ?std.Thread = null, running: bool = false, /// Owned for the worker's lifetime and zeroed by it, so the entry stage can /// drop its own buffer the moment the save starts. key: std.ArrayList(u8) = .empty, outcome: ?ApiKeySaveOutcome = null, deps: ApiKeySaveDeps = .{}, fn start(self: *Self, alloc: Allocator, key: std.ArrayList(u8), deps: ApiKeySaveDeps) bool { self.mutex.lockUncancelable(io_mod.getIo()); if (self.running or self.thread != null) { self.mutex.unlock(io_mod.getIo()); var rejected = key; secret.zeroAndFree(alloc, rejected.allocatedSlice()); return false; } self.running = true; self.key = key; self.deps = deps; self.mutex.unlock(io_mod.getIo()); self.thread = std.Thread.spawn(.{}, workerMain, .{ self, alloc }) catch { self.mutex.lockUncancelable(io_mod.getIo()); self.running = false; var abandoned = self.key; self.key = .empty; self.mutex.unlock(io_mod.getIo()); secret.zeroAndFree(alloc, abandoned.allocatedSlice()); debug_trace.logf("auth", "api key save worker failed to spawn", .{}); return false; }; return true; } fn workerMain(self: *Self, alloc: Allocator) void { const result = performApiKeySave(alloc, self.key.items, self.deps); self.mutex.lockUncancelable(io_mod.getIo()); defer self.mutex.unlock(io_mod.getIo()); var spent = self.key; self.key = .empty; secret.zeroAndFree(alloc, spent.allocatedSlice()); self.outcome = result; self.running = false; } /// Returns the finished outcome once, joining the worker first. Ownership of a /// loaded credential passes to the caller. fn take(self: *Self, alloc: Allocator) ?ApiKeySaveOutcome { self.mutex.lockUncancelable(io_mod.getIo()); if (self.running) { self.mutex.unlock(io_mod.getIo()); return null; } const thread = self.thread; self.thread = null; const outcome = self.outcome; self.outcome = null; self.mutex.unlock(io_mod.getIo()); if (thread) |handle| handle.join(); _ = alloc; return outcome; } fn isSaving(self: *const Self) bool { const mutable = @constCast(self); mutable.mutex.lockUncancelable(io_mod.getIo()); defer mutable.mutex.unlock(io_mod.getIo()); return self.running; } fn deinit(self: *Self, alloc: Allocator) void { const thread = self.thread; self.thread = null; if (thread) |handle| handle.join(); if (self.outcome) |*outcome| outcome.deinit(alloc); self.outcome = null; var spent = self.key; self.key = .empty; secret.zeroAndFree(alloc, spent.allocatedSlice()); self.running = false; }}; const ApiKeyExitReason = enum { cancel, saved, save_failed, screen_replacement, runtime_deinit,}; pub const Choice = union(enum) { source: credentials.Source, action: AcquisitionAction, team: usize, pub fn eql(self: Choice, other: Choice) bool { return switch (self) { .source => |source| switch (other) { .source => |other_source| source == other_source, .action, .team => false, }, .action => |action| switch (other) { .source, .team => false, .action => |other_action| action == other_action, }, .team => |team| switch (other) { .source, .action => false, .team => |other_team| team == other_team, }, }; }}; pub const PickerView = struct { active: bool, available_sources: SourceSet, selected_choice: ?Choice, active_source: ?credentials.Source, include_skip: bool, stage: PickerStage = .root, fx_login_session_available: bool = false, teams: []const login_flow.Team = &.{}, current_team: ?[]const u8 = null, team_query: []const u8 = &.{}, sign_in: login_flow.SignInSnapshot = .{}, api_key_mask_count: usize = 0, pub fn activeSourceLabel(self: PickerView) []const u8 { return sourceLabelOrMissing(self.active_source); } pub fn choiceCount(self: PickerView) usize { return switch (self.stage) { .root => if (self.include_skip) 2 else 4, .sign_in, .api_key => 0, .change_team => blk: { var count: usize = 0; for (self.teams) |team| { if (teamMatchesQuery(team, self.team_query)) count += 1; } break :blk count; }, .switch_credential => self.available_sources.count() + 1, }; } pub fn choiceAt(self: PickerView, index: usize) ?Choice { return switch (self.stage) { .root => if (self.include_skip) switch (index) { 0 => .{ .action = .login }, 1 => .{ .action = .setup }, else => null, } else switch (index) { 0 => .{ .action = .login }, 1 => .{ .action = .setup }, 2 => .{ .action = .change_team }, 3 => .{ .action = .switch_credential }, else => null, }, .sign_in, .api_key => null, .change_team => blk: { var visible_index: usize = 0; for (self.teams, 0..) |team, team_index| { if (!teamMatchesQuery(team, self.team_query)) continue; if (visible_index == index) break :blk .{ .team = team_index }; visible_index += 1; } break :blk null; }, .switch_credential => if (index < self.available_sources.count()) .{ .source = sourceAtIndex(self.available_sources, index).? } else if (index == self.available_sources.count()) .{ .action = .automatic } else null, }; } pub fn choiceIsSelected(self: PickerView, choice: Choice) bool { const selected = self.selected_choice orelse return false; return selected.eql(choice); } pub fn selectedIndex(self: PickerView) usize { const selected = self.selected_choice orelse return 0; var index: usize = 0; while (self.choiceAt(index)) |choice| : (index += 1) { if (choice.eql(selected)) return index; } return 0; } pub fn choiceLabel(self: PickerView, choice: Choice) []const u8 { return switch (choice) { .source => |source| credentials.sourceLabel(source), .action => |action| switch (action) { .login => "Sign in with Vercel", .setup => if (self.include_skip) "Add an API key" else "API key", .change_team => "Change team", .switch_credential => "Switch credential", .automatic => "Automatic", }, .team => |index| if (index < self.teams.len) self.teams[index].name else "", }; } pub fn choiceDescription(self: PickerView, choice: Choice) []const u8 { return switch (choice) { .source => |source| if (self.active_source == source) "current" else "available", .action => |action| switch (action) { .login, .setup, .switch_credential => "", .automatic => "use normal precedence", .change_team => if (self.fx_login_session_available) "choose a team" else "sign in first", }, .team => |index| if (self.teamIsCurrent(index)) "current" else "", }; } pub fn choiceEnabled(self: PickerView, choice: Choice) bool { return switch (choice) { .action => |action| action != .change_team or self.fx_login_session_available, .source, .team => true, }; } fn teamIsCurrent(self: PickerView, index: usize) bool { if (index >= self.teams.len) return false; const current = self.current_team orelse return false; const team = self.teams[index]; return std.mem.eql(u8, current, team.id) or std.mem.eql(u8, current, team.slug); }}; fn teamMatchesQuery(team: login_flow.Team, query: []const u8) bool { return text_utils.containsIgnoreCase(team.name, query) or text_utils.containsIgnoreCase(team.slug, query);} pub const GatewayTeamStatus = enum { set, unset, unknown, pub fn label(self: GatewayTeamStatus) []const u8 { return switch (self) { .set => "set", .unset => "unset", .unknown => "unknown", }; }}; pub const MissingHelpSurface = enum { cli, interactive,}; pub const StatusSnapshot = struct { active_source: ?credentials.Source = null, team: ?[]const u8 = null, owned_team: ?[]u8 = null, stored_key_status: credentials.StoredKeyReadStatus = .not_attempted, /// The active credential is past its refresh deadline. Distinct from `refreshable`, /// which answers whether this source type can refresh at all. expired: bool = false, pub fn deinit(self: *StatusSnapshot, alloc: Allocator) void { if (self.owned_team) |team| alloc.free(team); self.* = .{}; } pub fn activeSourceLabel(self: StatusSnapshot) []const u8 { return sourceLabelOrMissing(self.active_source); } pub fn refreshable(self: StatusSnapshot) bool { const source = self.active_source orelse return false; return credentials.sourceRefreshable(source); } pub fn missingHelp(self: StatusSnapshot, surface: MissingHelpSurface) ?[]const u8 { if (self.active_source != null) return null; if (self.stored_key_status == .unavailable) return credentials.unreadable_store_message; return switch (surface) { .cli => credentials.missing_credential_message,Lines 1–500 of 2476. Load more
const std = @import("std"); const api_key_validator = @import("api_key_validator.zig"); const credentials = @import("credentials.zig"); const host = @import("../hosts/host.zig"); const login_flow = @import("login_flow.zig"); const oauth_transport = @import("oauth_transport.zig"); const secret = @import("secret.zig"); const debug_trace = @import("../shared/debug_trace.zig"); const io_mod = @import("../shared/io.zig"); const text_utils = @import("../shared/text_utils.zig"); const Allocator = std.mem.Allocator; pub const SourceSet = std.EnumSet(credentials.Source); pub const CredentialRefreshMode = enum { if_needed, force, }; const credential_source_order = [_]credentials.Source{ .vercel_oidc_token, .ai_gateway_api_key, .fx_login, .stored_key, }; const SourceProbeFn = *const fn (?*anyopaque, Allocator, credentials.Source) anyerror!bool; const CredentialLoaderFn = *const fn (?*anyopaque, Allocator, credentials.Source) anyerror!?credentials.Credential; const StoredKeyStoreFn = *const fn (?*anyopaque, Allocator, []const u8) anyerror!void; const max_api_key_entry_bytes: usize = 8 * 1024; const max_api_key_mask_glyphs: usize = 32; const max_team_query_bytes: usize = 256; fn sourceLabelOrMissing(source: ?credentials.Source) []const u8 { return credentials.sourceLabel(source orelse return "missing"); } pub const FailureReason = enum { credential_refresh_failed, http_unauthorized, }; pub const FailureSnapshot = struct { source: credentials.Source, reason: FailureReason, http_status: ?std.http.Status = null, pub fn fromHttp(status: std.http.Status, source: ?credentials.Source) ?FailureSnapshot { if (status != .unauthorized) return null; return .{ .source = source orelse return null, .reason = .http_unauthorized, .http_status = status, }; } /// Returns owned, detail-free text. The caller owns the returned slice. pub fn renderText(self: FailureSnapshot, alloc: Allocator) ![]u8 { var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try out.writer.print("{s} {s}", .{ credentials.sourceLabel(self.source), switch (self.reason) { .credential_refresh_failed => "credential refresh failed", .http_unauthorized => "authentication failed", }, }); if (self.http_status) |status| { try out.writer.print(" · HTTP {d}", .{@intFromEnum(status)}); } return try out.toOwnedSlice(); } /// Returns owned JSON containing only the shared auth-failure facts. pub fn renderJson(self: FailureSnapshot, alloc: Allocator) ![]u8 { var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try self.writeJson(&out.writer); return try out.toOwnedSlice(); } pub fn writeJson(self: FailureSnapshot, writer: *std.Io.Writer) !void { try writer.writeAll("{\"source\":"); try std.json.Stringify.value(credentials.sourceLabel(self.source), .{}, writer); try writer.writeAll(",\"reason\":"); try std.json.Stringify.value(@tagName(self.reason), .{}, writer); if (self.http_status) |status| { try writer.print(",\"http_status\":{d}", .{@intFromEnum(status)}); } try writer.writeByte('}'); } }; /// Returns an owned token when the selected fx-login credential can be /// refreshed. The caller must release it with `secret.zeroAndFree`. pub fn refreshFxLoginToken( transport: oauth_transport.Provider, alloc: Allocator, source: credentials.Source, mode: CredentialRefreshMode, ) !?[]u8 { if (source != .fx_login) return null; var credential = switch (mode) { .if_needed => (try credentials.loadFxLoginCredential(alloc, transport)) orelse return null, .force => (try credentials.refreshFxLoginCredential(alloc, transport)) orelse return null, }; defer credential.deinit(alloc); const token = credential.token; credential.token = &.{}; return token; } pub const AcquisitionAction = enum { login, setup, change_team, switch_credential, /// Clears a remembered choice so resolution returns to plain precedence. /// Without it the only way back would be editing settings.json by hand. automatic, }; pub const PickerStage = enum { root, sign_in, api_key, change_team, switch_credential, }; pub const ApiKeySaveStart = enum { started, /// Nothing typed, so Enter is a no-op the user already understands. empty, /// A previous save is still in flight. The entered key is discarded rather /// than queued, so the caller must say so instead of failing silently. busy, }; pub const ApiKeySaveResult = union(enum) { empty, saved: bool, gateway_refused, gateway_unavailable, store_failed, reload_failed, }; /// The save does a gateway round trip and a key-store write, either of which can /// take seconds. It runs on a worker so the event loop keeps drawing; the worker /// performs I/O only and hands the loaded credential back for the main thread to /// adopt, keeping `selected_credential` single-threaded. pub const ApiKeySaveOutcome = union(enum) { gateway_refused, gateway_unavailable, store_failed, reload_failed, loaded: credentials.Credential, pub fn deinit(self: *ApiKeySaveOutcome, alloc: Allocator) void { switch (self.*) { .loaded => |*credential| credential.deinit(alloc), else => {}, } self.* = .reload_failed; } }; const ApiKeySaveDeps = struct { ctx: ?*anyopaque = null, validator: api_key_validator.Provider = api_key_validator.unavailable_provider, store: StoredKeyStoreFn = storeUnavailableSecret, loader: CredentialLoaderFn = loadCredentialSource, }; /// The whole save sequence with no runtime state, so outcome behaviour can be /// tested synchronously while the worker owns only threading. fn performApiKeySave(alloc: Allocator, key: []const u8, deps: ApiKeySaveDeps) ApiKeySaveOutcome { switch (deps.validator.validate(alloc, key)) { .accepted => {}, .refused => return .gateway_refused, .unavailable => return .gateway_unavailable, } deps.store(deps.ctx, alloc, key) catch |err| { debug_trace.logf("auth", "api key save failed step=store err={s}", .{@errorName(err)}); return .store_failed; }; const loaded = deps.loader(deps.ctx, alloc, .stored_key) catch |err| { debug_trace.logf("auth", "api key save failed step=reload err={s}", .{@errorName(err)}); return .reload_failed; }; const credential = loaded orelse return .reload_failed; if (credential.source != .stored_key) { var wrong = credential; wrong.deinit(alloc); return .reload_failed; } return .{ .loaded = credential }; } const ApiKeySaveRuntime = struct { const Self = @This(); mutex: std.Io.Mutex = .init, thread: ?std.Thread = null, running: bool = false, /// Owned for the worker's lifetime and zeroed by it, so the entry stage can /// drop its own buffer the moment the save starts. key: std.ArrayList(u8) = .empty, outcome: ?ApiKeySaveOutcome = null, deps: ApiKeySaveDeps = .{}, fn start(self: *Self, alloc: Allocator, key: std.ArrayList(u8), deps: ApiKeySaveDeps) bool { self.mutex.lockUncancelable(io_mod.getIo()); if (self.running or self.thread != null) { self.mutex.unlock(io_mod.getIo()); var rejected = key; secret.zeroAndFree(alloc, rejected.allocatedSlice()); return false; } self.running = true; self.key = key; self.deps = deps; self.mutex.unlock(io_mod.getIo()); self.thread = std.Thread.spawn(.{}, workerMain, .{ self, alloc }) catch { self.mutex.lockUncancelable(io_mod.getIo()); self.running = false; var abandoned = self.key; self.key = .empty; self.mutex.unlock(io_mod.getIo()); secret.zeroAndFree(alloc, abandoned.allocatedSlice()); debug_trace.logf("auth", "api key save worker failed to spawn", .{}); return false; }; return true; } fn workerMain(self: *Self, alloc: Allocator) void { const result = performApiKeySave(alloc, self.key.items, self.deps); self.mutex.lockUncancelable(io_mod.getIo()); defer self.mutex.unlock(io_mod.getIo()); var spent = self.key; self.key = .empty; secret.zeroAndFree(alloc, spent.allocatedSlice()); self.outcome = result; self.running = false; } /// Returns the finished outcome once, joining the worker first. Ownership of a /// loaded credential passes to the caller. fn take(self: *Self, alloc: Allocator) ?ApiKeySaveOutcome { self.mutex.lockUncancelable(io_mod.getIo()); if (self.running) { self.mutex.unlock(io_mod.getIo()); return null; } const thread = self.thread; self.thread = null; const outcome = self.outcome; self.outcome = null; self.mutex.unlock(io_mod.getIo()); if (thread) |handle| handle.join(); _ = alloc; return outcome; } fn isSaving(self: *const Self) bool { const mutable = @constCast(self); mutable.mutex.lockUncancelable(io_mod.getIo()); defer mutable.mutex.unlock(io_mod.getIo()); return self.running; } fn deinit(self: *Self, alloc: Allocator) void { const thread = self.thread; self.thread = null; if (thread) |handle| handle.join(); if (self.outcome) |*outcome| outcome.deinit(alloc); self.outcome = null; var spent = self.key; self.key = .empty; secret.zeroAndFree(alloc, spent.allocatedSlice()); self.running = false; } }; const ApiKeyExitReason = enum { cancel, saved, save_failed, screen_replacement, runtime_deinit, }; pub const Choice = union(enum) { source: credentials.Source, action: AcquisitionAction, team: usize, pub fn eql(self: Choice, other: Choice) bool { return switch (self) { .source => |source| switch (other) { .source => |other_source| source == other_source, .action, .team => false, }, .action => |action| switch (other) { .source, .team => false, .action => |other_action| action == other_action, }, .team => |team| switch (other) { .source, .action => false, .team => |other_team| team == other_team, }, }; } }; pub const PickerView = struct { active: bool, available_sources: SourceSet, selected_choice: ?Choice, active_source: ?credentials.Source, include_skip: bool, stage: PickerStage = .root, fx_login_session_available: bool = false, teams: []const login_flow.Team = &.{}, current_team: ?[]const u8 = null, team_query: []const u8 = &.{}, sign_in: login_flow.SignInSnapshot = .{}, api_key_mask_count: usize = 0, pub fn activeSourceLabel(self: PickerView) []const u8 { return sourceLabelOrMissing(self.active_source); } pub fn choiceCount(self: PickerView) usize { return switch (self.stage) { .root => if (self.include_skip) 2 else 4, .sign_in, .api_key => 0, .change_team => blk: { var count: usize = 0; for (self.teams) |team| { if (teamMatchesQuery(team, self.team_query)) count += 1; } break :blk count; }, .switch_credential => self.available_sources.count() + 1, }; } pub fn choiceAt(self: PickerView, index: usize) ?Choice { return switch (self.stage) { .root => if (self.include_skip) switch (index) { 0 => .{ .action = .login }, 1 => .{ .action = .setup }, else => null, } else switch (index) { 0 => .{ .action = .login }, 1 => .{ .action = .setup }, 2 => .{ .action = .change_team }, 3 => .{ .action = .switch_credential }, else => null, }, .sign_in, .api_key => null, .change_team => blk: { var visible_index: usize = 0; for (self.teams, 0..) |team, team_index| { if (!teamMatchesQuery(team, self.team_query)) continue; if (visible_index == index) break :blk .{ .team = team_index }; visible_index += 1; } break :blk null; }, .switch_credential => if (index < self.available_sources.count()) .{ .source = sourceAtIndex(self.available_sources, index).? } else if (index == self.available_sources.count()) .{ .action = .automatic } else null, }; } pub fn choiceIsSelected(self: PickerView, choice: Choice) bool { const selected = self.selected_choice orelse return false; return selected.eql(choice); } pub fn selectedIndex(self: PickerView) usize { const selected = self.selected_choice orelse return 0; var index: usize = 0; while (self.choiceAt(index)) |choice| : (index += 1) { if (choice.eql(selected)) return index; } return 0; } pub fn choiceLabel(self: PickerView, choice: Choice) []const u8 { return switch (choice) { .source => |source| credentials.sourceLabel(source), .action => |action| switch (action) { .login => "Sign in with Vercel", .setup => if (self.include_skip) "Add an API key" else "API key", .change_team => "Change team", .switch_credential => "Switch credential", .automatic => "Automatic", }, .team => |index| if (index < self.teams.len) self.teams[index].name else "", }; } pub fn choiceDescription(self: PickerView, choice: Choice) []const u8 { return switch (choice) { .source => |source| if (self.active_source == source) "current" else "available", .action => |action| switch (action) { .login, .setup, .switch_credential => "", .automatic => "use normal precedence", .change_team => if (self.fx_login_session_available) "choose a team" else "sign in first", }, .team => |index| if (self.teamIsCurrent(index)) "current" else "", }; } pub fn choiceEnabled(self: PickerView, choice: Choice) bool { return switch (choice) { .action => |action| action != .change_team or self.fx_login_session_available, .source, .team => true, }; } fn teamIsCurrent(self: PickerView, index: usize) bool { if (index >= self.teams.len) return false; const current = self.current_team orelse return false; const team = self.teams[index]; return std.mem.eql(u8, current, team.id) or std.mem.eql(u8, current, team.slug); } }; fn teamMatchesQuery(team: login_flow.Team, query: []const u8) bool { return text_utils.containsIgnoreCase(team.name, query) or text_utils.containsIgnoreCase(team.slug, query); } pub const GatewayTeamStatus = enum { set, unset, unknown, pub fn label(self: GatewayTeamStatus) []const u8 { return switch (self) { .set => "set", .unset => "unset", .unknown => "unknown", }; } }; pub const MissingHelpSurface = enum { cli, interactive, }; pub const StatusSnapshot = struct { active_source: ?credentials.Source = null, team: ?[]const u8 = null, owned_team: ?[]u8 = null, stored_key_status: credentials.StoredKeyReadStatus = .not_attempted, /// The active credential is past its refresh deadline. Distinct from `refreshable`, /// which answers whether this source type can refresh at all. expired: bool = false, pub fn deinit(self: *StatusSnapshot, alloc: Allocator) void { if (self.owned_team) |team| alloc.free(team); self.* = .{}; } pub fn activeSourceLabel(self: StatusSnapshot) []const u8 { return sourceLabelOrMissing(self.active_source); } pub fn refreshable(self: StatusSnapshot) bool { const source = self.active_source orelse return false; return credentials.sourceRefreshable(source); } pub fn missingHelp(self: StatusSnapshot, surface: MissingHelpSurface) ?[]const u8 { if (self.active_source != null) return null; if (self.stored_key_status == .unavailable) return credentials.unreadable_store_message; return switch (surface) { .cli => credentials.missing_credential_message,