+1
.gitignore
+1
.gitignore
+162
examples/web-demo-scope/QUICKSTART.md
+162
examples/web-demo-scope/QUICKSTART.md
···
1
+
# Quick Start Guide - Chat OAuth Demo
2
+
3
+
## What This Demo Does
4
+
5
+
This example demonstrates:
6
+
7
+
1. **Custom OAuth Scopes** - Requests the `transition:chat.bsky` scope in addition to standard scopes
8
+
2. **XRPC Direct Usage** - Shows how to make direct XRPC calls to Bluesky APIs
9
+
3. **Profile Fetching** - Uses `app.bsky.actor.getProfile` to fetch and display user profile information
10
+
11
+
## Quick Start
12
+
13
+
### 1. Start the Server
14
+
15
+
From this directory:
16
+
17
+
```bash
18
+
go run main.go
19
+
```
20
+
21
+
The server will start on port **8182** (different from the main demo's 8181).
22
+
23
+
### 2. Access the Demo
24
+
25
+
Open your browser to:
26
+
```
27
+
http://localhost:8182
28
+
```
29
+
30
+
### 3. Login
31
+
32
+
Enter your Bluesky handle (e.g., `yourname.bsky.social`) and click "Login with Bluesky".
33
+
34
+
You'll be redirected to Bluesky's authorization page where you'll see the requested scopes:
35
+
- `atproto` - Basic AT Protocol access
36
+
- `transition:generic` - Generic transition scope
37
+
- `transition:chat.bsky` - Chat-specific scope
38
+
39
+
### 4. View Your Profile
40
+
41
+
After successful authentication, click the "View My Profile (XRPC)" button.
42
+
43
+
The demo will:
44
+
1. Resolve your PDS endpoint from your DID
45
+
2. Create a DPoP-authenticated XRPC client
46
+
3. Call `app.bsky.actor.getProfile` to fetch your profile
47
+
4. Display:
48
+
- Your avatar
49
+
- Display name and handle
50
+
- Bio/description
51
+
- Follower count
52
+
- Following count
53
+
- Post count
54
+
- Raw JSON response from the API
55
+
56
+
## Understanding the Code
57
+
58
+
### Custom Scopes Configuration
59
+
60
+
Located in [main.go:97-101](main.go#L97-L101):
61
+
62
+
```go
63
+
client := bskyoauth.NewClientWithOptions(bskyoauth.ClientOptions{
64
+
BaseURL: baseURL,
65
+
HTTPClient: httpClient,
66
+
Scopes: []string{"atproto", "transition:generic", "transition:chat.bsky"},
67
+
})
68
+
```
69
+
70
+
### XRPC Profile Fetch
71
+
72
+
Located in [main.go:297-346](main.go#L297-L346):
73
+
74
+
The `getProfile` function shows how to:
75
+
1. Resolve a user's PDS endpoint from their DID
76
+
2. Create an XRPC client with DPoP authentication
77
+
3. Make authenticated API calls
78
+
4. Handle DPoP nonce updates
79
+
80
+
### Key Components
81
+
82
+
- **DPoP Transport**: Handles proof-of-possession token authentication
83
+
- **Identity Resolution**: Converts DIDs to PDS endpoints
84
+
- **XRPC Client**: Direct access to AT Protocol APIs
85
+
- **Token Refresh**: Automatic refresh of expired access tokens
86
+
87
+
## Environment Variables
88
+
89
+
Customize the demo with these environment variables:
90
+
91
+
```bash
92
+
# Server configuration
93
+
export BASE_URL="http://localhost:8182"
94
+
export SERVER_PORT="8182"
95
+
96
+
# Session timeout
97
+
export SESSION_TIMEOUT_DAYS="30"
98
+
99
+
# Rate limiting (format: "requests/sec,burst")
100
+
export RATE_LIMIT_AUTH="5,10"
101
+
export RATE_LIMIT_API="10,20"
102
+
```
103
+
104
+
## Running Both Demos Simultaneously
105
+
106
+
You can run both the standard demo and this chat demo at the same time:
107
+
108
+
**Terminal 1** (Standard Demo):
109
+
```bash
110
+
cd examples/web-demo
111
+
go run main.go
112
+
# Runs on http://localhost:8181
113
+
```
114
+
115
+
**Terminal 2** (Chat Demo):
116
+
```bash
117
+
cd examples/web-demo-chat
118
+
go run main.go
119
+
# Runs on http://localhost:8182
120
+
```
121
+
122
+
## Troubleshooting
123
+
124
+
### Port Already in Use
125
+
126
+
If port 8182 is in use, change it:
127
+
128
+
```bash
129
+
export SERVER_PORT="8183"
130
+
export BASE_URL="http://localhost:8183"
131
+
go run main.go
132
+
```
133
+
134
+
### Token Refresh Issues
135
+
136
+
If you see token refresh errors:
137
+
1. The demo automatically refreshes tokens when they expire
138
+
2. Check the console logs for detailed error messages
139
+
3. Try logging out and logging in again
140
+
141
+
### Profile Fetch Fails
142
+
143
+
If profile fetching fails:
144
+
1. Ensure you have a valid Bluesky account
145
+
2. Check that your handle/DID resolves correctly
146
+
3. Look for error messages in the console logs
147
+
148
+
## Next Steps
149
+
150
+
Extend this demo to:
151
+
- Fetch and display user posts
152
+
- Implement chat message retrieval using the chat scope
153
+
- Add timeline/feed functionality
154
+
- Implement search features
155
+
- Add notification handling
156
+
157
+
## Security Notes
158
+
159
+
- **Development Only**: This demo uses HTTP for local development
160
+
- **Production**: Use HTTPS by setting `BASE_URL` to an `https://` URL
161
+
- **Session Storage**: Uses in-memory storage (cleared on restart)
162
+
- **For Production**: Implement persistent session storage (Redis, database, etc.)
+135
examples/web-demo-scope/README.md
+135
examples/web-demo-scope/README.md
···
1
+
# Bluesky Chat OAuth Demo
2
+
3
+
This example demonstrates how to use the bskyoauth library with additional OAuth scopes, specifically the `transition:chat.bsky` scope. It shows how to:
4
+
5
+
1. Request multiple OAuth scopes including the chat scope
6
+
2. Use XRPC to call Bluesky APIs directly
7
+
3. Fetch and display user profile information using `app.bsky.actor.getProfile`
8
+
9
+
## Features
10
+
11
+
- **Extended OAuth Scopes**: Requests `atproto`, `transition:generic`, and `transition:chat.bsky` scopes
12
+
- **XRPC Integration**: Direct XRPC calls to fetch user profile data
13
+
- **Profile Display**: Shows user avatar, display name, handle, bio, and stats (followers, following, posts)
14
+
- **DPoP Authentication**: Properly handles DPoP tokens for authenticated requests
15
+
- **Token Refresh**: Automatic token refresh when tokens expire
16
+
17
+
## Configuration
18
+
19
+
The demo uses the following environment variables:
20
+
21
+
- `BASE_URL`: Base URL for the OAuth client (default: `http://localhost:8182`)
22
+
- `SERVER_PORT`: Port to run the server on (default: `8182`)
23
+
- `SESSION_TIMEOUT_DAYS`: Session timeout in days (default: `30`)
24
+
- `RATE_LIMIT_AUTH`: Auth endpoint rate limit in "req/sec,burst" format (default: `5,10`)
25
+
- `RATE_LIMIT_API`: API endpoint rate limit in "req/sec,burst" format (default: `10,20`)
26
+
27
+
## Running the Demo
28
+
29
+
1. Start the server:
30
+
31
+
```bash
32
+
cd examples/web-demo-chat
33
+
go run main.go
34
+
```
35
+
36
+
2. Open your browser to `http://localhost:8182`
37
+
38
+
3. Log in with your Bluesky handle (e.g., `yourname.bsky.social`)
39
+
40
+
4. After authentication, click "View My Profile (XRPC)" to see your profile fetched via XRPC
41
+
42
+
## How It Works
43
+
44
+
### Custom Scopes
45
+
46
+
The demo creates a client with custom scopes:
47
+
48
+
```go
49
+
client := bskyoauth.NewClientWithOptions(bskyoauth.ClientOptions{
50
+
BaseURL: baseURL,
51
+
HTTPClient: httpClient,
52
+
Scopes: []string{"atproto", "transition:generic", "transition:chat.bsky"},
53
+
})
54
+
```
55
+
56
+
### XRPC Profile Fetch
57
+
58
+
The `getProfile` function demonstrates direct XRPC usage:
59
+
60
+
```go
61
+
func getProfile(ctx context.Context, session *bskyoauth.Session) (*bsky.ActorDefs_ProfileViewDetailed, error) {
62
+
// Resolve PDS endpoint for the user
63
+
dir := identity.DefaultDirectory()
64
+
atid, err := syntax.ParseAtIdentifier(session.DID)
65
+
if err != nil {
66
+
return nil, fmt.Errorf("failed to parse DID: %w", err)
67
+
}
68
+
69
+
ident, err := dir.Lookup(ctx, *atid)
70
+
if err != nil {
71
+
return nil, fmt.Errorf("failed to lookup identity: %w", err)
72
+
}
73
+
74
+
pdsHost := ident.PDSEndpoint()
75
+
76
+
// Create DPoP transport for authenticated requests
77
+
transport := bskyoauth.NewDPoPTransport(
78
+
http.DefaultTransport,
79
+
session.DPoPKey,
80
+
session.AccessToken,
81
+
session.DPoPNonce,
82
+
)
83
+
84
+
httpClient := &http.Client{
85
+
Transport: transport,
86
+
}
87
+
88
+
xrpcClient := &xrpc.Client{
89
+
Host: pdsHost,
90
+
Client: httpClient,
91
+
}
92
+
93
+
// Call app.bsky.actor.getProfile
94
+
output, err := bsky.ActorGetProfile(ctx, xrpcClient, session.DID)
95
+
if err != nil {
96
+
return nil, fmt.Errorf("failed to get profile: %w", err)
97
+
}
98
+
99
+
return output, nil
100
+
}
101
+
```
102
+
103
+
## API Endpoints
104
+
105
+
- `GET /` - Home page with login form
106
+
- `GET /login?handle=<handle>` - Start OAuth flow
107
+
- `GET /callback` - OAuth callback handler
108
+
- `GET /profile` - Fetch and display user profile via XRPC
109
+
- `GET /logout` - Logout and clear session
110
+
- `GET /client-metadata.json` - OAuth client metadata
111
+
112
+
## Differences from Standard Demo
113
+
114
+
This demo differs from the standard web-demo in several ways:
115
+
116
+
1. **Additional Scope**: Includes `transition:chat.bsky` scope for chat-related APIs
117
+
2. **XRPC Usage**: Demonstrates direct XRPC client usage instead of using the high-level API
118
+
3. **Profile Display**: Shows how to fetch and render user profile data
119
+
4. **Different Port**: Runs on port 8182 by default to avoid conflicts with the standard demo
120
+
121
+
## Security Notes
122
+
123
+
- The demo runs on HTTP by default for local development
124
+
- In production, use HTTPS (set `BASE_URL` to an `https://` URL)
125
+
- Sessions are stored in memory and will be lost on restart
126
+
- For production, implement a persistent session store (Redis, database, etc.)
127
+
128
+
## Extending This Demo
129
+
130
+
You can extend this demo to:
131
+
132
+
- Fetch chat messages using the chat scope
133
+
- Display user posts and timelines
134
+
- Implement chat functionality
135
+
- Add more XRPC endpoints (search, notifications, etc.)
+73
examples/web-demo-scope/go.mod
+73
examples/web-demo-scope/go.mod
···
1
+
module example.com/web-demo-chat
2
+
3
+
go 1.25.3
4
+
5
+
replace github.com/shindakun/bskyoauth => ../..
6
+
7
+
require (
8
+
github.com/bluesky-social/indigo v0.0.0-20251029012702-8c31d8b88187
9
+
github.com/shindakun/bskyoauth v0.0.0-00010101000000-000000000000
10
+
golang.org/x/time v0.14.0
11
+
)
12
+
13
+
require (
14
+
github.com/beorn7/perks v1.0.1 // indirect
15
+
github.com/cespare/xxhash/v2 v2.3.0 // indirect
16
+
github.com/earthboundkid/versioninfo/v2 v2.24.1 // indirect
17
+
github.com/felixge/httpsnoop v1.0.4 // indirect
18
+
github.com/go-logr/logr v1.4.3 // indirect
19
+
github.com/go-logr/stdr v1.2.2 // indirect
20
+
github.com/gogo/protobuf v1.3.2 // indirect
21
+
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
22
+
github.com/google/uuid v1.6.0 // indirect
23
+
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
24
+
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
25
+
github.com/hashicorp/golang-lru v1.0.2 // indirect
26
+
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
27
+
github.com/ipfs/bbloom v0.0.4 // indirect
28
+
github.com/ipfs/boxo v0.35.0 // indirect
29
+
github.com/ipfs/go-block-format v0.2.3 // indirect
30
+
github.com/ipfs/go-cid v0.6.0 // indirect
31
+
github.com/ipfs/go-datastore v0.9.0 // indirect
32
+
github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect
33
+
github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect
34
+
github.com/ipfs/go-ipld-cbor v0.2.1 // indirect
35
+
github.com/ipfs/go-ipld-format v0.6.3 // indirect
36
+
github.com/ipfs/go-log v1.0.5 // indirect
37
+
github.com/ipfs/go-log/v2 v2.8.2 // indirect
38
+
github.com/ipfs/go-metrics-interface v0.3.0 // indirect
39
+
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
40
+
github.com/mattn/go-isatty v0.0.20 // indirect
41
+
github.com/minio/sha256-simd v1.0.1 // indirect
42
+
github.com/mr-tron/base58 v1.2.0 // indirect
43
+
github.com/multiformats/go-base32 v0.1.0 // indirect
44
+
github.com/multiformats/go-base36 v0.2.0 // indirect
45
+
github.com/multiformats/go-multibase v0.2.0 // indirect
46
+
github.com/multiformats/go-multihash v0.2.3 // indirect
47
+
github.com/multiformats/go-varint v0.1.0 // indirect
48
+
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
49
+
github.com/opentracing/opentracing-go v1.2.0 // indirect
50
+
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect
51
+
github.com/prometheus/client_golang v1.23.2 // indirect
52
+
github.com/prometheus/client_model v0.6.2 // indirect
53
+
github.com/prometheus/common v0.67.2 // indirect
54
+
github.com/prometheus/procfs v0.19.1 // indirect
55
+
github.com/spaolacci/murmur3 v1.1.0 // indirect
56
+
github.com/whyrusleeping/cbor-gen v0.3.1 // indirect
57
+
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect
58
+
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
59
+
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
60
+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
61
+
go.opentelemetry.io/otel v1.38.0 // indirect
62
+
go.opentelemetry.io/otel/metric v1.38.0 // indirect
63
+
go.opentelemetry.io/otel/trace v1.38.0 // indirect
64
+
go.uber.org/atomic v1.11.0 // indirect
65
+
go.uber.org/multierr v1.11.0 // indirect
66
+
go.uber.org/zap v1.27.0 // indirect
67
+
go.yaml.in/yaml/v2 v2.4.3 // indirect
68
+
golang.org/x/crypto v0.43.0 // indirect
69
+
golang.org/x/sys v0.37.0 // indirect
70
+
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
71
+
google.golang.org/protobuf v1.36.10 // indirect
72
+
lukechampine.com/blake3 v1.4.1 // indirect
73
+
)
+235
examples/web-demo-scope/go.sum
+235
examples/web-demo-scope/go.sum
···
1
+
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
2
+
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
3
+
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
4
+
github.com/bluesky-social/indigo v0.0.0-20251029012702-8c31d8b88187 h1:qLP5xM4nuPfSNEAouQmXcNK2XkH+zzhfNcZMytjBodw=
5
+
github.com/bluesky-social/indigo v0.0.0-20251029012702-8c31d8b88187/go.mod h1:GuGAU33qKulpZCZNPcUeIQ4RW6KzNvOy7s8MSUXbAng=
6
+
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
7
+
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
8
+
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
9
+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10
+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
11
+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
12
+
github.com/earthboundkid/versioninfo/v2 v2.24.1 h1:SJTMHaoUx3GzjjnUO1QzP3ZXK6Ee/nbWyCm58eY3oUg=
13
+
github.com/earthboundkid/versioninfo/v2 v2.24.1/go.mod h1:VcWEooDEuyUJnMfbdTh0uFN4cfEIg+kHMuWB2CDCLjw=
14
+
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
15
+
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
16
+
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
17
+
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
18
+
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
19
+
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
20
+
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
21
+
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
22
+
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
23
+
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
24
+
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
25
+
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
26
+
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
27
+
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
28
+
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
29
+
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
30
+
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
31
+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
32
+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
33
+
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
34
+
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4=
35
+
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
36
+
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
37
+
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
38
+
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
39
+
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
40
+
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
41
+
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
42
+
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
43
+
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
44
+
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
45
+
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
46
+
github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
47
+
github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
48
+
github.com/ipfs/boxo v0.35.0 h1:3Mku5arSbAZz0dvb4goXRsQuZkFkPrGr5yYdu0YM1pY=
49
+
github.com/ipfs/boxo v0.35.0/go.mod h1:uhaF0DGnbgEiXDTmD249jCGbxVkMm6+Ew85q6Uub7lo=
50
+
github.com/ipfs/go-block-format v0.2.3 h1:mpCuDaNXJ4wrBJLrtEaGFGXkferrw5eqVvzaHhtFKQk=
51
+
github.com/ipfs/go-block-format v0.2.3/go.mod h1:WJaQmPAKhD3LspLixqlqNFxiZ3BZ3xgqxxoSR/76pnA=
52
+
github.com/ipfs/go-cid v0.6.0 h1:DlOReBV1xhHBhhfy/gBNNTSyfOM6rLiIx9J7A4DGf30=
53
+
github.com/ipfs/go-cid v0.6.0/go.mod h1:NC4kS1LZjzfhK40UGmpXv5/qD2kcMzACYJNntCUiDhQ=
54
+
github.com/ipfs/go-datastore v0.9.0 h1:WocriPOayqalEsueHv6SdD4nPVl4rYMfYGLD4bqCZ+w=
55
+
github.com/ipfs/go-datastore v0.9.0/go.mod h1:uT77w/XEGrvJWwHgdrMr8bqCN6ZTW9gzmi+3uK+ouHg=
56
+
github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk=
57
+
github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps=
58
+
github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ=
59
+
github.com/ipfs/go-ipfs-blockstore v1.3.1/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE=
60
+
github.com/ipfs/go-ipfs-ds-help v1.1.1 h1:B5UJOH52IbcfS56+Ul+sv8jnIV10lbjLF5eOO0C66Nw=
61
+
github.com/ipfs/go-ipfs-ds-help v1.1.1/go.mod h1:75vrVCkSdSFidJscs8n4W+77AtTpCIAdDGAwjitJMIo=
62
+
github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0=
63
+
github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs=
64
+
github.com/ipfs/go-ipld-cbor v0.2.1 h1:H05yEJbK/hxg0uf2AJhyerBDbjOuHX4yi+1U/ogRa7E=
65
+
github.com/ipfs/go-ipld-cbor v0.2.1/go.mod h1:x9Zbeq8CoE5R2WicYgBMcr/9mnkQ0lHddYWJP2sMV3A=
66
+
github.com/ipfs/go-ipld-format v0.6.3 h1:9/lurLDTotJpZSuL++gh3sTdmcFhVkCwsgx2+rAh4j8=
67
+
github.com/ipfs/go-ipld-format v0.6.3/go.mod h1:74ilVN12NXVMIV+SrBAyC05UJRk0jVvGqdmrcYZvCBk=
68
+
github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8=
69
+
github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo=
70
+
github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g=
71
+
github.com/ipfs/go-log/v2 v2.8.2 h1:nVG4nNHUwwI/sTs9Bi5iE8sXFQwXs3AjkkuWhg7+Y2I=
72
+
github.com/ipfs/go-log/v2 v2.8.2/go.mod h1:UhIYAwMV7Nb4ZmihUxfIRM2Istw/y9cAk3xaK+4Zs2c=
73
+
github.com/ipfs/go-metrics-interface v0.3.0 h1:YwG7/Cy4R94mYDUuwsBfeziJCVm9pBMJ6q/JR9V40TU=
74
+
github.com/ipfs/go-metrics-interface v0.3.0/go.mod h1:OxxQjZDGocXVdyTPocns6cOLwHieqej/jos7H4POwoY=
75
+
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
76
+
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
77
+
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
78
+
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
79
+
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
80
+
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
81
+
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
82
+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
83
+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
84
+
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
85
+
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
86
+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
87
+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
88
+
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
89
+
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
90
+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
91
+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
92
+
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
93
+
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
94
+
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
95
+
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
96
+
github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
97
+
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
98
+
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
99
+
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
100
+
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
101
+
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
102
+
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
103
+
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
104
+
github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI=
105
+
github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI=
106
+
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
107
+
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
108
+
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
109
+
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
110
+
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
111
+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
112
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
113
+
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0=
114
+
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw=
115
+
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
116
+
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
117
+
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
118
+
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
119
+
github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8=
120
+
github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko=
121
+
github.com/prometheus/procfs v0.19.1 h1:QVtROpTkphuXuNlnCv3m1ut3JytkXHtQ3xvck/YmzMM=
122
+
github.com/prometheus/procfs v0.19.1/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
123
+
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
124
+
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
125
+
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
126
+
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
127
+
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
128
+
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
129
+
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
130
+
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
131
+
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
132
+
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
133
+
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
134
+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
135
+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
136
+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
137
+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
138
+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
139
+
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
140
+
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
141
+
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
142
+
github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0=
143
+
github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so=
144
+
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
145
+
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
146
+
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA=
147
+
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8=
148
+
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q=
149
+
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I=
150
+
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
151
+
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
152
+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
153
+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
154
+
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
155
+
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
156
+
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
157
+
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
158
+
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
159
+
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
160
+
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
161
+
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
162
+
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
163
+
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
164
+
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
165
+
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
166
+
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
167
+
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
168
+
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
169
+
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
170
+
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
171
+
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
172
+
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
173
+
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
174
+
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
175
+
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
176
+
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
177
+
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
178
+
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
179
+
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
180
+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
181
+
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
182
+
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
183
+
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
184
+
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
185
+
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
186
+
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
187
+
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
188
+
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
189
+
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
190
+
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
191
+
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
192
+
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
193
+
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
194
+
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
195
+
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
196
+
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
197
+
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
198
+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
199
+
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
200
+
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
201
+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
202
+
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
203
+
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
204
+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
205
+
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
206
+
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
207
+
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
208
+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
209
+
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
210
+
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
211
+
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
212
+
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
213
+
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
214
+
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
215
+
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
216
+
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
217
+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
218
+
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
219
+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
220
+
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
221
+
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
222
+
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
223
+
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
224
+
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
225
+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
226
+
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
227
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
228
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
229
+
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
230
+
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
231
+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
232
+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
233
+
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
234
+
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
235
+
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
+529
examples/web-demo-scope/main.go
+529
examples/web-demo-scope/main.go
···
1
+
package main
2
+
3
+
import (
4
+
"context"
5
+
"encoding/json"
6
+
"fmt"
7
+
"log"
8
+
"net/http"
9
+
"os"
10
+
"strconv"
11
+
"strings"
12
+
"time"
13
+
14
+
"github.com/bluesky-social/indigo/api/bsky"
15
+
"github.com/bluesky-social/indigo/atproto/identity"
16
+
"github.com/bluesky-social/indigo/atproto/syntax"
17
+
"github.com/bluesky-social/indigo/xrpc"
18
+
"golang.org/x/time/rate"
19
+
20
+
"github.com/shindakun/bskyoauth"
21
+
)
22
+
23
+
// getEnvInt returns an integer environment variable or default value.
24
+
func getEnvInt(key string, defaultVal int) int {
25
+
if val := os.Getenv(key); val != "" {
26
+
if parsed, err := strconv.Atoi(val); err == nil {
27
+
return parsed
28
+
}
29
+
log.Printf("Warning: Invalid %s value '%s', using default: %d", key, val, defaultVal)
30
+
}
31
+
return defaultVal
32
+
}
33
+
34
+
// getRateLimitConfig parses "requests/sec,burst" format (e.g., "5,10").
35
+
func getRateLimitConfig(key string, defaultReqSec float64, defaultBurst int) (float64, int) {
36
+
if val := os.Getenv(key); val != "" {
37
+
parts := strings.Split(val, ",")
38
+
if len(parts) == 2 {
39
+
reqSec, err1 := strconv.ParseFloat(strings.TrimSpace(parts[0]), 64)
40
+
burst, err2 := strconv.Atoi(strings.TrimSpace(parts[1]))
41
+
if err1 == nil && err2 == nil && reqSec > 0 && burst > 0 {
42
+
return reqSec, burst
43
+
}
44
+
}
45
+
log.Printf("Warning: Invalid %s format '%s', using defaults: %.0f,%d", key, val, defaultReqSec, defaultBurst)
46
+
}
47
+
return defaultReqSec, defaultBurst
48
+
}
49
+
50
+
func main() {
51
+
// Load configuration from environment variables
52
+
baseURL := os.Getenv("BASE_URL")
53
+
if baseURL == "" {
54
+
baseURL = "http://localhost:8182"
55
+
}
56
+
57
+
serverPort := os.Getenv("SERVER_PORT")
58
+
if serverPort == "" {
59
+
serverPort = "8182"
60
+
}
61
+
62
+
sessionTimeoutDays := getEnvInt("SESSION_TIMEOUT_DAYS", 30)
63
+
authReqSec, authBurst := getRateLimitConfig("RATE_LIMIT_AUTH", 5, 10)
64
+
apiReqSec, apiBurst := getRateLimitConfig("RATE_LIMIT_API", 10, 20)
65
+
66
+
// Configure structured logging based on environment
67
+
logger := bskyoauth.NewLoggerFromEnv(baseURL)
68
+
bskyoauth.SetLogger(logger)
69
+
70
+
// Security check: warn if not using HTTPS in non-local environments
71
+
if !strings.HasPrefix(baseURL, "https://") && !strings.Contains(baseURL, "localhost") && !strings.Contains(baseURL, "127.0.0.1") {
72
+
log.Println("WARNING: BASE_URL is not using HTTPS!")
73
+
log.Println("OAuth flows over HTTP expose credentials to interception.")
74
+
log.Println("HTTPS is REQUIRED for production deployments.")
75
+
}
76
+
77
+
// Create OAuth client with chat scope
78
+
httpClient := &http.Client{
79
+
Timeout: 30 * time.Second,
80
+
}
81
+
82
+
client := bskyoauth.NewClientWithOptions(bskyoauth.ClientOptions{
83
+
BaseURL: baseURL,
84
+
HTTPClient: httpClient,
85
+
Scopes: []string{"atproto", "transition:generic", "transition:chat.bsky"},
86
+
})
87
+
88
+
// Create rate limiters
89
+
authLimiter := bskyoauth.NewRateLimiter(rate.Limit(authReqSec), authBurst)
90
+
authLimiter.StartCleanup(5*time.Minute, 10*time.Minute)
91
+
92
+
apiLimiter := bskyoauth.NewRateLimiter(rate.Limit(apiReqSec), apiBurst)
93
+
apiLimiter.StartCleanup(5*time.Minute, 10*time.Minute)
94
+
95
+
// Set up HTTP handlers with rate limiting
96
+
mux := http.NewServeMux()
97
+
mux.HandleFunc("/", homeHandler(client))
98
+
mux.HandleFunc("/client-metadata.json", client.ClientMetadataHandler())
99
+
mux.HandleFunc("/login", authLimiter.Middleware(loginHandler(client)))
100
+
mux.HandleFunc("/callback", authLimiter.Middleware(client.CallbackHandler(callbackSuccessHandler(sessionTimeoutDays))))
101
+
mux.HandleFunc("/profile", apiLimiter.Middleware(profileHandler(client)))
102
+
mux.HandleFunc("/logout", logoutHandler(client))
103
+
104
+
// Apply security headers middleware
105
+
handler := bskyoauth.SecurityHeadersMiddleware()(mux)
106
+
107
+
// Display configuration
108
+
log.Printf("Chat Demo Server starting on :%s", serverPort)
109
+
log.Println("Base URL:", baseURL)
110
+
log.Println("OAuth Scopes: atproto, transition:generic, transition:chat.bsky")
111
+
if strings.HasPrefix(baseURL, "https://") {
112
+
log.Println("Using HTTPS - secure configuration")
113
+
}
114
+
115
+
log.Printf("Rate limiting enabled:")
116
+
log.Printf(" - Auth endpoints: %.0f req/s (burst: %d)", authReqSec, authBurst)
117
+
log.Printf(" - API endpoints: %.0f req/s (burst: %d)", apiReqSec, apiBurst)
118
+
log.Printf("Session timeout: %d days", sessionTimeoutDays)
119
+
120
+
// Configure HTTP server with timeouts
121
+
server := &http.Server{
122
+
Addr: ":" + serverPort,
123
+
Handler: handler,
124
+
ReadTimeout: 15 * time.Second,
125
+
WriteTimeout: 15 * time.Second,
126
+
IdleTimeout: 60 * time.Second,
127
+
}
128
+
129
+
log.Fatal(server.ListenAndServe())
130
+
}
131
+
132
+
// checkAndRefreshToken checks if the access token is expired and refreshes it if needed.
133
+
func checkAndRefreshToken(client *bskyoauth.Client, sessionID string, session *bskyoauth.Session, r *http.Request) (*bskyoauth.Session, error) {
134
+
if session.IsAccessTokenExpired(5 * time.Minute) {
135
+
log.Printf("Access token expired or expiring soon, attempting refresh for session: %s", sessionID)
136
+
137
+
requestID := bskyoauth.GenerateRequestID()
138
+
ctx := bskyoauth.WithRequestID(r.Context(), requestID)
139
+
140
+
newSession, err := client.RefreshToken(ctx, session)
141
+
if err != nil {
142
+
log.Printf("[%s] Token refresh failed: %v", requestID, err)
143
+
return nil, err
144
+
}
145
+
146
+
err = client.UpdateSession(sessionID, newSession)
147
+
if err != nil {
148
+
log.Printf("[%s] Failed to update session after refresh: %v", requestID, err)
149
+
return nil, err
150
+
}
151
+
152
+
log.Printf("[%s] Token refresh successful for session: %s", requestID, sessionID)
153
+
return newSession, nil
154
+
}
155
+
156
+
return session, nil
157
+
}
158
+
159
+
func homeHandler(client *bskyoauth.Client) http.HandlerFunc {
160
+
return func(w http.ResponseWriter, r *http.Request) {
161
+
sessionID, err := r.Cookie("session_id")
162
+
163
+
html := `<!DOCTYPE html>
164
+
<html>
165
+
<head>
166
+
<title>Bluesky Chat OAuth Demo</title>
167
+
<style>
168
+
body { font-family: Arial, sans-serif; max-width: 800px; margin: 50px auto; padding: 20px; }
169
+
h1 { color: #0066cc; }
170
+
.info { background: #f0f8ff; padding: 15px; border-radius: 5px; margin: 20px 0; }
171
+
.scope { font-family: monospace; background: #e0e0e0; padding: 2px 5px; border-radius: 3px; }
172
+
button, a.button {
173
+
background: #0066cc;
174
+
color: white;
175
+
padding: 10px 20px;
176
+
border: none;
177
+
border-radius: 5px;
178
+
cursor: pointer;
179
+
text-decoration: none;
180
+
display: inline-block;
181
+
margin: 5px;
182
+
}
183
+
button:hover, a.button:hover { background: #0052a3; }
184
+
input[type="text"] {
185
+
padding: 10px;
186
+
width: 300px;
187
+
border: 1px solid #ccc;
188
+
border-radius: 5px;
189
+
}
190
+
</style>
191
+
</head>
192
+
<body>
193
+
<h1>Bluesky Chat OAuth Demo</h1>
194
+
<div class="info">
195
+
<p><strong>This demo uses the chat scope:</strong></p>
196
+
<p>OAuth Scopes: <span class="scope">atproto</span> <span class="scope">transition:generic</span> <span class="scope">transition:chat.bsky</span></p>
197
+
<p>It demonstrates how to request additional scopes and use XRPC to fetch user profile data.</p>
198
+
</div>`
199
+
200
+
if err == nil {
201
+
session, err := client.GetSession(sessionID.Value)
202
+
if err == nil && session != nil {
203
+
html += fmt.Sprintf(`
204
+
<p>Logged in as: <strong>%s</strong></p>
205
+
<div>
206
+
<a href="/profile" class="button">View My Profile (XRPC)</a>
207
+
<a href="/logout" class="button">Logout</a>
208
+
</div>`, session.DID)
209
+
} else {
210
+
html += loginForm()
211
+
}
212
+
} else {
213
+
html += loginForm()
214
+
}
215
+
216
+
html += `</body></html>`
217
+
w.Header().Set("Content-Type", "text/html")
218
+
if _, err := w.Write([]byte(html)); err != nil {
219
+
log.Printf("Error writing response: %v", err)
220
+
}
221
+
}
222
+
}
223
+
224
+
func loginForm() string {
225
+
return `
226
+
<h2>Login to Bluesky</h2>
227
+
<form action="/login" method="get">
228
+
<input type="text" name="handle" placeholder="your-handle.bsky.social" required>
229
+
<button type="submit">Login with Bluesky</button>
230
+
</form>`
231
+
}
232
+
233
+
func loginHandler(client *bskyoauth.Client) http.HandlerFunc {
234
+
return func(w http.ResponseWriter, r *http.Request) {
235
+
requestID := bskyoauth.GenerateRequestID()
236
+
ctx := bskyoauth.WithRequestID(r.Context(), requestID)
237
+
238
+
handle := r.URL.Query().Get("handle")
239
+
if handle == "" {
240
+
http.Error(w, "handle parameter required", http.StatusBadRequest)
241
+
return
242
+
}
243
+
244
+
log.Printf("[%s] Starting auth flow for handle: %s", requestID, handle)
245
+
246
+
flowState, err := client.StartAuthFlow(ctx, handle)
247
+
if err != nil {
248
+
log.Printf("[%s] Failed to start auth flow: %v", requestID, err)
249
+
http.Error(w, "Failed to start auth flow: "+err.Error(), http.StatusInternalServerError)
250
+
return
251
+
}
252
+
253
+
log.Printf("[%s] Redirecting to: %s", requestID, flowState.AuthURL)
254
+
http.Redirect(w, r, flowState.AuthURL, http.StatusFound)
255
+
}
256
+
}
257
+
258
+
func callbackSuccessHandler(sessionTimeoutDays int) func(http.ResponseWriter, *http.Request, string) {
259
+
return func(w http.ResponseWriter, r *http.Request, sessionID string) {
260
+
requestID := bskyoauth.GenerateRequestID()
261
+
log.Printf("[%s] OAuth callback successful, session: %s", requestID, sessionID)
262
+
263
+
baseURL := os.Getenv("BASE_URL")
264
+
if baseURL == "" {
265
+
baseURL = "http://localhost:8182"
266
+
}
267
+
isSecure := strings.HasPrefix(baseURL, "https://")
268
+
269
+
http.SetCookie(w, &http.Cookie{
270
+
Name: "session_id",
271
+
Value: sessionID,
272
+
Path: "/",
273
+
HttpOnly: true,
274
+
Secure: isSecure,
275
+
SameSite: http.SameSiteLaxMode,
276
+
MaxAge: sessionTimeoutDays * 86400,
277
+
})
278
+
279
+
http.Redirect(w, r, "/", http.StatusFound)
280
+
}
281
+
}
282
+
283
+
func profileHandler(client *bskyoauth.Client) http.HandlerFunc {
284
+
return func(w http.ResponseWriter, r *http.Request) {
285
+
sessionID, err := r.Cookie("session_id")
286
+
if err != nil {
287
+
http.Redirect(w, r, "/", http.StatusFound)
288
+
return
289
+
}
290
+
291
+
session, err := client.GetSession(sessionID.Value)
292
+
if err != nil || session == nil {
293
+
http.Redirect(w, r, "/", http.StatusFound)
294
+
return
295
+
}
296
+
297
+
// Check and refresh token if needed
298
+
session, err = checkAndRefreshToken(client, sessionID.Value, session, r)
299
+
if err != nil {
300
+
log.Printf("Token refresh failed, redirecting to login: %v", err)
301
+
http.Redirect(w, r, "/", http.StatusFound)
302
+
return
303
+
}
304
+
305
+
// Get the user's profile using XRPC
306
+
profile, err := getProfile(r.Context(), session)
307
+
if err != nil {
308
+
http.Error(w, "Failed to get profile: "+err.Error(), http.StatusInternalServerError)
309
+
return
310
+
}
311
+
312
+
// Display profile as HTML
313
+
renderProfilePage(w, profile)
314
+
}
315
+
}
316
+
317
+
// getProfile fetches the user's profile using XRPC with app.bsky.actor.getProfile
318
+
func getProfile(ctx context.Context, session *bskyoauth.Session) (*bsky.ActorDefs_ProfileViewDetailed, error) {
319
+
log.Printf("Fetching profile for DID: %s", session.DID)
320
+
321
+
// Resolve PDS endpoint for the user
322
+
dir := identity.DefaultDirectory()
323
+
atid, err := syntax.ParseAtIdentifier(session.DID)
324
+
if err != nil {
325
+
return nil, fmt.Errorf("failed to parse DID: %w", err)
326
+
}
327
+
328
+
ident, err := dir.Lookup(ctx, *atid)
329
+
if err != nil {
330
+
return nil, fmt.Errorf("failed to lookup identity: %w", err)
331
+
}
332
+
333
+
pdsHost := ident.PDSEndpoint()
334
+
335
+
// Create DPoP transport
336
+
transport := bskyoauth.NewDPoPTransport(
337
+
http.DefaultTransport,
338
+
session.DPoPKey,
339
+
session.AccessToken,
340
+
session.DPoPNonce,
341
+
)
342
+
343
+
httpClient := &http.Client{
344
+
Transport: transport,
345
+
}
346
+
347
+
xrpcClient := &xrpc.Client{
348
+
Host: pdsHost,
349
+
Client: httpClient,
350
+
}
351
+
352
+
// Call app.bsky.actor.getProfile
353
+
output, err := bsky.ActorGetProfile(ctx, xrpcClient, session.DID)
354
+
if err != nil {
355
+
return nil, fmt.Errorf("failed to get profile: %w", err)
356
+
}
357
+
358
+
// Update session with the latest nonce if available
359
+
if nonceGetter, ok := transport.(interface{ GetNonce() string }); ok {
360
+
session.DPoPNonce = nonceGetter.GetNonce()
361
+
}
362
+
363
+
log.Printf("Successfully fetched profile for: %s", output.Handle)
364
+
return output, nil
365
+
}
366
+
367
+
func renderProfilePage(w http.ResponseWriter, profile *bsky.ActorDefs_ProfileViewDetailed) {
368
+
// Convert profile to JSON for display
369
+
profileJSON, _ := json.MarshalIndent(profile, "", " ")
370
+
371
+
html := `<!DOCTYPE html>
372
+
<html>
373
+
<head>
374
+
<title>Profile - Bluesky Chat OAuth Demo</title>
375
+
<style>
376
+
body { font-family: Arial, sans-serif; max-width: 1000px; margin: 50px auto; padding: 20px; }
377
+
h1 { color: #0066cc; }
378
+
.profile-header {
379
+
background: #f0f8ff;
380
+
padding: 20px;
381
+
border-radius: 10px;
382
+
margin: 20px 0;
383
+
display: flex;
384
+
align-items: center;
385
+
gap: 20px;
386
+
}
387
+
.profile-avatar {
388
+
width: 80px;
389
+
height: 80px;
390
+
border-radius: 50%;
391
+
object-fit: cover;
392
+
}
393
+
.profile-info h2 { margin: 0 0 5px 0; }
394
+
.profile-info .handle { color: #666; }
395
+
.profile-stats {
396
+
display: flex;
397
+
gap: 20px;
398
+
margin: 20px 0;
399
+
}
400
+
.stat {
401
+
background: #e8f4f8;
402
+
padding: 15px;
403
+
border-radius: 5px;
404
+
text-align: center;
405
+
}
406
+
.stat-value { font-size: 24px; font-weight: bold; color: #0066cc; }
407
+
.stat-label { font-size: 14px; color: #666; }
408
+
.profile-json {
409
+
background: #f5f5f5;
410
+
padding: 20px;
411
+
border-radius: 5px;
412
+
overflow-x: auto;
413
+
margin: 20px 0;
414
+
}
415
+
pre { margin: 0; font-size: 12px; }
416
+
.actions { margin: 20px 0; }
417
+
.actions a {
418
+
background: #0066cc;
419
+
color: white;
420
+
padding: 10px 20px;
421
+
text-decoration: none;
422
+
border-radius: 5px;
423
+
display: inline-block;
424
+
}
425
+
.actions a:hover { background: #0052a3; }
426
+
</style>
427
+
</head>
428
+
<body>
429
+
<h1>User Profile</h1>
430
+
431
+
<div class="profile-header">`
432
+
433
+
if profile.Avatar != nil && *profile.Avatar != "" {
434
+
html += fmt.Sprintf(`
435
+
<img src="%s" alt="Avatar" class="profile-avatar">`, *profile.Avatar)
436
+
}
437
+
438
+
displayName := profile.Handle
439
+
if profile.DisplayName != nil && *profile.DisplayName != "" {
440
+
displayName = *profile.DisplayName
441
+
}
442
+
443
+
html += fmt.Sprintf(`
444
+
<div class="profile-info">
445
+
<h2>%s</h2>
446
+
<p class="handle">@%s</p>`, displayName, profile.Handle)
447
+
448
+
if profile.Description != nil && *profile.Description != "" {
449
+
html += fmt.Sprintf(`
450
+
<p>%s</p>`, *profile.Description)
451
+
}
452
+
453
+
html += `
454
+
</div>
455
+
</div>
456
+
457
+
<div class="profile-stats">`
458
+
459
+
if profile.FollowersCount != nil {
460
+
html += fmt.Sprintf(`
461
+
<div class="stat">
462
+
<div class="stat-value">%d</div>
463
+
<div class="stat-label">Followers</div>
464
+
</div>`, *profile.FollowersCount)
465
+
}
466
+
467
+
if profile.FollowsCount != nil {
468
+
html += fmt.Sprintf(`
469
+
<div class="stat">
470
+
<div class="stat-value">%d</div>
471
+
<div class="stat-label">Following</div>
472
+
</div>`, *profile.FollowsCount)
473
+
}
474
+
475
+
if profile.PostsCount != nil {
476
+
html += fmt.Sprintf(`
477
+
<div class="stat">
478
+
<div class="stat-value">%d</div>
479
+
<div class="stat-label">Posts</div>
480
+
</div>`, *profile.PostsCount)
481
+
}
482
+
483
+
html += `
484
+
</div>
485
+
486
+
<h3>Raw Profile Data (XRPC Response)</h3>
487
+
<div class="profile-json">
488
+
<pre>` + string(profileJSON) + `</pre>
489
+
</div>
490
+
491
+
<div class="actions">
492
+
<a href="/">← Back to Home</a>
493
+
</div>
494
+
</body>
495
+
</html>`
496
+
497
+
w.Header().Set("Content-Type", "text/html")
498
+
w.WriteHeader(http.StatusOK)
499
+
w.Write([]byte(html))
500
+
}
501
+
502
+
func logoutHandler(client *bskyoauth.Client) http.HandlerFunc {
503
+
return func(w http.ResponseWriter, r *http.Request) {
504
+
sessionID, err := r.Cookie("session_id")
505
+
if err == nil {
506
+
if err := client.DeleteSession(sessionID.Value); err != nil {
507
+
log.Printf("Error deleting session %s: %v", sessionID.Value, err)
508
+
}
509
+
}
510
+
511
+
baseURL := os.Getenv("BASE_URL")
512
+
if baseURL == "" {
513
+
baseURL = "http://localhost:8182"
514
+
}
515
+
isSecure := strings.HasPrefix(baseURL, "https://")
516
+
517
+
http.SetCookie(w, &http.Cookie{
518
+
Name: "session_id",
519
+
Value: "",
520
+
Path: "/",
521
+
MaxAge: -1,
522
+
HttpOnly: true,
523
+
Secure: isSecure,
524
+
SameSite: http.SameSiteLaxMode,
525
+
})
526
+
527
+
http.Redirect(w, r, "/", http.StatusFound)
528
+
}
529
+
}