A community based topic aggregation platform built on atproto
1package communities
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "strings"
8
9 "github.com/bluesky-social/indigo/api/atproto"
10 "github.com/bluesky-social/indigo/xrpc"
11)
12
13// refreshPDSToken exchanges a refresh token for new access and refresh tokens
14// Uses com.atproto.server.refreshSession endpoint via Indigo SDK
15// CRITICAL: Refresh tokens are single-use - old refresh token is revoked on success
16func refreshPDSToken(ctx context.Context, pdsURL, currentAccessToken, refreshToken string) (newAccessToken, newRefreshToken string, err error) {
17 if pdsURL == "" {
18 return "", "", fmt.Errorf("PDS URL is required")
19 }
20 if refreshToken == "" {
21 return "", "", fmt.Errorf("refresh token is required")
22 }
23
24 // Create XRPC client with auth credentials
25 // The refresh endpoint requires authentication with the refresh token
26 client := &xrpc.Client{
27 Host: pdsURL,
28 Auth: &xrpc.AuthInfo{
29 AccessJwt: currentAccessToken, // Can be expired (not used for refresh auth)
30 RefreshJwt: refreshToken, // This is what authenticates the refresh request
31 },
32 }
33
34 // Call com.atproto.server.refreshSession
35 output, err := atproto.ServerRefreshSession(ctx, client)
36 if err != nil {
37 // Check for expired refresh token (401 Unauthorized)
38 // Try typed error first (more reliable), fallback to string check
39 var xrpcErr *xrpc.Error
40 if errors.As(err, &xrpcErr) && xrpcErr.StatusCode == 401 {
41 return "", "", fmt.Errorf("refresh token expired or invalid (needs password re-auth)")
42 }
43
44 // Fallback: string-based detection (in case error isn't wrapped as xrpc.Error)
45 errStr := err.Error()
46 if strings.Contains(errStr, "401") || strings.Contains(errStr, "Unauthorized") {
47 return "", "", fmt.Errorf("refresh token expired or invalid (needs password re-auth)")
48 }
49
50 return "", "", fmt.Errorf("failed to refresh session: %w", err)
51 }
52
53 // Validate response
54 if output.AccessJwt == "" || output.RefreshJwt == "" {
55 return "", "", fmt.Errorf("refresh response missing tokens")
56 }
57
58 return output.AccessJwt, output.RefreshJwt, nil
59}
60
61// reauthenticateWithPassword creates a new session using stored credentials
62// This is the fallback when refresh tokens expire (after ~2 months)
63// Uses com.atproto.server.createSession endpoint via Indigo SDK
64func reauthenticateWithPassword(ctx context.Context, pdsURL, email, password string) (accessToken, refreshToken string, err error) {
65 if pdsURL == "" {
66 return "", "", fmt.Errorf("PDS URL is required")
67 }
68 if email == "" {
69 return "", "", fmt.Errorf("email is required")
70 }
71 if password == "" {
72 return "", "", fmt.Errorf("password is required")
73 }
74
75 // Create unauthenticated XRPC client
76 client := &xrpc.Client{
77 Host: pdsURL,
78 }
79
80 // Prepare createSession input
81 // The identifier can be either email or handle
82 input := &atproto.ServerCreateSession_Input{
83 Identifier: email,
84 Password: password,
85 }
86
87 // Call com.atproto.server.createSession
88 output, err := atproto.ServerCreateSession(ctx, client, input)
89 if err != nil {
90 return "", "", fmt.Errorf("failed to create session: %w", err)
91 }
92
93 // Validate response
94 if output.AccessJwt == "" || output.RefreshJwt == "" {
95 return "", "", fmt.Errorf("createSession response missing tokens")
96 }
97
98 return output.AccessJwt, output.RefreshJwt, nil
99}