Vibe-guided bskyoauth and custom repo example code in Golang 🤖 probably not safe to use in prod

Add web-demo-scope example demonstrating custom OAuth scopes

- Rename web-demo-chat to web-demo-scope for clarity
- Add web-demo-scope executable to .gitignore
- Example demonstrates requesting custom scopes beyond default atproto scope

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Changed files
+1135
examples
+1
.gitignore
··· 34 34 35 35 # Demo executables 36 36 examples/web-demo/web-demo 37 + examples/web-demo-scope/web-demo-scope 37 38 38 39 # Environment files 39 40 .env
+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
··· 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
··· 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
··· 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
··· 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 + }