···11package client
2233import (
44- "bytes"
54 "context"
65 "fmt"
76 "io"
···2726 Endpoint syntax.NSID
28272928 // Optional request body (may be nil). If this is provided, then 'Content-Type' header should be specified
3030- Body io.ReadCloser
2929+ Body io.Reader
31303231 // Optional function to return new reader for request body; used for retries. strongly recommended if Body is defined. Body still needs to be defined, even if this function is provided.
3332 GetBody func() (io.ReadCloser, error)
···52515352 // logic to turn "whatever io.Reader we are handed" in to something relatively re-tryable (using GetBody)
5453 if body != nil {
5454+ // NOTE: http.NewRequestWithContext already handles GetBody() as well as ContentLength for specific types like bytes.Buffer and strings.Reader. We just want to add io.Seeker here, for things like files-on-disk.
5555 switch v := body.(type) {
5656- case *bytes.Buffer:
5757- req.Body = io.NopCloser(v)
5858- req.GetBody = func() (io.ReadCloser, error) {
5959- return io.NopCloser(v), nil
6060- }
6156 case io.Seeker:
6257 req.Body = io.NopCloser(body)
6358 req.GetBody = func() (io.ReadCloser, error) {
6459 v.Seek(0, 0)
6560 return io.NopCloser(body), nil
6661 }
6767- case io.ReadCloser:
6868- req.Body = v
6969- case io.Reader:
7070- req.Body = io.NopCloser(body)
6262+ default:
6363+ req.Body = body
7164 }
7265 }
7366 return &req
+49-17
atproto/client/password_auth.go
···1414)
15151616type PasswordAuth struct {
1717- Session SessionData
1717+ Session PasswordSessionData
1818 // TODO: RefreshCallback
19192020- lk sync.Mutex
2020+ // lock which protects concurrent access to AccessToken and RefreshToken in session data
2121+ lk sync.RWMutex
2222+}
2323+2424+type PasswordSessionData struct {
2525+ AccessToken string `json:"access_token"`
2626+ RefreshToken string `json:"refresh_token"`
2727+ AccountDID syntax.DID `json:"account_did"`
2828+ Host string `json:"host"`
2129}
22302323-type SessionData struct {
2424- AccessToken string
2525- RefreshToken string
2626- AccountDID syntax.DID
2727- Host string
3131+func (sd *PasswordSessionData) Clone() PasswordSessionData {
3232+ return PasswordSessionData{
3333+ AccessToken: sd.AccessToken,
3434+ RefreshToken: sd.RefreshToken,
3535+ AccountDID: sd.AccountDID,
3636+ Host: sd.Host,
3737+ }
2838}
29393040func (a *PasswordAuth) DoWithAuth(c *http.Client, req *http.Request) (*http.Response, error) {
3131- req.Header.Set("Authorization", "Bearer "+a.Session.AccessToken)
4141+ accessToken, refreshToken := a.GetTokens()
4242+ req.Header.Set("Authorization", "Bearer "+accessToken)
3243 resp, err := c.Do(req)
3344 if err != nil {
3445 return nil, err
···5061 }
51625263 // ok, we had an expired token, try a refresh
5353- if err := a.Refresh(req.Context(), c); err != nil {
6464+ if err := a.Refresh(req.Context(), c, refreshToken); err != nil {
5465 return nil, err
5566 }
5667···6273 }
6374 }
64756565- retry.Header.Set("Authorization", "Bearer "+a.Session.AccessToken)
7676+ accessToken, _ = a.GetTokens()
7777+7878+ retry.Header.Set("Authorization", "Bearer "+accessToken)
6679 retryResp, err := c.Do(retry)
6780 if err != nil {
6881 return nil, err
···7184 return retryResp, err
7285}
73868787+// Returns current access and refresh tokens (take a read-lock on session data)
8888+func (a *PasswordAuth) GetTokens() (string, string) {
8989+ a.lk.RLock()
9090+ defer a.lk.RUnlock()
9191+ return a.Session.AccessToken, a.Session.RefreshToken
9292+}
9393+9494+// Refreshes auth tokens (takes a write-lock on session data).
9595+//
9696+// `priorRefreshToken` argument is used to check if a concurrent refresh already took place.
9797+//
7498// TODO: need a "Logout" method as well? which takes the refresh token (not access token)
7575-func (a *PasswordAuth) Refresh(ctx context.Context, c *http.Client) error {
7676-7777- prior := a.Session.RefreshToken
9999+func (a *PasswordAuth) Refresh(ctx context.Context, c *http.Client, priorRefreshToken string) error {
7810079101 a.lk.Lock()
80102 defer a.lk.Unlock()
811038282- // XXX: basic concurrency check: if refresh token already changed, can bail here. should probably handle this better (accept refresh token as input?)
8383- if prior != a.Session.RefreshToken {
104104+ // basic concurrency check: if refresh token already changed, can bail here (releasing lock)
105105+ if priorRefreshToken != "" && priorRefreshToken != a.Session.RefreshToken {
84106 return nil
85107 }
86108···89111 if err != nil {
90112 return err
91113 }
9292- // TODO: this doesn't inherit User-Agent header
114114+ // NOTE: could try to pull User-Agent from a request and pass that through to here
93115 req.Header.Set("User-Agent", "indigo-sdk")
9411695117 // NOTE: using refresh token here, not access token
···158180 }
159181160182 ra := PasswordAuth{
161161- Session: SessionData{
183183+ Session: PasswordSessionData{
162184 AccessToken: out.AccessJwt,
163185 RefreshToken: out.RefreshJwt,
164186 AccountDID: ident.DID,
···169191 c.AccountDID = &ident.DID
170192 return c, nil
171193}
194194+195195+func ResumePasswordSession(data PasswordSessionData) *APIClient {
196196+ c := NewAPIClient(data.Host)
197197+ ra := PasswordAuth{
198198+ Session: data,
199199+ }
200200+ c.Auth = &ra
201201+ c.AccountDID = &data.AccountDID
202202+ return c
203203+}