2476 lines · 95.8 KB · Zig
RawDownload
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.