A community based topic aggregation platform built on atproto
1package pds
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7
8 "github.com/bluesky-social/indigo/atproto/atclient"
9 "github.com/bluesky-social/indigo/atproto/auth/oauth"
10 "github.com/bluesky-social/indigo/atproto/syntax"
11)
12
13// NewFromOAuthSession creates a PDS client from an OAuth session.
14// This uses DPoP authentication - the correct method for OAuth tokens.
15//
16// The oauthClient is used to resume the session and get a properly configured
17// APIClient that handles DPoP proof generation and nonce rotation automatically.
18func NewFromOAuthSession(ctx context.Context, oauthClient *oauth.ClientApp, sessionData *oauth.ClientSessionData) (Client, error) {
19 if oauthClient == nil {
20 return nil, fmt.Errorf("oauthClient is required")
21 }
22 if sessionData == nil {
23 return nil, fmt.Errorf("sessionData is required")
24 }
25
26 // ResumeSession reconstructs the OAuth session with DPoP key
27 // and returns a ClientSession that can generate authenticated requests
28 sess, err := oauthClient.ResumeSession(ctx, sessionData.AccountDID, sessionData.SessionID)
29 if err != nil {
30 return nil, fmt.Errorf("failed to resume OAuth session: %w", err)
31 }
32
33 // APIClient() returns an *atclient.APIClient configured with DPoP auth
34 apiClient := sess.APIClient()
35
36 return &client{
37 apiClient: apiClient,
38 did: sessionData.AccountDID.String(),
39 host: sessionData.HostURL,
40 }, nil
41}
42
43// NewFromPasswordAuth creates a PDS client using password authentication.
44// This uses Bearer token authentication from com.atproto.server.createSession.
45//
46// Primarily used for:
47// - E2E tests with local PDS
48// - Development/debugging tools
49// - Non-OAuth clients
50//
51// Note: This establishes a new session with the PDS. For repeated calls,
52// consider using NewFromAccessToken if you already have a valid access token.
53func NewFromPasswordAuth(ctx context.Context, host, handle, password string) (Client, error) {
54 if host == "" {
55 return nil, fmt.Errorf("host is required")
56 }
57 if handle == "" {
58 return nil, fmt.Errorf("handle is required")
59 }
60 if password == "" {
61 return nil, fmt.Errorf("password is required")
62 }
63
64 // LoginWithPasswordHost creates a session and returns an authenticated APIClient
65 // This handles the createSession call and Bearer token setup
66 apiClient, err := atclient.LoginWithPasswordHost(ctx, host, handle, password, "", nil)
67 if err != nil {
68 return nil, fmt.Errorf("failed to login with password: %w", err)
69 }
70
71 // Get DID from the authenticated client
72 did := ""
73 if apiClient.AccountDID != nil {
74 did = apiClient.AccountDID.String()
75 }
76
77 return &client{
78 apiClient: apiClient,
79 did: did,
80 host: host,
81 }, nil
82}
83
84// NewFromAccessToken creates a PDS client from an existing access token.
85// This is useful when you already have a valid Bearer token (e.g., from createSession)
86// and don't want to re-authenticate.
87//
88// WARNING: This creates a client with Bearer auth only. Do NOT use this with
89// OAuth access tokens - those require DPoP proofs. Use NewFromOAuthSession instead.
90func NewFromAccessToken(host, did, accessToken string) (Client, error) {
91 if host == "" {
92 return nil, fmt.Errorf("host is required")
93 }
94 if did == "" {
95 return nil, fmt.Errorf("did is required")
96 }
97 if accessToken == "" {
98 return nil, fmt.Errorf("accessToken is required")
99 }
100
101 // Create APIClient with Bearer auth
102 apiClient := atclient.NewAPIClient(host)
103 apiClient.Auth = &bearerAuth{token: accessToken}
104
105 return &client{
106 apiClient: apiClient,
107 did: did,
108 host: host,
109 }, nil
110}
111
112// bearerAuth implements atclient.AuthMethod for simple Bearer token auth.
113// This is used for password-based sessions where DPoP is not required.
114type bearerAuth struct {
115 token string
116}
117
118// Ensure bearerAuth implements atclient.AuthMethod.
119var _ atclient.AuthMethod = (*bearerAuth)(nil)
120
121// DoWithAuth adds the Bearer token to the request and executes it.
122func (b *bearerAuth) DoWithAuth(c *http.Client, req *http.Request, _ syntax.NSID) (*http.Response, error) {
123 req.Header.Set("Authorization", "Bearer "+b.token)
124 return c.Do(req)
125}