A fork of https://github.com/teal-fm/piper

atproto oauth :muscle:

Natalie B 7e6bd1a6 0e5b2801

+1286 -131
+5
config/config.go
··· 25 25 viper.SetDefault("tracker.interval", 30) 26 26 viper.SetDefault("db.path", "./data/piper.db") 27 27 28 + // server metadata 29 + viper.SetDefault("server.root_url", "http://localhost:8080") 30 + viper.SetDefault("atproto.metadata_url", "http://localhost:8080/metadata") 31 + viper.SetDefault("atproto.callback_url", "/metadata") 32 + 28 33 // Configure Viper to read environment variables 29 34 viper.AutomaticEnv() 30 35
+153
db/atproto.go
··· 1 + package db 2 + 3 + import ( 4 + "database/sql" 5 + "encoding/json" 6 + "fmt" 7 + "time" 8 + 9 + oauth "github.com/haileyok/atproto-oauth-golang" 10 + "github.com/haileyok/atproto-oauth-golang/helpers" 11 + "github.com/lestrrat-go/jwx/v2/jwk" 12 + "github.com/teal-fm/piper/models" 13 + ) 14 + 15 + type ATprotoAuthData struct { 16 + State string `json:"state"` 17 + DID string `json:"did"` 18 + PDSUrl string `json:"pds_url"` 19 + AuthServerIssuer string `json:"authserver_issuer"` 20 + PKCEVerifier string `json:"pkce_verifier"` 21 + DPoPAuthServerNonce string `json:"dpop_authserver_nonce"` 22 + DPoPPrivateJWK jwk.Key `json:"dpop_private_jwk"` 23 + CreatedAt time.Time `json:"created_at"` 24 + } 25 + 26 + func (db *DB) SaveATprotoAuthData(data *models.ATprotoAuthData) error { 27 + dpopPrivateJWKBytes, err := json.Marshal(data.DPoPPrivateJWK) 28 + if err != nil { 29 + return err 30 + } 31 + 32 + _, err = db.Exec(` 33 + INSERT INTO atproto_auth_data (state, did, pds_url, authserver_issuer, pkce_verifier, dpop_authserver_nonce, dpop_private_jwk) 34 + VALUES (?, ?, ?, ?, ?, ?, ?)`, 35 + data.State, data.DID, data.PDSUrl, data.AuthServerIssuer, data.PKCEVerifier, data.DPoPAuthServerNonce, string(dpopPrivateJWKBytes)) 36 + 37 + return err 38 + } 39 + 40 + func (db *DB) GetATprotoAuthData(state string) (*models.ATprotoAuthData, error) { 41 + var data models.ATprotoAuthData 42 + var dpopPrivateJWKString string // Temporary variable to hold the JSON string 43 + 44 + err := db.QueryRow(` 45 + SELECT state, did, pds_url, authserver_issuer, pkce_verifier, dpop_authserver_nonce, dpop_private_jwk 46 + FROM atproto_auth_data 47 + WHERE state = ?`, 48 + state).Scan( 49 + &data.State, 50 + &data.DID, 51 + &data.PDSUrl, 52 + &data.AuthServerIssuer, 53 + &data.PKCEVerifier, 54 + &data.DPoPAuthServerNonce, 55 + &dpopPrivateJWKString, // Scan into the temporary string 56 + ) 57 + if err != nil { 58 + // Return the original scan error if it occurred 59 + if err == sql.ErrNoRows { 60 + return nil, fmt.Errorf("no auth data found for state %s: %w", state, err) 61 + } 62 + return nil, fmt.Errorf("failed to scan auth data for state %s: %w", state, err) 63 + } 64 + 65 + key, err := helpers.ParseJWKFromBytes([]byte(dpopPrivateJWKString)) 66 + if err != nil { 67 + // Return an error if parsing fails 68 + return nil, fmt.Errorf("failed to parse DPoPPrivateJWK for state %s: %w", state, err) 69 + } 70 + data.DPoPPrivateJWK = key 71 + 72 + return &data, nil // Return nil error on success 73 + } 74 + 75 + func (db *DB) FindOrCreateUserByDID(did string) (*models.User, error) { 76 + var user models.User 77 + err := db.QueryRow(` 78 + SELECT id, atproto_did, created_at, updated_at 79 + FROM users 80 + WHERE atproto_did = ?`, 81 + did).Scan(&user.ID, &user.ATProtoDID, &user.CreatedAt, &user.UpdatedAt) 82 + 83 + if err == sql.ErrNoRows { 84 + now := time.Now() 85 + // create user! 86 + result, insertErr := db.Exec(` 87 + INSERT INTO users (atproto_did, created_at, updated_at) 88 + VALUES (?, ?, ?) 89 + `, 90 + did, 91 + now, 92 + now) 93 + if insertErr != nil { 94 + return nil, fmt.Errorf("failed to create user: %w", insertErr) 95 + } 96 + lastID, idErr := result.LastInsertId() 97 + if idErr != nil { 98 + return nil, fmt.Errorf("failed to get last insert id: %w", idErr) 99 + } 100 + // Populate the user struct with the newly created user's data 101 + user.ID = lastID 102 + user.ATProtoDID = &did 103 + user.CreatedAt = now 104 + user.UpdatedAt = now 105 + return &user, nil // Return the created user and nil error 106 + } else if err != nil { 107 + // Handle other potential errors from QueryRow 108 + return nil, fmt.Errorf("failed to find user by DID: %w", err) 109 + } 110 + 111 + return &user, err 112 + } 113 + 114 + // Create or update the current user's ATproto session data. 115 + func (db *DB) SaveATprotoSession(tokenResp *oauth.TokenResponse) error { 116 + 117 + expiryTime := time.Now().Add(time.Second * time.Duration(tokenResp.ExpiresIn)) 118 + now := time.Now() 119 + 120 + result, err := db.Exec(` 121 + UPDATE users 122 + SET atproto_access_token = ?, 123 + atproto_refresh_token = ?, 124 + atproto_token_expiry = ?, 125 + atproto_scope = ?, 126 + atproto_token_type = ?, 127 + updated_at = ? 128 + WHERE atproto_did = ?`, 129 + tokenResp.AccessToken, 130 + tokenResp.RefreshToken, 131 + expiryTime, 132 + tokenResp.Scope, 133 + tokenResp.TokenType, 134 + now, 135 + tokenResp.Sub, 136 + ) 137 + 138 + if err != nil { 139 + return fmt.Errorf("failed to update atproto session for did %s: %w", tokenResp.Sub, err) 140 + } 141 + 142 + rowsAffected, err := result.RowsAffected() 143 + if err != nil { 144 + // Error checking RowsAffected, but the update might have succeeded 145 + return fmt.Errorf("failed to check rows affected after updating atproto session for did %s: %w", tokenResp.Sub, err) 146 + } 147 + 148 + if rowsAffected == 0 { 149 + return fmt.Errorf("no user found with did %s to update session, creating new session", tokenResp.Sub) 150 + } 151 + 152 + return nil 153 + }
+98 -15
db/db.go
··· 2 2 3 3 import ( 4 4 "database/sql" 5 + "encoding/json" 5 6 "os" 6 7 "path/filepath" 7 8 "time" ··· 39 40 _, err := db.Exec(` 40 41 CREATE TABLE IF NOT EXISTS users ( 41 42 id INTEGER PRIMARY KEY AUTOINCREMENT, 42 - username TEXT NOT NULL, 43 - email TEXT UNIQUE, 44 - spotify_id TEXT UNIQUE, 45 - access_token TEXT, 46 - refresh_token TEXT, 47 - token_expiry TIMESTAMP, 48 - created_at TIMESTAMP, 49 - updated_at TIMESTAMP 43 + username TEXT, -- Made nullable, might not have username initially 44 + email TEXT UNIQUE, -- Made nullable 45 + spotify_id TEXT UNIQUE, -- Spotify specific ID 46 + access_token TEXT, -- Spotify access token 47 + refresh_token TEXT, -- Spotify refresh token 48 + token_expiry TIMESTAMP, -- Spotify token expiry 49 + atproto_did TEXT UNIQUE, -- Atproto DID (identifier) 50 + atproto_access_token TEXT, -- Atproto access token 51 + atproto_refresh_token TEXT, -- Atproto refresh token 52 + atproto_token_expiry TIMESTAMP, -- Atproto token expiry 53 + atproto_scope TEXT, -- Atproto token scope 54 + atproto_token_type TEXT, -- Atproto token type 55 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- Use default 56 + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -- Use default 50 57 )`) 51 58 if err != nil { 52 59 return err ··· 73 80 return err 74 81 } 75 82 83 + _, err = db.Exec(` 84 + CREATE TABLE IF NOT EXISTS atproto_auth_data ( 85 + id INTEGER PRIMARY KEY AUTOINCREMENT, 86 + state TEXT NOT NULL, 87 + did TEXT, 88 + pds_url TEXT NOT NULL, 89 + authserver_issuer TEXT NOT NULL, 90 + pkce_verifier TEXT NOT NULL, 91 + dpop_authserver_nonce TEXT NOT NULL, 92 + dpop_private_jwk TEXT NOT NULL, 93 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 94 + )`) 95 + if err != nil { 96 + return err 97 + } 98 + 76 99 return nil 77 100 } 78 101 102 + // create user without spotify id 79 103 func (db *DB) CreateUser(user *models.User) (int64, error) { 80 104 now := time.Now() 81 105 82 106 result, err := db.Exec(` 83 - INSERT INTO users (username, email, spotify_id, access_token, refresh_token, token_expiry, created_at, updated_at) 84 - VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, 85 - user.Username, user.Email, user.SpotifyID, user.AccessToken, user.RefreshToken, user.TokenExpiry, now, now) 107 + INSERT INTO users (username, email, created_at, updated_at) 108 + VALUES (?, ?, ?, ?)`, 109 + user.Username, user.Email, now, now) 86 110 87 111 if err != nil { 88 112 return 0, err ··· 91 115 return result.LastInsertId() 92 116 } 93 117 118 + // Add spotify session to user, returning the updated user 119 + func (db *DB) AddSpotifySession(userID int64, username, email, spotifyId, accessToken, refreshToken string, tokenExpiry time.Time) (*models.User, error) { 120 + now := time.Now() 121 + 122 + _, err := db.Exec(` 123 + UPDATE users SET username = ?, email = ?, spotify_id = ?, access_token = ?, refresh_token = ?, token_expiry = ?, created_at = ?, updated_at = ? 124 + WHERE id == ? 125 + `, 126 + username, email, spotifyId, accessToken, refreshToken, tokenExpiry, now, now, userID) 127 + if err != nil { 128 + return nil, err 129 + } 130 + 131 + user, err := db.GetUserByID(userID) 132 + if err != nil { 133 + return nil, err 134 + } 135 + 136 + return user, err 137 + } 138 + 139 + func (db *DB) GetUserByID(ID int64) (*models.User, error) { 140 + user := &models.User{} 141 + 142 + err := db.QueryRow(` 143 + SELECT id, username, email, spotify_id, access_token, refresh_token, token_expiry, created_at, updated_at 144 + FROM users WHERE id = ?`, ID).Scan( 145 + &user.ID, &user.Username, &user.Email, &user.SpotifyID, 146 + &user.AccessToken, &user.RefreshToken, &user.TokenExpiry, 147 + &user.CreatedAt, &user.UpdatedAt) 148 + 149 + if err == sql.ErrNoRows { 150 + return nil, nil 151 + } 152 + 153 + if err != nil { 154 + return nil, err 155 + } 156 + 157 + return user, nil 158 + } 159 + 94 160 func (db *DB) GetUserBySpotifyID(spotifyID string) (*models.User, error) { 95 161 user := &models.User{} 96 162 ··· 126 192 127 193 func (db *DB) SaveTrack(userID int64, track *models.Track) (int64, error) { 128 194 // Convert the Artist array to a string for storage 129 - // In a production environment, you'd want to use proper JSON serialization 130 195 artistString := "" 131 196 if len(track.Artist) > 0 { 132 - artistString = track.Artist[0].Name 197 + bytes, err := json.Marshal(track.Artist) 198 + if err != nil { 199 + return 0, err 200 + } else { 201 + artistString = string(bytes) 202 + } 133 203 } 134 204 135 205 var trackID int64 ··· 149 219 // In a production environment, you'd want to use proper JSON serialization 150 220 artistString := "" 151 221 if len(track.Artist) > 0 { 152 - artistString = track.Artist[0].Name 222 + bytes, err := json.Marshal(track.Artist) 223 + if err != nil { 224 + return err 225 + } else { 226 + artistString = string(bytes) 227 + } 153 228 } 154 229 155 230 _, err := db.Exec(` ··· 173 248 } 174 249 175 250 func (db *DB) GetRecentTracks(userID int64, limit int) ([]*models.Track, error) { 251 + // convert previous-format artist strings to current-format 252 + 176 253 rows, err := db.Query(` 177 254 SELECT id, name, artist, album, url, timestamp, duration_ms, progress_ms, service_base_url, isrc, has_stamped 178 255 FROM tracks ··· 209 286 } 210 287 211 288 // Convert the artist string to the Artist array structure 212 - track.Artist = []models.Artist{{Name: artistString}} 289 + var artists []models.Artist 290 + err = json.Unmarshal([]byte(artistString), &artists) 291 + if err != nil { 292 + // fallback to previous format 293 + artists = []models.Artist{{Name: artistString}} 294 + } 295 + track.Artist = artists 213 296 tracks = append(tracks, track) 214 297 } 215 298
+65 -6
go.mod
··· 3 3 go 1.24.0 4 4 5 5 require ( 6 + github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188 7 + github.com/haileyok/atproto-oauth-golang v0.0.2 8 + github.com/joho/godotenv v1.5.1 9 + github.com/lestrrat-go/jwx/v2 v2.0.12 10 + github.com/mattn/go-sqlite3 v1.14.27 11 + github.com/spf13/viper v1.20.1 12 + golang.org/x/oauth2 v0.28.0 13 + ) 14 + 15 + require ( 6 16 dario.cat/mergo v1.0.1 // indirect 7 17 github.com/air-verse/air v1.61.7 // indirect 8 18 github.com/bep/godartsass v1.2.0 // indirect 9 19 github.com/bep/godartsass/v2 v2.1.0 // indirect 10 20 github.com/bep/golibsass v1.2.0 // indirect 21 + github.com/carlmjohnson/versioninfo v0.22.5 // indirect 11 22 github.com/cli/safeexec v1.0.1 // indirect 12 23 github.com/creack/pty v1.1.23 // indirect 24 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect 13 25 github.com/fatih/color v1.17.0 // indirect 26 + github.com/felixge/httpsnoop v1.0.4 // indirect 14 27 github.com/fsnotify/fsnotify v1.8.0 // indirect 28 + github.com/go-logr/logr v1.4.2 // indirect 29 + github.com/go-logr/stdr v1.2.2 // indirect 15 30 github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 16 31 github.com/gobwas/glob v0.2.3 // indirect 32 + github.com/goccy/go-json v0.10.2 // indirect 33 + github.com/gogo/protobuf v1.3.2 // indirect 17 34 github.com/gohugoio/hugo v0.134.3 // indirect 18 - github.com/joho/godotenv v1.5.1 // indirect 35 + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect 36 + github.com/google/uuid v1.6.0 // indirect 37 + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 38 + github.com/hashicorp/go-retryablehttp v0.7.5 // indirect 39 + github.com/hashicorp/golang-lru v1.0.2 // indirect 40 + github.com/ipfs/bbloom v0.0.4 // indirect 41 + github.com/ipfs/go-block-format v0.2.0 // indirect 42 + github.com/ipfs/go-cid v0.4.1 // indirect 43 + github.com/ipfs/go-datastore v0.6.0 // indirect 44 + github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect 45 + github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect 46 + github.com/ipfs/go-ipfs-util v0.0.3 // indirect 47 + github.com/ipfs/go-ipld-cbor v0.1.0 // indirect 48 + github.com/ipfs/go-ipld-format v0.6.0 // indirect 49 + github.com/ipfs/go-log v1.0.5 // indirect 50 + github.com/ipfs/go-log/v2 v2.5.1 // indirect 51 + github.com/ipfs/go-metrics-interface v0.0.1 // indirect 52 + github.com/jbenet/goprocess v0.1.4 // indirect 53 + github.com/klauspost/cpuid/v2 v2.2.7 // indirect 54 + github.com/lestrrat-go/blackmagic v1.0.2 // indirect 55 + github.com/lestrrat-go/httpcc v1.0.1 // indirect 56 + github.com/lestrrat-go/httprc v1.0.4 // indirect 57 + github.com/lestrrat-go/iter v1.0.2 // indirect 58 + github.com/lestrrat-go/option v1.0.1 // indirect 19 59 github.com/mattn/go-colorable v0.1.13 // indirect 20 60 github.com/mattn/go-isatty v0.0.20 // indirect 21 - github.com/mattn/go-sqlite3 v1.14.27 // indirect 61 + github.com/minio/sha256-simd v1.0.1 // indirect 62 + github.com/mr-tron/base58 v1.2.0 // indirect 63 + github.com/multiformats/go-base32 v0.1.0 // indirect 64 + github.com/multiformats/go-base36 v0.2.0 // indirect 65 + github.com/multiformats/go-multibase v0.2.0 // indirect 66 + github.com/multiformats/go-multihash v0.2.3 // indirect 67 + github.com/multiformats/go-varint v0.0.7 // indirect 68 + github.com/opentracing/opentracing-go v1.2.0 // indirect 22 69 github.com/pelletier/go-toml v1.9.5 // indirect 23 70 github.com/pelletier/go-toml/v2 v2.2.3 // indirect 71 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect 24 72 github.com/sagikazarmark/locafero v0.7.0 // indirect 73 + github.com/segmentio/asm v1.2.0 // indirect 25 74 github.com/sourcegraph/conc v0.3.0 // indirect 75 + github.com/spaolacci/murmur3 v1.1.0 // indirect 26 76 github.com/spf13/afero v1.12.0 // indirect 27 77 github.com/spf13/cast v1.7.1 // indirect 28 78 github.com/spf13/pflag v1.0.6 // indirect 29 - github.com/spf13/viper v1.20.1 // indirect 30 79 github.com/subosito/gotenv v1.6.0 // indirect 31 80 github.com/tdewolff/parse/v2 v2.7.15 // indirect 32 - go.uber.org/atomic v1.9.0 // indirect 33 - go.uber.org/multierr v1.9.0 // indirect 34 - golang.org/x/oauth2 v0.28.0 // indirect 81 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e // indirect 82 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect 83 + go.opentelemetry.io/otel v1.29.0 // indirect 84 + go.opentelemetry.io/otel/metric v1.29.0 // indirect 85 + go.opentelemetry.io/otel/trace v1.29.0 // indirect 86 + go.uber.org/atomic v1.11.0 // indirect 87 + go.uber.org/multierr v1.11.0 // indirect 88 + go.uber.org/zap v1.26.0 // indirect 89 + golang.org/x/crypto v0.32.0 // indirect 35 90 golang.org/x/sys v0.29.0 // indirect 36 91 golang.org/x/text v0.21.0 // indirect 92 + golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect 37 93 google.golang.org/protobuf v1.36.1 // indirect 38 94 gopkg.in/yaml.v3 v3.0.1 // indirect 95 + lukechampine.com/blake3 v1.2.1 // indirect 39 96 ) 40 97 41 98 tool github.com/air-verse/air 99 + 100 + replace github.com/haileyok/atproto-oauth-golang => /Users/natalie/code/atproto-oauth-golang
+225
go.sum
··· 4 4 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 5 5 github.com/air-verse/air v1.61.7 h1:MtOZs6wYoYYXm+S4e+ORjkq9BjvyEamKJsHcvko8LrQ= 6 6 github.com/air-verse/air v1.61.7/go.mod h1:QW4HkIASdtSnwaYof1zgJCSxd41ebvix10t5ubtm9cg= 7 + github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 7 8 github.com/bep/godartsass v1.2.0 h1:E2VvQrxAHAFwbjyOIExAMmogTItSKodoKuijNrGm5yU= 8 9 github.com/bep/godartsass v1.2.0/go.mod h1:6LvK9RftsXMxGfsA0LDV12AGc4Jylnu6NgHL+Q5/pE8= 9 10 github.com/bep/godartsass/v2 v2.1.0 h1:fq5Y1xYf4diu4tXABiekZUCA+5l/dmNjGKCeQwdy+s0= 10 11 github.com/bep/godartsass/v2 v2.1.0/go.mod h1:AcP8QgC+OwOXEq6im0WgDRYK7scDsmZCEW62o1prQLo= 11 12 github.com/bep/golibsass v1.2.0 h1:nyZUkKP/0psr8nT6GR2cnmt99xS93Ji82ZD9AgOK6VI= 12 13 github.com/bep/golibsass v1.2.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA= 14 + github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188 h1:1sQaG37xk08/rpmdhrmMkfQWF9kZbnfHm9Zav3bbSMk= 15 + github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188/go.mod h1:NVBwZvbBSa93kfyweAmKwOLYawdVHdwZ9s+GZtBBVLA= 16 + github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc= 17 + github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8= 13 18 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 14 19 github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= 15 20 github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00= 16 21 github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= 17 22 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 23 + github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 18 24 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 19 25 github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= 20 26 github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= 21 27 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 22 28 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 29 + github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 30 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= 31 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 23 32 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 24 33 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 25 34 github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= 26 35 github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= 36 + github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 37 + github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 27 38 github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= 28 39 github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= 29 40 github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 30 41 github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 31 42 github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= 32 43 github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 44 + github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 45 + github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 46 + github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 47 + github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 48 + github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 33 49 github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= 34 50 github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 51 + github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= 35 52 github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= 36 53 github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= 54 + github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 55 + github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 56 + github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 57 + github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 37 58 github.com/gohugoio/hugo v0.134.3 h1:Pn2KECXAAQWCd2uryDcmtzVhNJWGF5Pt6CplQvLcWe0= 38 59 github.com/gohugoio/hugo v0.134.3/go.mod h1:/1gnGxlWfAzQarxcQ+tMvKw4e/IMBwy0DFbRxORwOtY= 60 + github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= 61 + github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 39 62 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 40 63 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 41 64 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= ··· 52 75 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 53 76 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 54 77 github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= 78 + github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 79 + github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 80 + github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 81 + github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 82 + github.com/haileyok/atproto-oauth-golang v0.0.2 h1:61KPkLB615LQXR2f5x1v3sf6vPe6dOXqNpTYCgZ0Fz8= 83 + github.com/haileyok/atproto-oauth-golang v0.0.2/go.mod h1:jcZ4GCjo5I5RuE/RsAXg1/b6udw7R4W+2rb/cGyTDK8= 84 + github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 85 + github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 86 + github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= 87 + github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= 88 + github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= 89 + github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 90 + github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 91 + github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= 92 + github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= 93 + github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs= 94 + github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM= 95 + github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= 96 + github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= 97 + github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk= 98 + github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= 99 + github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ= 100 + github.com/ipfs/go-ipfs-blockstore v1.3.1/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE= 101 + github.com/ipfs/go-ipfs-ds-help v1.1.1 h1:B5UJOH52IbcfS56+Ul+sv8jnIV10lbjLF5eOO0C66Nw= 102 + github.com/ipfs/go-ipfs-ds-help v1.1.1/go.mod h1:75vrVCkSdSFidJscs8n4W+77AtTpCIAdDGAwjitJMIo= 103 + github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0= 104 + github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= 105 + github.com/ipfs/go-ipld-cbor v0.1.0 h1:dx0nS0kILVivGhfWuB6dUpMa/LAwElHPw1yOGYopoYs= 106 + github.com/ipfs/go-ipld-cbor v0.1.0/go.mod h1:U2aYlmVrJr2wsUBU67K4KgepApSZddGRDWBYR0H4sCk= 107 + github.com/ipfs/go-ipld-format v0.6.0 h1:VEJlA2kQ3LqFSIm5Vu6eIlSxD/Ze90xtc4Meten1F5U= 108 + github.com/ipfs/go-ipld-format v0.6.0/go.mod h1:g4QVMTn3marU3qXchwjpKPKgJv+zF+OlaKMyhJ4LHPg= 109 + github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= 110 + github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= 111 + github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= 112 + github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= 113 + github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= 114 + github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg= 115 + github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY= 116 + github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= 117 + github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= 118 + github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= 55 119 github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 56 120 github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 121 + github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 122 + github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 123 + github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 124 + github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 125 + github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 57 126 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 58 127 github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 59 128 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 60 129 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 61 130 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 131 + github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= 132 + github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= 133 + github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= 134 + github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= 135 + github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= 136 + github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= 137 + github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= 138 + github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= 139 + github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= 140 + github.com/lestrrat-go/jwx/v2 v2.0.12 h1:3d589+5w/b9b7S3DneICPW16AqTyYXB7VRjgluSDWeA= 141 + github.com/lestrrat-go/jwx/v2 v2.0.12/go.mod h1:Mq4KN1mM7bp+5z/W5HS8aCNs5RKZ911G/0y2qUjAQuQ= 142 + github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 143 + github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= 144 + github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 62 145 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 63 146 github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 147 + github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 64 148 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 65 149 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 66 150 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 67 151 github.com/mattn/go-sqlite3 v1.14.27 h1:drZCnuvf37yPfs95E5jd9s3XhdVWLal+6BOK6qrv6IU= 68 152 github.com/mattn/go-sqlite3 v1.14.27/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 153 + github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 154 + github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 155 + github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 156 + github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 157 + github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= 158 + github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= 159 + github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= 160 + github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= 161 + github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= 162 + github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= 163 + github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= 164 + github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 165 + github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= 166 + github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= 167 + github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= 168 + github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 69 169 github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= 70 170 github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 71 171 github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 72 172 github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 173 + github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 73 174 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 175 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0= 176 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= 74 177 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 178 + github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 75 179 github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 180 + github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 76 181 github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= 77 182 github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= 183 + github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= 184 + github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= 185 + github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 186 + github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= 187 + github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= 78 188 github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 79 189 github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 190 + github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 191 + github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 80 192 github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 81 193 github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 82 194 github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= ··· 90 202 github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= 91 203 github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= 92 204 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 205 + github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 206 + github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 207 + github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 93 208 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 209 + github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 210 + github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 211 + github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 212 + github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 213 + github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 214 + github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 94 215 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 95 216 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 96 217 github.com/tdewolff/parse/v2 v2.7.15 h1:hysDXtdGZIRF5UZXwpfn3ZWRbm+ru4l53/ajBRGpCTw= 97 218 github.com/tdewolff/parse/v2 v2.7.15/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA= 98 219 github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= 220 + github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 221 + github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= 222 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4= 223 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 224 + github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 225 + github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 226 + github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 227 + github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 228 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= 229 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= 230 + go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= 231 + go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= 232 + go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= 233 + go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= 234 + go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= 235 + go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= 236 + go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 237 + go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 99 238 go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= 100 239 go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 240 + go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 241 + go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 242 + go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 243 + go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 244 + go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 101 245 go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= 102 246 go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= 247 + go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 248 + go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 249 + go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 250 + go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= 251 + go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= 252 + go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= 253 + go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 103 254 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 255 + golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 256 + golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 257 + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 258 + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 259 + golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= 260 + golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= 261 + golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= 104 262 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 105 263 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 106 264 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 107 265 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 266 + golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 267 + golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 268 + golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 269 + golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 270 + golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 271 + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 272 + golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 108 273 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 109 274 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 110 275 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 111 276 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 277 + golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 278 + golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 279 + golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 280 + golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 281 + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 282 + golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 283 + golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 284 + golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 285 + golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 112 286 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 113 287 golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= 114 288 golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 115 289 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 116 290 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 117 291 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 292 + golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 293 + golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 294 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 295 + golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 296 + golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 118 297 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 119 298 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 299 + golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 300 + golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 301 + golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 302 + golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 303 + golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 304 + golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 305 + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 306 + golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 307 + golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 308 + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 120 309 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 310 + golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 121 311 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 312 + golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 313 + golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 122 314 golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= 123 315 golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 124 316 golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 125 317 golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 318 + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 319 + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 320 + golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 321 + golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 322 + golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= 126 323 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 324 + golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 325 + golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 326 + golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 327 + golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 328 + golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 127 329 golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= 128 330 golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 129 331 golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 130 332 golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 333 + golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 131 334 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 132 335 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 133 336 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 337 + golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 134 338 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 339 + golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 340 + golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 341 + golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 342 + golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 343 + golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 344 + golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 345 + golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 346 + golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 347 + golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 348 + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 349 + golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 135 350 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 351 + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 352 + golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk= 353 + golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 136 354 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 137 355 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 138 356 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= ··· 156 374 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 157 375 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 158 376 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 377 + gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 378 + gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 379 + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 380 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 159 381 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 160 382 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 161 383 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 162 384 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 385 + honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 386 + lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= 387 + lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
+59 -11
main.go
··· 5 5 "fmt" 6 6 "log" 7 7 "net/http" 8 + "os" 8 9 "time" 9 10 10 11 "github.com/spf13/viper" 11 12 "github.com/teal-fm/piper/config" 12 13 "github.com/teal-fm/piper/db" 13 14 "github.com/teal-fm/piper/oauth" 15 + "github.com/teal-fm/piper/oauth/atproto" 14 16 apikeyService "github.com/teal-fm/piper/service/apikey" 15 17 "github.com/teal-fm/piper/service/spotify" 16 18 "github.com/teal-fm/piper/session" ··· 19 21 func home(w http.ResponseWriter, r *http.Request) { 20 22 w.Header().Set("Content-Type", "text/html") 21 23 22 - // Check if user is logged in 24 + // Check if user has an active session cookie 23 25 cookie, err := r.Cookie("session") 24 26 isLoggedIn := err == nil && cookie != nil 27 + // TODO: Add logic here to fetch user details from DB using session ID 28 + // to check if Spotify is already connected, if desired for finer control. 29 + // For now, we'll just check if *any* session exists. 25 30 26 31 html := ` 27 32 <html> ··· 57 62 </style> 58 63 </head> 59 64 <body> 60 - <h1>Piper - Multi-User Spotify Tracker</h1> 65 + <h1>Piper - Multi-User Spotify Tracker via ATProto</h1> 61 66 <div class="nav"> 62 67 <a href="/">Home</a>` 63 68 ··· 66 71 <a href="/current-track">Current Track</a> 67 72 <a href="/history">Track History</a> 68 73 <a href="/api-keys">API Keys</a> 74 + <a href="/login/spotify">Connect Spotify Account</a> <!-- Link to connect Spotify --> 69 75 <a href="/logout">Logout</a>` 70 76 } else { 71 77 html += ` 72 - <a href="/login/spotify">Login with Spotify</a>` 78 + <a href="/login/atproto">Login with ATProto</a>` // Primary login is ATProto 73 79 } 74 80 75 81 html += ` ··· 81 87 82 88 if !isLoggedIn { 83 89 html += ` 84 - <p><a href="/login/spotify">Login with Spotify</a> to get started!</p>` 90 + <p><a href="/login/atproto">Login with ATProto</a> to get started!</p>` // Prompt to login via ATProto 85 91 } else { 86 92 html += ` 87 - <p>You're logged in! Check out your <a href="/current-track">current track</a> or view your <a href="/history">listening history</a>.</p> 93 + <p>You're logged in! <a href="/login/spotify">Connect your Spotify account</a> to start tracking.</p> 94 + <p>Once connected, you can check out your <a href="/current-track">current track</a> or view your <a href="/history">listening history</a>.</p> 88 95 <p>You can also manage your <a href="/api-keys">API keys</a> for programmatic access.</p>` 89 96 } 90 97 91 98 html += ` 99 + </div> <!-- Close card div --> 92 100 </body> 93 101 </html> 94 - ` 102 + ` // Added closing div tag 95 103 96 104 w.Write([]byte(html)) 97 105 } ··· 158 166 log.Fatalf("Error initializing database: %v", err) 159 167 } 160 168 169 + spotifyService := spotify.NewSpotifyService(database) 170 + sessionManager := session.NewSessionManager() 161 171 oauthManager := oauth.NewOAuthServiceManager() 162 172 163 173 spotifyOAuth := oauth.NewOAuth2Service( ··· 166 176 viper.GetString("callback.spotify"), 167 177 viper.GetStringSlice("spotify.scopes"), 168 178 "spotify", 179 + spotifyService, 169 180 ) 170 - oauthManager.RegisterOAuth2Service("spotify", spotifyOAuth) 181 + oauthManager.RegisterService("spotify", spotifyOAuth) 182 + apiKeyService := apikeyService.NewAPIKeyService(database, sessionManager) 171 183 172 - spotifyService := spotify.NewSpotifyService(database) 173 - sessionManager := session.NewSessionManager() 174 - apiKeyService := apikeyService.NewAPIKeyService(database, sessionManager) 184 + // init atproto svc 185 + jwksBytes, err := os.ReadFile("./jwks.json") 186 + if err != nil { 187 + log.Fatalf("Error reading JWK file: %v", err) 188 + } 189 + 190 + jwks, err := atproto.LoadJwks(jwksBytes) 191 + if err != nil { 192 + log.Fatalf("Error loading JWK: %v", err) 193 + } 194 + 195 + atprotoService, err := atproto.NewATprotoAuthService( 196 + database, 197 + jwks, 198 + viper.GetString("atproto.client_id"), 199 + viper.GetString("atproto.callback_url"), 200 + ) 201 + if err != nil { 202 + log.Fatalf("Error creating ATproto auth service: %v", err) 203 + } 204 + 205 + oauthManager.RegisterService("atproto", atprotoService) 175 206 176 207 // Web browser routes 177 208 http.HandleFunc("/", home) 209 + 210 + // oauth (scraper) logins 178 211 http.HandleFunc("/login/spotify", oauthManager.HandleLogin("spotify")) 179 - http.HandleFunc("/callback/spotify", oauthManager.HandleCallback("spotify", spotifyService)) 212 + http.HandleFunc("/callback/spotify", session.WithPossibleAuth(oauthManager.HandleCallback("spotify"), sessionManager)) 213 + 214 + // atproto login 215 + http.HandleFunc("/login/atproto", oauthManager.HandleLogin("atproto")) 216 + http.HandleFunc("/callback/atproto", oauthManager.HandleCallback("atproto")) 217 + 180 218 http.HandleFunc("/current-track", session.WithAuth(spotifyService.HandleCurrentTrack, sessionManager)) 181 219 http.HandleFunc("/history", session.WithAuth(spotifyService.HandleTrackHistory, sessionManager)) 182 220 http.HandleFunc("/api-keys", session.WithAuth(apiKeyService.HandleAPIKeyManagement, sessionManager)) ··· 185 223 // API routes 186 224 http.HandleFunc("/api/v1/current-track", session.WithAPIAuth(apiCurrentTrack(spotifyService), sessionManager)) 187 225 http.HandleFunc("/api/v1/history", session.WithAPIAuth(apiTrackHistory(spotifyService), sessionManager)) 226 + 227 + serverUrlRoot := viper.GetString("server.root_url") 228 + atpClientId := viper.GetString("atproto.client_id") 229 + atpCallbackUrl := viper.GetString("atproto.callback_url") 230 + 231 + http.HandleFunc("/.well-known/client-metadata.json", func(w http.ResponseWriter, r *http.Request) { 232 + atprotoService.HandleClientMetadata(w, r, serverUrlRoot, atpClientId, atpCallbackUrl) 233 + }) 234 + 235 + http.HandleFunc("/oauth/jwks.json", atprotoService.HandleJwks) 188 236 189 237 trackerInterval := time.Duration(viper.GetInt("tracker.interval")) * time.Second 190 238
+17
models/atproto.go
··· 1 + // Add this struct definition to piper/models/atproto.go 2 + package models 3 + 4 + import ( 5 + "github.com/lestrrat-go/jwx/v2/jwk" 6 + ) 7 + 8 + type ATprotoAuthData struct { 9 + State string `json:"state"` 10 + DID string `json:"did"` 11 + PDSUrl string `json:"pds_url"` 12 + AuthServerIssuer string `json:"authserver_issuer"` 13 + PARState string `json:"par_state"` 14 + PKCEVerifier string `json:"pkce_verifier"` 15 + DPoPAuthServerNonce string `json:"dpop_authserver_nonce"` 16 + DPoPPrivateJWK jwk.Key `json:"dpop_private_jwk"` 17 + }
+14 -10
models/user.go
··· 4 4 5 5 // User represents a user of the application 6 6 type User struct { 7 - ID int64 `json:"id"` 8 - Username string `json:"username"` 9 - Email string `json:"email"` 10 - SpotifyID string `json:"spotify_id"` 11 - AccessToken string `json:"-"` // Not exposed in JSON 12 - RefreshToken string `json:"-"` // Not exposed in JSON 13 - TokenExpiry time.Time `json:"-"` // Not exposed in JSON 14 - CreatedAt time.Time `json:"created_at"` 15 - UpdatedAt time.Time `json:"updated_at"` 16 - } 7 + ID int64 8 + Username string 9 + Email *string // Use pointer for nullable fields 10 + SpotifyID *string // Use pointer for nullable fields 11 + AccessToken *string // Spotify Access Token 12 + RefreshToken *string // Spotify Refresh Token 13 + TokenExpiry *time.Time // Spotify Token Expiry 14 + CreatedAt time.Time 15 + UpdatedAt time.Time 16 + ATProtoDID *string // ATProto DID 17 + ATProtoAccessToken *string // ATProto Access Token 18 + ATProtoRefreshToken *string // ATProto Refresh Token 19 + ATProtoTokenExpiry *time.Time // ATProto Token Expiry 20 + }
+175
oauth/atproto/atproto.go
··· 1 + // Modify piper/oauth/atproto/atproto.go 2 + package atproto 3 + 4 + import ( 5 + "context" 6 + "fmt" 7 + "log" 8 + "net/http" 9 + "net/url" 10 + 11 + oauth "github.com/haileyok/atproto-oauth-golang" 12 + "github.com/haileyok/atproto-oauth-golang/helpers" 13 + "github.com/lestrrat-go/jwx/v2/jwk" 14 + "github.com/teal-fm/piper/db" 15 + "github.com/teal-fm/piper/models" 16 + // woof 17 + ) 18 + 19 + type ATprotoAuthService struct { 20 + client *oauth.Client 21 + jwks jwk.Key 22 + DB *db.DB 23 + clientId string 24 + } 25 + 26 + func NewATprotoAuthService(db *db.DB, jwks jwk.Key, clientId string, callbackUrl string) (*ATprotoAuthService, error) { 27 + fmt.Println(clientId, callbackUrl) 28 + cli, err := oauth.NewClient(oauth.ClientArgs{ 29 + ClientJwk: jwks, 30 + ClientId: clientId, 31 + RedirectUri: callbackUrl, 32 + }) 33 + if err != nil { 34 + return nil, fmt.Errorf("failed to create atproto oauth client: %w", err) 35 + } 36 + return &ATprotoAuthService{ 37 + client: cli, 38 + jwks: jwks, 39 + DB: db, 40 + clientId: clientId, 41 + }, nil 42 + } 43 + 44 + func LoadJwks(jwksBytes []byte) (jwk.Key, error) { 45 + key, err := helpers.ParseJWKFromBytes(jwksBytes) 46 + if err != nil { 47 + return nil, fmt.Errorf("failed to parse JWK from bytes: %w", err) 48 + } 49 + return key, nil 50 + } 51 + 52 + func (a *ATprotoAuthService) HandleLogin(w http.ResponseWriter, r *http.Request) { 53 + handle := r.URL.Query().Get("handle") 54 + if handle == "" { 55 + log.Printf("ATProto Login Error: handle is required") 56 + http.Error(w, "handle query parameter is required", http.StatusBadRequest) 57 + return 58 + } 59 + 60 + authUrl, err := a.getLoginUrlAndSaveState(r.Context(), handle) 61 + if err != nil { 62 + log.Printf("ATProto Login Error: Failed to get login URL for handle %s: %v", handle, err) 63 + http.Error(w, fmt.Sprintf("Error initiating login: %v", err), http.StatusInternalServerError) 64 + return 65 + } 66 + 67 + log.Printf("ATProto Login: Redirecting user %s to %s", handle, authUrl.String()) 68 + http.Redirect(w, r, authUrl.String(), http.StatusFound) 69 + } 70 + 71 + func (a *ATprotoAuthService) getLoginUrlAndSaveState(ctx context.Context, handle string) (*url.URL, error) { 72 + scope := "atproto" 73 + // resolve 74 + ui, err := a.getUserInformation(ctx, handle) 75 + if err != nil { 76 + return nil, fmt.Errorf("failed to get user information for %s: %w", handle, err) 77 + } 78 + 79 + // create a dpop jwk for this session 80 + k, err := helpers.GenerateKey(nil) // Generate ephemeral DPoP key for this flow 81 + if err != nil { 82 + return nil, fmt.Errorf("failed to generate DPoP key: %w", err) 83 + } 84 + 85 + // Send PAR auth req 86 + parResp, err := a.client.SendParAuthRequest(ctx, ui.AuthServer, ui.AuthMeta, ui.Handle, scope, k) 87 + if err != nil { 88 + return nil, fmt.Errorf("failed PAR request to %s: %w", ui.AuthServer, err) 89 + } 90 + 91 + // Save state including generated PKCE verifier and DPoP key 92 + data := &models.ATprotoAuthData{ 93 + State: parResp.State, 94 + DID: ui.DID, 95 + PDSUrl: ui.AuthServer, 96 + AuthServerIssuer: ui.AuthMeta.Issuer, 97 + PKCEVerifier: parResp.PkceVerifier, 98 + DPoPAuthServerNonce: parResp.DpopAuthserverNonce, 99 + DPoPPrivateJWK: k, 100 + } 101 + 102 + // print data 103 + fmt.Println(data) 104 + 105 + err = a.DB.SaveATprotoAuthData(data) 106 + if err != nil { 107 + return nil, fmt.Errorf("failed to save ATProto auth data for state %s: %w", parResp.State, err) 108 + } 109 + 110 + // Construct authorization URL using the request_uri from PAR response 111 + authEndpointURL, err := url.Parse(ui.AuthMeta.AuthorizationEndpoint) 112 + if err != nil { 113 + return nil, fmt.Errorf("invalid authorization endpoint URL %s: %w", ui.AuthMeta.AuthorizationEndpoint, err) 114 + } 115 + q := authEndpointURL.Query() 116 + q.Set("client_id", a.clientId) 117 + q.Set("request_uri", parResp.RequestUri) 118 + q.Set("state", parResp.State) 119 + authEndpointURL.RawQuery = q.Encode() 120 + 121 + return authEndpointURL, nil 122 + } 123 + 124 + func (a *ATprotoAuthService) HandleCallback(w http.ResponseWriter, r *http.Request) (int64, error) { 125 + state := r.URL.Query().Get("state") 126 + code := r.URL.Query().Get("code") 127 + issuer := r.URL.Query().Get("iss") // Issuer (PDS URL) is needed for token request 128 + 129 + if state == "" || code == "" || issuer == "" { 130 + errMsg := r.URL.Query().Get("error") 131 + errDesc := r.URL.Query().Get("error_description") 132 + log.Printf("ATProto Callback Error: Missing parameters. State: '%s', Code: '%s', Issuer: '%s'. Error: '%s', Description: '%s'", state, code, issuer, errMsg, errDesc) 133 + http.Error(w, fmt.Sprintf("Authorization callback failed: %s (%s). Missing state, code, or issuer.", errMsg, errDesc), http.StatusBadRequest) 134 + return 0, fmt.Errorf("missing state, code, or issuer") 135 + } 136 + 137 + // Retrieve saved data using state 138 + data, err := a.DB.GetATprotoAuthData(state) 139 + if err != nil { 140 + log.Printf("ATProto Callback Error: Failed to retrieve auth data for state '%s': %v", state, err) 141 + http.Error(w, "Invalid or expired state.", http.StatusBadRequest) 142 + return 0, fmt.Errorf("invalid or expired state") 143 + } 144 + 145 + // Clean up the temporary auth data now that we've retrieved it 146 + // defer a.DB.DeleteATprotoAuthData(state) // Consider adding deletion logic 147 + // if issuers don't match, return an error 148 + if data.AuthServerIssuer != issuer { 149 + log.Printf("ATProto Callback Error: Issuer mismatch for state '%s', expected '%s', got '%s'", state, data.AuthServerIssuer, issuer) 150 + http.Error(w, "Invalid or expired state.", http.StatusBadRequest) 151 + return 0, fmt.Errorf("issuer mismatch") 152 + } 153 + 154 + resp, err := a.client.InitialTokenRequest(r.Context(), code, issuer, data.PKCEVerifier, data.DPoPAuthServerNonce, data.DPoPPrivateJWK) 155 + if err != nil { 156 + log.Printf("ATProto Callback Error: Failed initial token request for state '%s', issuer '%s': %v", state, issuer, err) 157 + http.Error(w, fmt.Sprintf("Error exchanging code for token: %v", err), http.StatusInternalServerError) 158 + return 0, fmt.Errorf("failed initial token request") 159 + } 160 + 161 + userID, err := a.DB.FindOrCreateUserByDID(data.DID) 162 + if err != nil { 163 + log.Printf("ATProto Callback Error: Failed to find or create user for DID %s: %v", data.DID, err) 164 + http.Error(w, "Failed to process user information.", http.StatusInternalServerError) 165 + return 0, fmt.Errorf("failed to find or create user") 166 + } 167 + 168 + err = a.DB.SaveATprotoSession(resp) 169 + if err != nil { 170 + log.Printf("ATProto Callback Error: Failed to save ATProto tokens for user %d (DID %s): %v", userID.ID, data.DID, err) 171 + } 172 + 173 + log.Printf("ATProto Callback Success: User %d (DID: %s) authenticated.", userID.ID, data.DID) 174 + return userID.ID, nil // Return the piper user ID 175 + }
+49
oauth/atproto/http.go
··· 1 + // oauth/atproto/http.go 2 + package atproto 3 + 4 + import ( 5 + "encoding/json" 6 + "fmt" 7 + "log" 8 + "net/http" 9 + 10 + "github.com/haileyok/atproto-oauth-golang/helpers" 11 + ) 12 + 13 + func (a *ATprotoAuthService) HandleJwks(w http.ResponseWriter, r *http.Request) { 14 + pubKey, err := a.jwks.PublicKey() 15 + if err != nil { 16 + http.Error(w, fmt.Sprintf("Error getting public key from JWK: %v", err), http.StatusInternalServerError) 17 + log.Printf("Error getting public key from JWK: %v", err) 18 + return 19 + } 20 + 21 + w.Header().Set("Content-Type", "application/json") 22 + if err := json.NewEncoder(w).Encode(helpers.CreateJwksResponseObject(pubKey)); err != nil { 23 + log.Printf("Error encoding JWKS response: %v", err) 24 + } 25 + } 26 + 27 + func (a *ATprotoAuthService) HandleClientMetadata(w http.ResponseWriter, r *http.Request, serverUrlRoot, serverMetadataUrl, serverCallbackUrl string) { 28 + metadata := map[string]any{ 29 + "client_id": serverMetadataUrl, 30 + "client_name": "Piper Telekinesis", 31 + "client_uri": serverUrlRoot, 32 + "logo_uri": fmt.Sprintf("%s/logo.png", serverUrlRoot), 33 + "tos_uri": fmt.Sprintf("%s/tos", serverUrlRoot), 34 + "policy_url": fmt.Sprintf("%s/policy", serverUrlRoot), 35 + "redirect_uris": []string{serverCallbackUrl}, 36 + "grant_types": []string{"authorization_code", "refresh_token"}, 37 + "response_types": []string{"code"}, 38 + "application_type": "web", 39 + "dpop_bound_access_tokens": true, 40 + "jwks_uri": fmt.Sprintf("%s/oauth/jwks.json", serverUrlRoot), 41 + "scope": "atproto transition:generic", 42 + "token_endpoint_auth_method": "private_key_jwt", 43 + "token_endpoint_auth_signing_alg": "ES256", 44 + } 45 + w.Header().Set("Content-Type", "application/json") 46 + if err := json.NewEncoder(w).Encode(metadata); err != nil { 47 + log.Printf("Error encoding client metadata: %v", err) 48 + } 49 + }
+204
oauth/atproto/resolve.go
··· 1 + package atproto 2 + 3 + // Stolen from https://github.com/haileyok/atproto-oauth-golang/blob/f780d3716e2b8a06c87271a2930894319526550e/cmd/web_server_demo/resolution.go 4 + 5 + import ( 6 + "context" 7 + "encoding/json" 8 + "fmt" 9 + "io" 10 + "net" 11 + "net/http" 12 + "strings" 13 + 14 + "github.com/bluesky-social/indigo/atproto/syntax" 15 + oauth "github.com/haileyok/atproto-oauth-golang" 16 + ) 17 + 18 + // user information struct 19 + type UserInformation struct { 20 + AuthService string `json:"authService"` 21 + AuthServer string `json:"authServer"` 22 + AuthMeta *oauth.OauthAuthorizationMetadata `json:"authMeta"` 23 + // do NOT save the current handle permanently! 24 + Handle string `json:"handle"` 25 + DID string `json:"did"` 26 + } 27 + 28 + type Identity struct { 29 + AlsoKnownAs []string `json:"alsoKnownAs"` 30 + Service []struct { 31 + ID string `json:"id"` 32 + Type string `json:"type"` 33 + ServiceEndpoint string `json:"serviceEndpoint"` 34 + } `json:"service"` 35 + } 36 + 37 + func (a *ATprotoAuthService) getUserInformation(ctx context.Context, handleOrDid string) (*UserInformation, error) { 38 + cli := a.client 39 + 40 + // if we have a did skip this 41 + did := handleOrDid 42 + err := error(nil) 43 + // technically checking SHOULD be more rigorous. 44 + if !strings.HasPrefix(handleOrDid, "did:") { 45 + did, err = resolveHandle(ctx, did) 46 + if err != nil { 47 + return nil, err 48 + } 49 + } else { 50 + did = handleOrDid 51 + } 52 + 53 + doc, err := getIdentityDocument(ctx, did) 54 + if err != nil { 55 + return nil, err 56 + } 57 + 58 + service, err := getAtprotoPdsService(doc) 59 + if err != nil { 60 + return nil, err 61 + } 62 + 63 + authserver, err := cli.ResolvePdsAuthServer(ctx, service) 64 + if err != nil { 65 + return nil, err 66 + } 67 + 68 + authmeta, err := cli.FetchAuthServerMetadata(ctx, authserver) 69 + if err != nil { 70 + return nil, err 71 + } 72 + 73 + if len(doc.AlsoKnownAs) == 0 { 74 + return nil, fmt.Errorf("alsoKnownAs is empty, couldn't acquire handle: %w", err) 75 + 76 + } 77 + handle := strings.Replace(doc.AlsoKnownAs[0], "at://", "", 1) 78 + 79 + return &UserInformation{ 80 + AuthService: service, 81 + AuthServer: authserver, 82 + AuthMeta: authmeta, 83 + Handle: handle, 84 + DID: did, 85 + }, nil 86 + } 87 + 88 + func resolveHandle(ctx context.Context, handle string) (string, error) { 89 + var did string 90 + 91 + _, err := syntax.ParseHandle(handle) 92 + if err != nil { 93 + return "", err 94 + } 95 + 96 + recs, err := net.LookupTXT(fmt.Sprintf("_atproto.%s", handle)) 97 + if err == nil { 98 + for _, rec := range recs { 99 + if strings.HasPrefix(rec, "did=") { 100 + did = strings.Split(rec, "did=")[1] 101 + break 102 + } 103 + } 104 + } 105 + 106 + if did == "" { 107 + req, err := http.NewRequestWithContext( 108 + ctx, 109 + "GET", 110 + fmt.Sprintf("https://%s/.well-known/atproto-did", handle), 111 + nil, 112 + ) 113 + if err != nil { 114 + return "", err 115 + } 116 + 117 + resp, err := http.DefaultClient.Do(req) 118 + if err != nil { 119 + return "", err 120 + } 121 + defer resp.Body.Close() 122 + 123 + if resp.StatusCode != http.StatusOK { 124 + io.Copy(io.Discard, resp.Body) 125 + return "", fmt.Errorf("unable to resolve handle") 126 + } 127 + 128 + b, err := io.ReadAll(resp.Body) 129 + if err != nil { 130 + return "", err 131 + } 132 + 133 + maybeDid := string(b) 134 + 135 + if _, err := syntax.ParseDID(maybeDid); err != nil { 136 + return "", fmt.Errorf("unable to resolve handle") 137 + } 138 + 139 + did = maybeDid 140 + } 141 + 142 + return did, nil 143 + } 144 + 145 + // Get the Identity document for a given DID 146 + func getIdentityDocument(ctx context.Context, did string) (*Identity, error) { 147 + var ustr string 148 + if strings.HasPrefix(did, "did:plc:") { 149 + ustr = fmt.Sprintf("https://plc.directory/%s", did) 150 + } else if strings.HasPrefix(did, "did:web:") { 151 + ustr = fmt.Sprintf("https://%s/.well-known/did.json", strings.TrimPrefix(did, "did:web:")) 152 + } else { 153 + return nil, fmt.Errorf("did was not a supported did type") 154 + } 155 + 156 + req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil) 157 + if err != nil { 158 + return nil, err 159 + } 160 + 161 + resp, err := http.DefaultClient.Do(req) 162 + if err != nil { 163 + return nil, err 164 + } 165 + defer resp.Body.Close() 166 + 167 + if resp.StatusCode != http.StatusOK { 168 + io.Copy(io.Discard, resp.Body) 169 + return nil, fmt.Errorf("could not find identity in plc registry") 170 + } 171 + 172 + var identity Identity 173 + if err := json.NewDecoder(resp.Body).Decode(&identity); err != nil { 174 + return nil, err 175 + } 176 + 177 + return &identity, nil 178 + } 179 + 180 + // Get the atproto PDS service endpoint from an Identity document 181 + func getAtprotoPdsService(identity *Identity) (string, error) { 182 + var service string 183 + for _, svc := range identity.Service { 184 + if svc.ID == "#atproto_pds" { 185 + service = svc.ServiceEndpoint 186 + break 187 + } 188 + } 189 + 190 + if service == "" { 191 + return "", fmt.Errorf("could not find atproto_pds service in identity services") 192 + } 193 + 194 + return service, nil 195 + } 196 + 197 + func resolveServiceFromDoc(identity *Identity) (string, error) { 198 + service, err := getAtprotoPdsService(identity) 199 + if err != nil { 200 + return "", err 201 + } 202 + 203 + return service, nil 204 + }
+58 -25
oauth/oauth2.go
··· 1 + // Modify piper/oauth/oauth2.go 1 2 package oauth 2 3 3 4 import ( ··· 5 6 "crypto/rand" 6 7 "crypto/sha256" 7 8 "encoding/base64" 9 + "errors" 8 10 "fmt" 11 + "log" 9 12 "net/http" 10 13 "strings" 11 14 15 + "github.com/teal-fm/piper/session" 12 16 "golang.org/x/oauth2" 13 17 "golang.org/x/oauth2/spotify" 14 18 ) ··· 18 22 state string 19 23 codeVerifier string 20 24 codeChallenge string 25 + // Added TokenReceiver field to handle user lookup/creation based on token 26 + tokenReceiver TokenReceiver 21 27 } 22 28 23 - func generateRandomState() string { 29 + func GenerateRandomState() string { 24 30 b := make([]byte, 16) 25 31 rand.Read(b) 26 32 return base64.URLEncoding.EncodeToString(b) 27 33 } 28 34 29 - func NewOAuth2Service(clientID, clientSecret, redirectURI string, scopes []string, provider string) *OAuth2Service { 35 + func NewOAuth2Service(clientID, clientSecret, redirectURI string, scopes []string, provider string, tokenReceiver TokenReceiver) *OAuth2Service { 30 36 var endpoint oauth2.Endpoint 31 37 32 38 switch strings.ToLower(provider) { 33 39 case "spotify": 34 40 endpoint = spotify.Endpoint 41 + // Add other providers like Last.fm here 35 42 default: 36 - // TODO: support custom endpoints plus lastfm 43 + // Placeholder for unconfigured providers 44 + log.Printf("Warning: OAuth2 provider '%s' not explicitly configured. Using placeholder endpoints.", provider) 37 45 endpoint = oauth2.Endpoint{ 38 - AuthURL: "https://example.com/auth", 46 + AuthURL: "https://example.com/auth", // Replace with actual endpoints if needed 39 47 TokenURL: "https://example.com/token", 40 48 } 41 49 } 42 50 43 - codeVerifier := generateCodeVerifier() 44 - codeChallenge := generateCodeChallenge(codeVerifier) 51 + codeVerifier := GenerateCodeVerifier() 52 + codeChallenge := GenerateCodeChallenge(codeVerifier) 45 53 46 54 return &OAuth2Service{ 47 55 config: oauth2.Config{ ··· 51 59 Scopes: scopes, 52 60 Endpoint: endpoint, 53 61 }, 54 - state: generateRandomState(), 62 + state: GenerateRandomState(), 55 63 codeVerifier: codeVerifier, 56 64 codeChallenge: codeChallenge, 65 + tokenReceiver: tokenReceiver, // Store the token receiver 57 66 } 58 67 } 59 68 60 69 // generateCodeVerifier creates a random code verifier for PKCE 61 - func generateCodeVerifier() string { 62 - // Generate a random string of 32-96 bytes as per RFC 7636 63 - b := make([]byte, 64) // Using 64 bytes (512 bits) 70 + func GenerateCodeVerifier() string { 71 + b := make([]byte, 64) 64 72 rand.Read(b) 65 73 return base64.RawURLEncoding.EncodeToString(b) 66 74 } 67 75 68 76 // generateCodeChallenge creates a code challenge from the code verifier using S256 method 69 - func generateCodeChallenge(verifier string) string { 70 - // S256 method: SHA256 hash of the code verifier 77 + func GenerateCodeChallenge(verifier string) string { 71 78 h := sha256.New() 72 79 h.Write([]byte(verifier)) 73 80 return base64.RawURLEncoding.EncodeToString(h.Sum(nil)) 74 81 } 75 82 76 - // redirect to auth page 83 + // HandleLogin implements the AuthService interface method. 77 84 func (o *OAuth2Service) HandleLogin(w http.ResponseWriter, r *http.Request) { 78 - // use pkce here 79 85 opts := []oauth2.AuthCodeOption{ 80 86 oauth2.SetAuthURLParam("code_challenge", o.codeChallenge), 81 87 oauth2.SetAuthURLParam("code_challenge_method", "S256"), ··· 84 90 http.Redirect(w, r, authURL, http.StatusSeeOther) 85 91 } 86 92 87 - func (o *OAuth2Service) HandleCallback(w http.ResponseWriter, r *http.Request, tokenReceiver TokenReceiver) int64 { 88 - // Verify state 93 + func (o *OAuth2Service) HandleCallback(w http.ResponseWriter, r *http.Request) (int64, error) { 89 94 state := r.URL.Query().Get("state") 90 95 if state != o.state { 96 + log.Printf("OAuth2 Callback Error: State mismatch. Expected '%s', got '%s'", o.state, state) 91 97 http.Error(w, "State mismatch", http.StatusBadRequest) 92 - return 0 98 + return 0, errors.New("state mismatch") 93 99 } 94 100 95 101 code := r.URL.Query().Get("code") 96 102 if code == "" { 97 - http.Error(w, "No code provided", http.StatusBadRequest) 98 - return 0 103 + errMsg := r.URL.Query().Get("error") 104 + errDesc := r.URL.Query().Get("error_description") 105 + log.Printf("OAuth2 Callback Error: No code provided. Error: '%s', Description: '%s'", errMsg, errDesc) 106 + http.Error(w, fmt.Sprintf("Authorization failed: %s (%s)", errMsg, errDesc), http.StatusBadRequest) 107 + return 0, errors.New("no code provided") 108 + } 109 + 110 + if o.tokenReceiver == nil { 111 + log.Printf("OAuth2 Callback Error: TokenReceiver is not configured for this service.") 112 + http.Error(w, "Internal server configuration error", http.StatusInternalServerError) 113 + return 0, errors.New("token receiver not configured") 99 114 } 100 115 101 116 opts := []oauth2.AuthCodeOption{ 102 117 oauth2.SetAuthURLParam("code_verifier", o.codeVerifier), 103 118 } 104 119 120 + log.Println(code) 121 + 105 122 token, err := o.config.Exchange(context.Background(), code, opts...) 106 123 if err != nil { 124 + log.Printf("OAuth2 Callback Error: Failed to exchange code for token: %v", err) 107 125 http.Error(w, fmt.Sprintf("Error exchanging code for token: %v", err), http.StatusInternalServerError) 108 - return 0 126 + return 0, errors.New("failed to exchange code for token") 109 127 } 110 128 111 - // Store access token 112 - userID := tokenReceiver.SetAccessToken(token.AccessToken) 129 + userId, hasSession := session.GetUserID(r.Context()) 130 + 131 + // Use the token receiver to store the token and get the user ID 132 + userID, err := o.tokenReceiver.SetAccessToken(token.AccessToken, userId, hasSession) 133 + if err != nil { 134 + log.Printf("OAuth2 Callback Info: TokenReceiver did not return a valid user ID for token: %s...", token.AccessToken[:min(10, len(token.AccessToken))]) 135 + } 113 136 114 - return userID 137 + log.Printf("OAuth2 Callback Success: Exchanged code for token, UserID: %d", userID) 138 + return userID, nil 115 139 } 116 140 117 - // GetToken returns the OAuth2 token using the authorization code 141 + // GetToken remains unchanged 118 142 func (o *OAuth2Service) GetToken(code string) (*oauth2.Token, error) { 119 143 opts := []oauth2.AuthCodeOption{ 120 144 oauth2.SetAuthURLParam("code_verifier", o.codeVerifier), 121 145 } 122 - 123 146 return o.config.Exchange(context.Background(), code, opts...) 124 147 } 125 148 149 + // GetClient remains unchanged 126 150 func (o *OAuth2Service) GetClient(token *oauth2.Token) *http.Client { 127 151 return o.config.Client(context.Background(), token) 128 152 } 129 153 154 + // RefreshToken remains unchanged 130 155 func (o *OAuth2Service) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) { 131 156 source := o.config.TokenSource(context.Background(), token) 132 157 return oauth2.ReuseTokenSource(token, source).Token() 133 158 } 159 + 160 + // Helper function 161 + func min(a, b int) int { 162 + if a < b { 163 + return a 164 + } 165 + return b 166 + }
+40 -27
oauth/oauth_manager.go
··· 1 + // Modify piper/oauth/oauth_manager.go 1 2 package oauth 2 3 3 4 import ( ··· 9 10 "github.com/teal-fm/piper/session" 10 11 ) 11 12 12 - // TokenReceiver interface for services that can receive OAuth tokens 13 - type TokenReceiver interface { 14 - SetAccessToken(token string) int64 15 - } 16 - 17 - // manages multiple oauth2 client services 13 + // manages multiple oauth client services 18 14 type OAuthServiceManager struct { 19 - oauth2Services map[string]*OAuth2Service 15 + services map[string]AuthService // Changed from *OAuth2Service to AuthService interface 20 16 sessionManager *session.SessionManager 21 17 mu sync.RWMutex 22 18 } 23 19 24 20 func NewOAuthServiceManager() *OAuthServiceManager { 25 21 return &OAuthServiceManager{ 26 - oauth2Services: make(map[string]*OAuth2Service), 22 + services: make(map[string]AuthService), // Initialize the new map 27 23 sessionManager: session.NewSessionManager(), 28 24 } 29 25 } 30 26 31 - func (m *OAuthServiceManager) RegisterOAuth2Service(name string, service *OAuth2Service) { 27 + // RegisterService registers any service that implements the AuthService interface. 28 + func (m *OAuthServiceManager) RegisterService(name string, service AuthService) { 32 29 m.mu.Lock() 33 30 defer m.mu.Unlock() 34 - m.oauth2Services[name] = service 31 + m.services[name] = service 32 + log.Printf("Registered auth service: %s", name) 35 33 } 36 34 37 - func (m *OAuthServiceManager) GetOAuth2Service(name string) (*OAuth2Service, bool) { 35 + // GetService retrieves a registered AuthService by name. 36 + func (m *OAuthServiceManager) GetService(name string) (AuthService, bool) { 38 37 m.mu.RLock() 39 38 defer m.mu.RUnlock() 40 - service, exists := m.oauth2Services[name] 39 + service, exists := m.services[name] 41 40 return service, exists 42 41 } 43 42 44 43 func (m *OAuthServiceManager) HandleLogin(serviceName string) http.HandlerFunc { 45 44 return func(w http.ResponseWriter, r *http.Request) { 46 45 m.mu.RLock() 47 - oauth2Service, oauth2Exists := m.oauth2Services[serviceName] 46 + service, exists := m.services[serviceName] 48 47 m.mu.RUnlock() 49 48 50 - if oauth2Exists { 51 - oauth2Service.HandleLogin(w, r) 49 + if exists { 50 + service.HandleLogin(w, r) // Call interface method 52 51 return 53 52 } 54 53 55 - http.Error(w, fmt.Sprintf("OAuth service '%s' not found", serviceName), http.StatusNotFound) 54 + log.Printf("Auth service '%s' not found for login request", serviceName) 55 + http.Error(w, fmt.Sprintf("Auth service '%s' not found", serviceName), http.StatusNotFound) 56 56 } 57 57 } 58 58 59 - func (m *OAuthServiceManager) HandleCallback(serviceName string, tokenReceiver TokenReceiver) http.HandlerFunc { 59 + func (m *OAuthServiceManager) HandleCallback(serviceName string) http.HandlerFunc { 60 60 return func(w http.ResponseWriter, r *http.Request) { 61 61 m.mu.RLock() 62 - oauth2Service, oauth2Exists := m.oauth2Services[serviceName] 62 + service, exists := m.services[serviceName] 63 63 m.mu.RUnlock() 64 64 65 - var userID int64 65 + log.Printf("Logging in with service %s", serviceName) 66 66 67 - if oauth2Exists { 68 - // Handle OAuth2 with PKCE callback 69 - userID = oauth2Service.HandleCallback(w, r, tokenReceiver) 70 - } else { 67 + if !exists { 68 + log.Printf("Auth service '%s' not found for callback request", serviceName) 71 69 http.Error(w, fmt.Sprintf("OAuth service '%s' not found", serviceName), http.StatusNotFound) 72 70 return 73 71 } 74 72 73 + // Call the service's HandleCallback, which now returns the user ID 74 + userID, err := service.HandleCallback(w, r) // Call interface method 75 + 76 + if err != nil { 77 + log.Printf("Error handling callback for service '%s': %v", serviceName, err) 78 + http.Error(w, fmt.Sprintf("Error handling callback for service '%s'", serviceName), http.StatusInternalServerError) 79 + return 80 + } 81 + 75 82 if userID > 0 { 76 83 // Create session for the user 77 84 session := m.sessionManager.CreateSession(userID) ··· 79 86 // Set session cookie 80 87 m.sessionManager.SetSessionCookie(w, session) 81 88 82 - log.Printf("Created session for user %d", userID) 83 - } 89 + log.Printf("Created session for user %d via service %s", userID, serviceName) 84 90 85 - // Redirect to homepage 86 - http.Redirect(w, r, "/", http.StatusSeeOther) 91 + // Redirect to homepage after successful login and session creation 92 + http.Redirect(w, r, "/", http.StatusSeeOther) 93 + } else { 94 + log.Printf("Callback for service '%s' did not result in a valid user ID.", serviceName) 95 + // Optionally redirect to an error page or show an error message 96 + // For now, just redirecting home, but this might hide errors. 97 + // Consider adding error handling based on why userID might be 0. 98 + http.Redirect(w, r, "/", http.StatusSeeOther) // Or redirect to a login/error page 99 + } 87 100 } 88 101 }
+24
oauth/service.go
··· 1 + // Create piper/oauth/auth_service.go 2 + package oauth 3 + 4 + import ( 5 + "net/http" 6 + ) 7 + 8 + // AuthService defines the interface for different authentication services 9 + // that can be managed by the OAuthServiceManager. 10 + type AuthService interface { 11 + // HandleLogin initiates the login flow for the specific service. 12 + HandleLogin(w http.ResponseWriter, r *http.Request) 13 + // HandleCallback handles the callback from the authentication provider, 14 + // processes the response (e.g., exchanges code for token), finds or creates 15 + // the user in the local system, and returns the user ID. 16 + // Returns 0 if authentication failed or user could not be determined. 17 + HandleCallback(w http.ResponseWriter, r *http.Request) (int64, error) 18 + } 19 + 20 + type TokenReceiver interface { 21 + // SetAccessToken stores the access token for the user and returns the user ID. 22 + // If the user is already logged in, the current ID is provided. 23 + SetAccessToken(token string, currentId int64, hasSession bool) (int64, error) 24 + }
+44 -33
service/spotify/spotify.go
··· 30 30 } 31 31 } 32 32 33 - // SetAccessToken is called from OAuth callback and now identifies the user 34 - // SetAccessToken is called from OAuth callback and now identifies the user 35 - func (s *SpotifyService) SetAccessToken(token string) int64 { 33 + func (s *SpotifyService) SetAccessToken(token string, userId int64, hasSession bool) (int64, error) { 36 34 // Identify the user synchronously instead of in a goroutine 37 - userID := s.identifyAndStoreUser(token) 38 - return userID 35 + userID, err := s.identifyAndStoreUser(token, userId, hasSession) 36 + if err != nil { 37 + log.Printf("Error identifying and storing user: %v", err) 38 + return 0, err 39 + } 40 + return userID, nil 39 41 } 40 42 41 - func (s *SpotifyService) identifyAndStoreUser(token string) int64 { 43 + func (s *SpotifyService) identifyAndStoreUser(token string, userId int64, hasSession bool) (int64, error) { 42 44 // Get Spotify user profile 43 45 userProfile, err := s.fetchSpotifyProfile(token) 44 46 if err != nil { 45 47 log.Printf("Error fetching Spotify profile: %v", err) 46 - return 0 48 + return 0, err 47 49 } 50 + 51 + fmt.Printf("uid: %d hasSession: %t", userId, hasSession) 48 52 49 53 // Check if user exists 50 54 user, err := s.DB.GetUserBySpotifyID(userProfile.ID) 51 55 if err != nil { 52 - log.Printf("Error checking for user: %v", err) 53 - return 0 56 + // This error might mean DB connection issue, not just user not found. 57 + log.Printf("Error checking for user by Spotify ID %s: %v", userProfile.ID, err) 58 + return 0, err 54 59 } 55 60 56 - // If user doesn't exist, create them 61 + tokenExpiryTime := time.Now().Add(1 * time.Hour) // Spotify tokens last ~1 hour 62 + 63 + // We don't intend users to log in via spotify! 57 64 if user == nil { 58 - user = &models.User{ 59 - Username: userProfile.DisplayName, 60 - Email: userProfile.Email, 61 - SpotifyID: userProfile.ID, 62 - AccessToken: token, 63 - TokenExpiry: time.Now().Add(1 * time.Hour), // Spotify tokens last ~1 hour 64 - } 65 - 66 - userID, err := s.DB.CreateUser(user) 67 - if err != nil { 68 - log.Printf("Error creating user: %v", err) 69 - return 0 65 + if !hasSession { 66 + log.Printf("User does not seem to exist") 67 + return 0, fmt.Errorf("user does not seem to exist") 68 + } else { 69 + // overwrite prev user 70 + user, err = s.DB.AddSpotifySession(userId, userProfile.DisplayName, userProfile.Email, userProfile.ID, token, "", tokenExpiryTime) 71 + if err != nil { 72 + log.Printf("Error adding Spotify session for user ID %d: %v", userId, err) 73 + return 0, err 74 + } 70 75 } 71 - user.ID = userID 72 76 } else { 73 - // Update token 74 - err = s.DB.UpdateUserToken(user.ID, token, "", time.Now().Add(1*time.Hour)) 77 + // Update existing user's token and expiry 78 + err = s.DB.UpdateUserToken(user.ID, token, "", tokenExpiryTime) 75 79 if err != nil { 76 - log.Printf("Error updating user token: %v", err) 80 + log.Printf("Error updating user token for user ID %d: %v", user.ID, err) 81 + // Consider if we should return 0 or the user ID even if update fails 82 + // Sticking to original behavior: log and continue 83 + } else { 84 + log.Printf("Updated token for existing user: %s (ID: %d)", user.Username, user.ID) 77 85 } 78 86 } 87 + // Keep the local 'user' object consistent (optional but good practice) 88 + user.AccessToken = &token 89 + user.TokenExpiry = &tokenExpiryTime 79 90 80 - // Store token in memory 91 + // Store token in memory cache regardless of new/existing user 81 92 s.mu.Lock() 82 93 s.userTokens[user.ID] = token 83 94 s.mu.Unlock() 84 95 85 - log.Printf("User authenticated: %s (ID: %d)", user.Username, user.ID) 86 - return user.ID 96 + log.Printf("User authenticated via Spotify: %s (ID: %d)", user.Username, user.ID) 97 + return user.ID, nil 87 98 } 88 99 89 100 type spotifyProfile struct { ··· 105 116 count := 0 106 117 for _, user := range users { 107 118 // Only load users with valid tokens 108 - if user.AccessToken != "" && user.TokenExpiry.After(time.Now()) { 109 - s.userTokens[user.ID] = user.AccessToken 119 + if user.AccessToken != nil && user.TokenExpiry.After(time.Now()) { 120 + s.userTokens[user.ID] = *user.AccessToken 110 121 count++ 111 122 } 112 123 } ··· 124 135 return fmt.Errorf("error loading user: %v", err) 125 136 } 126 137 127 - if user.RefreshToken == "" { 138 + if user.RefreshToken == nil { 128 139 return fmt.Errorf("no refresh token for user %s", userID) 129 140 } 130 141 ··· 150 161 refreshed := 0 151 162 for _, user := range users { 152 163 // Skip users without refresh tokens 153 - if user.RefreshToken == "" { 164 + if user.RefreshToken == nil { 154 165 continue 155 166 } 156 167
+56 -4
session/session.go
··· 13 13 "github.com/teal-fm/piper/db/apikey" 14 14 ) 15 15 16 + // session/session.go 16 17 type Session struct { 17 - ID string 18 - UserID int64 19 - CreatedAt time.Time 20 - ExpiresAt time.Time 18 + ID string 19 + UserID int64 20 + ATprotoDID string 21 + ATprotoAccessToken string 22 + ATprotoRefreshToken string 23 + CreatedAt time.Time 24 + ExpiresAt time.Time 21 25 } 22 26 23 27 type SessionManager struct { ··· 251 255 } 252 256 } 253 257 258 + func WithPossibleAuth(handler http.HandlerFunc, sm *SessionManager) http.HandlerFunc { 259 + return func(w http.ResponseWriter, r *http.Request) { 260 + ctx := r.Context() 261 + authenticated := false // Default to not authenticated 262 + 263 + // 1. Try API key authentication 264 + apiKeyStr, apiKeyErr := apikey.ExtractApiKey(r) 265 + if apiKeyErr == nil && apiKeyStr != "" { 266 + apiKey, valid := sm.apiKeyMgr.GetApiKey(apiKeyStr) 267 + if valid { 268 + // API Key valid: Add UserID, API flag, and set auth status 269 + ctx = WithUserID(ctx, apiKey.UserID) 270 + ctx = WithAPIRequest(ctx, true) 271 + authenticated = true 272 + // Update request context and call handler 273 + r = r.WithContext(WithAuthStatus(ctx, authenticated)) 274 + handler(w, r) 275 + return 276 + } 277 + // If API key was provided but invalid, we still proceed without auth 278 + } 279 + 280 + // 2. If no valid API key, try cookie authentication 281 + if !authenticated { // Only check cookies if API key didn't authenticate 282 + cookie, err := r.Cookie("session") 283 + if err == nil { // Cookie exists 284 + session, exists := sm.GetSession(cookie.Value) 285 + if exists { 286 + // Session valid: Add UserID and set auth status 287 + ctx = WithUserID(ctx, session.UserID) 288 + // ctx = WithAPIRequest(ctx, false) // Not strictly needed, default is false 289 + authenticated = true 290 + } 291 + // If session cookie exists but is invalid/expired, we proceed without auth 292 + } 293 + } 294 + 295 + // 3. Set final auth status (could be true or false) and call handler 296 + r = r.WithContext(WithAuthStatus(ctx, authenticated)) 297 + handler(w, r) 298 + } 299 + } 300 + 254 301 // WithAPIAuth is a middleware specifically for API-only endpoints (no cookie fallback, returns 401 instead of redirect) 255 302 func WithAPIAuth(handler http.HandlerFunc, sm *SessionManager) http.HandlerFunc { 256 303 return func(w http.ResponseWriter, r *http.Request) { ··· 288 335 const ( 289 336 userIDKey contextKey = iota 290 337 apiRequestKey 338 + authStatusKey 291 339 ) 292 340 293 341 func WithUserID(ctx context.Context, userID int64) context.Context { ··· 297 345 func GetUserID(ctx context.Context) (int64, bool) { 298 346 userID, ok := ctx.Value(userIDKey).(int64) 299 347 return userID, ok 348 + } 349 + 350 + func WithAuthStatus(ctx context.Context, isAuthed bool) context.Context { 351 + return context.WithValue(ctx, authStatusKey, isAuthed) 300 352 } 301 353 302 354 func WithAPIRequest(ctx context.Context, isAPI bool) context.Context {