forked from hailey.at/cocoon
An atproto PDS written in Go

initial commit (squashed)

hailey.at 96f71046

+2
.env.example
···
··· 1 + COCOON_DID= 2 + COCOON_HOSTNAME=
+4
.gitignore
···
··· 1 + *.db 2 + .env 3 + /cocoon 4 + *.key
+42
Makefile
···
··· 1 + SHELL = /bin/bash 2 + .SHELLFLAGS = -o pipefail -c 3 + GIT_TAG := $(shell git describe --tags --exact-match 2>/dev/null) 4 + GIT_COMMIT := $(shell git rev-parse --short=9 HEAD) 5 + VERSION := $(if $(GIT_TAG),$(GIT_TAG),dev-$(GIT_COMMIT)) 6 + 7 + .PHONY: help 8 + help: ## Print info about all commands 9 + @echo "Commands:" 10 + @echo 11 + @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[01;32m%-20s\033[0m %s\n", $$1, $$2}' 12 + 13 + .PHONY: build 14 + build: ## Build all executables 15 + go build -ldflags "-X main.Version=$(VERSION)" -o cocoon ./cmd/cocoon 16 + 17 + .PHONY: run 18 + run: 19 + go build -ldflags "-X main.Version=dev-local" -o cocoon ./cmd/cocoon && ./cocoon run 20 + 21 + .PHONY: all 22 + all: build 23 + 24 + .PHONY: test 25 + test: ## Run tests 26 + go clean -testcache && go test -v ./... 27 + 28 + .PHONY: lint 29 + lint: ## Verify code style and run static checks 30 + go vet ./... 31 + test -z $(gofmt -l ./...) 32 + 33 + .PHONY: fmt 34 + fmt: ## Run syntax re-formatting (modify in place) 35 + go fmt ./... 36 + 37 + .PHONY: check 38 + check: ## Compile everything, checking syntax (does not output binaries) 39 + go build ./... 40 + 41 + .env: 42 + if [ ! -f ".env" ]; then cp example.dev.env .env; fi
+63
README.md
···
··· 1 + # Cocoon 2 + 3 + > [!WARNING] 4 + You should not use this PDS. You should not rely on this code as a reference for a PDS implementation. You should not trust this code. Using this PDS implementation may result in data loss, corruption, etc. 5 + 6 + Cocoon is a PDS implementation in Go. It is highly experimental, and is not ready for any production use. 7 + 8 + ### Impmlemented Endpoints 9 + 10 + - [ ] com.atproto.identity.getRecommendedDidCredentials 11 + - [ ] com.atproto.identity.requestPlcOperationSignature 12 + - [x] com.atproto.identity.resolveHandle 13 + - [ ] com.atproto.identity.signPlcOperation 14 + - [ ] com.atproto.identity.submitPlcOperatioin 15 + - [ ] com.atproto.identity.updateHandle 16 + - [ ] com.atproto.label.queryLabels 17 + - [ ] com.atproto.moderation.createReport 18 + 19 + - [ ] com.atproto.repo.applyWrites 20 + - [x] com.atproto.repo.createRecord 21 + - [x] com.atproto.repo.putRecord 22 + - [ ] com.atproto.repo.deleteRecord 23 + - [x] com.atproto.repo.describeRepo 24 + - [x] com.atproto.repo.getRecord 25 + - [ ] com.atproto.repo.importRepo 26 + - [ ] com.atproto.repo.listMissingBlobs 27 + - [x] com.atproto.repo.listRecords 28 + - [ ] com.atproto.repo.listMissingBlobs 29 + 30 + 31 + - [ ] com.atproto.server.activateAccount 32 + - [ ] com.atproto.server.checkAccountStatus 33 + - [ ] com.atproto.server.confirmEmail 34 + - [x] com.atproto.server.createAccount 35 + - [ ] com.atproto.server.deactivateAccount 36 + - [ ] com.atproto.server.deleteAccount 37 + - [x] com.atproto.server.deleteSession 38 + - [x] com.atproto.server.describeServer 39 + - [ ] com.atproto.server.getAccountInviteCodes 40 + - [ ] com.atproto.server.getServiceAuth 41 + - [ ] com.atproto.server.listAppPasswords 42 + - [x] com.atproto.server.refreshSession 43 + - [ ] com.atproto.server.requestAccountDelete 44 + - [ ] com.atproto.server.requestEmailConfirmation 45 + - [ ] com.atproto.server.requestEmailUpdate 46 + - [ ] com.atproto.server.requestPasswordReset 47 + - [ ] com.atproto.server.reserveSigningKey 48 + - [ ] com.atproto.server.resetPassword 49 + - [ ] com.atproto.server.revokeAppPassword 50 + - [ ] com.atproto.server.updateEmail 51 + 52 + - [ ] com.atproto.sync.getBlob 53 + - [x] com.atproto.sync.getBlocks 54 + - [x] com.atproto.sync.getLatestCommit 55 + - [x] com.atproto.sync.getRecord 56 + - [x] com.atproto.sync.getRepoStatus 57 + - [x] com.atproto.sync.getRepo 58 + - [ ] com.atproto.sync.listBlobs 59 + - [x] com.atproto.sync.listRepos 60 + - [ ] com.atproto.sync.notifyOfUpdate - BGS doesn't even have this implemented lol 61 + - [x] com.atproto.sync.requestCrawl 62 + - [x] com.atproto.sync.subscribeRepos 63 +
+126
blockstore/blockstore.go
···
··· 1 + package blockstore 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + 7 + "github.com/bluesky-social/indigo/atproto/syntax" 8 + "github.com/haileyok/cocoon/models" 9 + blocks "github.com/ipfs/go-block-format" 10 + "github.com/ipfs/go-cid" 11 + "gorm.io/gorm" 12 + "gorm.io/gorm/clause" 13 + ) 14 + 15 + type SqliteBlockstore struct { 16 + db *gorm.DB 17 + did string 18 + readonly bool 19 + inserts []blocks.Block 20 + } 21 + 22 + func New(did string, db *gorm.DB) *SqliteBlockstore { 23 + return &SqliteBlockstore{ 24 + did: did, 25 + db: db, 26 + readonly: false, 27 + inserts: []blocks.Block{}, 28 + } 29 + } 30 + 31 + func NewReadOnly(did string, db *gorm.DB) *SqliteBlockstore { 32 + return &SqliteBlockstore{ 33 + did: did, 34 + db: db, 35 + readonly: true, 36 + inserts: []blocks.Block{}, 37 + } 38 + } 39 + 40 + func (bs *SqliteBlockstore) Get(ctx context.Context, cid cid.Cid) (blocks.Block, error) { 41 + var block models.Block 42 + if err := bs.db.Raw("SELECT * FROM blocks WHERE did = ? AND cid = ?", bs.did, cid.Bytes()).Scan(&block).Error; err != nil { 43 + return nil, err 44 + } 45 + 46 + b, err := blocks.NewBlockWithCid(block.Value, cid) 47 + if err != nil { 48 + return nil, err 49 + } 50 + 51 + return b, nil 52 + } 53 + 54 + func (bs *SqliteBlockstore) Put(ctx context.Context, block blocks.Block) error { 55 + bs.inserts = append(bs.inserts, block) 56 + 57 + if bs.readonly { 58 + return nil 59 + } 60 + 61 + b := models.Block{ 62 + Did: bs.did, 63 + Cid: block.Cid().Bytes(), 64 + Rev: syntax.NewTIDNow(0).String(), // TODO: WARN, this is bad. don't do this 65 + Value: block.RawData(), 66 + } 67 + 68 + if err := bs.db.Clauses(clause.OnConflict{ 69 + Columns: []clause.Column{{Name: "did"}, {Name: "cid"}}, 70 + UpdateAll: true, 71 + }).Create(&b).Error; err != nil { 72 + return err 73 + } 74 + 75 + return nil 76 + } 77 + 78 + func (bs *SqliteBlockstore) DeleteBlock(context.Context, cid.Cid) error { 79 + panic("not implemented") 80 + } 81 + 82 + func (bs *SqliteBlockstore) Has(context.Context, cid.Cid) (bool, error) { 83 + panic("not implemented") 84 + } 85 + 86 + func (bs *SqliteBlockstore) GetSize(context.Context, cid.Cid) (int, error) { 87 + panic("not implemented") 88 + } 89 + 90 + func (bs *SqliteBlockstore) PutMany(context.Context, []blocks.Block) error { 91 + panic("not implemented") 92 + } 93 + 94 + func (bs *SqliteBlockstore) AllKeysChan(ctx context.Context) (<-chan cid.Cid, error) { 95 + panic("not implemented") 96 + } 97 + 98 + func (bs *SqliteBlockstore) HashOnRead(enabled bool) { 99 + panic("not implemented") 100 + } 101 + 102 + func (bs *SqliteBlockstore) UpdateRepo(ctx context.Context, root cid.Cid, rev string) error { 103 + if err := bs.db.Exec("UPDATE repos SET root = ?, rev = ? WHERE did = ?", root.Bytes(), rev, bs.did).Error; err != nil { 104 + return err 105 + } 106 + 107 + return nil 108 + } 109 + 110 + func (bs *SqliteBlockstore) Execute(ctx context.Context) error { 111 + if !bs.readonly { 112 + return fmt.Errorf("blockstore was not readonly") 113 + } 114 + 115 + bs.readonly = false 116 + for _, b := range bs.inserts { 117 + bs.Put(ctx, b) 118 + } 119 + bs.readonly = true 120 + 121 + return nil 122 + } 123 + 124 + func (bs *SqliteBlockstore) GetLog() []blocks.Block { 125 + return bs.inserts 126 + }
+94
cmd/admin/main.go
···
··· 1 + package main 2 + 3 + import ( 4 + "crypto/ecdsa" 5 + "crypto/elliptic" 6 + "crypto/rand" 7 + "encoding/json" 8 + "fmt" 9 + "os" 10 + "time" 11 + 12 + "github.com/bluesky-social/indigo/atproto/crypto" 13 + "github.com/lestrrat-go/jwx/v2/jwk" 14 + "github.com/urfave/cli/v2" 15 + ) 16 + 17 + func main() { 18 + app := cli.App{ 19 + Name: "admin", 20 + Commands: cli.Commands{ 21 + runCreateRotationKey, 22 + runCreatePrivateJwk, 23 + }, 24 + ErrWriter: os.Stdout, 25 + } 26 + 27 + app.Run(os.Args) 28 + } 29 + 30 + var runCreateRotationKey = &cli.Command{ 31 + Name: "create-rotation-key", 32 + Usage: "creates a rotation key for your pds", 33 + Flags: []cli.Flag{ 34 + &cli.StringFlag{ 35 + Name: "out", 36 + Required: true, 37 + Usage: "output file for your rotation key", 38 + }, 39 + }, 40 + Action: func(cmd *cli.Context) error { 41 + key, err := crypto.GeneratePrivateKeyK256() 42 + if err != nil { 43 + return err 44 + } 45 + 46 + bytes := key.Bytes() 47 + 48 + if err := os.WriteFile(cmd.String("out"), bytes, 0644); err != nil { 49 + return err 50 + } 51 + 52 + return nil 53 + }, 54 + } 55 + 56 + var runCreatePrivateJwk = &cli.Command{ 57 + Name: "create-private-jwk", 58 + Usage: "creates a private jwk for your pds", 59 + Flags: []cli.Flag{ 60 + &cli.StringFlag{ 61 + Name: "out", 62 + Required: true, 63 + Usage: "output file for your jwk", 64 + }, 65 + }, 66 + Action: func(cmd *cli.Context) error { 67 + privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 68 + if err != nil { 69 + return err 70 + } 71 + 72 + key, err := jwk.FromRaw(privKey) 73 + if err != nil { 74 + return err 75 + } 76 + 77 + kid := fmt.Sprintf("%d", time.Now().Unix()) 78 + 79 + if err := key.Set(jwk.KeyIDKey, kid); err != nil { 80 + return err 81 + } 82 + 83 + b, err := json.Marshal(key) 84 + if err != nil { 85 + return err 86 + } 87 + 88 + if err := os.WriteFile(cmd.String("out"), b, 0644); err != nil { 89 + return err 90 + } 91 + 92 + return nil 93 + }, 94 + }
+97
cmd/cocoon/main.go
···
··· 1 + package main 2 + 3 + import ( 4 + "fmt" 5 + "os" 6 + 7 + "github.com/haileyok/cocoon/server" 8 + _ "github.com/joho/godotenv/autoload" 9 + "github.com/urfave/cli/v2" 10 + ) 11 + 12 + var Version = "dev" 13 + 14 + func main() { 15 + app := &cli.App{ 16 + Name: "cocoon", 17 + Usage: "An atproto PDS", 18 + Flags: []cli.Flag{ 19 + &cli.StringFlag{ 20 + Name: "addr", 21 + Value: ":8080", 22 + EnvVars: []string{"COCOON_ADDR"}, 23 + }, 24 + &cli.StringFlag{ 25 + Name: "db-name", 26 + Value: "cocoon.db", 27 + EnvVars: []string{"COCOON_DB_NAME"}, 28 + }, 29 + &cli.StringFlag{ 30 + Name: "did", 31 + Required: true, 32 + EnvVars: []string{"COCOON_DID"}, 33 + }, 34 + &cli.StringFlag{ 35 + Name: "hostname", 36 + Required: true, 37 + EnvVars: []string{"COCOON_HOSTNAME"}, 38 + }, 39 + &cli.StringFlag{ 40 + Name: "rotation-key-path", 41 + Required: true, 42 + EnvVars: []string{"COCOON_ROTATION_KEY_PATH"}, 43 + }, 44 + &cli.StringFlag{ 45 + Name: "jwk-path", 46 + Required: true, 47 + EnvVars: []string{"COCOON_JWK_PATH"}, 48 + }, 49 + &cli.StringFlag{ 50 + Name: "contact-email", 51 + Required: true, 52 + EnvVars: []string{"COCOON_CONTACT_EMAIL"}, 53 + }, 54 + &cli.StringSliceFlag{ 55 + Name: "relays", 56 + Required: true, 57 + EnvVars: []string{"COCOON_RELAYS"}, 58 + }, 59 + }, 60 + Commands: []*cli.Command{ 61 + run, 62 + }, 63 + ErrWriter: os.Stdout, 64 + Version: Version, 65 + } 66 + 67 + app.Run(os.Args) 68 + } 69 + 70 + var run = &cli.Command{ 71 + Name: "run", 72 + Usage: "Start the cocoon PDS", 73 + Flags: []cli.Flag{}, 74 + Action: func(cmd *cli.Context) error { 75 + s, err := server.New(&server.Args{ 76 + Addr: cmd.String("addr"), 77 + DbName: cmd.String("db-name"), 78 + Did: cmd.String("did"), 79 + Hostname: cmd.String("hostname"), 80 + RotationKeyPath: cmd.String("rotation-key-path"), 81 + JwkPath: cmd.String("jwk-path"), 82 + ContactEmail: cmd.String("contact-email"), 83 + Version: Version, 84 + Relays: cmd.StringSlice("relays"), 85 + }) 86 + if err != nil { 87 + return err 88 + } 89 + 90 + if err := s.Serve(cmd.Context); err != nil { 91 + fmt.Printf("error starting cocoon: %v", err) 92 + return err 93 + } 94 + 95 + return nil 96 + }, 97 + }
+135
go.mod
···
··· 1 + module github.com/haileyok/cocoon 2 + 3 + go 1.24.1 4 + 5 + require ( 6 + github.com/Azure/go-autorest/autorest/to v0.4.1 7 + github.com/bluesky-social/indigo v0.0.0-20250322011324-8e3fa7af986a 8 + github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 9 + github.com/go-playground/validator v9.31.0+incompatible 10 + github.com/golang-jwt/jwt/v4 v4.5.2 11 + github.com/google/uuid v1.4.0 12 + github.com/ipfs/go-block-format v0.2.0 13 + github.com/ipfs/go-cid v0.4.1 14 + github.com/ipfs/go-ipld-cbor v0.1.0 15 + github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4 16 + github.com/joho/godotenv v1.5.1 17 + github.com/labstack/echo/v4 v4.13.3 18 + github.com/lestrrat-go/jwx/v2 v2.0.12 19 + github.com/samber/slog-echo v1.16.1 20 + github.com/urfave/cli/v2 v2.27.6 21 + golang.org/x/crypto v0.36.0 22 + gorm.io/driver/sqlite v1.5.7 23 + gorm.io/gorm v1.25.12 24 + ) 25 + 26 + require ( 27 + github.com/Azure/go-autorest v14.2.0+incompatible // indirect 28 + github.com/RussellLuo/slidingwindow v0.0.0-20200528002341-535bb99d338b // indirect 29 + github.com/beorn7/perks v1.0.1 // indirect 30 + github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect 31 + github.com/carlmjohnson/versioninfo v0.22.5 // indirect 32 + github.com/cespare/xxhash/v2 v2.2.0 // indirect 33 + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect 34 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect 35 + github.com/felixge/httpsnoop v1.0.4 // indirect 36 + github.com/go-logr/logr v1.4.2 // indirect 37 + github.com/go-logr/stdr v1.2.2 // indirect 38 + github.com/go-playground/locales v0.14.1 // indirect 39 + github.com/go-playground/universal-translator v0.18.1 // indirect 40 + github.com/goccy/go-json v0.10.2 // indirect 41 + github.com/gocql/gocql v1.7.0 // indirect 42 + github.com/gogo/protobuf v1.3.2 // indirect 43 + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect 44 + github.com/golang/snappy v0.0.4 // indirect 45 + github.com/gorilla/websocket v1.5.1 // indirect 46 + github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect 47 + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 48 + github.com/hashicorp/go-retryablehttp v0.7.5 // indirect 49 + github.com/hashicorp/golang-lru v1.0.2 // indirect 50 + github.com/hashicorp/golang-lru/arc/v2 v2.0.6 // indirect 51 + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 52 + github.com/ipfs/bbloom v0.0.4 // indirect 53 + github.com/ipfs/go-blockservice v0.5.2 // indirect 54 + github.com/ipfs/go-datastore v0.6.0 // indirect 55 + github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect 56 + github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect 57 + github.com/ipfs/go-ipfs-exchange-interface v0.2.1 // indirect 58 + github.com/ipfs/go-ipfs-util v0.0.3 // indirect 59 + github.com/ipfs/go-ipld-format v0.6.0 // indirect 60 + github.com/ipfs/go-ipld-legacy v0.2.1 // indirect 61 + github.com/ipfs/go-libipfs v0.7.0 // indirect 62 + github.com/ipfs/go-log v1.0.5 // indirect 63 + github.com/ipfs/go-log/v2 v2.5.1 // indirect 64 + github.com/ipfs/go-merkledag v0.11.0 // indirect 65 + github.com/ipfs/go-metrics-interface v0.0.1 // indirect 66 + github.com/ipfs/go-verifcid v0.0.3 // indirect 67 + github.com/ipld/go-car/v2 v2.13.1 // indirect 68 + github.com/ipld/go-codec-dagpb v1.6.0 // indirect 69 + github.com/ipld/go-ipld-prime v0.21.0 // indirect 70 + github.com/jackc/pgpassfile v1.0.0 // indirect 71 + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 72 + github.com/jackc/pgx/v5 v5.5.0 // indirect 73 + github.com/jackc/puddle/v2 v2.2.1 // indirect 74 + github.com/jbenet/goprocess v0.1.4 // indirect 75 + github.com/jinzhu/inflection v1.0.0 // indirect 76 + github.com/jinzhu/now v1.1.5 // indirect 77 + github.com/klauspost/cpuid/v2 v2.2.7 // indirect 78 + github.com/labstack/gommon v0.4.2 // indirect 79 + github.com/leodido/go-urn v1.4.0 // indirect 80 + github.com/lestrrat-go/blackmagic v1.0.1 // indirect 81 + github.com/lestrrat-go/httpcc v1.0.1 // indirect 82 + github.com/lestrrat-go/httprc v1.0.4 // indirect 83 + github.com/lestrrat-go/iter v1.0.2 // indirect 84 + github.com/lestrrat-go/option v1.0.1 // indirect 85 + github.com/mattn/go-colorable v0.1.13 // indirect 86 + github.com/mattn/go-isatty v0.0.20 // indirect 87 + github.com/mattn/go-sqlite3 v1.14.22 // indirect 88 + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect 89 + github.com/minio/sha256-simd v1.0.1 // indirect 90 + github.com/mr-tron/base58 v1.2.0 // indirect 91 + github.com/multiformats/go-base32 v0.1.0 // indirect 92 + github.com/multiformats/go-base36 v0.2.0 // indirect 93 + github.com/multiformats/go-multibase v0.2.0 // indirect 94 + github.com/multiformats/go-multicodec v0.9.0 // indirect 95 + github.com/multiformats/go-multihash v0.2.3 // indirect 96 + github.com/multiformats/go-varint v0.0.7 // indirect 97 + github.com/opentracing/opentracing-go v1.2.0 // indirect 98 + github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 // indirect 99 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect 100 + github.com/prometheus/client_golang v1.17.0 // indirect 101 + github.com/prometheus/client_model v0.5.0 // indirect 102 + github.com/prometheus/common v0.45.0 // indirect 103 + github.com/prometheus/procfs v0.12.0 // indirect 104 + github.com/russross/blackfriday/v2 v2.1.0 // indirect 105 + github.com/samber/lo v1.49.1 // indirect 106 + github.com/segmentio/asm v1.2.0 // indirect 107 + github.com/spaolacci/murmur3 v1.1.0 // indirect 108 + github.com/valyala/bytebufferpool v1.0.0 // indirect 109 + github.com/valyala/fasttemplate v1.2.2 // indirect 110 + github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11 // indirect 111 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e // indirect 112 + github.com/whyrusleeping/go-did v0.0.0-20230824162731-404d1707d5d6 // indirect 113 + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 114 + gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 115 + gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 116 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect 117 + go.opentelemetry.io/otel v1.29.0 // indirect 118 + go.opentelemetry.io/otel/metric v1.29.0 // indirect 119 + go.opentelemetry.io/otel/trace v1.29.0 // indirect 120 + go.uber.org/atomic v1.11.0 // indirect 121 + go.uber.org/multierr v1.11.0 // indirect 122 + go.uber.org/zap v1.26.0 // indirect 123 + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect 124 + golang.org/x/net v0.33.0 // indirect 125 + golang.org/x/sync v0.12.0 // indirect 126 + golang.org/x/sys v0.31.0 // indirect 127 + golang.org/x/text v0.23.0 // indirect 128 + golang.org/x/time v0.8.0 // indirect 129 + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect 130 + google.golang.org/protobuf v1.33.0 // indirect 131 + gopkg.in/go-playground/assert.v1 v1.2.1 // indirect 132 + gopkg.in/inf.v0 v0.9.1 // indirect 133 + gorm.io/driver/postgres v1.5.7 // indirect 134 + lukechampine.com/blake3 v1.2.1 // indirect 135 + )
+501
go.sum
···
··· 1 + github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= 2 + github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= 3 + github.com/Azure/go-autorest/autorest/to v0.4.1 h1:CxNHBqdzTr7rLtdrtb5CMjJcDut+WNGCVv7OmS5+lTc= 4 + github.com/Azure/go-autorest/autorest/to v0.4.1/go.mod h1:EtaofgU4zmtvn1zT2ARsjRFdq9vXx0YWtmElwL+GZ9M= 5 + github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 6 + github.com/RussellLuo/slidingwindow v0.0.0-20200528002341-535bb99d338b h1:5/++qT1/z812ZqBvqQt6ToRswSuPZ/B33m6xVHRzADU= 7 + github.com/RussellLuo/slidingwindow v0.0.0-20200528002341-535bb99d338b/go.mod h1:4+EPqMRApwwE/6yo6CxiHoSnBzjRr3jsqer7frxP8y4= 8 + github.com/alexbrainman/goissue34681 v0.0.0-20191006012335-3fc7a47baff5 h1:iW0a5ljuFxkLGPNem5Ui+KBjFJzKg4Fv2fnxe4dvzpM= 9 + github.com/alexbrainman/goissue34681 v0.0.0-20191006012335-3fc7a47baff5/go.mod h1:Y2QMoi1vgtOIfc+6DhrMOGkLoGzqSV2rKp4Sm+opsyA= 10 + github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 11 + github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= 12 + github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 13 + github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 14 + github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 15 + github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= 16 + github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= 17 + github.com/bluesky-social/indigo v0.0.0-20250322011324-8e3fa7af986a h1:clnSZRgkiifbvfqu9++OHfIh2DWuIoZ8CucxLueQxO0= 18 + github.com/bluesky-social/indigo v0.0.0-20250322011324-8e3fa7af986a/go.mod h1:NVBwZvbBSa93kfyweAmKwOLYawdVHdwZ9s+GZtBBVLA= 19 + github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= 20 + github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= 21 + github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= 22 + github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= 23 + github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= 24 + github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= 25 + github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc= 26 + github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8= 27 + github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 28 + github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 29 + github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 30 + github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= 31 + github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 32 + github.com/cskr/pubsub v1.0.2 h1:vlOzMhl6PFn60gRlTQQsIfVwaPB/B/8MziK8FhEPt/0= 33 + github.com/cskr/pubsub v1.0.2/go.mod h1:/8MzYXk/NJAz782G8RPkFzXTZVu63VotefPnR9TIRis= 34 + github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 35 + github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 36 + github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 37 + github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 38 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= 39 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 40 + github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 41 + github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 42 + github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 43 + github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 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= 49 + github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 50 + github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 51 + github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 52 + github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 53 + github.com/go-playground/validator v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp4Mit+3FDh548oRqwVgNsHA= 54 + github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig= 55 + github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= 56 + github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= 57 + github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= 58 + github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 59 + github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 60 + github.com/gocql/gocql v1.7.0 h1:O+7U7/1gSN7QTEAaMEsJc1Oq2QHXvCWoF3DFK9HDHus= 61 + github.com/gocql/gocql v1.7.0/go.mod h1:vnlvXyFZeLBF0Wy+RS8hrOdbn0UWsWtdg07XJnFxZ+4= 62 + github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 63 + github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 64 + github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 65 + github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 66 + github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= 67 + github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 68 + github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 69 + github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 70 + github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 71 + github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 72 + github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 73 + github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= 74 + github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 75 + github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 76 + github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= 77 + github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 78 + github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 79 + github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 80 + github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= 81 + github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= 82 + github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= 83 + github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= 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 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= 87 + github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= 88 + github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= 89 + github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= 90 + github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 91 + github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 92 + github.com/hashicorp/golang-lru/arc/v2 v2.0.6 h1:4NU7uP5vSoK6TbaMj3NtY478TTAWLso/vL1gpNrInHg= 93 + github.com/hashicorp/golang-lru/arc/v2 v2.0.6/go.mod h1:cfdDIX05DWvYV6/shsxDfa/OVcRieOt+q4FnM8x+Xno= 94 + github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 95 + github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 96 + github.com/huin/goupnp v1.0.3 h1:N8No57ls+MnjlB+JPiCVSOyy/ot7MJTqlo7rn+NYSqQ= 97 + github.com/huin/goupnp v1.0.3/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y= 98 + github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= 99 + github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= 100 + github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA= 101 + github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU= 102 + github.com/ipfs/go-bitswap v0.11.0 h1:j1WVvhDX1yhG32NTC9xfxnqycqYIlhzEzLXG/cU1HyQ= 103 + github.com/ipfs/go-bitswap v0.11.0/go.mod h1:05aE8H3XOU+LXpTedeAS0OZpcO1WFsj5niYQH9a1Tmk= 104 + github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs= 105 + github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM= 106 + github.com/ipfs/go-blockservice v0.5.2 h1:in9Bc+QcXwd1apOVM7Un9t8tixPKdaHQFdLSUM1Xgk8= 107 + github.com/ipfs/go-blockservice v0.5.2/go.mod h1:VpMblFEqG67A/H2sHKAemeH9vlURVavlysbdUI632yk= 108 + github.com/ipfs/go-bs-sqlite3 v0.0.0-20221122195556-bfcee1be620d h1:9V+GGXCuOfDiFpdAHz58q9mKLg447xp0cQKvqQrAwYE= 109 + github.com/ipfs/go-bs-sqlite3 v0.0.0-20221122195556-bfcee1be620d/go.mod h1:pMbnFyNAGjryYCLCe59YDLRv/ujdN+zGJBT1umlvYRM= 110 + github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= 111 + github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= 112 + github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk= 113 + github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= 114 + github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= 115 + github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= 116 + github.com/ipfs/go-ds-flatfs v0.5.1 h1:ZCIO/kQOS/PSh3vcF1H6a8fkRGS7pOfwfPdx4n/KJH4= 117 + github.com/ipfs/go-ds-flatfs v0.5.1/go.mod h1:RWTV7oZD/yZYBKdbVIFXTX2fdY2Tbvl94NsWqmoyAX4= 118 + github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ= 119 + github.com/ipfs/go-ipfs-blockstore v1.3.1/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE= 120 + github.com/ipfs/go-ipfs-blocksutil v0.0.1 h1:Eh/H4pc1hsvhzsQoMEP3Bke/aW5P5rVM1IWFJMcGIPQ= 121 + github.com/ipfs/go-ipfs-blocksutil v0.0.1/go.mod h1:Yq4M86uIOmxmGPUHv/uI7uKqZNtLb449gwKqXjIsnRk= 122 + github.com/ipfs/go-ipfs-chunker v0.0.5 h1:ojCf7HV/m+uS2vhUGWcogIIxiO5ubl5O57Q7NapWLY8= 123 + github.com/ipfs/go-ipfs-chunker v0.0.5/go.mod h1:jhgdF8vxRHycr00k13FM8Y0E+6BoalYeobXmUyTreP8= 124 + github.com/ipfs/go-ipfs-delay v0.0.1 h1:r/UXYyRcddO6thwOnhiznIAiSvxMECGgtv35Xs1IeRQ= 125 + github.com/ipfs/go-ipfs-delay v0.0.1/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw= 126 + github.com/ipfs/go-ipfs-ds-help v1.1.1 h1:B5UJOH52IbcfS56+Ul+sv8jnIV10lbjLF5eOO0C66Nw= 127 + github.com/ipfs/go-ipfs-ds-help v1.1.1/go.mod h1:75vrVCkSdSFidJscs8n4W+77AtTpCIAdDGAwjitJMIo= 128 + github.com/ipfs/go-ipfs-exchange-interface v0.2.1 h1:jMzo2VhLKSHbVe+mHNzYgs95n0+t0Q69GQ5WhRDZV/s= 129 + github.com/ipfs/go-ipfs-exchange-interface v0.2.1/go.mod h1:MUsYn6rKbG6CTtsDp+lKJPmVt3ZrCViNyH3rfPGsZ2E= 130 + github.com/ipfs/go-ipfs-exchange-offline v0.3.0 h1:c/Dg8GDPzixGd0MC8Jh6mjOwU57uYokgWRFidfvEkuA= 131 + github.com/ipfs/go-ipfs-exchange-offline v0.3.0/go.mod h1:MOdJ9DChbb5u37M1IcbrRB02e++Z7521fMxqCNRrz9s= 132 + github.com/ipfs/go-ipfs-pq v0.0.3 h1:YpoHVJB+jzK15mr/xsWC574tyDLkezVrDNeaalQBsTE= 133 + github.com/ipfs/go-ipfs-pq v0.0.3/go.mod h1:btNw5hsHBpRcSSgZtiNm/SLj5gYIZ18AKtv3kERkRb4= 134 + github.com/ipfs/go-ipfs-routing v0.3.0 h1:9W/W3N+g+y4ZDeffSgqhgo7BsBSJwPMcyssET9OWevc= 135 + github.com/ipfs/go-ipfs-routing v0.3.0/go.mod h1:dKqtTFIql7e1zYsEuWLyuOU+E0WJWW8JjbTPLParDWo= 136 + github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0= 137 + github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= 138 + github.com/ipfs/go-ipld-cbor v0.1.0 h1:dx0nS0kILVivGhfWuB6dUpMa/LAwElHPw1yOGYopoYs= 139 + github.com/ipfs/go-ipld-cbor v0.1.0/go.mod h1:U2aYlmVrJr2wsUBU67K4KgepApSZddGRDWBYR0H4sCk= 140 + github.com/ipfs/go-ipld-format v0.6.0 h1:VEJlA2kQ3LqFSIm5Vu6eIlSxD/Ze90xtc4Meten1F5U= 141 + github.com/ipfs/go-ipld-format v0.6.0/go.mod h1:g4QVMTn3marU3qXchwjpKPKgJv+zF+OlaKMyhJ4LHPg= 142 + github.com/ipfs/go-ipld-legacy v0.2.1 h1:mDFtrBpmU7b//LzLSypVrXsD8QxkEWxu5qVxN99/+tk= 143 + github.com/ipfs/go-ipld-legacy v0.2.1/go.mod h1:782MOUghNzMO2DER0FlBR94mllfdCJCkTtDtPM51otM= 144 + github.com/ipfs/go-libipfs v0.7.0 h1:Mi54WJTODaOL2/ZSm5loi3SwI3jI2OuFWUrQIkJ5cpM= 145 + github.com/ipfs/go-libipfs v0.7.0/go.mod h1:KsIf/03CqhICzyRGyGo68tooiBE2iFbI/rXW7FhAYr0= 146 + github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= 147 + github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= 148 + github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= 149 + github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= 150 + github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= 151 + github.com/ipfs/go-merkledag v0.11.0 h1:DgzwK5hprESOzS4O1t/wi6JDpyVQdvm9Bs59N/jqfBY= 152 + github.com/ipfs/go-merkledag v0.11.0/go.mod h1:Q4f/1ezvBiJV0YCIXvt51W/9/kqJGH4I1LsA7+djsM4= 153 + github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg= 154 + github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY= 155 + github.com/ipfs/go-peertaskqueue v0.8.1 h1:YhxAs1+wxb5jk7RvS0LHdyiILpNmRIRnZVztekOF0pg= 156 + github.com/ipfs/go-peertaskqueue v0.8.1/go.mod h1:Oxxd3eaK279FxeydSPPVGHzbwVeHjatZ2GA8XD+KbPU= 157 + github.com/ipfs/go-unixfsnode v1.8.0 h1:yCkakzuE365glu+YkgzZt6p38CSVEBPgngL9ZkfnyQU= 158 + github.com/ipfs/go-unixfsnode v1.8.0/go.mod h1:HxRu9HYHOjK6HUqFBAi++7DVoWAHn0o4v/nZ/VA+0g8= 159 + github.com/ipfs/go-verifcid v0.0.3 h1:gmRKccqhWDocCRkC+a59g5QW7uJw5bpX9HWBevXa0zs= 160 + github.com/ipfs/go-verifcid v0.0.3/go.mod h1:gcCtGniVzelKrbk9ooUSX/pM3xlH73fZZJDzQJRvOUw= 161 + github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4 h1:oFo19cBmcP0Cmg3XXbrr0V/c+xU9U1huEZp8+OgBzdI= 162 + github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4/go.mod h1:6nkFF8OmR5wLKBzRKi7/YFJpyYR7+oEn1DX+mMWnlLA= 163 + github.com/ipld/go-car/v2 v2.13.1 h1:KnlrKvEPEzr5IZHKTXLAEub+tPrzeAFQVRlSQvuxBO4= 164 + github.com/ipld/go-car/v2 v2.13.1/go.mod h1:QkdjjFNGit2GIkpQ953KBwowuoukoM75nP/JI1iDJdo= 165 + github.com/ipld/go-codec-dagpb v1.6.0 h1:9nYazfyu9B1p3NAgfVdpRco3Fs2nFC72DqVsMj6rOcc= 166 + github.com/ipld/go-codec-dagpb v1.6.0/go.mod h1:ANzFhfP2uMJxRBr8CE+WQWs5UsNa0pYtmKZ+agnUw9s= 167 + github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E= 168 + github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ= 169 + github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20230102063945-1a409dc236dd h1:gMlw/MhNr2Wtp5RwGdsW23cs+yCuj9k2ON7i9MiJlRo= 170 + github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20230102063945-1a409dc236dd/go.mod h1:wZ8hH8UxeryOs4kJEJaiui/s00hDSbE37OKsL47g+Sw= 171 + github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 172 + github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 173 + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= 174 + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 175 + github.com/jackc/pgx/v5 v5.5.0 h1:NxstgwndsTRy7eq9/kqYc/BZh5w2hHJV86wjvO+1xPw= 176 + github.com/jackc/pgx/v5 v5.5.0/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= 177 + github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= 178 + github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 179 + github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= 180 + github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= 181 + github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= 182 + github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= 183 + github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= 184 + github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 185 + github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 186 + github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 187 + github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 188 + github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 189 + github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 190 + github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 191 + github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 192 + github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 193 + github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 194 + github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 195 + github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 196 + github.com/koron/go-ssdp v0.0.3 h1:JivLMY45N76b4p/vsWGOKewBQu6uf39y8l+AQ7sDKx8= 197 + github.com/koron/go-ssdp v0.0.3/go.mod h1:b2MxI6yh02pKrsyNoQUsk4+YNikaGhe4894J+Q5lDvA= 198 + github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 199 + github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 200 + github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 201 + github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 202 + github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 203 + github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 204 + github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 205 + github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= 206 + github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= 207 + github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= 208 + github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 209 + github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 210 + github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 211 + github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80= 212 + github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= 213 + github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= 214 + github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= 215 + github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= 216 + github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= 217 + github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= 218 + github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= 219 + github.com/lestrrat-go/jwx/v2 v2.0.12 h1:3d589+5w/b9b7S3DneICPW16AqTyYXB7VRjgluSDWeA= 220 + github.com/lestrrat-go/jwx/v2 v2.0.12/go.mod h1:Mq4KN1mM7bp+5z/W5HS8aCNs5RKZ911G/0y2qUjAQuQ= 221 + github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 222 + github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= 223 + github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 224 + github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= 225 + github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= 226 + github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c= 227 + github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= 228 + github.com/libp2p/go-libp2p v0.25.1 h1:YK+YDCHpYyTvitKWVxa5PfElgIpOONU01X5UcLEwJGA= 229 + github.com/libp2p/go-libp2p v0.25.1/go.mod h1:xnK9/1d9+jeQCVvi/f1g12KqtVi/jP/SijtKV1hML3g= 230 + github.com/libp2p/go-libp2p-asn-util v0.2.0 h1:rg3+Os8jbnO5DxkC7K/Utdi+DkY3q/d1/1q+8WeNAsw= 231 + github.com/libp2p/go-libp2p-asn-util v0.2.0/go.mod h1:WoaWxbHKBymSN41hWSq/lGKJEca7TNm58+gGJi2WsLI= 232 + github.com/libp2p/go-libp2p-record v0.2.0 h1:oiNUOCWno2BFuxt3my4i1frNrt7PerzB3queqa1NkQ0= 233 + github.com/libp2p/go-libp2p-record v0.2.0/go.mod h1:I+3zMkvvg5m2OcSdoL0KPljyJyvNDFGKX7QdlpYUcwk= 234 + github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= 235 + github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= 236 + github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= 237 + github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= 238 + github.com/libp2p/go-nat v0.1.0 h1:MfVsH6DLcpa04Xr+p8hmVRG4juse0s3J8HyNWYHffXg= 239 + github.com/libp2p/go-nat v0.1.0/go.mod h1:X7teVkwRHNInVNWQiO/tAiAVRwSr5zoRz4YSTC3uRBM= 240 + github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU= 241 + github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ= 242 + github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 243 + github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 244 + github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 245 + github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 246 + github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 247 + github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 248 + github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= 249 + github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 250 + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= 251 + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= 252 + github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= 253 + github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= 254 + github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 255 + github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 256 + github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 257 + github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 258 + github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= 259 + github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= 260 + github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= 261 + github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= 262 + github.com/multiformats/go-multiaddr v0.8.0 h1:aqjksEcqK+iD/Foe1RRFsGZh8+XFiGo7FgUCZlpv3LU= 263 + github.com/multiformats/go-multiaddr v0.8.0/go.mod h1:Fs50eBDWvZu+l3/9S6xAE7ZYj6yhxlvaVZjakWN7xRs= 264 + github.com/multiformats/go-multiaddr-dns v0.3.1 h1:QgQgR+LQVt3NPTjbrLLpsaT2ufAA2y0Mkk+QRVJbW3A= 265 + github.com/multiformats/go-multiaddr-dns v0.3.1/go.mod h1:G/245BRQ6FJGmryJCrOuTdB37AMA5AMOVuO6NY3JwTk= 266 + github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= 267 + github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= 268 + github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= 269 + github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= 270 + github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= 271 + github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= 272 + github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= 273 + github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 274 + github.com/multiformats/go-multistream v0.4.1 h1:rFy0Iiyn3YT0asivDUIR05leAdwZq3de4741sbiSdfo= 275 + github.com/multiformats/go-multistream v0.4.1/go.mod h1:Mz5eykRVAjJWckE2U78c6xqdtyNUEhKSM0Lwar2p77Q= 276 + github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= 277 + github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= 278 + github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= 279 + github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 280 + github.com/orandin/slog-gorm v1.3.2 h1:C0lKDQPAx/pF+8K2HL7bdShPwOEJpPM0Bn80zTzxU1g= 281 + github.com/orandin/slog-gorm v1.3.2/go.mod h1:MoZ51+b7xE9lwGNPYEhxcUtRNrYzjdcKvA8QXQQGEPA= 282 + github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 h1:1/WtZae0yGtPq+TI6+Tv1WTxkukpXeMlviSxvL7SRgk= 283 + github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9/go.mod h1:x3N5drFsm2uilKKuuYo6LdyD8vZAW55sH/9w+pbo1sw= 284 + github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 285 + github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 286 + github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 287 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0= 288 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= 289 + github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= 290 + github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= 291 + github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= 292 + github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= 293 + github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= 294 + github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= 295 + github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 296 + github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 297 + github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 298 + github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 299 + github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 300 + github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 301 + github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 302 + github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 303 + github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= 304 + github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= 305 + github.com/samber/slog-echo v1.16.1 h1:5Q5IUROkFqKcu/qJM/13AP1d3gd1RS+Q/4EvKQU1fuo= 306 + github.com/samber/slog-echo v1.16.1/go.mod h1:f+B3WR06saRXcaGRZ/I/UPCECDPqTUqadRIf7TmyRhI= 307 + github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= 308 + github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= 309 + github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 310 + github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= 311 + github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= 312 + github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= 313 + github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= 314 + github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 315 + github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 316 + github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 317 + github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 318 + github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 319 + github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 320 + github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 321 + github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 322 + github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 323 + github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 324 + github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 325 + github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 326 + github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 327 + github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 328 + github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 329 + github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 330 + github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= 331 + github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= 332 + github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 333 + github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 334 + github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 335 + github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 336 + github.com/warpfork/go-testmark v0.12.1 h1:rMgCpJfwy1sJ50x0M0NgyphxYYPMOODIJHhsXyEHU0s= 337 + github.com/warpfork/go-testmark v0.12.1/go.mod h1:kHwy7wfvGSPh1rQJYKayD4AbtNaeyZdcGi9tNJTaa5Y= 338 + github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= 339 + github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= 340 + github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11 h1:5HZfQkwe0mIfyDmc1Em5GqlNRzcdtlv4HTNmdpt7XH0= 341 + github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11/go.mod h1:Wlo/SzPmxVp6vXpGt/zaXhHH0fn4IxgqZc82aKg6bpQ= 342 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4= 343 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 344 + github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f h1:jQa4QT2UP9WYv2nzyawpKMOCl+Z/jW7djv2/J50lj9E= 345 + github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f/go.mod h1:p9UJB6dDgdPgMJZs7UjUOdulKyRr9fqkS+6JKAInPy8= 346 + github.com/whyrusleeping/go-did v0.0.0-20230824162731-404d1707d5d6 h1:yJ9/LwIGIk/c0CdoavpC9RNSGSruIspSZtxG3Nnldic= 347 + github.com/whyrusleeping/go-did v0.0.0-20230824162731-404d1707d5d6/go.mod h1:39U9RRVr4CKbXpXYopWn+FSH5s+vWu6+RmguSPWAq5s= 348 + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= 349 + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= 350 + github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 351 + github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 352 + github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 353 + github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 354 + gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= 355 + gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= 356 + gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q= 357 + gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= 358 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= 359 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= 360 + go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= 361 + go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= 362 + go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= 363 + go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= 364 + go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= 365 + go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= 366 + go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 367 + go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 368 + go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 369 + go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 370 + go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 371 + go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= 372 + go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= 373 + go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 374 + go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 375 + go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 376 + go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 377 + go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 378 + go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= 379 + go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= 380 + go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= 381 + go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 382 + golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 383 + golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 384 + golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 385 + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 386 + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 387 + golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= 388 + golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 389 + golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 390 + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= 391 + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= 392 + golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 393 + golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 394 + golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 395 + golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 396 + golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 397 + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 398 + golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 399 + golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= 400 + golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 401 + golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 402 + golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 403 + golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 404 + golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 405 + golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 406 + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 407 + golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 408 + golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 409 + golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 410 + golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 411 + golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 412 + golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 413 + golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 414 + golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 415 + golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 416 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 417 + golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 418 + golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 419 + golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 420 + golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 421 + golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 422 + golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 423 + golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 424 + golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 425 + golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 426 + golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 427 + golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 428 + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 429 + golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 430 + golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 431 + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 432 + golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 433 + golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 434 + golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 435 + golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 436 + golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 437 + golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 438 + golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 439 + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 440 + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 441 + golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 442 + golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 443 + golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= 444 + golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 445 + golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 446 + golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 447 + golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 448 + golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 449 + golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 450 + golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 451 + golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 452 + golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= 453 + golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 454 + golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 455 + golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 456 + golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 457 + golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 458 + golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 459 + golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 460 + golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 461 + golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 462 + golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 463 + golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 464 + golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 465 + golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 466 + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= 467 + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 468 + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 469 + golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 470 + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 471 + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 472 + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= 473 + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 474 + google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 475 + google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 476 + gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 477 + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 478 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 479 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 480 + gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 481 + gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 482 + gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 483 + gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 484 + gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 485 + gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 486 + gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 487 + gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 488 + gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 489 + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 490 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 491 + gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 492 + gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 493 + gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM= 494 + gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= 495 + gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= 496 + gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= 497 + gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= 498 + gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= 499 + honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 500 + lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= 501 + lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
+238
identity/identity.go
···
··· 1 + package identity 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "fmt" 7 + "io" 8 + "net" 9 + "net/http" 10 + "strings" 11 + 12 + "github.com/bluesky-social/indigo/atproto/syntax" 13 + ) 14 + 15 + func ResolveHandle(ctx context.Context, handle string) (string, error) { 16 + var did string 17 + 18 + _, err := syntax.ParseHandle(handle) 19 + if err != nil { 20 + return "", err 21 + } 22 + 23 + recs, err := net.LookupTXT(fmt.Sprintf("_atproto.%s", handle)) 24 + if err == nil { 25 + for _, rec := range recs { 26 + if strings.HasPrefix(rec, "did=") { 27 + did = strings.Split(rec, "did=")[1] 28 + break 29 + } 30 + } 31 + } else { 32 + fmt.Printf("erorr getting txt records: %v\n", err) 33 + } 34 + 35 + if did == "" { 36 + req, err := http.NewRequestWithContext( 37 + ctx, 38 + "GET", 39 + fmt.Sprintf("https://%s/.well-known/atproto-did", handle), 40 + nil, 41 + ) 42 + if err != nil { 43 + return "", nil 44 + } 45 + 46 + resp, err := http.DefaultClient.Do(req) 47 + if err != nil { 48 + return "", nil 49 + } 50 + defer resp.Body.Close() 51 + 52 + if resp.StatusCode != http.StatusOK { 53 + io.Copy(io.Discard, resp.Body) 54 + return "", fmt.Errorf("unable to resolve handle") 55 + } 56 + 57 + b, err := io.ReadAll(resp.Body) 58 + if err != nil { 59 + return "", err 60 + } 61 + 62 + maybeDid := string(b) 63 + 64 + if _, err := syntax.ParseDID(maybeDid); err != nil { 65 + return "", fmt.Errorf("unable to resolve handle") 66 + } 67 + 68 + did = maybeDid 69 + } 70 + 71 + return did, nil 72 + } 73 + 74 + type DidDoc struct { 75 + Context []string `json:"@context"` 76 + Id string `json:"id"` 77 + AlsoKnownAs []string `json:"alsoKnownAs"` 78 + VerificationMethods []DidDocVerificationMethod `json:"verificationMethods"` 79 + Service []DidDocService `json:"service"` 80 + } 81 + 82 + type DidDocVerificationMethod struct { 83 + Id string `json:"id"` 84 + Type string `json:"type"` 85 + Controller string `json:"controller"` 86 + PublicKeyMultibase string `json:"publicKeyMultibase"` 87 + } 88 + 89 + type DidDocService struct { 90 + Id string `json:"id"` 91 + Type string `json:"type"` 92 + ServiceEndpoint string `json:"serviceEndpoint"` 93 + } 94 + 95 + type DidData struct { 96 + Did string `json:"did"` 97 + VerificationMethods map[string]string `json:"verificationMethods"` 98 + RotationKeys []string `json:"rotationKeys"` 99 + AlsoKnownAs []string `json:"alsoKnownAs"` 100 + Services map[string]DidDataService `json:"services"` 101 + } 102 + 103 + type DidDataService struct { 104 + Type string `json:"type"` 105 + Endpoint string `json:"endpoint"` 106 + } 107 + 108 + type DidLog []DidLogEntry 109 + 110 + type DidLogEntry struct { 111 + Sig string `json:"sig"` 112 + Prev *string `json:"prev"` 113 + Type string `json:"string"` 114 + Services map[string]DidDataService `json:"services"` 115 + AlsoKnownAs []string `json:"alsoKnownAs"` 116 + RotationKeys []string `json:"rotationKeys"` 117 + VerificationMethods map[string]string `json:"verificationMethods"` 118 + } 119 + 120 + type DidAuditEntry struct { 121 + Did string `json:"did"` 122 + Operation DidLogEntry `json:"operation"` 123 + Cid string `json:"cid"` 124 + Nullified bool `json:"nullified"` 125 + CreatedAt string `json:"createdAt"` 126 + } 127 + 128 + type DidAuditLog []DidAuditEntry 129 + 130 + func FetchDidDoc(ctx context.Context, did string) (*DidDoc, error) { 131 + var ustr string 132 + if strings.HasPrefix(did, "did:plc:") { 133 + ustr = fmt.Sprintf("https://plc.directory/%s", did) 134 + } else if strings.HasPrefix(did, "did:web:") { 135 + ustr = fmt.Sprintf("https://%s/.well-known/did.json", strings.TrimPrefix(did, "did:web:")) 136 + } else { 137 + return nil, fmt.Errorf("did was not a supported did type") 138 + } 139 + 140 + req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil) 141 + if err != nil { 142 + return nil, err 143 + } 144 + 145 + resp, err := http.DefaultClient.Do(req) 146 + if err != nil { 147 + return nil, err 148 + } 149 + defer resp.Body.Close() 150 + 151 + if resp.StatusCode != 200 { 152 + io.Copy(io.Discard, resp.Body) 153 + return nil, fmt.Errorf("could not find identity in plc registry") 154 + } 155 + 156 + var diddoc DidDoc 157 + if err := json.NewDecoder(resp.Body).Decode(&diddoc); err != nil { 158 + return nil, err 159 + } 160 + 161 + return &diddoc, nil 162 + } 163 + 164 + func FetchDidData(ctx context.Context, did string) (*DidData, error) { 165 + var ustr string 166 + ustr = fmt.Sprintf("https://plc.directory/%s/data", did) 167 + 168 + req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil) 169 + if err != nil { 170 + return nil, err 171 + } 172 + 173 + resp, err := http.DefaultClient.Do(req) 174 + if err != nil { 175 + return nil, err 176 + } 177 + defer resp.Body.Close() 178 + 179 + if resp.StatusCode != 200 { 180 + io.Copy(io.Discard, resp.Body) 181 + return nil, fmt.Errorf("could not find identity in plc registry") 182 + } 183 + 184 + var diddata DidData 185 + if err := json.NewDecoder(resp.Body).Decode(&diddata); err != nil { 186 + return nil, err 187 + } 188 + 189 + return &diddata, nil 190 + } 191 + 192 + func FetchDidAuditLog(ctx context.Context, did string) (DidAuditLog, error) { 193 + var ustr string 194 + ustr = fmt.Sprintf("https://plc.directory/%s/log/audit", did) 195 + 196 + req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil) 197 + if err != nil { 198 + return nil, err 199 + } 200 + 201 + resp, err := http.DefaultClient.Do(req) 202 + if err != nil { 203 + return nil, err 204 + } 205 + defer resp.Body.Close() 206 + 207 + if resp.StatusCode != 200 { 208 + io.Copy(io.Discard, resp.Body) 209 + return nil, fmt.Errorf("could not find identity in plc registry") 210 + } 211 + 212 + var didlog DidAuditLog 213 + if err := json.NewDecoder(resp.Body).Decode(&didlog); err != nil { 214 + return nil, err 215 + } 216 + 217 + return didlog, nil 218 + } 219 + 220 + func ResolveService(ctx context.Context, did string) (string, error) { 221 + diddoc, err := FetchDidDoc(ctx, did) 222 + if err != nil { 223 + return "", err 224 + } 225 + 226 + var service string 227 + for _, svc := range diddoc.Service { 228 + if svc.Id == "#atproto_pds" { 229 + service = svc.ServiceEndpoint 230 + } 231 + } 232 + 233 + if service == "" { 234 + return "", fmt.Errorf("could not find atproto_pds service in identity services") 235 + } 236 + 237 + return service, nil 238 + }
+50
identity/mem_cache.go
···
··· 1 + package identity 2 + 3 + import ( 4 + "time" 5 + 6 + "github.com/hashicorp/golang-lru/v2/expirable" 7 + ) 8 + 9 + type MemCache struct { 10 + docCache *expirable.LRU[string, *DidDoc] 11 + didCache *expirable.LRU[string, string] 12 + } 13 + 14 + func NewMemCache(size int) *MemCache { 15 + docCache := expirable.NewLRU[string, *DidDoc](size, nil, 5*time.Minute) 16 + didCache := expirable.NewLRU[string, string](size, nil, 5*time.Minute) 17 + 18 + return &MemCache{ 19 + docCache: docCache, 20 + didCache: didCache, 21 + } 22 + } 23 + 24 + func (mc *MemCache) GetDoc(did string) (*DidDoc, bool) { 25 + return mc.docCache.Get(did) 26 + } 27 + 28 + func (mc *MemCache) PutDoc(did string, doc *DidDoc) error { 29 + mc.docCache.Add(did, doc) 30 + return nil 31 + } 32 + 33 + func (mc *MemCache) BustDoc(did string) error { 34 + mc.docCache.Remove(did) 35 + return nil 36 + } 37 + 38 + func (mc *MemCache) GetDid(handle string) (string, bool) { 39 + return mc.didCache.Get(handle) 40 + } 41 + 42 + func (mc *MemCache) PutDid(handle string, did string) error { 43 + mc.didCache.Add(handle, did) 44 + return nil 45 + } 46 + 47 + func (mc *MemCache) BustDid(handle string) error { 48 + mc.didCache.Remove(handle) 49 + return nil 50 + }
+79
identity/passport.go
···
··· 1 + package identity 2 + 3 + import ( 4 + "context" 5 + "sync" 6 + ) 7 + 8 + type BackingCache interface { 9 + GetDoc(did string) (*DidDoc, bool) 10 + PutDoc(did string, doc *DidDoc) error 11 + BustDoc(did string) error 12 + 13 + GetDid(handle string) (string, bool) 14 + PutDid(handle string, did string) error 15 + BustDid(handle string) error 16 + } 17 + 18 + type Passport struct { 19 + bc BackingCache 20 + lk sync.Mutex 21 + } 22 + 23 + func NewPassport(bc BackingCache) *Passport { 24 + return &Passport{ 25 + bc: bc, 26 + lk: sync.Mutex{}, 27 + } 28 + } 29 + 30 + func (p *Passport) FetchDoc(ctx context.Context, did string) (*DidDoc, error) { 31 + skipCache, _ := ctx.Value("skip-cache").(bool) 32 + 33 + if !skipCache { 34 + cached, ok := p.bc.GetDoc(did) 35 + if ok { 36 + return cached, nil 37 + } 38 + } 39 + 40 + p.lk.Lock() // this is pretty pathetic, and i should rethink this. but for now, fuck it 41 + defer p.lk.Unlock() 42 + 43 + doc, err := FetchDidDoc(ctx, did) 44 + if err != nil { 45 + return nil, err 46 + } 47 + 48 + p.bc.PutDoc(did, doc) 49 + 50 + return doc, nil 51 + } 52 + 53 + func (p *Passport) ResolveHandle(ctx context.Context, handle string) (string, error) { 54 + skipCache, _ := ctx.Value("skip-cache").(bool) 55 + 56 + if !skipCache { 57 + cached, ok := p.bc.GetDid(handle) 58 + if ok { 59 + return cached, nil 60 + } 61 + } 62 + 63 + did, err := ResolveHandle(ctx, handle) 64 + if err != nil { 65 + return "", err 66 + } 67 + 68 + p.bc.PutDid(handle, did) 69 + 70 + return did, nil 71 + } 72 + 73 + func (p *Passport) BustDoc(ctx context.Context, did string) error { 74 + return p.bc.BustDoc(did) 75 + } 76 + 77 + func (p *Passport) BustDid(ctx context.Context, handle string) error { 78 + return p.bc.BustDid(handle) 79 + }
+25
internal/helpers/helpers.go
···
··· 1 + package helpers 2 + 3 + import "github.com/labstack/echo/v4" 4 + 5 + func InputError(e echo.Context, custom *string) error { 6 + msg := "InvalidRequest" 7 + if custom != nil { 8 + msg = *custom 9 + } 10 + return genericError(e, 400, msg) 11 + } 12 + 13 + func ServerError(e echo.Context, suffix *string) error { 14 + msg := "Internal server error" 15 + if suffix != nil { 16 + msg += ". " + *suffix 17 + } 18 + return genericError(e, 400, msg) 19 + } 20 + 21 + func genericError(e echo.Context, code int, msg string) error { 22 + return e.JSON(code, map[string]string{ 23 + "error": msg, 24 + }) 25 + }
+96
models/models.go
···
··· 1 + package models 2 + 3 + import ( 4 + "context" 5 + "time" 6 + 7 + "github.com/bluesky-social/indigo/atproto/crypto" 8 + ) 9 + 10 + type Repo struct { 11 + Did string `gorm:"primaryKey"` 12 + CreatedAt time.Time 13 + Email string `gorm:"uniqueIndex"` 14 + EmailConfirmedAt *time.Time 15 + Password string 16 + SigningKey []byte 17 + Rev string 18 + Root []byte 19 + Preferences []byte 20 + } 21 + 22 + func (r *Repo) SignFor(ctx context.Context, did string, msg []byte) ([]byte, error) { 23 + k, err := crypto.ParsePrivateBytesK256(r.SigningKey) 24 + if err != nil { 25 + return nil, err 26 + } 27 + 28 + sig, err := k.HashAndSign(msg) 29 + if err != nil { 30 + return nil, err 31 + } 32 + 33 + return sig, nil 34 + } 35 + 36 + type Actor struct { 37 + Did string `gorm:"primaryKey"` 38 + Handle string `gorm:"uniqueIndex"` 39 + } 40 + 41 + type RepoActor struct { 42 + Repo 43 + Actor 44 + } 45 + 46 + type InviteCode struct { 47 + Code string `gorm:"primaryKey"` 48 + Did string `gorm:"index"` 49 + RemainingUseCount int 50 + } 51 + 52 + type Token struct { 53 + Token string `gorm:"primaryKey"` 54 + Did string `gorm:"index"` 55 + RefreshToken string `gorm:"index"` 56 + CreatedAt time.Time 57 + ExpiresAt time.Time `gorm:"index:,sort:asc"` 58 + } 59 + 60 + type RefreshToken struct { 61 + Token string `gorm:"primaryKey"` 62 + Did string `gorm:"index"` 63 + CreatedAt time.Time 64 + ExpiresAt time.Time `gorm:"index:,sort:asc"` 65 + } 66 + 67 + type Record struct { 68 + Did string `gorm:"primaryKey:idx_record_did_created_at;index:idx_record_did_nsid"` 69 + CreatedAt string `gorm:"index;index:idx_record_did_created_at,sort:desc"` 70 + Nsid string `gorm:"primaryKey;index:idx_record_did_nsid"` 71 + Rkey string `gorm:"primaryKey"` 72 + Cid string 73 + Value []byte 74 + } 75 + 76 + type Block struct { 77 + Did string `gorm:"primaryKey;index:idx_blocks_by_rev"` 78 + Cid []byte `gorm:"primaryKey"` 79 + Rev string `gorm:"index:idx_blocks_by_rev,sort:desc"` 80 + Value []byte 81 + } 82 + 83 + type Blob struct { 84 + ID uint 85 + CreatedAt string `gorm:"index"` 86 + Did string `gorm:"index;index:idx_blob_did_cid"` 87 + Cid []byte `gorm:"index;index:idx_blob_did_cid"` 88 + RefCount int 89 + } 90 + 91 + type BlobPart struct { 92 + Blob Blob 93 + BlobID uint `gorm:"primaryKey"` 94 + Idx int `gorm:"primaryKey"` 95 + Data []byte 96 + }
+182
plc/client.go
···
··· 1 + package plc 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "crypto/sha256" 7 + "encoding/base32" 8 + "encoding/base64" 9 + "encoding/json" 10 + "fmt" 11 + "io" 12 + "net/http" 13 + "net/url" 14 + "strings" 15 + "time" 16 + 17 + "github.com/bluesky-social/indigo/atproto/crypto" 18 + "github.com/bluesky-social/indigo/atproto/data" 19 + "github.com/bluesky-social/indigo/did" 20 + "github.com/bluesky-social/indigo/plc" 21 + "github.com/bluesky-social/indigo/util" 22 + ) 23 + 24 + type Client struct { 25 + plc.CachingDidResolver 26 + 27 + h *http.Client 28 + 29 + service string 30 + rotationKey *crypto.PrivateKeyK256 31 + recoveryKey string 32 + pdsHostname string 33 + } 34 + 35 + type ClientArgs struct { 36 + Service string 37 + RotationKey []byte 38 + RecoveryKey string 39 + PdsHostname string 40 + } 41 + 42 + func NewClient(args *ClientArgs) (*Client, error) { 43 + if args.Service == "" { 44 + args.Service = "https://plc.directory" 45 + } 46 + 47 + rk, err := crypto.ParsePrivateBytesK256([]byte(args.RotationKey)) 48 + if err != nil { 49 + return nil, err 50 + } 51 + 52 + resolver := did.NewMultiResolver() 53 + return &Client{ 54 + CachingDidResolver: *plc.NewCachingDidResolver(resolver, 5*time.Minute, 100_000), 55 + h: util.RobustHTTPClient(), 56 + service: args.Service, 57 + rotationKey: rk, 58 + recoveryKey: args.RecoveryKey, 59 + pdsHostname: args.PdsHostname, 60 + }, nil 61 + } 62 + 63 + func (c *Client) CreateDID(ctx context.Context, sigkey *crypto.PrivateKeyK256, recovery string, handle string) (string, map[string]any, error) { 64 + pubrotkey, err := c.rotationKey.PublicKey() 65 + if err != nil { 66 + return "", nil, err 67 + } 68 + 69 + // todo 70 + rotationKeys := []string{pubrotkey.DIDKey()} 71 + if c.recoveryKey != "" { 72 + rotationKeys = []string{c.recoveryKey, rotationKeys[0]} 73 + } 74 + if recovery != "" { 75 + rotationKeys = func(recovery string) []string { 76 + newRotationKeys := []string{recovery} 77 + for _, k := range rotationKeys { 78 + newRotationKeys = append(newRotationKeys, k) 79 + } 80 + return newRotationKeys 81 + }(recovery) 82 + } 83 + 84 + op, err := c.FormatAndSignAtprotoOp(sigkey, handle, rotationKeys, nil) 85 + if err != nil { 86 + return "", nil, err 87 + } 88 + 89 + did, err := didForCreateOp(op) 90 + if err != nil { 91 + return "", nil, err 92 + } 93 + 94 + return did, op, nil 95 + } 96 + 97 + func (c *Client) UpdateUserHandle(ctx context.Context, didstr string, nhandle string) error { 98 + return nil 99 + } 100 + 101 + func (c *Client) FormatAndSignAtprotoOp(sigkey *crypto.PrivateKeyK256, handle string, rotationKeys []string, prev *string) (map[string]any, error) { 102 + pubsigkey, err := sigkey.PublicKey() 103 + if err != nil { 104 + return nil, err 105 + } 106 + 107 + op := map[string]any{ 108 + "type": "plc_operation", 109 + "verificationMethods": map[string]string{ 110 + "atproto": pubsigkey.DIDKey(), 111 + }, 112 + "rotationKeys": rotationKeys, 113 + "alsoKnownAs": []string{"at://" + handle}, 114 + "services": map[string]any{ 115 + "atproto_pds": map[string]string{ 116 + "type": "AtprotoPersonalDataServer", 117 + "endpoint": "https://" + c.pdsHostname, 118 + }, 119 + }, 120 + "prev": prev, 121 + } 122 + 123 + b, err := data.MarshalCBOR(op) 124 + if err != nil { 125 + return nil, err 126 + } 127 + 128 + sig, err := c.rotationKey.HashAndSign(b) 129 + if err != nil { 130 + return nil, err 131 + } 132 + 133 + op["sig"] = base64.RawURLEncoding.EncodeToString(sig) 134 + 135 + return op, nil 136 + } 137 + 138 + func didForCreateOp(op map[string]any) (string, error) { 139 + b, err := data.MarshalCBOR(op) 140 + if err != nil { 141 + return "", err 142 + } 143 + 144 + h := sha256.New() 145 + h.Write(b) 146 + bs := h.Sum(nil) 147 + 148 + b32 := strings.ToLower(base32.StdEncoding.EncodeToString(bs)) 149 + 150 + return "did:plc:" + b32[0:24], nil 151 + } 152 + 153 + func (c *Client) SendOperation(ctx context.Context, did string, op any) error { 154 + b, err := json.Marshal(op) 155 + if err != nil { 156 + return err 157 + } 158 + 159 + req, err := http.NewRequestWithContext(ctx, "POST", c.service+"/"+url.QueryEscape(did), bytes.NewBuffer(b)) 160 + if err != nil { 161 + return err 162 + } 163 + 164 + req.Header.Add("content-type", "application/json") 165 + 166 + resp, err := c.h.Do(req) 167 + if err != nil { 168 + return err 169 + } 170 + defer resp.Body.Close() 171 + 172 + fmt.Println(resp.StatusCode) 173 + 174 + b, err = io.ReadAll(resp.Body) 175 + if err != nil { 176 + return err 177 + } 178 + 179 + fmt.Println(string(b)) 180 + 181 + return nil 182 + }
+29
server/common.go
···
··· 1 + package server 2 + 3 + import ( 4 + "github.com/haileyok/cocoon/models" 5 + ) 6 + 7 + func (s *Server) getActorByHandle(handle string) (*models.Actor, error) { 8 + var actor models.Actor 9 + if err := s.db.First(&actor, models.Actor{Handle: handle}).Error; err != nil { 10 + return nil, err 11 + } 12 + return &actor, nil 13 + } 14 + 15 + func (s *Server) getRepoByEmail(email string) (*models.Repo, error) { 16 + var repo models.Repo 17 + if err := s.db.First(&repo, models.Repo{Email: email}).Error; err != nil { 18 + return nil, err 19 + } 20 + return &repo, nil 21 + } 22 + 23 + func (s *Server) getRepoActorByDid(did string) (*models.RepoActor, error) { 24 + var repo models.RepoActor 25 + if err := s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.did = ?", did).Scan(&repo).Error; err != nil { 26 + return nil, err 27 + } 28 + return &repo, nil 29 + }
+24
server/handle_actor_get_preferences.go
···
··· 1 + package server 2 + 3 + import ( 4 + "encoding/json" 5 + 6 + "github.com/haileyok/cocoon/models" 7 + "github.com/labstack/echo/v4" 8 + ) 9 + 10 + // This is kinda lame. Not great to implement app.bsky in the pds, but alas 11 + 12 + func (s *Server) handleActorGetPreferences(e echo.Context) error { 13 + repo := e.Get("repo").(*models.RepoActor) 14 + 15 + var prefs map[string]any 16 + err := json.Unmarshal(repo.Preferences, &prefs) 17 + if err != nil { 18 + prefs = map[string]any{ 19 + "preferences": map[string]any{}, 20 + } 21 + } 22 + 23 + return e.JSON(200, prefs) 24 + }
+30
server/handle_actor_put_preferences.go
···
··· 1 + package server 2 + 3 + import ( 4 + "encoding/json" 5 + 6 + "github.com/haileyok/cocoon/models" 7 + "github.com/labstack/echo/v4" 8 + ) 9 + 10 + // This is kinda lame. Not great to implement app.bsky in the pds, but alas 11 + 12 + func (s *Server) handleActorPutPreferences(e echo.Context) error { 13 + repo := e.Get("repo").(*models.RepoActor) 14 + 15 + var prefs map[string]any 16 + if err := json.NewDecoder(e.Request().Body).Decode(&prefs); err != nil { 17 + return err 18 + } 19 + 20 + b, err := json.Marshal(prefs) 21 + if err != nil { 22 + return err 23 + } 24 + 25 + if err := s.db.Exec("UPDATE repos SET preferences = ? WHERE did = ?", b, repo.Repo.Did).Error; err != nil { 26 + return err 27 + } 28 + 29 + return nil 30 + }
+9
server/handle_health.go
···
··· 1 + package server 2 + 3 + import "github.com/labstack/echo/v4" 4 + 5 + func (s *Server) handleHealth(e echo.Context) error { 6 + return e.JSON(200, map[string]string{ 7 + "version": "cocoon " + s.config.Version, 8 + }) 9 + }
+89
server/handle_identity_update_handle.go
···
··· 1 + package server 2 + 3 + import ( 4 + "context" 5 + "strings" 6 + "time" 7 + 8 + "github.com/Azure/go-autorest/autorest/to" 9 + "github.com/bluesky-social/indigo/api/atproto" 10 + "github.com/bluesky-social/indigo/atproto/crypto" 11 + "github.com/bluesky-social/indigo/events" 12 + "github.com/bluesky-social/indigo/util" 13 + "github.com/haileyok/cocoon/identity" 14 + "github.com/haileyok/cocoon/internal/helpers" 15 + "github.com/haileyok/cocoon/models" 16 + "github.com/labstack/echo/v4" 17 + ) 18 + 19 + type ComAtprotoIdentityUpdateHandleRequest struct { 20 + Handle string `json:"handle" validate:"atproto-handle"` 21 + } 22 + 23 + func (s *Server) handleIdentityUpdateHandle(e echo.Context) error { 24 + repo := e.Get("repo").(*models.RepoActor) 25 + 26 + var req ComAtprotoIdentityUpdateHandleRequest 27 + if err := e.Bind(&req); err != nil { 28 + s.logger.Error("error binding", "error", err) 29 + return helpers.ServerError(e, nil) 30 + } 31 + 32 + req.Handle = strings.ToLower(req.Handle) 33 + 34 + if err := e.Validate(req); err != nil { 35 + return helpers.InputError(e, nil) 36 + } 37 + 38 + ctx := context.WithValue(e.Request().Context(), "skip-cache", true) 39 + 40 + if strings.HasPrefix(repo.Repo.Did, "did:plc:") { 41 + log, err := identity.FetchDidAuditLog(ctx, repo.Repo.Did) 42 + if err != nil { 43 + s.logger.Error("error fetching doc", "error", err) 44 + return helpers.ServerError(e, nil) 45 + } 46 + 47 + latest := log[len(log)-1] 48 + 49 + k, err := crypto.ParsePrivateBytesK256(repo.SigningKey) 50 + if err != nil { 51 + s.logger.Error("error parsing signing key", "error", err) 52 + return helpers.ServerError(e, nil) 53 + } 54 + 55 + op, err := s.plcClient.FormatAndSignAtprotoOp(k, req.Handle, latest.Operation.RotationKeys, &latest.Cid) 56 + if err != nil { 57 + return err 58 + } 59 + 60 + if err := s.plcClient.SendOperation(context.TODO(), repo.Repo.Did, op); err != nil { 61 + return err 62 + } 63 + } 64 + 65 + s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{ 66 + RepoHandle: &atproto.SyncSubscribeRepos_Handle{ 67 + Did: repo.Repo.Did, 68 + Handle: req.Handle, 69 + Seq: time.Now().UnixMicro(), // TODO: no 70 + Time: time.Now().Format(util.ISO8601), 71 + }, 72 + }) 73 + 74 + s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{ 75 + RepoIdentity: &atproto.SyncSubscribeRepos_Identity{ 76 + Did: repo.Repo.Did, 77 + Handle: to.StringPtr(req.Handle), 78 + Seq: time.Now().UnixMicro(), // TODO: no 79 + Time: time.Now().Format(util.ISO8601), 80 + }, 81 + }) 82 + 83 + if err := s.db.Exec("UPDATE actors SET handle = ? WHERE did = ?", req.Handle, repo.Repo.Did).Error; err != nil { 84 + s.logger.Error("error updating handle in db", "error", err) 85 + return helpers.ServerError(e, nil) 86 + } 87 + 88 + return nil 89 + }
+144
server/handle_proxy.go
···
··· 1 + package server 2 + 3 + import ( 4 + "crypto/rand" 5 + "crypto/sha256" 6 + "encoding/base64" 7 + "encoding/json" 8 + "fmt" 9 + "net/http" 10 + "strings" 11 + "time" 12 + 13 + "github.com/google/uuid" 14 + "github.com/haileyok/cocoon/internal/helpers" 15 + "github.com/haileyok/cocoon/models" 16 + "github.com/labstack/echo/v4" 17 + secp256k1secec "gitlab.com/yawning/secp256k1-voi/secec" 18 + ) 19 + 20 + func (s *Server) handleProxy(e echo.Context) error { 21 + repo, isAuthed := e.Get("repo").(*models.RepoActor) 22 + 23 + pts := strings.Split(e.Request().URL.Path, "/") 24 + if len(pts) != 3 { 25 + return fmt.Errorf("incorrect number of parts") 26 + } 27 + 28 + svc := e.Request().Header.Get("atproto-proxy") 29 + if svc == "" { 30 + svc = "did:web:api.bsky.app#bsky_appview" // TODO: should be a config var probably 31 + } 32 + 33 + svcPts := strings.Split(svc, "#") 34 + if len(svcPts) != 2 { 35 + return fmt.Errorf("invalid service header") 36 + } 37 + 38 + svcDid := svcPts[0] 39 + svcId := "#" + svcPts[1] 40 + 41 + doc, err := s.passport.FetchDoc(e.Request().Context(), svcDid) 42 + if err != nil { 43 + return err 44 + } 45 + 46 + var endpoint string 47 + for _, s := range doc.Service { 48 + if s.Id == svcId { 49 + endpoint = s.ServiceEndpoint 50 + } 51 + } 52 + 53 + requrl := e.Request().URL 54 + requrl.Host = strings.TrimPrefix(endpoint, "https://") 55 + requrl.Scheme = "https" 56 + 57 + body := e.Request().Body 58 + if e.Request().Method == "GET" { 59 + body = nil 60 + } 61 + 62 + req, err := http.NewRequest(e.Request().Method, requrl.String(), body) 63 + if err != nil { 64 + return err 65 + } 66 + 67 + req.Header = e.Request().Header.Clone() 68 + 69 + if isAuthed { 70 + // this is a little dumb. i should probably figure out a better way to do this, and use 71 + // a single way of creating/signing jwts throughout the pds. kinda limited here because 72 + // im using the atproto crypto lib for this though. will come back to it 73 + 74 + header := map[string]string{ 75 + "alg": "ES256K", 76 + "crv": "secp256k1", 77 + "typ": "JWT", 78 + } 79 + hj, err := json.Marshal(header) 80 + if err != nil { 81 + s.logger.Error("error marshaling header", "error", err) 82 + return helpers.ServerError(e, nil) 83 + } 84 + 85 + encheader := strings.TrimRight(base64.RawURLEncoding.EncodeToString(hj), "=") 86 + 87 + payload := map[string]any{ 88 + "iss": repo.Repo.Did, 89 + "aud": svcDid, 90 + "lxm": pts[2], 91 + "jti": uuid.NewString(), 92 + "exp": time.Now().Add(1 * time.Minute).UTC().Unix(), 93 + } 94 + pj, err := json.Marshal(payload) 95 + if err != nil { 96 + s.logger.Error("error marashaling payload", "error", err) 97 + return helpers.ServerError(e, nil) 98 + } 99 + 100 + encpayload := strings.TrimRight(base64.RawURLEncoding.EncodeToString(pj), "=") 101 + 102 + input := fmt.Sprintf("%s.%s", encheader, encpayload) 103 + hash := sha256.Sum256([]byte(input)) 104 + 105 + sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey) 106 + if err != nil { 107 + s.logger.Error("can't load private key", "error", err) 108 + return err 109 + } 110 + 111 + R, S, _, err := sk.SignRaw(rand.Reader, hash[:]) 112 + if err != nil { 113 + s.logger.Error("error signing", "error", err) 114 + } 115 + 116 + rBytes := R.Bytes() 117 + sBytes := S.Bytes() 118 + 119 + rPadded := make([]byte, 32) 120 + sPadded := make([]byte, 32) 121 + copy(rPadded[32-len(rBytes):], rBytes) 122 + copy(sPadded[32-len(sBytes):], sBytes) 123 + 124 + rawsig := append(rPadded, sPadded...) 125 + encsig := strings.TrimRight(base64.RawURLEncoding.EncodeToString(rawsig), "=") 126 + token := fmt.Sprintf("%s.%s", input, encsig) 127 + 128 + req.Header.Set("authorization", "Bearer "+token) 129 + } else { 130 + req.Header.Del("authorization") 131 + } 132 + 133 + resp, err := http.DefaultClient.Do(req) 134 + if err != nil { 135 + return err 136 + } 137 + defer resp.Body.Close() 138 + 139 + for k, v := range resp.Header { 140 + e.Response().Header().Set(k, strings.Join(v, ",")) 141 + } 142 + 143 + return e.Stream(resp.StatusCode, e.Response().Header().Get("content-type"), resp.Body) 144 + }
+58
server/handle_repo_apply_writes.go
···
··· 1 + package server 2 + 3 + import ( 4 + "github.com/haileyok/cocoon/internal/helpers" 5 + "github.com/haileyok/cocoon/models" 6 + "github.com/labstack/echo/v4" 7 + ) 8 + 9 + type ComAtprotoRepoApplyWritesRequest struct { 10 + Repo string `json:"repo" validate:"required,atproto-did"` 11 + Validate *bool `json:"bool,omitempty"` 12 + Writes []ComAtprotoRepoApplyWritesItem `json:"writes"` 13 + SwapCommit *string `json:"swapCommit"` 14 + } 15 + 16 + type ComAtprotoRepoApplyWritesItem struct { 17 + Type string `json:"$type"` 18 + Collection string `json:"collection"` 19 + Rkey string `json:"rkey"` 20 + Value *MarshalableMap `json:"value,omitempty"` 21 + } 22 + 23 + func (s *Server) handleApplyWrites(e echo.Context) error { 24 + repo := e.Get("repo").(*models.RepoActor) 25 + 26 + var req ComAtprotoRepoApplyWritesRequest 27 + if err := e.Bind(&req); err != nil { 28 + s.logger.Error("error binding", "error", err) 29 + return helpers.ServerError(e, nil) 30 + } 31 + 32 + if err := e.Validate(req); err != nil { 33 + s.logger.Error("error validating", "error", err) 34 + return helpers.InputError(e, nil) 35 + } 36 + 37 + if repo.Repo.Did != req.Repo { 38 + s.logger.Warn("mismatched repo/auth") 39 + return helpers.InputError(e, nil) 40 + } 41 + 42 + ops := []Op{} 43 + for _, item := range req.Writes { 44 + ops = append(ops, Op{ 45 + Type: OpType(item.Type), 46 + Collection: item.Collection, 47 + Rkey: &item.Rkey, 48 + Record: item.Value, 49 + }) 50 + } 51 + 52 + if err := s.repoman.applyWrites(repo.Repo, ops, req.SwapCommit); err != nil { 53 + s.logger.Error("error applying writes", "error", err) 54 + return helpers.ServerError(e, nil) 55 + } 56 + 57 + return nil 58 + }
+58
server/handle_repo_create_record.go
···
··· 1 + package server 2 + 3 + import ( 4 + "github.com/haileyok/cocoon/internal/helpers" 5 + "github.com/haileyok/cocoon/models" 6 + "github.com/labstack/echo/v4" 7 + ) 8 + 9 + type ComAtprotoRepoCreateRecordRequest struct { 10 + Repo string `json:"repo" validate:"required,atproto-did"` 11 + Collection string `json:"collection" validate:"required,atproto-nsid"` 12 + Rkey *string `json:"rkey,omitempty"` 13 + Validate *bool `json:"bool,omitempty"` 14 + Record MarshalableMap `json:"record" validate:"required"` 15 + SwapRecord *string `json:"swapRecord"` 16 + SwapCommit *string `json:"swapCommit"` 17 + } 18 + 19 + func (s *Server) handleCreateRecord(e echo.Context) error { 20 + repo := e.Get("repo").(*models.RepoActor) 21 + 22 + var req ComAtprotoRepoCreateRecordRequest 23 + if err := e.Bind(&req); err != nil { 24 + s.logger.Error("error binding", "error", err) 25 + return helpers.ServerError(e, nil) 26 + } 27 + 28 + if err := e.Validate(req); err != nil { 29 + s.logger.Error("error validating", "error", err) 30 + return helpers.InputError(e, nil) 31 + } 32 + 33 + if repo.Repo.Did != req.Repo { 34 + s.logger.Warn("mismatched repo/auth") 35 + return helpers.InputError(e, nil) 36 + } 37 + 38 + optype := OpTypeCreate 39 + if req.SwapRecord != nil { 40 + optype = OpTypeUpdate 41 + } 42 + 43 + if err := s.repoman.applyWrites(repo.Repo, []Op{ 44 + { 45 + Type: optype, 46 + Collection: req.Collection, 47 + Rkey: req.Rkey, 48 + Validate: req.Validate, 49 + Record: &req.Record, 50 + SwapRecord: req.SwapRecord, 51 + }, 52 + }, req.SwapCommit); err != nil { 53 + s.logger.Error("error applying writes", "error", err) 54 + return helpers.ServerError(e, nil) 55 + } 56 + 57 + return nil 58 + }
+84
server/handle_repo_describe_repo.go
···
··· 1 + package server 2 + 3 + import ( 4 + "strings" 5 + 6 + "github.com/Azure/go-autorest/autorest/to" 7 + "github.com/haileyok/cocoon/identity" 8 + "github.com/haileyok/cocoon/internal/helpers" 9 + "github.com/haileyok/cocoon/models" 10 + "github.com/labstack/echo/v4" 11 + "gorm.io/gorm" 12 + ) 13 + 14 + type ComAtprotoRepoDescribeRepoResponse struct { 15 + Did string `json:"did"` 16 + Handle string `json:"handle"` 17 + DidDoc identity.DidDoc `json:"didDoc"` 18 + Collections []string `json:"collections"` 19 + HandleIsCorrect bool `json:"handleIsCorrect"` 20 + } 21 + 22 + func (s *Server) handleDescribeRepo(e echo.Context) error { 23 + did := e.QueryParam("repo") 24 + repo, err := s.getRepoActorByDid(did) 25 + if err != nil { 26 + if err == gorm.ErrRecordNotFound { 27 + return helpers.InputError(e, to.StringPtr("RepoNotFound")) 28 + } 29 + 30 + s.logger.Error("error looking up repo", "error", err) 31 + return helpers.ServerError(e, nil) 32 + } 33 + 34 + handleIsCorrect := true 35 + 36 + diddoc, err := s.passport.FetchDoc(e.Request().Context(), repo.Repo.Did) 37 + if err != nil { 38 + s.logger.Error("error fetching diddoc", "error", err) 39 + return helpers.ServerError(e, nil) 40 + } 41 + 42 + dochandle := "" 43 + for _, aka := range diddoc.AlsoKnownAs { 44 + if strings.HasPrefix(aka, "at://") { 45 + dochandle = strings.TrimPrefix(aka, "at://") 46 + break 47 + } 48 + } 49 + 50 + if repo.Handle != dochandle { 51 + handleIsCorrect = false 52 + } 53 + 54 + if handleIsCorrect { 55 + resolvedDid, err := s.passport.ResolveHandle(e.Request().Context(), repo.Handle) 56 + if err != nil { 57 + e.Logger().Error("error resolving handle", "error", err) 58 + return helpers.ServerError(e, nil) 59 + } 60 + 61 + if resolvedDid != repo.Repo.Did { 62 + handleIsCorrect = false 63 + } 64 + } 65 + 66 + var records []models.Record 67 + if err := s.db.Raw("SELECT DISTINCT(nsid) FROM records WHERE did = ?", repo.Repo.Did).Scan(&records).Error; err != nil { 68 + s.logger.Error("error getting collections", "error", err) 69 + return helpers.ServerError(e, nil) 70 + } 71 + 72 + var collections []string 73 + for _, r := range records { 74 + collections = append(collections, r.Nsid) 75 + } 76 + 77 + return e.JSON(200, ComAtprotoRepoDescribeRepoResponse{ 78 + Did: repo.Repo.Did, 79 + Handle: repo.Handle, 80 + DidDoc: *diddoc, 81 + Collections: collections, 82 + HandleIsCorrect: handleIsCorrect, 83 + }) 84 + }
+50
server/handle_repo_get_record.go
···
··· 1 + package server 2 + 3 + import ( 4 + "github.com/bluesky-social/indigo/atproto/data" 5 + "github.com/bluesky-social/indigo/atproto/syntax" 6 + "github.com/haileyok/cocoon/models" 7 + "github.com/labstack/echo/v4" 8 + ) 9 + 10 + type ComAtprotoRepoGetRecordResponse struct { 11 + Uri string `json:"uri"` 12 + Cid string `json:"cid"` 13 + Value map[string]any `json:"value"` 14 + } 15 + 16 + func (s *Server) handleRepoGetRecord(e echo.Context) error { 17 + repo := e.QueryParam("repo") 18 + collection := e.QueryParam("collection") 19 + rkey := e.QueryParam("rkey") 20 + cidstr := e.QueryParam("cid") 21 + 22 + params := []any{repo, collection, rkey} 23 + cidquery := "" 24 + 25 + if cidstr != "" { 26 + c, err := syntax.ParseCID(cidstr) 27 + if err != nil { 28 + return err 29 + } 30 + params = append(params, c.String()) 31 + cidquery = " AND cid = ?" 32 + } 33 + 34 + var record models.Record 35 + if err := s.db.Raw("SELECT * FROM records WHERE did = ? AND nsid = ? AND rkey = ?"+cidquery, params...).Scan(&record).Error; err != nil { 36 + // TODO: handle error nicely 37 + return err 38 + } 39 + 40 + val, err := data.UnmarshalCBOR(record.Value) 41 + if err != nil { 42 + return s.handleProxy(e) // TODO: this should be getting handled like...if we don't find it in the db. why doesn't it throw error up there? 43 + } 44 + 45 + return e.JSON(200, ComAtprotoRepoGetRecordResponse{ 46 + Uri: "at://" + record.Did + "/" + record.Nsid + "/" + record.Rkey, 47 + Cid: record.Cid, 48 + Value: val, 49 + }) 50 + }
+95
server/handle_repo_list_records.go
···
··· 1 + package server 2 + 3 + import ( 4 + "strconv" 5 + "strings" 6 + 7 + "github.com/Azure/go-autorest/autorest/to" 8 + "github.com/bluesky-social/indigo/atproto/data" 9 + "github.com/haileyok/cocoon/internal/helpers" 10 + "github.com/haileyok/cocoon/models" 11 + "github.com/labstack/echo/v4" 12 + ) 13 + 14 + type ComAtprotoRepoListRecordsResponse struct { 15 + Cursor *string `json:"cursor,omitempty"` 16 + Records []ComAtprotoRepoListRecordsRecordItem `json:"records"` 17 + } 18 + 19 + type ComAtprotoRepoListRecordsRecordItem struct { 20 + Uri string `json:"uri"` 21 + Cid string `json:"cid"` 22 + Value map[string]any `json:"value"` 23 + } 24 + 25 + func getLimitFromContext(e echo.Context, def int) (int, error) { 26 + limit := def 27 + limitstr := e.QueryParam("limit") 28 + 29 + if limitstr != "" { 30 + l64, err := strconv.ParseInt(limitstr, 10, 32) 31 + if err != nil { 32 + return 0, err 33 + } 34 + limit = int(l64) 35 + } 36 + 37 + return limit, nil 38 + } 39 + 40 + func (s *Server) handleListRecords(e echo.Context) error { 41 + did := e.QueryParam("repo") 42 + collection := e.QueryParam("collection") 43 + cursor := e.QueryParam("cursor") 44 + reverse := e.QueryParam("reverse") 45 + limit, err := getLimitFromContext(e, 50) 46 + if err != nil { 47 + return helpers.InputError(e, nil) 48 + } 49 + 50 + sort := "DESC" 51 + dir := "<" 52 + cursorquery := "" 53 + 54 + if strings.ToLower(reverse) == "true" { 55 + sort = "ASC" 56 + dir = ">" 57 + } 58 + 59 + params := []any{did, collection} 60 + if cursor != "" { 61 + params = append(params, cursor) 62 + cursorquery = "AND created_at " + dir + " ?" 63 + } 64 + params = append(params, limit) 65 + 66 + var records []models.Record 67 + if err := s.db.Raw("SELECT * FROM records WHERE did = ? AND nsid = ? "+cursorquery+" ORDER BY created_at "+sort+" limit ?", params...).Scan(&records).Error; err != nil { 68 + s.logger.Error("error getting records", "error", err) 69 + return helpers.ServerError(e, nil) 70 + } 71 + 72 + items := []ComAtprotoRepoListRecordsRecordItem{} 73 + for _, r := range records { 74 + val, err := data.UnmarshalCBOR(r.Value) 75 + if err != nil { 76 + return err 77 + } 78 + 79 + items = append(items, ComAtprotoRepoListRecordsRecordItem{ 80 + Uri: "at://" + r.Did + "/" + r.Nsid + "/" + r.Rkey, 81 + Cid: r.Cid, 82 + Value: val, 83 + }) 84 + } 85 + 86 + var newcursor *string 87 + if len(records) == 50 { 88 + newcursor = to.StringPtr(records[len(records)-1].CreatedAt) 89 + } 90 + 91 + return e.JSON(200, ComAtprotoRepoListRecordsResponse{ 92 + Cursor: newcursor, 93 + Records: items, 94 + }) 95 + }
+49
server/handle_repo_list_repos.go
···
··· 1 + package server 2 + 3 + import ( 4 + "github.com/haileyok/cocoon/models" 5 + "github.com/ipfs/go-cid" 6 + "github.com/labstack/echo/v4" 7 + ) 8 + 9 + type ComAtprotoSyncListReposResponse struct { 10 + Cursor *string `json:"cursor,omitempty"` 11 + Repos []ComAtprotoSyncListReposRepoItem `json:"repos"` 12 + } 13 + 14 + type ComAtprotoSyncListReposRepoItem struct { 15 + Did string `json:"did"` 16 + Head string `json:"head"` 17 + Rev string `json:"rev"` 18 + Active bool `json:"active"` 19 + Status *string `json:"status,omitempty"` 20 + } 21 + 22 + // TODO: paginate this bitch 23 + func (s *Server) handleListRepos(e echo.Context) error { 24 + var repos []models.Repo 25 + if err := s.db.Raw("SELECT * FROM repos ORDER BY created_at DESC LIMIT 500").Scan(&repos).Error; err != nil { 26 + return err 27 + } 28 + 29 + var items []ComAtprotoSyncListReposRepoItem 30 + for _, r := range repos { 31 + c, err := cid.Cast(r.Root) 32 + if err != nil { 33 + return err 34 + } 35 + 36 + items = append(items, ComAtprotoSyncListReposRepoItem{ 37 + Did: r.Did, 38 + Head: c.String(), 39 + Rev: r.Rev, 40 + Active: true, 41 + Status: nil, 42 + }) 43 + } 44 + 45 + return e.JSON(200, ComAtprotoSyncListReposResponse{ 46 + Cursor: nil, 47 + Repos: items, 48 + }) 49 + }
+58
server/handle_repo_put_record.go
···
··· 1 + package server 2 + 3 + import ( 4 + "github.com/haileyok/cocoon/internal/helpers" 5 + "github.com/haileyok/cocoon/models" 6 + "github.com/labstack/echo/v4" 7 + ) 8 + 9 + type ComAtprotoRepoPutRecordRequest struct { 10 + Repo string `json:"repo" validate:"required,atproto-did"` 11 + Collection string `json:"collection" validate:"required,atproto-nsid"` 12 + Rkey string `json:"rkey" validate:"required,atproto-rkey"` 13 + Validate *bool `json:"bool,omitempty"` 14 + Record MarshalableMap `json:"record" validate:"required"` 15 + SwapRecord *string `json:"swapRecord"` 16 + SwapCommit *string `json:"swapCommit"` 17 + } 18 + 19 + func (s *Server) handlePutRecord(e echo.Context) error { 20 + repo := e.Get("repo").(*models.RepoActor) 21 + 22 + var req ComAtprotoRepoPutRecordRequest 23 + if err := e.Bind(&req); err != nil { 24 + s.logger.Error("error binding", "error", err) 25 + return helpers.ServerError(e, nil) 26 + } 27 + 28 + if err := e.Validate(req); err != nil { 29 + s.logger.Error("error validating", "error", err) 30 + return helpers.InputError(e, nil) 31 + } 32 + 33 + if repo.Repo.Did != req.Repo { 34 + s.logger.Warn("mismatched repo/auth") 35 + return helpers.InputError(e, nil) 36 + } 37 + 38 + optype := OpTypeCreate 39 + if req.SwapRecord != nil { 40 + optype = OpTypeUpdate 41 + } 42 + 43 + if err := s.repoman.applyWrites(repo.Repo, []Op{ 44 + { 45 + Type: optype, 46 + Collection: req.Collection, 47 + Rkey: &req.Rkey, 48 + Validate: req.Validate, 49 + Record: &req.Record, 50 + SwapRecord: req.SwapRecord, 51 + }, 52 + }, req.SwapCommit); err != nil { 53 + s.logger.Error("error applying writes", "error", err) 54 + return helpers.ServerError(e, nil) 55 + } 56 + 57 + return nil 58 + }
+105
server/handle_repo_upload_blob.go
···
··· 1 + package server 2 + 3 + import ( 4 + "bytes" 5 + "io" 6 + 7 + "github.com/haileyok/cocoon/internal/helpers" 8 + "github.com/haileyok/cocoon/models" 9 + "github.com/ipfs/go-cid" 10 + "github.com/labstack/echo/v4" 11 + "github.com/multiformats/go-multihash" 12 + ) 13 + 14 + const ( 15 + blockSize = 0x10000 16 + ) 17 + 18 + type ComAtprotoRepoUploadBlobResponse struct { 19 + Blob struct { 20 + Type string `json:"$type"` 21 + Ref struct { 22 + Link string `json:"$link"` 23 + } `json:"ref"` 24 + MimeType string `json:"mimeType"` 25 + Size int `json:"size"` 26 + } `json:"blob"` 27 + } 28 + 29 + func (s *Server) handleRepoUploadBlob(e echo.Context) error { 30 + urepo := e.Get("repo").(*models.RepoActor) 31 + 32 + mime := e.Request().Header.Get("content-type") 33 + if mime == "" { 34 + mime = "application/octet-stream" 35 + } 36 + 37 + blob := models.Blob{ 38 + Did: urepo.Repo.Did, 39 + RefCount: 0, 40 + CreatedAt: s.repoman.clock.Next().String(), 41 + } 42 + 43 + if err := s.db.Create(&blob).Error; err != nil { 44 + s.logger.Error("error creating new blob in db", "error", err) 45 + return helpers.ServerError(e, nil) 46 + } 47 + 48 + read := 0 49 + part := 0 50 + 51 + buf := make([]byte, 0x10000) 52 + fulldata := new(bytes.Buffer) 53 + 54 + for { 55 + n, err := io.ReadFull(e.Request().Body, buf) 56 + if err == io.ErrUnexpectedEOF || err == io.EOF { 57 + if n == 0 { 58 + break 59 + } 60 + } else if err != nil && err != io.ErrUnexpectedEOF { 61 + s.logger.Error("error reading blob", "error", err) 62 + return helpers.ServerError(e, nil) 63 + } 64 + 65 + data := buf[:n] 66 + read += n 67 + fulldata.Write(data) 68 + 69 + blobPart := models.BlobPart{ 70 + BlobID: blob.ID, 71 + Idx: part, 72 + Data: data, 73 + } 74 + 75 + if err := s.db.Create(&blobPart).Error; err != nil { 76 + s.logger.Error("error adding blob part to db", "error", err) 77 + return helpers.ServerError(e, nil) 78 + } 79 + part++ 80 + 81 + if n < blockSize { 82 + break 83 + } 84 + } 85 + 86 + c, err := cid.NewPrefixV1(cid.Raw, multihash.SHA2_256).Sum(fulldata.Bytes()) 87 + if err != nil { 88 + s.logger.Error("error creating cid prefix", "error", err) 89 + return helpers.ServerError(e, nil) 90 + } 91 + 92 + if err := s.db.Exec("UPDATE blobs SET cid = ? WHERE id = ?", c.Bytes(), blob.ID).Error; err != nil { 93 + // there should probably be somme handling here if this fails... 94 + s.logger.Error("error updating blob", "error", err) 95 + return helpers.ServerError(e, nil) 96 + } 97 + 98 + resp := ComAtprotoRepoUploadBlobResponse{} 99 + resp.Blob.Type = "blob" 100 + resp.Blob.Ref.Link = c.String() 101 + resp.Blob.MimeType = mime 102 + resp.Blob.Size = read 103 + 104 + return e.JSON(200, resp) 105 + }
+7
server/handle_robots.go
···
··· 1 + package server 2 + 3 + import "github.com/labstack/echo/v4" 4 + 5 + func (s *Server) handleRobots(e echo.Context) error { 6 + return e.String(200, "# Beep boop beep boop\n\n# Crawl me 🥺\nUser-agent: *\nAllow: /") 7 + }
+40
server/handle_root.go
···
··· 1 + package server 2 + 3 + import "github.com/labstack/echo/v4" 4 + 5 + func (s *Server) handleRoot(e echo.Context) error { 6 + return e.String(200, ` 7 + 8 + ....-*%%%##### 9 + .%#+++****#%%%%%%%%%#+:.... 10 + .%+++**++++*%%%%..... 11 + .%+++*****#%%%%#.. %#%... 12 + ***+*****%%%%%... =.. 13 + *****%%%%.. +=++.. 14 + %%%%%... .+----==++. 15 + .-::----===++ 16 + .=-:.------==+++ 17 + +-:::-:----===++.. 18 + =-::-----:-==+++-. 19 + .==*=------==++++. 20 + +-:--=++===*=--++. 21 + +:::--:=++=----=+.. 22 + *::::---=+#----=+. 23 + =::::----=+#---=+.. 24 + .::::----==+=--=+.. 25 + .-::-----==++=-=+.. 26 + -::-----==++===+.. 27 + =::-----==++==++ 28 + +::----:==++=+++ 29 + :-:----:==+++++. 30 + .=:=----=+++++. 31 + +=-=====+++.. 32 + =====++. 33 + =++... 34 + 35 + 36 + This is an AT Protocol Personal Data Server (aka, an atproto PDS) 37 + 38 + Code: https://github.com/haileyok/cocoon 39 + Version: `+s.config.Version+"\n") 40 + }
+208
server/handle_server_create_account.go
···
··· 1 + package server 2 + 3 + import ( 4 + "context" 5 + "errors" 6 + "strings" 7 + "time" 8 + 9 + "github.com/Azure/go-autorest/autorest/to" 10 + "github.com/bluesky-social/indigo/api/atproto" 11 + "github.com/bluesky-social/indigo/atproto/crypto" 12 + "github.com/bluesky-social/indigo/events" 13 + "github.com/bluesky-social/indigo/repo" 14 + "github.com/bluesky-social/indigo/util" 15 + "github.com/haileyok/cocoon/blockstore" 16 + "github.com/haileyok/cocoon/internal/helpers" 17 + "github.com/haileyok/cocoon/models" 18 + "github.com/labstack/echo/v4" 19 + "golang.org/x/crypto/bcrypt" 20 + "gorm.io/gorm" 21 + ) 22 + 23 + type ComAtprotoServerCreateAccountRequest struct { 24 + Email string `json:"email" validate:"required,email"` 25 + Handle string `json:"handle" validate:"required,atproto-handle"` 26 + Did *string `json:"did" validate:"atproto-did"` 27 + Password string `json:"password" validate:"required"` 28 + InviteCode string `json:"inviteCode" validate:"required"` 29 + } 30 + 31 + type ComAtprotoServerCreateAccountResponse struct { 32 + AccessJwt string `json:"accessJwt"` 33 + RefreshJwt string `json:"refreshJwt"` 34 + Handle string `json:"handle"` 35 + Did string `json:"did"` 36 + } 37 + 38 + func (s *Server) handleCreateAccount(e echo.Context) error { 39 + var request ComAtprotoServerCreateAccountRequest 40 + 41 + if err := e.Bind(&request); err != nil { 42 + s.logger.Error("error receiving request", "endpoint", "com.atproto.server.createAccount", "error", err) 43 + return helpers.ServerError(e, nil) 44 + } 45 + 46 + request.Handle = strings.ToLower(request.Handle) 47 + 48 + if err := e.Validate(request); err != nil { 49 + s.logger.Error("error validating request", "endpoint", "com.atproto.server.createAccount", "error", err) 50 + 51 + var verr ValidationError 52 + if errors.As(err, &verr) { 53 + if verr.Field == "Email" { 54 + // TODO: what is this supposed to be? `InvalidEmail` isn't listed in doc 55 + return helpers.InputError(e, to.StringPtr("InvalidEmail")) 56 + } 57 + 58 + if verr.Field == "Handle" { 59 + return helpers.InputError(e, to.StringPtr("InvalidHandle")) 60 + } 61 + 62 + if verr.Field == "Password" { 63 + return helpers.InputError(e, to.StringPtr("InvalidPassword")) 64 + } 65 + 66 + if verr.Field == "InviteCode" { 67 + return helpers.InputError(e, to.StringPtr("InvalidInviteCode")) 68 + } 69 + } 70 + } 71 + 72 + // see if the handle is already taken 73 + _, err := s.getActorByHandle(request.Handle) 74 + if err != nil && err != gorm.ErrRecordNotFound { 75 + s.logger.Error("error looking up handle in db", "endpoint", "com.atproto.server.createAccount", "error", err) 76 + return helpers.ServerError(e, nil) 77 + } 78 + if err == nil { 79 + return helpers.InputError(e, to.StringPtr("HandleNotAvailable")) 80 + } 81 + 82 + if did, err := s.passport.ResolveHandle(e.Request().Context(), request.Handle); err == nil && did != "" { 83 + return helpers.InputError(e, to.StringPtr("HandleNotAvailable")) 84 + } 85 + 86 + var ic models.InviteCode 87 + if err := s.db.Raw("SELECT * FROM invite_codes WHERE code = ?", request.InviteCode).Scan(&ic).Error; err != nil { 88 + if err == gorm.ErrRecordNotFound { 89 + return helpers.InputError(e, to.StringPtr("InvalidInviteCode")) 90 + } 91 + s.logger.Error("error getting invite code from db", "error", err) 92 + return helpers.ServerError(e, nil) 93 + } 94 + 95 + if ic.RemainingUseCount < 1 { 96 + return helpers.InputError(e, to.StringPtr("InvalidInviteCode")) 97 + } 98 + 99 + // see if the email is already taken 100 + _, err = s.getRepoByEmail(request.Email) 101 + if err != nil && err != gorm.ErrRecordNotFound { 102 + s.logger.Error("error looking up email in db", "endpoint", "com.atproto.server.createAccount", "error", err) 103 + return helpers.ServerError(e, nil) 104 + } 105 + if err == nil { 106 + return helpers.InputError(e, to.StringPtr("EmailNotAvailable")) 107 + } 108 + 109 + // TODO: unsupported domains 110 + 111 + // TODO: did stuff 112 + 113 + k, err := crypto.GeneratePrivateKeyK256() 114 + if err != nil { 115 + s.logger.Error("error creating signing key", "endpoint", "com.atproto.server.createAccount", "error", err) 116 + return helpers.ServerError(e, nil) 117 + } 118 + 119 + did, op, err := s.plcClient.CreateDID(e.Request().Context(), k, "", request.Handle) 120 + if err != nil { 121 + s.logger.Error("error creating operation", "endpoint", "com.atproto.server.createAccount", "error", err) 122 + return helpers.ServerError(e, nil) 123 + } 124 + 125 + if err := s.plcClient.SendOperation(e.Request().Context(), did, op); err != nil { 126 + s.logger.Error("error sending plc op", "endpoint", "com.atproto.server.createAccount", "error", err) 127 + return helpers.ServerError(e, nil) 128 + } 129 + 130 + hashed, err := bcrypt.GenerateFromPassword([]byte(request.Password), 10) 131 + if err != nil { 132 + s.logger.Error("error hashing password", "error", err) 133 + return helpers.ServerError(e, nil) 134 + } 135 + 136 + urepo := models.Repo{ 137 + Did: did, 138 + CreatedAt: time.Now(), 139 + Email: request.Email, 140 + Password: string(hashed), 141 + SigningKey: k.Bytes(), 142 + } 143 + 144 + actor := models.Actor{ 145 + Did: did, 146 + Handle: request.Handle, 147 + } 148 + 149 + if err := s.db.Create(&urepo).Error; err != nil { 150 + s.logger.Error("error inserting new repo", "error", err) 151 + return helpers.ServerError(e, nil) 152 + } 153 + 154 + bs := blockstore.New(did, s.db) 155 + r := repo.NewRepo(context.TODO(), did, bs) 156 + 157 + root, rev, err := r.Commit(context.TODO(), urepo.SignFor) 158 + if err != nil { 159 + s.logger.Error("error committing", "error", err) 160 + return helpers.ServerError(e, nil) 161 + } 162 + 163 + if err := bs.UpdateRepo(context.TODO(), root, rev); err != nil { 164 + s.logger.Error("error updating repo after commit", "error", err) 165 + return helpers.ServerError(e, nil) 166 + } 167 + 168 + s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{ 169 + RepoHandle: &atproto.SyncSubscribeRepos_Handle{ 170 + Did: urepo.Did, 171 + Handle: request.Handle, 172 + Seq: time.Now().UnixMicro(), // TODO: no 173 + Time: time.Now().Format(util.ISO8601), 174 + }, 175 + }) 176 + 177 + s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{ 178 + RepoIdentity: &atproto.SyncSubscribeRepos_Identity{ 179 + Did: urepo.Did, 180 + Handle: to.StringPtr(request.Handle), 181 + Seq: time.Now().UnixMicro(), // TODO: no 182 + Time: time.Now().Format(util.ISO8601), 183 + }, 184 + }) 185 + 186 + if err := s.db.Create(&actor).Error; err != nil { 187 + s.logger.Error("error inserting new actor", "error", err) 188 + return helpers.ServerError(e, nil) 189 + } 190 + 191 + if err := s.db.Raw("UPDATE invite_codes SET remaining_use_count = remaining_use_count - 1 WHERE code = ?", request.InviteCode).Scan(&ic).Error; err != nil { 192 + s.logger.Error("error decrementing use count", "error", err) 193 + return helpers.ServerError(e, nil) 194 + } 195 + 196 + sess, err := s.createSession(&urepo) 197 + if err != nil { 198 + s.logger.Error("error creating new session", "error", err) 199 + return helpers.ServerError(e, nil) 200 + } 201 + 202 + return e.JSON(200, ComAtprotoServerCreateAccountResponse{ 203 + AccessJwt: sess.AccessToken, 204 + RefreshJwt: sess.RefreshToken, 205 + Handle: request.Handle, 206 + Did: did, 207 + }) 208 + }
+17
server/handle_server_create_invite_code.go
···
··· 1 + package server 2 + 3 + import ( 4 + "github.com/google/uuid" 5 + "github.com/haileyok/cocoon/models" 6 + "github.com/labstack/echo/v4" 7 + ) 8 + 9 + func (s *Server) handleCreateInviteCode(e echo.Context) error { 10 + ic := models.InviteCode{ 11 + Code: uuid.NewString(), 12 + } 13 + 14 + return e.JSON(200, map[string]string{ 15 + "code": ic.Code, 16 + }) 17 + }
+108
server/handle_server_create_session.go
···
··· 1 + package server 2 + 3 + import ( 4 + "errors" 5 + "strings" 6 + 7 + "github.com/Azure/go-autorest/autorest/to" 8 + "github.com/bluesky-social/indigo/atproto/syntax" 9 + "github.com/haileyok/cocoon/internal/helpers" 10 + "github.com/haileyok/cocoon/models" 11 + "github.com/labstack/echo/v4" 12 + "golang.org/x/crypto/bcrypt" 13 + "gorm.io/gorm" 14 + ) 15 + 16 + type ComAtprotoServerCreateSessionRequest struct { 17 + Identifier string `json:"identifier" validate:"required"` 18 + Password string `json:"password" validate:"required"` 19 + AuthFactorToken *string `json:"authFactorToken,omitempty"` 20 + } 21 + 22 + type ComAtprotoServerCreateSessionResponse struct { 23 + AccessJwt string `json:"accessJwt"` 24 + RefreshJwt string `json:"refreshJwt"` 25 + Handle string `json:"handle"` 26 + Did string `json:"did"` 27 + Email string `json:"email"` 28 + EmailConfirmed bool `json:"emailConfirmed"` 29 + EmailAuthFactor bool `json:"emailAuthFactor"` 30 + Active bool `json:"active"` 31 + Status *string `json:"status,omitempty"` 32 + } 33 + 34 + func (s *Server) handleCreateSession(e echo.Context) error { 35 + var req ComAtprotoServerCreateSessionRequest 36 + if err := e.Bind(&req); err != nil { 37 + s.logger.Error("error binding request", "endpoint", "com.atproto.server.serverCreateSession", "error", err) 38 + return helpers.ServerError(e, nil) 39 + } 40 + 41 + if err := e.Validate(req); err != nil { 42 + var verr ValidationError 43 + if errors.As(err, &verr) { 44 + if verr.Field == "Identifier" { 45 + return helpers.InputError(e, to.StringPtr("InvalidRequest")) 46 + } 47 + 48 + if verr.Field == "Password" { 49 + return helpers.InputError(e, to.StringPtr("InvalidRequest")) 50 + } 51 + } 52 + } 53 + 54 + req.Identifier = strings.ToLower(req.Identifier) 55 + var idtype string 56 + if _, err := syntax.ParseDID(req.Identifier); err == nil { 57 + idtype = "did" 58 + } else if _, err := syntax.ParseHandle(req.Identifier); err == nil { 59 + idtype = "handle" 60 + } else { 61 + idtype = "email" 62 + } 63 + 64 + var repo models.RepoActor 65 + var err error 66 + switch idtype { 67 + case "did": 68 + err = s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.did = ?", req.Identifier).Scan(&repo).Error 69 + case "handle": 70 + err = s.db.Raw("SELECT r.*, a.* FROM actors a LEFT JOIN repos r ON a.did = r.did WHERE a.handle = ?", req.Identifier).Scan(&repo).Error 71 + case "email": 72 + err = s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE a.email = ?", req.Identifier).Scan(&repo).Error 73 + } 74 + 75 + if err != nil { 76 + if err == gorm.ErrRecordNotFound { 77 + return helpers.InputError(e, to.StringPtr("InvalidRequest")) 78 + } 79 + 80 + s.logger.Error("erorr looking up repo", "endpoint", "com.atproto.server.createSession", "error", err) 81 + return helpers.ServerError(e, nil) 82 + } 83 + 84 + if err := bcrypt.CompareHashAndPassword([]byte(repo.Password), []byte(req.Password)); err != nil { 85 + if err != bcrypt.ErrMismatchedHashAndPassword { 86 + s.logger.Error("erorr comparing hash and password", "error", err) 87 + } 88 + return helpers.InputError(e, to.StringPtr("InvalidRequest")) 89 + } 90 + 91 + sess, err := s.createSession(&repo.Repo) 92 + if err != nil { 93 + s.logger.Error("error creating session", "error", err) 94 + return helpers.ServerError(e, nil) 95 + } 96 + 97 + return e.JSON(200, ComAtprotoServerCreateSessionResponse{ 98 + AccessJwt: sess.AccessToken, 99 + RefreshJwt: sess.RefreshToken, 100 + Handle: repo.Handle, 101 + Did: repo.Repo.Did, 102 + Email: repo.Email, 103 + EmailConfirmed: repo.EmailConfirmedAt != nil, 104 + EmailAuthFactor: false, 105 + Active: true, // TODO: eventually do takedowns 106 + Status: nil, // TODO eventually do takedowns 107 + }) 108 + }
+24
server/handle_server_delete_session.go
···
··· 1 + package server 2 + 3 + import ( 4 + "github.com/haileyok/cocoon/internal/helpers" 5 + "github.com/haileyok/cocoon/models" 6 + "github.com/labstack/echo/v4" 7 + ) 8 + 9 + func (s *Server) handleDeleteSession(e echo.Context) error { 10 + token := e.Get("token").(string) 11 + 12 + var acctok models.Token 13 + if err := s.db.Raw("DELETE FROM tokens WHERE token = ? RETURNING *", token).Scan(&acctok).Error; err != nil { 14 + s.logger.Error("error deleting access token from db", "error", err) 15 + return helpers.ServerError(e, nil) 16 + } 17 + 18 + if err := s.db.Exec("DELETE FROM refresh_tokens WHERE token = ?", acctok.RefreshToken).Error; err != nil { 19 + s.logger.Error("error deleting refresh token from db", "error", err) 20 + return helpers.ServerError(e, nil) 21 + } 22 + 23 + return e.NoContent(200) 24 + }
+37
server/handle_server_describe_server.go
···
··· 1 + package server 2 + 3 + import "github.com/labstack/echo/v4" 4 + 5 + type ComAtprotoServerDescribeServerResponseLinks struct { 6 + PrivacyPolicy *string `json:"privacyPolicy,omitempty"` 7 + TermsOfService *string `json:"termsOfService,omitempty"` 8 + } 9 + 10 + type ComAtprotoServerDescribeServerResponseContact struct { 11 + Email string `json:"email"` 12 + } 13 + 14 + type ComAtprotoServerDescribeServerResponse struct { 15 + InviteCodeRequired bool `json:"inviteCodeRequired"` 16 + PhoneVerificationRequired bool `json:"phoneVerificationRequired"` 17 + AvailableUserDomains []string `json:"availableUserDomains"` 18 + Links ComAtprotoServerDescribeServerResponseLinks `json:"links"` 19 + Contact ComAtprotoServerDescribeServerResponseContact `json:"contact"` 20 + Did string `json:"did"` 21 + } 22 + 23 + func (s *Server) handleDescribeServer(e echo.Context) error { 24 + return e.JSON(200, ComAtprotoServerDescribeServerResponse{ 25 + InviteCodeRequired: true, 26 + PhoneVerificationRequired: false, 27 + AvailableUserDomains: []string{"." + s.config.Hostname}, // TODO: more 28 + Links: ComAtprotoServerDescribeServerResponseLinks{ 29 + PrivacyPolicy: nil, 30 + TermsOfService: nil, 31 + }, 32 + Contact: ComAtprotoServerDescribeServerResponseContact{ 33 + Email: s.config.ContactEmail, 34 + }, 35 + Did: s.config.Did, 36 + }) 37 + }
+30
server/handle_server_get_session.go
···
··· 1 + package server 2 + 3 + import ( 4 + "github.com/haileyok/cocoon/models" 5 + "github.com/labstack/echo/v4" 6 + ) 7 + 8 + type ComAtprotoServerGetSessionResponse struct { 9 + Handle string `json:"handle"` 10 + Did string `json:"did"` 11 + Email string `json:"email"` 12 + EmailConfirmed bool `json:"emailConfirmed"` 13 + EmailAuthFactor bool `json:"emailAuthFactor"` 14 + Active bool `json:"active"` 15 + Status *string `json:"status,omitempty"` 16 + } 17 + 18 + func (s *Server) handleGetSession(e echo.Context) error { 19 + repo := e.Get("repo").(*models.RepoActor) 20 + 21 + return e.JSON(200, ComAtprotoServerGetSessionResponse{ 22 + Handle: repo.Handle, 23 + Did: repo.Repo.Did, 24 + Email: repo.Email, 25 + EmailConfirmed: repo.EmailConfirmedAt != nil, 26 + EmailAuthFactor: false, // TODO: todo todo 27 + Active: true, 28 + Status: nil, 29 + }) 30 + }
+46
server/handle_server_refresh_session.go
···
··· 1 + package server 2 + 3 + import ( 4 + "github.com/haileyok/cocoon/internal/helpers" 5 + "github.com/haileyok/cocoon/models" 6 + "github.com/labstack/echo/v4" 7 + ) 8 + 9 + type ComAtprotoServerRefreshSessionResponse struct { 10 + AccessJwt string `json:"accessJwt"` 11 + RefreshJwt string `json:"refreshJwt"` 12 + Handle string `json:"handle"` 13 + Did string `json:"did"` 14 + Active bool `json:"active"` 15 + Status *string `json:"status,omitempty"` 16 + } 17 + 18 + func (s *Server) handleRefreshSession(e echo.Context) error { 19 + token := e.Get("token").(string) 20 + repo := e.Get("repo").(*models.RepoActor) 21 + 22 + if err := s.db.Exec("DELETE FROM refresh_tokens WHERE token = ?", token).Error; err != nil { 23 + s.logger.Error("error getting refresh token from db", "error", err) 24 + return helpers.ServerError(e, nil) 25 + } 26 + 27 + if err := s.db.Exec("DELETE FROM tokens WHERE refresh_token = ?", token).Error; err != nil { 28 + s.logger.Error("error deleting access token from db", "error", err) 29 + return helpers.ServerError(e, nil) 30 + } 31 + 32 + sess, err := s.createSession(&repo.Repo) 33 + if err != nil { 34 + s.logger.Error("error creating new session for refresh", "error", err) 35 + return helpers.ServerError(e, nil) 36 + } 37 + 38 + return e.JSON(200, ComAtprotoServerRefreshSessionResponse{ 39 + AccessJwt: sess.AccessToken, 40 + RefreshJwt: sess.RefreshToken, 41 + Handle: repo.Handle, 42 + Did: repo.Repo.Did, 43 + Active: true, 44 + Status: nil, 45 + }) 46 + }
+38
server/handle_server_resolve_handle.go
···
··· 1 + package server 2 + 3 + import ( 4 + "context" 5 + 6 + "github.com/Azure/go-autorest/autorest/to" 7 + "github.com/bluesky-social/indigo/atproto/syntax" 8 + "github.com/haileyok/cocoon/internal/helpers" 9 + "github.com/labstack/echo/v4" 10 + ) 11 + 12 + func (s *Server) handleResolveHandle(e echo.Context) error { 13 + type Resp struct { 14 + Did string `json:"did"` 15 + } 16 + 17 + handle := e.QueryParam("handle") 18 + 19 + if handle == "" { 20 + return helpers.InputError(e, to.StringPtr("Handle must be supplied in request.")) 21 + } 22 + 23 + parsed, err := syntax.ParseHandle(handle) 24 + if err != nil { 25 + return helpers.InputError(e, to.StringPtr("Invalid handle.")) 26 + } 27 + 28 + ctx := context.WithValue(e.Request().Context(), "skip-cache", true) 29 + did, err := s.passport.ResolveHandle(ctx, parsed.String()) 30 + if err != nil { 31 + s.logger.Error("error resolving handle", "error", err) 32 + return helpers.ServerError(e, nil) 33 + } 34 + 35 + return e.JSON(200, Resp{ 36 + Did: did, 37 + }) 38 + }
+48
server/handle_sync_get_blob.go
···
··· 1 + package server 2 + 3 + import ( 4 + "bytes" 5 + 6 + "github.com/haileyok/cocoon/internal/helpers" 7 + "github.com/haileyok/cocoon/models" 8 + "github.com/ipfs/go-cid" 9 + "github.com/labstack/echo/v4" 10 + ) 11 + 12 + func (s *Server) handleSyncGetBlob(e echo.Context) error { 13 + did := e.QueryParam("did") 14 + if did == "" { 15 + return helpers.InputError(e, nil) 16 + } 17 + 18 + cstr := e.QueryParam("cid") 19 + if cstr == "" { 20 + return helpers.InputError(e, nil) 21 + } 22 + 23 + c, err := cid.Parse(cstr) 24 + if err != nil { 25 + return helpers.InputError(e, nil) 26 + } 27 + 28 + var blob models.Blob 29 + if err := s.db.Raw("SELECT * FROM blobs WHERE did = ? AND cid = ?", did, c.Bytes()).Scan(&blob).Error; err != nil { 30 + s.logger.Error("error looking up blob", "error", err) 31 + return helpers.ServerError(e, nil) 32 + } 33 + 34 + buf := new(bytes.Buffer) 35 + 36 + var parts []models.BlobPart 37 + if err := s.db.Raw("SELECT * FROM blob_parts WHERE blob_id = ? ORDER BY idx", blob.ID).Scan(&parts).Error; err != nil { 38 + s.logger.Error("error getting blob parts", "error", err) 39 + return helpers.ServerError(e, nil) 40 + } 41 + 42 + // TODO: we can just stream this, don't need to make a buffer 43 + for _, p := range parts { 44 + buf.Write(p.Data) 45 + } 46 + 47 + return e.Stream(200, "application/octet-stream", buf) 48 + }
+71
server/handle_sync_get_blocks.go
···
··· 1 + package server 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "strings" 7 + 8 + "github.com/bluesky-social/indigo/carstore" 9 + "github.com/haileyok/cocoon/blockstore" 10 + "github.com/haileyok/cocoon/internal/helpers" 11 + "github.com/ipfs/go-cid" 12 + cbor "github.com/ipfs/go-ipld-cbor" 13 + "github.com/ipld/go-car" 14 + "github.com/labstack/echo/v4" 15 + ) 16 + 17 + func (s *Server) handleGetBlocks(e echo.Context) error { 18 + did := e.QueryParam("did") 19 + cidsstr := e.QueryParam("cids") 20 + if did == "" { 21 + return helpers.InputError(e, nil) 22 + } 23 + 24 + cidstrs := strings.Split(cidsstr, ",") 25 + cids := []cid.Cid{} 26 + 27 + for _, cs := range cidstrs { 28 + c, err := cid.Cast([]byte(cs)) 29 + if err != nil { 30 + return err 31 + } 32 + 33 + cids = append(cids, c) 34 + } 35 + 36 + urepo, err := s.getRepoActorByDid(did) 37 + if err != nil { 38 + return helpers.ServerError(e, nil) 39 + } 40 + 41 + buf := new(bytes.Buffer) 42 + rc, err := cid.Cast(urepo.Root) 43 + if err != nil { 44 + return err 45 + } 46 + 47 + hb, err := cbor.DumpObject(&car.CarHeader{ 48 + Roots: []cid.Cid{rc}, 49 + Version: 1, 50 + }) 51 + 52 + if _, err := carstore.LdWrite(buf, hb); err != nil { 53 + s.logger.Error("error writing to car", "error", err) 54 + return helpers.ServerError(e, nil) 55 + } 56 + 57 + bs := blockstore.New(urepo.Repo.Did, s.db) 58 + 59 + for _, c := range cids { 60 + b, err := bs.Get(context.TODO(), c) 61 + if err != nil { 62 + return err 63 + } 64 + 65 + if _, err := carstore.LdWrite(buf, b.Cid().Bytes(), b.RawData()); err != nil { 66 + return err 67 + } 68 + } 69 + 70 + return e.Stream(200, "application/vnd.ipld.car", bytes.NewReader(buf.Bytes())) 71 + }
+34
server/handle_sync_get_latest_commit.go
···
··· 1 + package server 2 + 3 + import ( 4 + "github.com/haileyok/cocoon/internal/helpers" 5 + "github.com/ipfs/go-cid" 6 + "github.com/labstack/echo/v4" 7 + ) 8 + 9 + type ComAtprotoSyncGetLatestCommitResponse struct { 10 + Cid string `json:"string"` 11 + Rev string `json:"rev"` 12 + } 13 + 14 + func (s *Server) handleSyncGetLatestCommit(e echo.Context) error { 15 + did := e.QueryParam("did") 16 + if did == "" { 17 + return helpers.InputError(e, nil) 18 + } 19 + 20 + urepo, err := s.getRepoActorByDid(did) 21 + if err != nil { 22 + return err 23 + } 24 + 25 + c, err := cid.Cast(urepo.Root) 26 + if err != nil { 27 + return err 28 + } 29 + 30 + return e.JSON(200, ComAtprotoSyncGetLatestCommitResponse{ 31 + Cid: c.String(), 32 + Rev: urepo.Rev, 33 + }) 34 + }
+51
server/handle_sync_get_record.go
···
··· 1 + package server 2 + 3 + import ( 4 + "bytes" 5 + 6 + "github.com/bluesky-social/indigo/carstore" 7 + "github.com/haileyok/cocoon/internal/helpers" 8 + "github.com/haileyok/cocoon/models" 9 + "github.com/ipfs/go-cid" 10 + cbor "github.com/ipfs/go-ipld-cbor" 11 + "github.com/ipld/go-car" 12 + "github.com/labstack/echo/v4" 13 + ) 14 + 15 + func (s *Server) handleSyncGetRecord(e echo.Context) error { 16 + did := e.QueryParam("did") 17 + collection := e.QueryParam("collection") 18 + rkey := e.QueryParam("rkey") 19 + 20 + var urepo models.Repo 21 + if err := s.db.Raw("SELECT * FROM repos WHERE did = ?", did).Scan(&urepo).Error; err != nil { 22 + s.logger.Error("error getting repo", "error", err) 23 + return helpers.ServerError(e, nil) 24 + } 25 + 26 + root, blocks, err := s.repoman.getRecordProof(urepo, collection, rkey) 27 + if err != nil { 28 + return err 29 + } 30 + 31 + buf := new(bytes.Buffer) 32 + 33 + hb, err := cbor.DumpObject(&car.CarHeader{ 34 + Roots: []cid.Cid{root}, 35 + Version: 1, 36 + }) 37 + 38 + if _, err := carstore.LdWrite(buf, hb); err != nil { 39 + s.logger.Error("error writing to car", "error", err) 40 + return helpers.ServerError(e, nil) 41 + } 42 + 43 + for _, blk := range blocks { 44 + if _, err := carstore.LdWrite(buf, blk.Cid().Bytes(), blk.RawData()); err != nil { 45 + s.logger.Error("error writing to car", "error", err) 46 + return helpers.ServerError(e, nil) 47 + } 48 + } 49 + 50 + return e.Stream(200, "application/vnd.ipld.car", bytes.NewReader(buf.Bytes())) 51 + }
+55
server/handle_sync_get_repo.go
···
··· 1 + package server 2 + 3 + import ( 4 + "bytes" 5 + 6 + "github.com/bluesky-social/indigo/carstore" 7 + "github.com/haileyok/cocoon/internal/helpers" 8 + "github.com/haileyok/cocoon/models" 9 + "github.com/ipfs/go-cid" 10 + cbor "github.com/ipfs/go-ipld-cbor" 11 + "github.com/ipld/go-car" 12 + "github.com/labstack/echo/v4" 13 + ) 14 + 15 + func (s *Server) handleSyncGetRepo(e echo.Context) error { 16 + did := e.QueryParam("did") 17 + if did == "" { 18 + return helpers.InputError(e, nil) 19 + } 20 + 21 + urepo, err := s.getRepoActorByDid(did) 22 + if err != nil { 23 + return err 24 + } 25 + 26 + rc, err := cid.Cast(urepo.Root) 27 + if err != nil { 28 + return err 29 + } 30 + 31 + hb, err := cbor.DumpObject(&car.CarHeader{ 32 + Roots: []cid.Cid{rc}, 33 + Version: 1, 34 + }) 35 + 36 + buf := new(bytes.Buffer) 37 + 38 + if _, err := carstore.LdWrite(buf, hb); err != nil { 39 + s.logger.Error("error writing to car", "error", err) 40 + return helpers.ServerError(e, nil) 41 + } 42 + 43 + var blocks []models.Block 44 + if err := s.db.Raw("SELECT * FROM blocks WHERE did = ? ORDER BY rev ASC", urepo.Repo.Did).Scan(&blocks).Error; err != nil { 45 + return err 46 + } 47 + 48 + for _, block := range blocks { 49 + if _, err := carstore.LdWrite(buf, block.Cid, block.Value); err != nil { 50 + return err 51 + } 52 + } 53 + 54 + return e.Stream(200, "application/vnd.ipld.car", bytes.NewReader(buf.Bytes())) 55 + }
+33
server/handle_sync_get_repo_status.go
···
··· 1 + package server 2 + 3 + import ( 4 + "github.com/haileyok/cocoon/internal/helpers" 5 + "github.com/labstack/echo/v4" 6 + ) 7 + 8 + type ComAtprotoSyncGetRepoStatusResponse struct { 9 + Did string `json:"did"` 10 + Active bool `json:"active"` 11 + Status *string `json:"status,omitempty"` 12 + Rev *string `json:"rev,omitempty"` 13 + } 14 + 15 + // TODO: make this actually do the right thing 16 + func (s *Server) handleSyncGetRepoStatus(e echo.Context) error { 17 + did := e.QueryParam("did") 18 + if did == "" { 19 + return helpers.InputError(e, nil) 20 + } 21 + 22 + urepo, err := s.getRepoActorByDid(did) 23 + if err != nil { 24 + return err 25 + } 26 + 27 + return e.JSON(200, ComAtprotoSyncGetRepoStatusResponse{ 28 + Did: urepo.Repo.Did, 29 + Active: true, 30 + Status: nil, 31 + Rev: &urepo.Rev, 32 + }) 33 + }
+62
server/handle_sync_list_blobs.go
···
··· 1 + package server 2 + 3 + import ( 4 + "github.com/haileyok/cocoon/internal/helpers" 5 + "github.com/haileyok/cocoon/models" 6 + "github.com/ipfs/go-cid" 7 + "github.com/labstack/echo/v4" 8 + ) 9 + 10 + type ComAtprotoSyncListBlobsResponse struct { 11 + Cursor *string `json:"cursor,omitempty"` 12 + Cids []string `json:"cids"` 13 + } 14 + 15 + func (s *Server) handleSyncListBlobs(e echo.Context) error { 16 + did := e.QueryParam("did") 17 + if did == "" { 18 + return helpers.InputError(e, nil) 19 + } 20 + 21 + // TODO: add tid param 22 + cursor := e.QueryParam("cursor") 23 + limit, err := getLimitFromContext(e, 50) 24 + if err != nil { 25 + return helpers.InputError(e, nil) 26 + } 27 + 28 + cursorquery := "" 29 + 30 + params := []any{did} 31 + if cursor != "" { 32 + params = append(params, cursor) 33 + cursorquery = "AND created_at < ?" 34 + } 35 + params = append(params, limit) 36 + 37 + var blobs []models.Blob 38 + if err := s.db.Raw("SELECT * FROM blobs WHERE did = ? "+cursorquery+" ORDER BY created_at DESC LIMIT ?", params...).Scan(&blobs).Error; err != nil { 39 + s.logger.Error("error getting records", "error", err) 40 + return helpers.ServerError(e, nil) 41 + } 42 + 43 + var cstrs []string 44 + for _, b := range blobs { 45 + c, err := cid.Cast(b.Cid) 46 + if err != nil { 47 + s.logger.Error("error casting cid", "error", err) 48 + return helpers.ServerError(e, nil) 49 + } 50 + cstrs = append(cstrs, c.String()) 51 + } 52 + 53 + var newcursor *string 54 + if len(blobs) == 50 { 55 + newcursor = &blobs[len(blobs)-1].CreatedAt 56 + } 57 + 58 + return e.JSON(200, ComAtprotoSyncListBlobsResponse{ 59 + Cursor: newcursor, 60 + Cids: cstrs, 61 + }) 62 + }
+93
server/handle_sync_subscribe_repos.go
···
··· 1 + package server 2 + 3 + import ( 4 + "fmt" 5 + "net/http" 6 + 7 + "github.com/bluesky-social/indigo/events" 8 + "github.com/bluesky-social/indigo/lex/util" 9 + "github.com/btcsuite/websocket" 10 + "github.com/labstack/echo/v4" 11 + ) 12 + 13 + var upgrader = websocket.Upgrader{ 14 + ReadBufferSize: 1024, 15 + WriteBufferSize: 1024, 16 + CheckOrigin: func(r *http.Request) bool { 17 + return true 18 + }, 19 + } 20 + 21 + func (s *Server) handleSyncSubscribeRepos(e echo.Context) error { 22 + conn, err := websocket.Upgrade(e.Response().Writer, e.Request(), e.Response().Header(), 1<<10, 1<<10) 23 + if err != nil { 24 + return err 25 + } 26 + 27 + s.logger.Info("new connection", "ua", e.Request().UserAgent()) 28 + 29 + ctx := e.Request().Context() 30 + 31 + ident := e.RealIP() + "-" + e.Request().UserAgent() 32 + 33 + evts, cancel, err := s.evtman.Subscribe(ctx, ident, func(evt *events.XRPCStreamEvent) bool { 34 + return true 35 + }, nil) 36 + if err != nil { 37 + return err 38 + } 39 + defer cancel() 40 + 41 + header := events.EventHeader{Op: events.EvtKindMessage} 42 + for evt := range evts { 43 + wc, err := conn.NextWriter(websocket.BinaryMessage) 44 + if err != nil { 45 + return err 46 + } 47 + 48 + var obj util.CBOR 49 + 50 + switch { 51 + case evt.Error != nil: 52 + header.Op = events.EvtKindErrorFrame 53 + obj = evt.Error 54 + case evt.RepoCommit != nil: 55 + header.MsgType = "#commit" 56 + obj = evt.RepoCommit 57 + case evt.RepoHandle != nil: 58 + header.MsgType = "#handle" 59 + obj = evt.RepoHandle 60 + case evt.RepoIdentity != nil: 61 + header.MsgType = "#identity" 62 + obj = evt.RepoIdentity 63 + case evt.RepoAccount != nil: 64 + header.MsgType = "#account" 65 + obj = evt.RepoAccount 66 + case evt.RepoInfo != nil: 67 + header.MsgType = "#info" 68 + obj = evt.RepoInfo 69 + case evt.RepoMigrate != nil: 70 + header.MsgType = "#migrate" 71 + obj = evt.RepoMigrate 72 + case evt.RepoTombstone != nil: 73 + header.MsgType = "#tombstone" 74 + obj = evt.RepoTombstone 75 + default: 76 + return fmt.Errorf("unrecognized event kind") 77 + } 78 + 79 + if err := header.MarshalCBOR(wc); err != nil { 80 + return fmt.Errorf("failed to write header: %w", err) 81 + } 82 + 83 + if err := obj.MarshalCBOR(wc); err != nil { 84 + return fmt.Errorf("failed to write event: %w", err) 85 + } 86 + 87 + if err := wc.Close(); err != nil { 88 + return fmt.Errorf("failed to flush-close our event write: %w", err) 89 + } 90 + } 91 + 92 + return nil 93 + }
+21
server/handle_well_known.go
···
··· 1 + package server 2 + 3 + import ( 4 + "github.com/labstack/echo/v4" 5 + ) 6 + 7 + func (s *Server) handleWellKnown(e echo.Context) error { 8 + return e.JSON(200, map[string]any{ 9 + "@context": []string{ 10 + "https://www.w3.org/ns/did/v1", 11 + }, 12 + "id": s.config.Did, 13 + "service": []map[string]string{ 14 + { 15 + "id": "#atproto_pds", 16 + "type": "AtprotoPersonalDataServer", 17 + "serviceEndpoint": "https://" + s.config.Hostname, 18 + }, 19 + }, 20 + }) 21 + }
+329
server/repo.go
···
··· 1 + package server 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "fmt" 7 + "io" 8 + "time" 9 + 10 + "github.com/Azure/go-autorest/autorest/to" 11 + "github.com/bluesky-social/indigo/api/atproto" 12 + "github.com/bluesky-social/indigo/atproto/data" 13 + "github.com/bluesky-social/indigo/atproto/syntax" 14 + "github.com/bluesky-social/indigo/carstore" 15 + "github.com/bluesky-social/indigo/events" 16 + lexutil "github.com/bluesky-social/indigo/lex/util" 17 + "github.com/bluesky-social/indigo/repo" 18 + "github.com/bluesky-social/indigo/util" 19 + "github.com/haileyok/cocoon/blockstore" 20 + "github.com/haileyok/cocoon/models" 21 + blocks "github.com/ipfs/go-block-format" 22 + "github.com/ipfs/go-cid" 23 + cbor "github.com/ipfs/go-ipld-cbor" 24 + "github.com/ipld/go-car" 25 + "gorm.io/gorm" 26 + "gorm.io/gorm/clause" 27 + ) 28 + 29 + type RepoMan struct { 30 + db *gorm.DB 31 + s *Server 32 + clock *syntax.TIDClock 33 + } 34 + 35 + func NewRepoMan(s *Server) *RepoMan { 36 + clock := syntax.NewTIDClock(0) 37 + 38 + return &RepoMan{ 39 + s: s, 40 + db: s.db, 41 + clock: &clock, 42 + } 43 + } 44 + 45 + type OpType string 46 + 47 + var ( 48 + OpTypeCreate = OpType("com.atproto.repo.applyWrites#create") 49 + OpTypeUpdate = OpType("com.atproto.repo.applyWrites#update") 50 + OpTypeDelete = OpType("com.atproto.repo.applyWrites#delete") 51 + ) 52 + 53 + func (ot OpType) String() string { 54 + return ot.String() 55 + } 56 + 57 + type Op struct { 58 + Type OpType `json:"$type"` 59 + Collection string `json:"collection"` 60 + Rkey *string `json:"rkey,omitempty"` 61 + Validate *bool `json:"validate,omitempty"` 62 + SwapRecord *string `json:"swapRecord,omitempty"` 63 + Record *MarshalableMap `json:"record,omitempty"` 64 + } 65 + 66 + type MarshalableMap map[string]any 67 + 68 + type FirehoseOp struct { 69 + Cid cid.Cid 70 + Path string 71 + Action string 72 + } 73 + 74 + func (mm *MarshalableMap) MarshalCBOR(w io.Writer) error { 75 + data, err := data.MarshalCBOR(*mm) 76 + if err != nil { 77 + return err 78 + } 79 + 80 + w.Write(data) 81 + 82 + return nil 83 + } 84 + 85 + // TODO make use of swap commit 86 + func (rm *RepoMan) applyWrites(urepo models.Repo, writes []Op, swapCommit *string) error { 87 + rootcid, err := cid.Cast(urepo.Root) 88 + if err != nil { 89 + return err 90 + } 91 + 92 + dbs := blockstore.New(urepo.Did, rm.db) 93 + r, err := repo.OpenRepo(context.TODO(), dbs, rootcid) 94 + 95 + entries := []models.Record{} 96 + 97 + for i, op := range writes { 98 + if op.Type != OpTypeCreate && op.Rkey == nil { 99 + return fmt.Errorf("invalid rkey") 100 + } else if op.Rkey == nil { 101 + op.Rkey = to.StringPtr(rm.clock.Next().String()) 102 + writes[i].Rkey = op.Rkey 103 + } 104 + 105 + _, err := syntax.ParseRecordKey(*op.Rkey) 106 + if err != nil { 107 + return err 108 + } 109 + 110 + switch op.Type { 111 + case OpTypeCreate: 112 + nc, err := r.PutRecord(context.TODO(), op.Collection+"/"+*op.Rkey, op.Record) 113 + if err != nil { 114 + return err 115 + } 116 + 117 + d, _ := data.MarshalCBOR(*op.Record) 118 + entries = append(entries, models.Record{ 119 + Did: urepo.Did, 120 + CreatedAt: rm.clock.Next().String(), 121 + Nsid: op.Collection, 122 + Rkey: *op.Rkey, 123 + Cid: nc.String(), 124 + Value: d, 125 + }) 126 + case OpTypeDelete: 127 + err := r.DeleteRecord(context.TODO(), op.Collection+"/"+*op.Rkey) 128 + if err != nil { 129 + return err 130 + } 131 + case OpTypeUpdate: 132 + nc, err := r.UpdateRecord(context.TODO(), op.Collection+"/"+*op.Rkey, op.Record) 133 + if err != nil { 134 + return err 135 + } 136 + 137 + d, _ := data.MarshalCBOR(*op.Record) 138 + entries = append(entries, models.Record{ 139 + Did: urepo.Did, 140 + CreatedAt: rm.clock.Next().String(), 141 + Nsid: op.Collection, 142 + Rkey: *op.Rkey, 143 + Cid: nc.String(), 144 + Value: d, 145 + }) 146 + } 147 + } 148 + 149 + newroot, rev, err := r.Commit(context.TODO(), urepo.SignFor) 150 + if err != nil { 151 + return err 152 + } 153 + 154 + buf := new(bytes.Buffer) 155 + 156 + hb, err := cbor.DumpObject(&car.CarHeader{ 157 + Roots: []cid.Cid{newroot}, 158 + Version: 1, 159 + }) 160 + 161 + if _, err := carstore.LdWrite(buf, hb); err != nil { 162 + return err 163 + } 164 + 165 + diffops, err := r.DiffSince(context.TODO(), rootcid) 166 + if err != nil { 167 + return err 168 + } 169 + 170 + ops := make([]*atproto.SyncSubscribeRepos_RepoOp, 0, len(diffops)) 171 + 172 + for _, op := range diffops { 173 + switch op.Op { 174 + case "add", "mut": 175 + kind := "create" 176 + if op.Op == "mut" { 177 + kind = "update" 178 + } 179 + 180 + ll := lexutil.LexLink(op.NewCid) 181 + ops = append(ops, &atproto.SyncSubscribeRepos_RepoOp{ 182 + Action: kind, 183 + Path: op.Rpath, 184 + Cid: &ll, 185 + }) 186 + 187 + case "del": 188 + ops = append(ops, &atproto.SyncSubscribeRepos_RepoOp{ 189 + Action: "delete", 190 + Path: op.Rpath, 191 + Cid: nil, 192 + }) 193 + } 194 + 195 + blk, err := dbs.Get(context.TODO(), op.NewCid) 196 + if err != nil { 197 + return err 198 + } 199 + 200 + if _, err := carstore.LdWrite(buf, blk.Cid().Bytes(), blk.RawData()); err != nil { 201 + return err 202 + } 203 + } 204 + 205 + for _, op := range dbs.GetLog() { 206 + if _, err := carstore.LdWrite(buf, op.Cid().Bytes(), op.RawData()); err != nil { 207 + return err 208 + } 209 + } 210 + 211 + var blobs []lexutil.LexLink 212 + for _, entry := range entries { 213 + if err := rm.s.db.Clauses(clause.OnConflict{ 214 + Columns: []clause.Column{{Name: "did"}, {Name: "nsid"}, {Name: "rkey"}}, 215 + UpdateAll: true, 216 + }).Create(&entry).Error; err != nil { 217 + return err 218 + } 219 + 220 + // we should actually check the type (i.e. delete, create,., update) here but we'll do it later 221 + cids, err := rm.incrementBlobRefs(urepo, entry.Value) 222 + if err != nil { 223 + return err 224 + } 225 + 226 + for _, c := range cids { 227 + blobs = append(blobs, lexutil.LexLink(c)) 228 + } 229 + } 230 + 231 + rm.s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{ 232 + RepoCommit: &atproto.SyncSubscribeRepos_Commit{ 233 + Repo: urepo.Did, 234 + Blocks: buf.Bytes(), 235 + Blobs: blobs, 236 + Rev: rev, 237 + Since: &urepo.Rev, 238 + Commit: lexutil.LexLink(newroot), 239 + Time: time.Now().Format(util.ISO8601), 240 + Ops: ops, 241 + TooBig: false, 242 + }, 243 + }) 244 + 245 + if err := dbs.UpdateRepo(context.TODO(), newroot, rev); err != nil { 246 + return err 247 + } 248 + 249 + return nil 250 + } 251 + 252 + func (rm *RepoMan) getRecordProof(urepo models.Repo, collection, rkey string) (cid.Cid, []blocks.Block, error) { 253 + c, err := cid.Cast(urepo.Root) 254 + if err != nil { 255 + return cid.Undef, nil, err 256 + } 257 + 258 + dbs := blockstore.New(urepo.Did, rm.db) 259 + bs := util.NewLoggingBstore(dbs) 260 + 261 + r, err := repo.OpenRepo(context.TODO(), bs, c) 262 + if err != nil { 263 + return cid.Undef, nil, err 264 + } 265 + 266 + _, _, err = r.GetRecordBytes(context.TODO(), collection+"/"+rkey) 267 + if err != nil { 268 + return cid.Undef, nil, err 269 + } 270 + 271 + return c, bs.GetLoggedBlocks(), nil 272 + } 273 + 274 + func (rm *RepoMan) incrementBlobRefs(urepo models.Repo, cbor []byte) ([]cid.Cid, error) { 275 + cids, err := getBlobCidsFromCbor(cbor) 276 + if err != nil { 277 + return nil, err 278 + } 279 + 280 + for _, c := range cids { 281 + if err := rm.db.Exec("UPDATE blobs SET ref_count = ref_count + 1 WHERE did = ? AND cid = ?", urepo.Did, c.Bytes()).Error; err != nil { 282 + return nil, err 283 + } 284 + } 285 + 286 + return cids, nil 287 + } 288 + 289 + // to be honest, we could just store both the cbor and non-cbor in []entries above to avoid an additional 290 + // unmarshal here. this will work for now though 291 + func getBlobCidsFromCbor(cbor []byte) ([]cid.Cid, error) { 292 + var cids []cid.Cid 293 + 294 + decoded, err := data.UnmarshalCBOR(cbor) 295 + if err != nil { 296 + return nil, fmt.Errorf("error unmarshaling cbor: %w", err) 297 + } 298 + 299 + var deepiter func(interface{}) error 300 + deepiter = func(item interface{}) error { 301 + switch val := item.(type) { 302 + case map[string]interface{}: 303 + if val["$type"] == "blob" { 304 + if ref, ok := val["ref"].(string); ok { 305 + c, err := cid.Parse(ref) 306 + if err != nil { 307 + return err 308 + } 309 + cids = append(cids, c) 310 + } 311 + for _, v := range val { 312 + return deepiter(v) 313 + } 314 + } 315 + case []interface{}: 316 + for _, v := range val { 317 + deepiter(v) 318 + } 319 + } 320 + 321 + return nil 322 + } 323 + 324 + if err := deepiter(decoded); err != nil { 325 + return nil, err 326 + } 327 + 328 + return cids, nil 329 + }
+402
server/server.go
···
··· 1 + package server 2 + 3 + import ( 4 + "context" 5 + "crypto/ecdsa" 6 + "errors" 7 + "fmt" 8 + "log/slog" 9 + "net/http" 10 + "os" 11 + "strings" 12 + "time" 13 + 14 + "github.com/Azure/go-autorest/autorest/to" 15 + "github.com/bluesky-social/indigo/api/atproto" 16 + "github.com/bluesky-social/indigo/atproto/syntax" 17 + "github.com/bluesky-social/indigo/events" 18 + "github.com/bluesky-social/indigo/xrpc" 19 + "github.com/go-playground/validator" 20 + "github.com/golang-jwt/jwt/v4" 21 + "github.com/haileyok/cocoon/identity" 22 + "github.com/haileyok/cocoon/internal/helpers" 23 + "github.com/haileyok/cocoon/models" 24 + "github.com/haileyok/cocoon/plc" 25 + "github.com/labstack/echo/v4" 26 + "github.com/labstack/echo/v4/middleware" 27 + "github.com/lestrrat-go/jwx/v2/jwk" 28 + slogecho "github.com/samber/slog-echo" 29 + "gorm.io/driver/sqlite" 30 + "gorm.io/gorm" 31 + ) 32 + 33 + type Server struct { 34 + httpd *http.Server 35 + echo *echo.Echo 36 + db *gorm.DB 37 + plcClient *plc.Client 38 + logger *slog.Logger 39 + config *config 40 + privateKey *ecdsa.PrivateKey 41 + repoman *RepoMan 42 + evtman *events.EventManager 43 + passport *identity.Passport 44 + } 45 + 46 + type Args struct { 47 + Addr string 48 + DbName string 49 + Logger *slog.Logger 50 + Version string 51 + Did string 52 + Hostname string 53 + RotationKeyPath string 54 + JwkPath string 55 + ContactEmail string 56 + Relays []string 57 + } 58 + 59 + type config struct { 60 + Version string 61 + Did string 62 + Hostname string 63 + ContactEmail string 64 + EnforcePeering bool 65 + Relays []string 66 + } 67 + 68 + type CustomValidator struct { 69 + validator *validator.Validate 70 + } 71 + 72 + type ValidationError struct { 73 + error 74 + Field string 75 + Tag string 76 + } 77 + 78 + func (cv *CustomValidator) Validate(i any) error { 79 + if err := cv.validator.Struct(i); err != nil { 80 + var validateErrors validator.ValidationErrors 81 + if errors.As(err, &validateErrors) && len(validateErrors) > 0 { 82 + first := validateErrors[0] 83 + return ValidationError{ 84 + error: err, 85 + Field: first.Field(), 86 + Tag: first.Tag(), 87 + } 88 + } 89 + 90 + return err 91 + } 92 + 93 + return nil 94 + } 95 + 96 + func (s *Server) handleSessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 97 + return func(e echo.Context) error { 98 + authheader := e.Request().Header.Get("authorization") 99 + if authheader == "" { 100 + return e.JSON(401, map[string]string{"error": "Unauthorized"}) 101 + } 102 + 103 + pts := strings.Split(authheader, " ") 104 + if len(pts) != 2 { 105 + return helpers.ServerError(e, nil) 106 + } 107 + 108 + tokenstr := pts[1] 109 + 110 + token, err := new(jwt.Parser).Parse(tokenstr, func(t *jwt.Token) (any, error) { 111 + if _, ok := t.Method.(*jwt.SigningMethodECDSA); !ok { 112 + return nil, fmt.Errorf("unsupported signing method: %v", t.Header["alg"]) 113 + } 114 + 115 + return s.privateKey.Public(), nil 116 + }) 117 + if err != nil { 118 + s.logger.Error("error parsing jwt", "error", err) 119 + return helpers.InputError(e, to.StringPtr("InvalidToken")) 120 + } 121 + 122 + claims, ok := token.Claims.(jwt.MapClaims) 123 + if !ok || !token.Valid { 124 + return helpers.InputError(e, to.StringPtr("InvalidToken")) 125 + } 126 + 127 + isRefresh := e.Request().URL.Path == "/xrpc/com.atproto.server.refreshSession" 128 + scope := claims["scope"].(string) 129 + 130 + if isRefresh && scope != "com.atproto.refresh" { 131 + return helpers.InputError(e, to.StringPtr("InvalidToken")) 132 + } else if !isRefresh && scope != "com.atproto.access" { 133 + return helpers.InputError(e, to.StringPtr("InvalidToken")) 134 + } 135 + 136 + table := "tokens" 137 + if isRefresh { 138 + table = "refresh_tokens" 139 + } 140 + 141 + type Result struct { 142 + Found bool 143 + } 144 + var result Result 145 + if err := s.db.Raw("SELECT EXISTS(SELECT 1 FROM "+table+" WHERE token = ?) AS found", tokenstr).Scan(&result).Error; err != nil { 146 + if err == gorm.ErrRecordNotFound { 147 + return helpers.InputError(e, to.StringPtr("InvalidToken")) 148 + } 149 + 150 + s.logger.Error("error getting token from db", "error", err) 151 + return helpers.ServerError(e, nil) 152 + } 153 + 154 + if !result.Found { 155 + return helpers.InputError(e, to.StringPtr("InvalidToken")) 156 + } 157 + 158 + exp, ok := claims["exp"].(float64) 159 + if !ok { 160 + s.logger.Error("error getting iat from token") 161 + return helpers.ServerError(e, nil) 162 + } 163 + 164 + if exp < float64(time.Now().UTC().Unix()) { 165 + return helpers.InputError(e, to.StringPtr("ExpiredToken")) 166 + } 167 + 168 + e.Set("did", claims["sub"]) 169 + 170 + repo, err := s.getRepoActorByDid(claims["sub"].(string)) 171 + if err != nil { 172 + s.logger.Error("error fetching repo", "error", err) 173 + return helpers.ServerError(e, nil) 174 + } 175 + e.Set("repo", repo) 176 + 177 + e.Set("token", tokenstr) 178 + 179 + if err := next(e); err != nil { 180 + e.Error(err) 181 + } 182 + 183 + return nil 184 + } 185 + } 186 + 187 + func New(args *Args) (*Server, error) { 188 + if args.Addr == "" { 189 + return nil, fmt.Errorf("addr must be set") 190 + } 191 + 192 + if args.DbName == "" { 193 + return nil, fmt.Errorf("db name must be set") 194 + } 195 + 196 + if args.Did == "" { 197 + return nil, fmt.Errorf("cocoon did must be set") 198 + } 199 + 200 + if args.ContactEmail == "" { 201 + return nil, fmt.Errorf("cocoon contact email is required") 202 + } 203 + 204 + if _, err := syntax.ParseDID(args.Did); err != nil { 205 + return nil, fmt.Errorf("error parsing cocoon did: %w", err) 206 + } 207 + 208 + if args.Hostname == "" { 209 + return nil, fmt.Errorf("cocoon hostname must be set") 210 + } 211 + 212 + if args.Logger == nil { 213 + args.Logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{})) 214 + } 215 + 216 + e := echo.New() 217 + 218 + e.Pre(middleware.RemoveTrailingSlash()) 219 + e.Pre(slogecho.New(args.Logger)) 220 + e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ 221 + AllowOrigins: []string{"*"}, 222 + AllowHeaders: []string{"*"}, 223 + AllowMethods: []string{"*"}, 224 + AllowCredentials: true, 225 + MaxAge: 100_000_000, 226 + })) 227 + 228 + vdtor := validator.New() 229 + vdtor.RegisterValidation("atproto-handle", func(fl validator.FieldLevel) bool { 230 + if _, err := syntax.ParseHandle(fl.Field().String()); err != nil { 231 + return false 232 + } 233 + return true 234 + }) 235 + vdtor.RegisterValidation("atproto-did", func(fl validator.FieldLevel) bool { 236 + if _, err := syntax.ParseDID(fl.Field().String()); err != nil { 237 + return false 238 + } 239 + return true 240 + }) 241 + vdtor.RegisterValidation("atproto-rkey", func(fl validator.FieldLevel) bool { 242 + if _, err := syntax.ParseRecordKey(fl.Field().String()); err != nil { 243 + return false 244 + } 245 + return true 246 + }) 247 + vdtor.RegisterValidation("atproto-nsid", func(fl validator.FieldLevel) bool { 248 + if _, err := syntax.ParseNSID(fl.Field().String()); err != nil { 249 + return false 250 + } 251 + return true 252 + }) 253 + 254 + e.Validator = &CustomValidator{validator: vdtor} 255 + 256 + httpd := &http.Server{ 257 + Addr: args.Addr, 258 + Handler: e, 259 + } 260 + 261 + db, err := gorm.Open(sqlite.Open("cocoon.db"), &gorm.Config{}) 262 + if err != nil { 263 + return nil, err 264 + } 265 + 266 + rkbytes, err := os.ReadFile(args.RotationKeyPath) 267 + if err != nil { 268 + return nil, err 269 + } 270 + 271 + plcClient, err := plc.NewClient(&plc.ClientArgs{ 272 + Service: "https://plc.directory", 273 + PdsHostname: args.Hostname, 274 + RotationKey: rkbytes, 275 + }) 276 + if err != nil { 277 + return nil, err 278 + } 279 + 280 + jwkbytes, err := os.ReadFile(args.JwkPath) 281 + if err != nil { 282 + return nil, err 283 + } 284 + 285 + key, err := jwk.ParseKey(jwkbytes) 286 + if err != nil { 287 + return nil, err 288 + } 289 + 290 + var pkey ecdsa.PrivateKey 291 + if err := key.Raw(&pkey); err != nil { 292 + return nil, err 293 + } 294 + 295 + s := &Server{ 296 + httpd: httpd, 297 + echo: e, 298 + logger: args.Logger, 299 + db: db, 300 + plcClient: plcClient, 301 + privateKey: &pkey, 302 + config: &config{ 303 + Version: args.Version, 304 + Did: args.Did, 305 + Hostname: args.Hostname, 306 + ContactEmail: args.ContactEmail, 307 + EnforcePeering: false, 308 + Relays: args.Relays, 309 + }, 310 + evtman: events.NewEventManager(events.NewMemPersister()), 311 + passport: identity.NewPassport(identity.NewMemCache(10_000)), 312 + } 313 + 314 + s.repoman = NewRepoMan(s) // TODO: this is way too lazy, stop it 315 + 316 + return s, nil 317 + } 318 + 319 + func (s *Server) addRoutes() { 320 + s.echo.GET("/", s.handleRoot) 321 + s.echo.GET("/xrpc/_health", s.handleHealth) 322 + s.echo.GET("/.well-known/did.json", s.handleWellKnown) 323 + s.echo.GET("/robots.txt", s.handleRobots) 324 + 325 + // public 326 + s.echo.GET("/xrpc/com.atproto.identity.resolveHandle", s.handleResolveHandle) 327 + s.echo.POST("/xrpc/com.atproto.server.createAccount", s.handleCreateAccount) 328 + s.echo.POST("/xrpc/com.atproto.server.createAccount", s.handleCreateAccount) 329 + s.echo.POST("/xrpc/com.atproto.server.createSession", s.handleCreateSession) 330 + s.echo.GET("/xrpc/com.atproto.server.describeServer", s.handleDescribeServer) 331 + 332 + s.echo.GET("/xrpc/com.atproto.repo.describeRepo", s.handleDescribeRepo) 333 + s.echo.GET("/xrpc/com.atproto.sync.listRepos", s.handleListRepos) 334 + s.echo.GET("/xrpc/com.atproto.repo.listRecords", s.handleListRecords) 335 + s.echo.GET("/xrpc/com.atproto.repo.getRecord", s.handleRepoGetRecord) 336 + s.echo.GET("/xrpc/com.atproto.sync.getRecord", s.handleSyncGetRecord) 337 + s.echo.GET("/xrpc/com.atproto.sync.getBlocks", s.handleGetBlocks) 338 + s.echo.GET("/xrpc/com.atproto.sync.getLatestCommit", s.handleSyncGetLatestCommit) 339 + s.echo.GET("/xrpc/com.atproto.sync.getRepoStatus", s.handleSyncGetRepoStatus) 340 + s.echo.GET("/xrpc/com.atproto.sync.getRepo", s.handleSyncGetRepo) 341 + s.echo.GET("/xrpc/com.atproto.sync.subscribeRepos", s.handleSyncSubscribeRepos) 342 + s.echo.GET("/xrpc/com.atproto.sync.listBlobs", s.handleSyncListBlobs) 343 + s.echo.GET("/xrpc/com.atproto.sync.getBlob", s.handleSyncGetBlob) 344 + 345 + // authed 346 + s.echo.GET("/xrpc/com.atproto.server.getSession", s.handleGetSession, s.handleSessionMiddleware) 347 + s.echo.POST("/xrpc/com.atproto.server.refreshSession", s.handleRefreshSession, s.handleSessionMiddleware) 348 + s.echo.POST("/xrpc/com.atproto.server.deleteSession", s.handleDeleteSession, s.handleSessionMiddleware) 349 + s.echo.POST("/xrpc/com.atproto.identity.updateHandle", s.handleIdentityUpdateHandle, s.handleSessionMiddleware) 350 + 351 + // repo 352 + s.echo.POST("/xrpc/com.atproto.repo.createRecord", s.handleCreateRecord, s.handleSessionMiddleware) 353 + s.echo.POST("/xrpc/com.atproto.repo.putRecord", s.handlePutRecord, s.handleSessionMiddleware) 354 + s.echo.POST("/xrpc/com.atproto.repo.applyWrites", s.handleApplyWrites, s.handleSessionMiddleware) 355 + s.echo.POST("/xrpc/com.atproto.repo.uploadBlob", s.handleRepoUploadBlob, s.handleSessionMiddleware) 356 + 357 + // stupid silly endpoints 358 + s.echo.GET("/xrpc/app.bsky.actor.getPreferences", s.handleActorGetPreferences, s.handleSessionMiddleware) 359 + s.echo.POST("/xrpc/app.bsky.actor.putPreferences", s.handleActorPutPreferences, s.handleSessionMiddleware) 360 + 361 + s.echo.GET("/xrpc/*", s.handleProxy, s.handleSessionMiddleware) 362 + s.echo.POST("/xrpc/*", s.handleProxy, s.handleSessionMiddleware) 363 + } 364 + 365 + func (s *Server) Serve(ctx context.Context) error { 366 + s.addRoutes() 367 + 368 + s.logger.Info("migrating...") 369 + 370 + s.db.AutoMigrate( 371 + &models.Actor{}, 372 + &models.Repo{}, 373 + &models.InviteCode{}, 374 + &models.Token{}, 375 + &models.RefreshToken{}, 376 + &models.Block{}, 377 + &models.Record{}, 378 + &models.Blob{}, 379 + &models.BlobPart{}, 380 + ) 381 + 382 + s.logger.Info("starting cocoon") 383 + 384 + go func() { 385 + if err := s.httpd.ListenAndServe(); err != nil { 386 + panic(err) 387 + } 388 + }() 389 + 390 + for _, relay := range s.config.Relays { 391 + cli := xrpc.Client{Host: relay} 392 + atproto.SyncRequestCrawl(context.TODO(), &cli, &atproto.SyncRequestCrawl_Input{ 393 + Hostname: s.config.Hostname, 394 + }) 395 + } 396 + 397 + <-ctx.Done() 398 + 399 + fmt.Println("shut down") 400 + 401 + return nil 402 + }
+75
server/session.go
···
··· 1 + package server 2 + 3 + import ( 4 + "time" 5 + 6 + "github.com/golang-jwt/jwt/v4" 7 + "github.com/google/uuid" 8 + "github.com/haileyok/cocoon/models" 9 + ) 10 + 11 + type Session struct { 12 + AccessToken string 13 + RefreshToken string 14 + } 15 + 16 + func (s *Server) createSession(repo *models.Repo) (*Session, error) { 17 + now := time.Now() 18 + accexp := now.Add(3 * time.Hour) 19 + refexp := now.Add(7 * 24 * time.Hour) 20 + jti := uuid.NewString() 21 + 22 + accessClaims := jwt.MapClaims{ 23 + "scope": "com.atproto.access", 24 + "aud": s.config.Did, 25 + "sub": repo.Did, 26 + "iat": now.UTC().Unix(), 27 + "exp": accexp.UTC().Unix(), 28 + "jti": jti, 29 + } 30 + 31 + accessToken := jwt.NewWithClaims(jwt.SigningMethodES256, accessClaims) 32 + accessString, err := accessToken.SignedString(s.privateKey) 33 + if err != nil { 34 + return nil, err 35 + } 36 + 37 + refreshClaims := jwt.MapClaims{ 38 + "scope": "com.atproto.refresh", 39 + "aud": s.config.Did, 40 + "sub": repo.Did, 41 + "iat": now.UTC().Unix(), 42 + "exp": refexp.UTC().Unix(), 43 + "jti": jti, 44 + } 45 + 46 + refreshToken := jwt.NewWithClaims(jwt.SigningMethodES256, refreshClaims) 47 + refreshString, err := refreshToken.SignedString(s.privateKey) 48 + if err != nil { 49 + return nil, err 50 + } 51 + 52 + if err := s.db.Create(&models.Token{ 53 + Token: accessString, 54 + Did: repo.Did, 55 + RefreshToken: refreshString, 56 + CreatedAt: now, 57 + ExpiresAt: accexp, 58 + }).Error; err != nil { 59 + return nil, err 60 + } 61 + 62 + if err := s.db.Create(&models.RefreshToken{ 63 + Token: refreshString, 64 + Did: repo.Did, 65 + CreatedAt: now, 66 + ExpiresAt: refexp, 67 + }).Error; err != nil { 68 + return nil, err 69 + } 70 + 71 + return &Session{ 72 + AccessToken: accessString, 73 + RefreshToken: refreshString, 74 + }, nil 75 + }
+120
test.go
···
··· 1 + package main 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "fmt" 7 + "log/slog" 8 + "net/http" 9 + "net/url" 10 + "strings" 11 + 12 + "github.com/bluesky-social/indigo/api/atproto" 13 + "github.com/bluesky-social/indigo/atproto/syntax" 14 + "github.com/bluesky-social/indigo/events" 15 + "github.com/bluesky-social/indigo/events/schedulers/parallel" 16 + lexutil "github.com/bluesky-social/indigo/lex/util" 17 + "github.com/bluesky-social/indigo/repo" 18 + "github.com/bluesky-social/indigo/repomgr" 19 + "github.com/gorilla/websocket" 20 + ) 21 + 22 + func main() { 23 + runFirehoseConsumer("ws://localhost:8080") 24 + } 25 + 26 + func runFirehoseConsumer(relayHost string) error { 27 + dialer := websocket.DefaultDialer 28 + u, err := url.Parse("wss://cocoon.hailey.at") 29 + if err != nil { 30 + return fmt.Errorf("invalid relayHost: %w", err) 31 + } 32 + 33 + u.Path = "xrpc/com.atproto.sync.subscribeRepos" 34 + conn, _, err := dialer.Dial(u.String(), http.Header{ 35 + "User-Agent": []string{fmt.Sprintf("hot-topic/0.0.0")}, 36 + }) 37 + if err != nil { 38 + return fmt.Errorf("subscribing to firehose failed (dialing): %w", err) 39 + } 40 + 41 + rsc := &events.RepoStreamCallbacks{ 42 + RepoCommit: func(evt *atproto.SyncSubscribeRepos_Commit) error { 43 + fmt.Println(evt.Repo) 44 + return handleRepoCommit(evt) 45 + }, 46 + RepoIdentity: func(evt *atproto.SyncSubscribeRepos_Identity) error { 47 + fmt.Println(evt.Did, evt.Handle) 48 + return nil 49 + }, 50 + } 51 + 52 + var scheduler events.Scheduler 53 + parallelism := 700 54 + scheduler = parallel.NewScheduler(parallelism, 1000, relayHost, rsc.EventHandler) 55 + 56 + return events.HandleRepoStream(context.TODO(), conn, scheduler, slog.Default()) 57 + } 58 + 59 + func splitRepoPath(path string) (syntax.NSID, syntax.RecordKey, error) { 60 + parts := strings.SplitN(path, "/", 3) 61 + if len(parts) != 2 { 62 + return "", "", fmt.Errorf("invalid record path: %s", path) 63 + } 64 + collection, err := syntax.ParseNSID(parts[0]) 65 + if err != nil { 66 + return "", "", err 67 + } 68 + rkey, err := syntax.ParseRecordKey(parts[1]) 69 + if err != nil { 70 + return "", "", err 71 + } 72 + return collection, rkey, nil 73 + } 74 + 75 + func handleRepoCommit(evt *atproto.SyncSubscribeRepos_Commit) error { 76 + if evt.TooBig { 77 + return nil 78 + } 79 + 80 + did, err := syntax.ParseDID(evt.Repo) 81 + if err != nil { 82 + panic(err) 83 + } 84 + 85 + rr, err := repo.ReadRepoFromCar(context.TODO(), bytes.NewReader(evt.Blocks)) 86 + if err != nil { 87 + panic(err) 88 + } 89 + 90 + for _, op := range evt.Ops { 91 + collection, rkey, err := splitRepoPath(op.Path) 92 + if err != nil { 93 + panic(err) 94 + } 95 + 96 + ek := repomgr.EventKind(op.Action) 97 + 98 + go func() { 99 + switch ek { 100 + case repomgr.EvtKindCreateRecord, repomgr.EvtKindUpdateRecord: 101 + rc, recordCBOR, err := rr.GetRecordBytes(context.TODO(), op.Path) 102 + if err != nil { 103 + panic(err) 104 + } 105 + 106 + if op.Cid == nil || lexutil.LexLink(rc) != *op.Cid { 107 + panic("nocid") 108 + } 109 + 110 + _ = collection 111 + _ = rkey 112 + _ = recordCBOR 113 + _ = did 114 + 115 + } 116 + }() 117 + } 118 + 119 + return nil 120 + }