diff --git a/cmd/entire/cli/auth/client.go b/cmd/entire/cli/auth/client.go index cb8bb8d47..34e36d8cf 100644 --- a/cmd/entire/cli/auth/client.go +++ b/cmd/entire/cli/auth/client.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/entireio/auth-go/authcode" "github.com/entireio/auth-go/deviceflow" "github.com/entireio/auth-go/tokens" "github.com/entireio/cli/cmd/entire/cli/api" @@ -41,7 +42,8 @@ type DeviceAuthPoll struct { // (ENTIRE_AUTH_PROVIDER_VERSION wins, then split-host auto-detect, // then v1 fallback). type Client struct { - inner *deviceflow.Client + inner *deviceflow.Client + browser *authcode.Client } // NewClient constructs a Client targeting the active provider version. @@ -61,20 +63,76 @@ func NewClient(httpClient *http.Client, allowInsecureHTTP bool) *Client { if httpClient != nil { transport = httpClient.Transport } - return &Client{inner: &deviceflow.Client{ - Transport: transport, - BaseURL: issuer, - ClientID: p.ClientID, - // offline_access asks the authorization server for a refresh token. - // The server only mints one when it's requested (it's client-gated), - // so without this the device login is access-token-only and silent - // refresh is impossible. - Scope: "cli offline_access", - UserAgent: p.ClientID, - DeviceCodePath: p.DeviceCodePath, - TokenPath: p.TokenPath, - AllowInsecureHTTP: allowInsecureHTTP || isLoopbackHTTP(issuer), - }} + // offline_access asks the authorization server for a refresh token. + // The server only mints one when it's requested (it's client-gated), + // so without this the login is access-token-only and silent refresh is + // impossible. Both flows request it identically. + const scope = "cli offline_access" + allowHTTP := allowInsecureHTTP || isLoopbackHTTP(issuer) + return &Client{ + inner: &deviceflow.Client{ + Transport: transport, + BaseURL: issuer, + ClientID: p.ClientID, + Scope: scope, + UserAgent: p.ClientID, + DeviceCodePath: p.DeviceCodePath, + TokenPath: p.TokenPath, + AllowInsecureHTTP: allowHTTP, + }, + browser: &authcode.Client{ + Transport: transport, + BaseURL: issuer, + ClientID: p.ClientID, + Scope: scope, + UserAgent: p.ClientID, + AuthorizePath: p.AuthorizePath, + TokenPath: p.TokenPath, + AllowInsecureHTTP: allowHTTP, + }, + } +} + +// BrowserAuthFlow is one in-progress loopback authorization-code login. It +// wraps an authcode.Flow, flattening the TokenSet to the (access, refresh) +// pair login.go persists — mirroring how PollDeviceAuth flattens the +// device-flow result. login.go depends on a small local interface that this +// concrete type satisfies, so it can fake the flow in tests. +type BrowserAuthFlow struct { + inner *authcode.Flow +} + +// AuthorizationURL is the URL to open in the user's browser. +func (f *BrowserAuthFlow) AuthorizationURL() string { return f.inner.AuthorizationURL } + +// Wait blocks until the browser is redirected to the loopback listener, +// returning the authorization code. +func (f *BrowserAuthFlow) Wait(ctx context.Context) (string, error) { + return f.inner.Wait(ctx) //nolint:wrapcheck // shim preserves the lib's wrapped errors verbatim for errors.Is +} + +// Exchange redeems code for access + refresh tokens. +func (f *BrowserAuthFlow) Exchange(ctx context.Context, code string) (accessToken, refreshToken string, err error) { + ts, err := f.inner.Exchange(ctx, code) + if err != nil { + return "", "", err //nolint:wrapcheck // shim returns authcode errors verbatim so callers can errors.Is on sentinels + } + return ts.AccessToken, ts.RefreshToken, nil +} + +// Close tears down the loopback listener. Safe to call after Wait. +func (f *BrowserAuthFlow) Close() error { + return f.inner.Close() //nolint:wrapcheck // shutdown error is best-effort; caller logs at most +} + +// StartBrowserAuth begins the loopback authorization-code flow: it binds a +// local listener and returns a flow carrying the browser URL to open. +func (c *Client) StartBrowserAuth(ctx context.Context) (*BrowserAuthFlow, error) { + f, err := c.browser.Start(ctx) + if err != nil { + return nil, err //nolint:wrapcheck // shim returns authcode errors verbatim so callers can errors.Is on sentinels + } + return &BrowserAuthFlow{inner: f}, nil } // BaseURL returns the issuer base URL this client talks to. diff --git a/cmd/entire/cli/auth/provider.go b/cmd/entire/cli/auth/provider.go index c6b291026..7ecaea4e5 100644 --- a/cmd/entire/cli/auth/provider.go +++ b/cmd/entire/cli/auth/provider.go @@ -24,6 +24,7 @@ const ProviderVersionEnvVar = "ENTIRE_AUTH_PROVIDER_VERSION" type Provider struct { ClientID string DeviceCodePath string + AuthorizePath string TokenPath string STSPath string } @@ -32,6 +33,7 @@ var providers = map[string]Provider{ "v1": { //nolint:gosec // OAuth client_id and endpoint paths, not credentials ClientID: "entire-cli", DeviceCodePath: "/oauth/device/code", + AuthorizePath: "/oauth/authorize", TokenPath: "/oauth/token", }, "v2": { //nolint:gosec // OAuth client_id and endpoint paths, not credentials @@ -42,8 +44,11 @@ var providers = map[string]Provider{ // exchange at the shared /oauth/token endpoint. ClientID: "entire-cli", DeviceCodePath: "/device_authorization", - TokenPath: "/oauth/token", - STSPath: "/oauth/token", + // OIDC-standard authorization_endpoint. Verified against + // us.auth.entire.io's /.well-known/openid-configuration. + AuthorizePath: "/authorize", + TokenPath: "/oauth/token", + STSPath: "/oauth/token", }, } diff --git a/cmd/entire/cli/integration_test/login_test.go b/cmd/entire/cli/integration_test/login_test.go index a6afd2c71..8b1609fb1 100644 --- a/cmd/entire/cli/integration_test/login_test.go +++ b/cmd/entire/cli/integration_test/login_test.go @@ -11,6 +11,7 @@ import ( "io" "net/http" "net/http/httptest" + "net/url" "path/filepath" "strings" "sync" @@ -186,6 +187,63 @@ func TestLogin_DeniedFlow(t *testing.T) { } } +// TestLogin_BrowserFlow_SavesToken drives the loopback authorization-code +// flow end to end: ENTIRE_TEST_TTY=1 forces the interactive (browser) +// default, openBrowser reports failure under test (no usable browser on a +// headless host) so the flow prints the fallback URL, and the test plays +// the role of the browser by parsing that URL and GETting the loopback +// callback with a code + the state from it. +func TestLogin_BrowserFlow_SavesToken(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost && r.URL.Path == "/oauth/token" { + if err := r.ParseForm(); err != nil { + t.Errorf("parse token form: %v", err) + } + if got := r.PostForm.Get("grant_type"); got != "authorization_code" { + t.Errorf("grant_type = %q, want authorization_code", got) + } + if r.PostForm.Get("code_verifier") == "" { + t.Error("token request missing code_verifier") + } + writeJSON(t, w, http.StatusOK, map[string]any{ + "access_token": "browser-token", "token_type": "Bearer", "expires_in": 3600, "scope": "cli offline_access", + }) + return + } + http.NotFound(w, r) + })) + defer server.Close() + + proc := startLoginProcess(t, server.URL, []string{"ENTIRE_TEST_TTY=1"}, "login", "--insecure-http-auth") + + authURL := waitForBrowserPrompt(t, proc.stdout) + u, err := url.Parse(authURL) + if err != nil { + t.Fatalf("parse authorization URL %q: %v", authURL, err) + } + q := u.Query() + redirectURI, state := q.Get("redirect_uri"), q.Get("state") + if redirectURI == "" || state == "" { + t.Fatalf("authorization URL missing redirect_uri/state: %s", authURL) + } + + cbResp, err := http.Get(redirectURI + "?" + url.Values{"code": {"auth-code-1"}, "state": {state}}.Encode()) //nolint:noctx // test + if err != nil { + t.Fatalf("GET loopback callback: %v", err) + } + _ = cbResp.Body.Close() + + output, waitErr := proc.wait() + if waitErr != nil { + t.Fatalf("login command failed: %v\nOutput:\n%s", waitErr, output) + } + if !strings.Contains(output, "Login complete.") { + t.Fatalf("output missing login complete message:\n%s", output) + } +} + type loginProcess struct { stdout *bufio.Reader waitFn func() (string, error) @@ -193,10 +251,17 @@ type loginProcess struct { func runLoginProcess(t *testing.T, apiBaseURL string) *loginProcess { t.Helper() + // No ENTIRE_TEST_TTY: NonInteractive + non-interactive default routes + // `entire login` to the device-code flow. + return startLoginProcess(t, apiBaseURL, nil, "login", "--insecure-http-auth") +} + +func startLoginProcess(t *testing.T, apiBaseURL string, extraEnv []string, args ...string) *loginProcess { + t.Helper() env := NewTestEnv(t) - cmd := execx.NonInteractive(context.Background(), getTestBinary(), "login", "--insecure-http-auth") + cmd := execx.NonInteractive(context.Background(), getTestBinary(), args...) cmd.Dir = env.RepoDir cmd.Env = append(testutil.GitIsolatedEnv(), "ENTIRE_TEST_CLAUDE_PROJECT_DIR="+env.ClaudeProjectDir, @@ -204,11 +269,12 @@ func runLoginProcess(t *testing.T, apiBaseURL string) *loginProcess { "ENTIRE_TEST_OPENCODE_PROJECT_DIR="+env.OpenCodeProjectDir, "ENTIRE_API_BASE_URL="+apiBaseURL, // AuthBaseURL no longer inherits from BaseURL; pin both at the test - // server so the device flow stays in-process instead of reaching - // out to the production us.auth.entire.io default. + // server so the flow stays in-process instead of reaching out to the + // production us.auth.entire.io default. "ENTIRE_AUTH_BASE_URL="+apiBaseURL, "ENTIRE_TEST_AUTH_STORE_FILE="+filepath.Join(env.RepoDir, ".entire-test-auth-store.json"), ) + cmd.Env = append(cmd.Env, extraEnv...) stdoutPipe, err := cmd.StdoutPipe() if err != nil { @@ -268,6 +334,31 @@ func waitForLoginPrompt(t *testing.T, stdout *bufio.Reader) (string, string) { return "", "" } +// waitForBrowserPrompt reads login stdout until it finds the +// "Open this URL in your browser to sign in: " fallback line and +// returns the URL. Under test openBrowser reports failure (no usable +// browser on a headless host), so the browser flow always prints this +// fallback — which is how the test recovers the ephemeral callback URL. +func waitForBrowserPrompt(t *testing.T, stdout *bufio.Reader) string { + t.Helper() + + const prefix = "Open this URL in your browser to sign in: " + deadline := time.Now().Add(10 * time.Second) + for time.Now().Before(deadline) { + line, err := stdout.ReadString('\n') + if err != nil { + t.Fatalf("read login output: %v", err) + } + line = strings.TrimSpace(line) + if after, ok := strings.CutPrefix(line, prefix); ok { + return after + } + } + + t.Fatal("timed out waiting for browser login prompt") + return "" +} + func writeJSON(t *testing.T, w http.ResponseWriter, status int, body map[string]any) { t.Helper() w.Header().Set("Content-Type", "application/json") diff --git a/cmd/entire/cli/login.go b/cmd/entire/cli/login.go index e6d799986..cbfe4a1eb 100644 --- a/cmd/entire/cli/login.go +++ b/cmd/entire/cli/login.go @@ -47,8 +47,19 @@ type deviceAuthClient interface { BaseURL() string } +// browserAuthFlow abstracts an in-progress loopback authorization-code +// login so runBrowserLogin can be unit-tested with a fake instead of a real +// listener. *auth.BrowserAuthFlow satisfies it. +type browserAuthFlow interface { + AuthorizationURL() string + Wait(ctx context.Context) (code string, err error) + Exchange(ctx context.Context, code string) (accessToken, refreshToken string, err error) + Close() error +} + func newLoginCmd() *cobra.Command { var insecureHTTPAuth bool + var useDevice bool cmd := &cobra.Command{ Use: "login", Short: "Log in to Entire", @@ -56,10 +67,30 @@ func newLoginCmd() *cobra.Command { if err := requireSecureBaseURL(insecureHTTPAuth); err != nil { return err } - return runLogin(cmd.Context(), cmd.OutOrStdout(), cmd.ErrOrStderr(), auth.NewClient(nil, insecureHTTPAuth), openBrowser) + client := auth.NewClient(nil, insecureHTTPAuth) + outW, errW := cmd.OutOrStdout(), cmd.ErrOrStderr() + + // Default to the browser (loopback authorization-code) flow: + // no code to type, no poll latency. It needs a local browser and + // a reachable 127.0.0.1, so when there's no interactive terminal + // (CI, piped, SSH without a tty) fall back to the device flow — + // the same both-flows-with-fallback shape gh / gcloud / aws sso + // ship. --device forces the device flow explicitly. + if shouldUseBrowserLogin(useDevice, interactive.CanPromptInteractively()) { + flow, err := client.StartBrowserAuth(cmd.Context()) + if err != nil { + return fmt.Errorf("start login: %w", err) + } + return runBrowserLogin(cmd.Context(), outW, errW, flow, client.BaseURL(), openBrowser) + } + if !useDevice { + fmt.Fprintln(errW, "No interactive terminal detected; using device-code flow.") + } + return runLogin(cmd.Context(), outW, errW, client, openBrowser) }, } addInsecureHTTPAuthFlag(cmd, &insecureHTTPAuth) + cmd.Flags().BoolVar(&useDevice, "device", false, "Use the device-code flow (enter a code in your browser) instead of the default browser redirect") return cmd } @@ -102,7 +133,72 @@ func runLogin(ctx context.Context, outW, errW io.Writer, client deviceAuthClient return fmt.Errorf("complete login: %w", err) } - if err := validateReceivedToken(token, client.BaseURL(), time.Now()); err != nil { + return persistLogin(outW, errW, client.BaseURL(), token, refreshToken) +} + +// shouldUseBrowserLogin reports whether `entire login` should use the +// loopback authorization-code (browser) flow. The browser flow is the +// default but needs a local browser + reachable 127.0.0.1, so it's only +// chosen when --device wasn't passed and an interactive terminal is +// present; otherwise the caller falls back to the device flow. +func shouldUseBrowserLogin(useDevice, canPrompt bool) bool { + return !useDevice && canPrompt +} + +// runBrowserLogin runs the loopback authorization-code flow on an +// already-started flow: open the authorization URL in the user's browser, +// wait for the redirect back to the local listener, then exchange the code +// for tokens. Shares the token validation + persistence tail with runLogin +// via persistLogin. +func runBrowserLogin(ctx context.Context, outW, errW io.Writer, flow browserAuthFlow, baseURL string, openURL browserOpenFunc) error { + // Wait tears the listener down on return, but Close is idempotent and + // covers the error paths before Wait runs. + defer func() { _ = flow.Close() }() + + // Mirror the device flow's interactive shape: show the URL, pause on + // Enter before opening the browser, then wait on the same line so + // persistLogin's "Login complete." reads "Waiting for sign-in... + // Login complete." runBrowserLogin is only reached interactively (see + // shouldUseBrowserLogin), so the Enter prompt is unconditional here. + authURL := flow.AuthorizationURL() + // Show the auth host, not the full authorize URL — the PKCE challenge + + // loopback redirect make it long and unreadable, and the browser is + // opened for the user anyway. The full URL is only printed below as a + // fallback when the browser can't be opened. + fmt.Fprintf(outW, "Logging in to: %s\n\n", baseURL) + fmt.Fprint(outW, "Press Enter to open in browser...") + + // Read from /dev/tty so we get a real keypress and don't consume piped stdin. + if err := waitForEnter(ctx); err != nil { + return fmt.Errorf("wait for input: %w", err) + } + fmt.Fprintln(outW) + + if err := openURL(ctx, authURL); err != nil { + fmt.Fprintf(errW, "Warning: failed to open browser: %v\n", err) + fmt.Fprintf(outW, "Open this URL in your browser to sign in: %s\n", authURL) + } + + fmt.Fprint(outW, "Waiting for sign-in... ") + + code, err := flow.Wait(ctx) + if err != nil { + return fmt.Errorf("complete login: %w", err) + } + + token, refreshToken, err := flow.Exchange(ctx, code) + if err != nil { + return fmt.Errorf("complete login: %w", err) + } + + return persistLogin(outW, errW, baseURL, token, refreshToken) +} + +// persistLogin validates the freshly-issued access token, saves it to the +// keyring, and dual-writes the shared contexts.json credential model. +// Shared by the device-code and browser flows. +func persistLogin(outW, errW io.Writer, baseURL, token, refreshToken string) error { + if err := validateReceivedToken(token, baseURL, time.Now()); err != nil { return fmt.Errorf("reject login token: %w", err) } @@ -110,8 +206,8 @@ func runLogin(ctx context.Context, outW, errW io.Writer, client deviceAuthClient // Login deliberately uses the legacy SaveToken (string, string) // surface — we only have an access-token string at this point; - // the deviceflow client doesn't return a TokenSet here. - if err := store.SaveToken(client.BaseURL(), token); err != nil { + // neither flow's client returns a TokenSet here. + if err := store.SaveToken(baseURL, token); err != nil { return fmt.Errorf("save auth token: %w", err) } @@ -254,6 +350,13 @@ func waitForApproval(ctx context.Context, poller deviceAuthClient, deviceCode st // If /dev/tty cannot be opened (e.g. on Windows), it returns immediately. // Returns ctx.Err() if the context is cancelled before the user presses Enter. func waitForEnter(ctx context.Context) error { + // Under test (in-process go test, or a child with ENTIRE_TEST_TTY set) + // don't block on a real /dev/tty read — tests that force interactive + // mode still need this prompt to return. Mirrors openBrowser's guard. + if interactive.UnderTest() { + return nil + } + tty, err := os.Open("/dev/tty") if err != nil { return nil //nolint:nilerr // tty unavailable (e.g. Windows) — skip prompt silently @@ -283,6 +386,15 @@ func openBrowser(ctx context.Context, browserURL string) error { return fmt.Errorf("refusing to open non-HTTP URL: %s", browserURL) } + // Under test there's no usable browser, and we must not spawn a real one + // on a dev/CI host. Report failure so the caller takes the "here's the + // URL" fallback — exactly the path a genuinely headless machine hits, and + // what lets an integration test recover the loopback callback URL from + // stdout. URL validation above still applies. + if interactive.UnderTest() { + return errors.New("browser unavailable under test") + } + var command string var args []string diff --git a/cmd/entire/cli/login_test.go b/cmd/entire/cli/login_test.go index c5bdc3106..6b5734539 100644 --- a/cmd/entire/cli/login_test.go +++ b/cmd/entire/cli/login_test.go @@ -1,6 +1,7 @@ package cli import ( + "bytes" "context" "errors" "strings" @@ -300,3 +301,139 @@ func TestWaitForApproval_ContextCancelled(t *testing.T) { t.Fatalf("err = %v, want context canceled", err) } } + +// fakeBrowserFlow implements the browserAuthFlow interface for unit tests. +type fakeBrowserFlow struct { + authURL string + waitCode string + waitErr error + exchAccess string + exchRefresh string + exchErr error + + gotExchangeCode string + closed bool +} + +func (f *fakeBrowserFlow) AuthorizationURL() string { return f.authURL } + +func (f *fakeBrowserFlow) Wait(context.Context) (string, error) { return f.waitCode, f.waitErr } + +func (f *fakeBrowserFlow) Exchange(_ context.Context, code string) (string, string, error) { + f.gotExchangeCode = code + return f.exchAccess, f.exchRefresh, f.exchErr +} + +func (f *fakeBrowserFlow) Close() error { + f.closed = true + return nil +} + +func TestShouldUseBrowserLogin(t *testing.T) { + t.Parallel() + + cases := []struct { + useDevice bool + canPrompt bool + want bool + }{ + {useDevice: false, canPrompt: true, want: true}, // default interactive → browser + {useDevice: false, canPrompt: false, want: false}, // headless → fall back to device + {useDevice: true, canPrompt: true, want: false}, // --device forces device + {useDevice: true, canPrompt: false, want: false}, + } + for _, tc := range cases { + if got := shouldUseBrowserLogin(tc.useDevice, tc.canPrompt); got != tc.want { + t.Errorf("shouldUseBrowserLogin(%v, %v) = %v, want %v", tc.useDevice, tc.canPrompt, got, tc.want) + } + } +} + +func TestRunBrowserLogin_OpensAuthorizationURL(t *testing.T) { + t.Parallel() + + flow := &fakeBrowserFlow{authURL: "https://auth.test/authorize?x=1", waitErr: errors.New("stop")} + + var openedURL string + openURL := func(_ context.Context, u string) error { + openedURL = u + return nil + } + + var out bytes.Buffer + // The stubbed Wait returns an error, so runBrowserLogin stops before + // persistLogin (which would hit the real keyring); we assert on the + // side effects up to that point. + if err := runBrowserLogin(context.Background(), &out, &bytes.Buffer{}, flow, "https://auth.test", openURL); err == nil { + t.Fatal("expected error from stubbed Wait") + } + + if openedURL != flow.authURL { + t.Errorf("opened URL = %q, want %q", openedURL, flow.authURL) + } + // Happy path shows the auth host, not the full authorize URL, and + // doesn't print the URL at all (the browser opened fine). + if !strings.Contains(out.String(), "Logging in to:") { + t.Errorf("output missing 'Logging in to:' line:\n%s", out.String()) + } + if strings.Contains(out.String(), flow.authURL) { + t.Errorf("happy path should not print the full authorize URL:\n%s", out.String()) + } + if !strings.Contains(out.String(), "Press Enter to open in browser...") { + t.Errorf("output missing enter-to-open prompt:\n%s", out.String()) + } + if !flow.closed { + t.Error("flow was not closed") + } +} + +func TestRunBrowserLogin_OpenBrowserFallback(t *testing.T) { + t.Parallel() + + flow := &fakeBrowserFlow{authURL: "https://auth.test/authorize", waitErr: errors.New("stop")} + failOpen := func(context.Context, string) error { return errors.New("no browser") } + + var out, errW bytes.Buffer + if err := runBrowserLogin(context.Background(), &out, &errW, flow, "https://auth.test", failOpen); err == nil { + t.Fatal("expected error from stubbed Wait") + } + + if !strings.Contains(errW.String(), "failed to open browser") { + t.Errorf("stderr missing warning:\n%s", errW.String()) + } + if !strings.Contains(out.String(), flow.authURL) { + t.Errorf("stdout missing fallback URL:\n%s", out.String()) + } +} + +func TestRunBrowserLogin_WaitError(t *testing.T) { + t.Parallel() + + denied := errors.New("access_denied") + flow := &fakeBrowserFlow{authURL: "https://auth.test/authorize", waitErr: denied} + noopOpen := func(context.Context, string) error { return nil } + + err := runBrowserLogin(context.Background(), &bytes.Buffer{}, &bytes.Buffer{}, flow, "https://auth.test", noopOpen) + if !errors.Is(err, denied) { + t.Fatalf("err = %v, want wrapped %v", err, denied) + } +} + +func TestRunBrowserLogin_ExchangeError(t *testing.T) { + t.Parallel() + + flow := &fakeBrowserFlow{ + authURL: "https://auth.test/authorize", + waitCode: "the-code", + exchErr: errors.New("invalid_grant"), + } + noopOpen := func(context.Context, string) error { return nil } + + err := runBrowserLogin(context.Background(), &bytes.Buffer{}, &bytes.Buffer{}, flow, "https://auth.test", noopOpen) + if err == nil || !strings.Contains(err.Error(), "complete login") { + t.Fatalf("err = %v, want complete login error", err) + } + if flow.gotExchangeCode != "the-code" { + t.Errorf("Exchange got code %q, want the-code", flow.gotExchangeCode) + } +} diff --git a/go.mod b/go.mod index 23c4e3c97..ef67dfd99 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/charmbracelet/x/ansi v0.11.7 github.com/creack/pty v1.1.24 github.com/denisbrodbeck/machineid v1.0.1 - github.com/entireio/auth-go v0.4.1-0.20260603125945-62cd5140d2d4 + github.com/entireio/auth-go v0.4.1-0.20260604093244-dfb6f1d8eb12 github.com/go-faster/errors v0.7.1 github.com/go-faster/jx v1.2.0 github.com/go-git/go-billy/v6 v6.0.0-alpha.1.0.20260519112248-0095b064a6c6 diff --git a/go.sum b/go.sum index a3642cf87..86ad0864f 100644 --- a/go.sum +++ b/go.sum @@ -109,8 +109,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/entireio/auth-go v0.4.1-0.20260603125945-62cd5140d2d4 h1:apyo1X5SUGjbvFJyzaw2WSTTq7suvtYZ5JMA83lbx7M= -github.com/entireio/auth-go v0.4.1-0.20260603125945-62cd5140d2d4/go.mod h1:eqFYgiNSBw6HXYR3j8DRW0/WTV1dX3SWxr2D6YCYNQ0= +github.com/entireio/auth-go v0.4.1-0.20260604093244-dfb6f1d8eb12 h1:9xItErYdM5WKYjsdBqwCKmdCqEj+KhAgqSRXdOtWUCQ= +github.com/entireio/auth-go v0.4.1-0.20260604093244-dfb6f1d8eb12/go.mod h1:eqFYgiNSBw6HXYR3j8DRW0/WTV1dX3SWxr2D6YCYNQ0= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/fatih/semgroup v1.2.0 h1:h/OLXwEM+3NNyAdZEpMiH1OzfplU09i2qXPVThGZvyg=