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, 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 refresh token as the auth credential
25 // IMPORTANT: The xrpc client always sends AccessJwt as the Authorization header,
26 // but refreshSession requires the refresh token in that header.
27 // So we put the refresh token in AccessJwt to make it work correctly.
28 client := &xrpc.Client{
29 Host: pdsURL,
30 Auth: &xrpc.AuthInfo{
31 AccessJwt: refreshToken, // Refresh token goes here (sent as Authorization header)
32 RefreshJwt: refreshToken, // Also set here for completeness
33 },
34 }
35
36 // Call com.atproto.server.refreshSession
37 output, err := atproto.ServerRefreshSession(ctx, client)
38 if err != nil {
39 // Check for expired refresh token (401 Unauthorized)
40 // Try typed error first (more reliable), fallback to string check
41 var xrpcErr *xrpc.Error
42 if errors.As(err, &xrpcErr) && xrpcErr.StatusCode == 401 {
43 return "", "", fmt.Errorf("refresh token expired or invalid (needs password re-auth)")
44 }
45
46 // Fallback: string-based detection (in case error isn't wrapped as xrpc.Error)
47 errStr := err.Error()
48 if strings.Contains(errStr, "401") || strings.Contains(errStr, "Unauthorized") {
49 return "", "", fmt.Errorf("refresh token expired or invalid (needs password re-auth)")
50 }
51
52 return "", "", fmt.Errorf("failed to refresh session: %w", err)
53 }
54
55 // Validate response
56 if output.AccessJwt == "" || output.RefreshJwt == "" {
57 return "", "", fmt.Errorf("refresh response missing tokens")
58 }
59
60 return output.AccessJwt, output.RefreshJwt, nil
61}
62
63// reauthenticateWithPassword creates a new session using stored credentials
64// This is the fallback when refresh tokens expire (after ~2 months)
65// Uses com.atproto.server.createSession endpoint via Indigo SDK
66func reauthenticateWithPassword(ctx context.Context, pdsURL, email, password string) (accessToken, refreshToken string, err error) {
67 if pdsURL == "" {
68 return "", "", fmt.Errorf("PDS URL is required")
69 }
70 if email == "" {
71 return "", "", fmt.Errorf("email is required")
72 }
73 if password == "" {
74 return "", "", fmt.Errorf("password is required")
75 }
76
77 // Create unauthenticated XRPC client
78 client := &xrpc.Client{
79 Host: pdsURL,
80 }
81
82 // Prepare createSession input
83 // The identifier can be either email or handle
84 input := &atproto.ServerCreateSession_Input{
85 Identifier: email,
86 Password: password,
87 }
88
89 // Call com.atproto.server.createSession
90 output, err := atproto.ServerCreateSession(ctx, client, input)
91 if err != nil {
92 return "", "", fmt.Errorf("failed to create session: %w", err)
93 }
94
95 // Validate response
96 if output.AccessJwt == "" || output.RefreshJwt == "" {
97 return "", "", fmt.Errorf("createSession response missing tokens")
98 }
99
100 return output.AccessJwt, output.RefreshJwt, nil
101}