From c7ec171f52ee518828ca9d76f702e357ea1d0f95 Mon Sep 17 00:00:00 2001 From: karitham Date: Fri, 23 Jan 2026 10:02:16 +0100 Subject: [PATCH] atproto/client: upstreamed the refresh fix --- atproto/client.go | 103 ---------------------------------------- atproto/client_test.go | 104 ----------------------------------------- flake.nix | 4 +- go.mod | 2 +- go.sum | 4 +- 5 files changed, 5 insertions(+), 212 deletions(-) diff --git a/atproto/client.go b/atproto/client.go index 9e56a39..e53e4b8 100644 --- a/atproto/client.go +++ b/atproto/client.go @@ -10,7 +10,6 @@ import ( "time" "github.com/bluesky-social/indigo/atproto/atclient" - "github.com/bluesky-social/indigo/atproto/syntax" ) const ( @@ -127,108 +126,10 @@ func ResolveMiniDoc(ctx context.Context, identifier string, opts *ClientOptions) return result.DID, result.PDS, result.SigningKey, nil } -type FixedPasswordAuth struct { - *atclient.PasswordAuth - lk sync.RWMutex - RefreshCallback func(ctx context.Context, session atclient.PasswordSessionData) -} - -func (a *FixedPasswordAuth) DoWithAuth(c *http.Client, req *http.Request, endpoint syntax.NSID) (*http.Response, error) { - accessToken, refreshToken := a.GetTokens() - req.Header.Set("Authorization", "Bearer "+accessToken) - resp, err := c.Do(req) - if err != nil { - return nil, err - } - - if resp.StatusCode != http.StatusBadRequest { - return resp, nil - } - - if !hasJSONContent(resp.Header) { - return resp, nil - } - - defer resp.Body.Close() - var eb atclient.ErrorBody - if err := json.NewDecoder(resp.Body).Decode(&eb); err != nil { - return nil, &atclient.APIError{StatusCode: resp.StatusCode} - } - if eb.Name != "ExpiredToken" { - return nil, eb.APIError(resp.StatusCode) - } - - if err := a.Refresh(req.Context(), c, refreshToken); err != nil { - return nil, err - } - - retry := req.Clone(req.Context()) - if req.GetBody != nil { - retryBody, err := req.GetBody() - if err != nil { - return nil, fmt.Errorf("API request retry GetBody failed: %w", err) - } - retry.Body = retryBody - } - - accessToken, _ = a.GetTokens() - retry.Header.Set("Authorization", "Bearer "+accessToken) - return c.Do(retry) -} - func hasJSONContent(header http.Header) bool { return len(header.Get("Content-Type")) > 0 && header.Get("Content-Type")[0:19] == "application/json" } -func (a *FixedPasswordAuth) Refresh(ctx context.Context, c *http.Client, priorRefreshToken string) error { - a.lk.Lock() - defer a.lk.Unlock() - - if priorRefreshToken != "" && priorRefreshToken != a.Session.RefreshToken { - return nil - } - - u := a.Session.Host + "/xrpc/com.atproto.server.refreshSession" - req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, nil) - if err != nil { - return err - } - req.Header.Set("User-Agent", "indigo-sdk") - req.Header.Set("Authorization", "Bearer "+a.Session.RefreshToken) - - resp, err := c.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - var eb atclient.ErrorBody - if err := json.NewDecoder(resp.Body).Decode(&eb); err != nil { - return &atclient.APIError{StatusCode: resp.StatusCode} - } - return eb.APIError(resp.StatusCode) - } - - var out struct { - AccessJwt string `json:"accessJwt"` - RefreshJwt string `json:"refreshJwt"` - } - if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { - return err - } - - a.Session.AccessToken = out.AccessJwt - a.Session.RefreshToken = out.RefreshJwt - - if a.RefreshCallback != nil { - snapshot := a.Session.Clone() - a.RefreshCallback(ctx, snapshot) - } - - return nil -} - func ResolveIdentity(ctx context.Context, handle string, opts *ClientOptions) (resolvedIdentity, error) { if handle == "" { return resolvedIdentity{}, fmt.Errorf("handle cannot be empty") @@ -276,10 +177,6 @@ func NewClient(ctx context.Context, handle, password string, opts ...func(*Clien return nil, fmt.Errorf("login failed: %w", err) } - if pa, ok := client.Auth.(*atclient.PasswordAuth); ok { - client.Auth = &FixedPasswordAuth{PasswordAuth: pa} - } - return &Client{ client: client, resolvedIdentity: identity, diff --git a/atproto/client_test.go b/atproto/client_test.go index c4e4c8a..16b33a0 100644 --- a/atproto/client_test.go +++ b/atproto/client_test.go @@ -598,107 +598,3 @@ func (m *mockRepoClient[T]) ApplyWrites(ctx context.Context, collection string, func (m *mockRepoClient[T]) DeleteRecord(ctx context.Context, collection, rkey string) error { return nil } - -func TestFixedPasswordAuth_RefreshUsesPOST(t *testing.T) { - var capturedMethod string - var capturedPath string - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - capturedMethod = r.Method - capturedPath = r.URL.Path - - if capturedMethod == http.MethodPost && capturedPath == "/xrpc/com.atproto.server.refreshSession" { - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]any{ - "accessJwt": "new-access-token", - "refreshJwt": "new-refresh-token", - }) - return - } - - if capturedPath == "/xrpc/com.atproto.server.createSession" { - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]any{ - "accessJwt": "access-token", - "refreshJwt": "refresh-token", - "did": "did:plc:test", - }) - return - } - - w.WriteHeader(http.StatusBadRequest) - })) - defer server.Close() - - pdsURL := server.URL - - session := atclient.PasswordSessionData{ - AccessToken: "old-access-token", - RefreshToken: "refresh-token", - Host: pdsURL, - } - - auth := &FixedPasswordAuth{ - PasswordAuth: &atclient.PasswordAuth{Session: session}, - } - - httpClient := server.Client() - - ctx := context.Background() - err := auth.Refresh(ctx, httpClient, "refresh-token") - if err != nil { - t.Fatalf("Refresh failed: %v", err) - } - - if capturedMethod != http.MethodPost { - t.Errorf("Refresh used %s, want POST", capturedMethod) - } - - if capturedPath != "/xrpc/com.atproto.server.refreshSession" { - t.Errorf("Refresh hit %s, want /xrpc/com.atproto.server.refreshSession", capturedPath) - } -} - -func TestFixedPasswordAuth_IndigoBugCheck(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/xrpc/com.atproto.server.refreshSession" { - if r.Method != http.MethodPost { - t.Errorf("REFRESH BUG: indigo uses %s for refreshSession, should use POST", r.Method) - } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]any{ - "accessJwt": "new-access-token", - "refreshJwt": "new-refresh-token", - }) - return - } - if r.URL.Path == "/xrpc/com.atproto.server.createSession" { - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]any{ - "accessJwt": "access-token", - "refreshJwt": "refresh-token", - "did": "did:plc:test", - }) - return - } - w.WriteHeader(http.StatusBadRequest) - })) - defer server.Close() - - pdsURL := server.URL - - pa := &atclient.PasswordAuth{ - Session: atclient.PasswordSessionData{ - AccessToken: "old-access-token", - RefreshToken: "refresh-token", - Host: pdsURL, - }, - } - - fixedAuth := &FixedPasswordAuth{PasswordAuth: pa} - - err := fixedAuth.Refresh(context.Background(), server.Client(), "refresh-token") - if err != nil { - t.Fatalf("FixedPasswordAuth.Refresh failed: %v", err) - } -} diff --git a/flake.nix b/flake.nix index 14ed226..92808da 100644 --- a/flake.nix +++ b/flake.nix @@ -17,9 +17,9 @@ let lazuli = pkgs.buildGoModule rec { name = "lazuli"; - version = "0.1.4"; + version = "0.1.5"; src = pkgs.nix-gitignore.gitignoreSource [ "*.csv" "*.zip" "*.json" ] ./.; - vendorHash = "sha256-MfBPv/L7wHuUGXx4BDd+DFq0RB11KuMHCzPjFv6FMgs="; + vendorHash = "sha256-O6R8jC8Ms5gsY2FUmuL8lTGTODfMW1CsSWuWbN27zeY="; ldflags = [ "-X" "main.Version=${version}" diff --git a/go.mod b/go.mod index c064cb3..ef02765 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module tangled.org/karitham.dev/lazuli go 1.25.5 require ( - github.com/bluesky-social/indigo v0.0.0-20260120225912-12d69fa4d209 + github.com/bluesky-social/indigo v0.0.0-20260122235001-7f2e6b43efbb github.com/failsafe-go/failsafe-go v0.9.5 github.com/urfave/cli/v3 v3.6.2 go.etcd.io/bbolt v1.4.3 diff --git a/go.sum b/go.sum index 3662d09..76221e2 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE= github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= -github.com/bluesky-social/indigo v0.0.0-20260120225912-12d69fa4d209 h1:W01PGqjCexVBzIZ4FoNe4iO8OhI9XbSE7ieWL0QnMu8= -github.com/bluesky-social/indigo v0.0.0-20260120225912-12d69fa4d209/go.mod h1:KIy0FgNQacp4uv2Z7xhNkV3qZiUSGuRky97s7Pa4v+o= +github.com/bluesky-social/indigo v0.0.0-20260122235001-7f2e6b43efbb h1:3FvzRkxe85/HsnQubXgdg8Vf38J5d1Sk9XmOkm2TCvY= +github.com/bluesky-social/indigo v0.0.0-20260122235001-7f2e6b43efbb/go.mod h1:KIy0FgNQacp4uv2Z7xhNkV3qZiUSGuRky97s7Pa4v+o= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -- 2.51.2