An atproto PDS written in Go

initial commit (squashed)

hailey.at 96f71046

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