A fork of the Cocoon PDS but being made more distributed.
11

Configure Feed

Select the types of activity you want to include in your feed.

initial commit (squashed)

Hailey (Mar 28, 2025, 11:10 PM -0700) 96f71046

+4565
+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 + }