diff --git a/atproto/netclient/network_client.go b/atproto/netclient/network_client.go new file mode 100644 index 0000000..a7d3962 --- /dev/null +++ b/atproto/netclient/network_client.go @@ -0,0 +1,39 @@ +package netclient + +import ( + "context" + "encoding/json" + "errors" + "io" + + "github.com/bluesky-social/indigo/atproto/syntax" +) + +// API for clients which pull data from the public atproto network. +// +// Implementations of this interface might resolve PDS instances for DIDs, and fetch data from there. Or they might talk to an archival relay or other network mirroring service. +type NetworkClient interface { + // Fetches record JSON, without verification or validation. A version (CID) can optionally be specified; use empty string to fetch the latest. + // Returns the record as JSON, and the CID indicated by the server. Does not verify that the data (as CBOR) matches the CID, and does not cryptographically verify a "proof chain" to the record. + GetRecordJSON(ctx context.Context, aturi syntax.ATURI, version syntax.CID) (*json.RawMessage, *syntax.CID, error) + + // Fetches the indicated record as CBOR, and authenticates it by checking both the cryptographic signature and Merkle Tree hashes from the current repo revision. A version (CID) can optionally be specified; use empty string to fetch the latest. + // Returns the record as CBOR; the CID of the validated record, and the repo commit revision. + VerifyRecordCBOR(ctx context.Context, aturi syntax.ATURI, version syntax.CID) (*[]byte, *syntax.CID, string, error) + + // Fetches repo export (CAR file). Optionally attempts to fetch only the diff "since" an earlier repo revision. + GetRepoCAR(ctx context.Context, did syntax.DID, since string) (*io.Reader, error) + + // Fetches indicated blob. Does not validate the CID. Returns a reader (which calling code is responsible for closing). + GetBlob(ctx context.Context, did syntax.DID, cid syntax.CID) (*io.ReadCloser, error) + GetAccountStatus(ctx context.Context, did syntax.DID) (*AccountStatus, error) +} + +// XXX: type alias to codegen? or just copy? this is protocol-level +type AccountStatus struct { +} + +func VerifyBlobCID(blob []byte, cid syntax.CID) error { + // XXX: compute hash, check against provided CID + return errors.New("Not Implemented") +} -- 2.51.2 From 16e6d1007259f40d5f67772e1df0edd2ab3c8ee1 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Thu, 31 Jul 2025 20:58:15 +0200 Subject: [PATCH 2/3] progress on netclient --- atproto/netclient/cid.go | 17 +++ atproto/netclient/examples_test.go | 70 +++++++++++ atproto/netclient/netclient.go | 178 ++++++++++++++++++++++++++++ atproto/netclient/network_client.go | 39 ------ go.mod | 8 ++ go.sum | 84 +++++++++++++ 6 files changed, 357 insertions(+), 39 deletions(-) create mode 100644 atproto/netclient/cid.go create mode 100644 atproto/netclient/examples_test.go create mode 100644 atproto/netclient/netclient.go delete mode 100644 atproto/netclient/network_client.go diff --git a/atproto/netclient/cid.go b/atproto/netclient/cid.go new file mode 100644 index 0000000..a849ac9 --- /dev/null +++ b/atproto/netclient/cid.go @@ -0,0 +1,17 @@ +package netclient + +import ( + "github.com/ipfs/go-cid" + "github.com/multiformats/go-multihash" +) + +func computeCID(b []byte) (*cid.Cid, error) { + // TODO: not sure why this would ever fail; could we ignore or panic? + // TODO: is there a more performant way to call SHA256, then wrap? + builder := cid.NewPrefixV1(cid.Raw, multihash.SHA2_256) + c, err := builder.Sum(b) + if err != nil { + return nil, err + } + return &c, err +} diff --git a/atproto/netclient/examples_test.go b/atproto/netclient/examples_test.go new file mode 100644 index 0000000..a438df9 --- /dev/null +++ b/atproto/netclient/examples_test.go @@ -0,0 +1,70 @@ +package netclient + +import ( + "bytes" + "context" + "fmt" + + "github.com/bluesky-social/indigo/atproto/repo" + "github.com/bluesky-social/indigo/atproto/syntax" +) + +func ExampleNetClient_GetRepoCAR() { + + ctx := context.Background() + nc := NewNetClient() + did := syntax.DID("did:plc:ewvi7nxzyoun6zhxrhs64oiz") + + stream, err := nc.GetRepoCAR(ctx, did) + if err != nil { + panic("failed to download CAR: " + err.Error()) + } + defer stream.Close() + + // NOTE: could also use LoadCommitFromCAR + commit, _, err := repo.LoadRepoFromCAR(ctx, stream) + if err != nil { + panic("failed to parse CAR: " + err.Error()) + } + + ident, _ := nc.Dir.LookupDID(ctx, did) + pub, _ := ident.PublicKey() + + if err := commit.VerifySignature(pub); err != nil { + panic("failed to verify commit signature: " + err.Error()) + } + + fmt.Println(commit.DID) + // did:plc:ewvi7nxzyoun6zhxrhs64oiz +} + +func ExampleNetClient_GetBlob() { + + ctx := context.Background() + nc := NewNetClient() + did := syntax.DID("did:plc:ewvi7nxzyoun6zhxrhs64oiz") + cid := syntax.CID("bafkreieya7iitpu4okjtm7iexiwikj7t63ttlthad32ojsvjqhqbc3iwmi") + + buf := bytes.Buffer{} + if err := nc.GetBlob(ctx, did, cid, &buf); err != nil { + panic("failed to download blob: " + err.Error()) + } + + fmt.Println(buf.Len()) + // 518394 +} + +func ExampleNetClient_GetAccountStatus() { + + ctx := context.Background() + nc := NewNetClient() + did := syntax.DID("did:plc:ewvi7nxzyoun6zhxrhs64oiz") + + active, status, err := nc.GetAccountStatus(ctx, did) + if err != nil { + panic("failed to check account status: " + err.Error()) + } + + fmt.Printf("active=%t status=%s\n", active, status) + // Output: active=true status= +} diff --git a/atproto/netclient/netclient.go b/atproto/netclient/netclient.go new file mode 100644 index 0000000..cb5f3be --- /dev/null +++ b/atproto/netclient/netclient.go @@ -0,0 +1,178 @@ +package netclient + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log/slog" + "net/http" + + "github.com/bluesky-social/indigo/atproto/identity" + "github.com/bluesky-social/indigo/atproto/syntax" +) + +type NetClient struct { + Client *http.Client + // NOTE: maybe should use a "resolver" which doesn't do handle resolution? or leave that to calling code to configure + Dir identity.Directory + UserAgent string +} + +func NewNetClient() *NetClient { + return &NetClient{ + // TODO: maybe custom client: SSRF, retries, timeout + Client: http.DefaultClient, + Dir: identity.DefaultDirectory(), + UserAgent: "cobalt-netclient", + } +} + +// Fetches repo export (CAR file). Calling code is responsible for closing the returned [io.ReadCloser] on success (often an HTTP response body). Does not verify signatures or CAR format or structure in any way. +func (nc *NetClient) GetRepoCAR(ctx context.Context, did syntax.DID) (io.ReadCloser, error) { + ident, err := nc.Dir.LookupDID(ctx, did) + if err != nil { + return nil, err + } + host := ident.PDSEndpoint() + if host == "" { + return nil, fmt.Errorf("account has no PDS host registered: %s", did.String()) + } + // TODO: validate host + // TODO: DID escaping (?) + u := fmt.Sprintf("%s/xrpc/com.atproto.sync.getRepo?did=%s", host, did) + + slog.Debug("downloading repo CAR", "did", did, "url", u) + req, err := http.NewRequestWithContext(ctx, "GET", u, nil) + if err != nil { + return nil, err + } + if nc.UserAgent != "" { + req.Header.Set("User-Agent", nc.UserAgent) + } + req.Header.Set("Accept", "application/vnd.ipld.car") + + resp, err := nc.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("fetching repo CAR file (%s): %w", did, err) + } + + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, fmt.Errorf("HTTP error fetching repo CAR file (%s): %d", did, resp.StatusCode) + } + + return resp.Body, nil +} + +// Resolves and fetches blob from the network. Calling code must close the returned [io.ReadCloser] (eg, HTTP response body). Does not verify CID. +func (nc *NetClient) GetBlobReader(ctx context.Context, did syntax.DID, cid syntax.CID) (io.ReadCloser, error) { + ident, err := nc.Dir.LookupDID(ctx, did) + if err != nil { + return nil, err + } + host := ident.PDSEndpoint() + if host == "" { + return nil, fmt.Errorf("account has no PDS host registered: %s", did.String()) + } + // TODO: validate host + // TODO: DID escaping (?) + u := fmt.Sprintf("%s/xrpc/com.atproto.sync.getBlob?did=%s&cid=%s", host, did, cid) + + slog.Debug("downloading blob", "did", did, "cid", cid, "url", u) + req, err := http.NewRequestWithContext(ctx, "GET", u, nil) + if err != nil { + return nil, err + } + if nc.UserAgent != "" { + req.Header.Set("User-Agent", nc.UserAgent) + } + req.Header.Set("Accept", "*/*") + + resp, err := nc.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("fetching blob (%s, %s): %w", did, cid, err) + } + + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, fmt.Errorf("HTTP error fetching blob (%s, %s): %d", did, cid, resp.StatusCode) + } + + return resp.Body, nil +} + +var ErrMismatchedBlobCID = errors.New("mismatched blob CID") + +// Fetches blob, writes in to provided buffer, and verified CID hash. +func (nc *NetClient) GetBlob(ctx context.Context, did syntax.DID, cid syntax.CID, buf *bytes.Buffer) error { + stream, err := nc.GetBlobReader(ctx, did, cid) + if err != nil { + return err + } + defer stream.Close() + + if _, err := io.Copy(buf, stream); err != nil { + return err + } + + c, err := computeCID(buf.Bytes()) + if err != nil { + return err + } + + if c.String() != cid.String() { + return ErrMismatchedBlobCID + } + return nil +} + +type repoStatusResp struct { + Active bool `json:"active"` + DID string `json:"did"` + Status string `json:"status,omitempty"` +} + +// Fetches account status. Returns a boolean indicating active state, and a string describing any non-active status. +func (nc *NetClient) GetAccountStatus(ctx context.Context, did syntax.DID) (active bool, status string, err error) { + ident, err := nc.Dir.LookupDID(ctx, did) + if err != nil { + return false, "", err + } + host := ident.PDSEndpoint() + if host == "" { + return false, "", fmt.Errorf("account has no PDS host registered: %s", did.String()) + } + // TODO: validate host + // TODO: DID escaping (?) + u := fmt.Sprintf("%s/xrpc/com.atproto.sync.getRepoStatus?did=%s", host, did) + + slog.Debug("fetching account status", "did", did, "url", u) + req, err := http.NewRequestWithContext(ctx, "GET", u, nil) + if err != nil { + return false, "", err + } + if nc.UserAgent != "" { + req.Header.Set("User-Agent", nc.UserAgent) + } + req.Header.Set("Accept", "application/json") + + resp, err := nc.Client.Do(req) + if err != nil { + return false, "", fmt.Errorf("fetching account status (%s): %w", did, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return false, "", fmt.Errorf("HTTP error fetching account status (%s): %d", did, resp.StatusCode) + } + + var rsr repoStatusResp + if err := json.NewDecoder(resp.Body).Decode(&rsr); err != nil { + return false, "", fmt.Errorf("failed decoding account status response: %w", err) + } + + return rsr.Active, rsr.Status, nil +} diff --git a/atproto/netclient/network_client.go b/atproto/netclient/network_client.go deleted file mode 100644 index a7d3962..0000000 --- a/atproto/netclient/network_client.go +++ /dev/null @@ -1,39 +0,0 @@ -package netclient - -import ( - "context" - "encoding/json" - "errors" - "io" - - "github.com/bluesky-social/indigo/atproto/syntax" -) - -// API for clients which pull data from the public atproto network. -// -// Implementations of this interface might resolve PDS instances for DIDs, and fetch data from there. Or they might talk to an archival relay or other network mirroring service. -type NetworkClient interface { - // Fetches record JSON, without verification or validation. A version (CID) can optionally be specified; use empty string to fetch the latest. - // Returns the record as JSON, and the CID indicated by the server. Does not verify that the data (as CBOR) matches the CID, and does not cryptographically verify a "proof chain" to the record. - GetRecordJSON(ctx context.Context, aturi syntax.ATURI, version syntax.CID) (*json.RawMessage, *syntax.CID, error) - - // Fetches the indicated record as CBOR, and authenticates it by checking both the cryptographic signature and Merkle Tree hashes from the current repo revision. A version (CID) can optionally be specified; use empty string to fetch the latest. - // Returns the record as CBOR; the CID of the validated record, and the repo commit revision. - VerifyRecordCBOR(ctx context.Context, aturi syntax.ATURI, version syntax.CID) (*[]byte, *syntax.CID, string, error) - - // Fetches repo export (CAR file). Optionally attempts to fetch only the diff "since" an earlier repo revision. - GetRepoCAR(ctx context.Context, did syntax.DID, since string) (*io.Reader, error) - - // Fetches indicated blob. Does not validate the CID. Returns a reader (which calling code is responsible for closing). - GetBlob(ctx context.Context, did syntax.DID, cid syntax.CID) (*io.ReadCloser, error) - GetAccountStatus(ctx context.Context, did syntax.DID) (*AccountStatus, error) -} - -// XXX: type alias to codegen? or just copy? this is protocol-level -type AccountStatus struct { -} - -func VerifyBlobCID(blob []byte, cid syntax.CID) error { - // XXX: compute hash, check against provided CID - return errors.New("Not Implemented") -} diff --git a/go.mod b/go.mod index 084516c..f07f0fb 100644 --- a/go.mod +++ b/go.mod @@ -39,15 +39,23 @@ require ( github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/ipfs/bbloom v0.0.4 // indirect github.com/ipfs/go-block-format v0.2.0 // 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-cbor v0.1.0 // 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-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 v0.6.2 // indirect + github.com/ipld/go-codec-dagpb v1.6.0 // indirect + github.com/ipld/go-ipld-prime v0.21.0 // 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 diff --git a/go.sum b/go.sum index 5a312fb..052b81d 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 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/bluesky-social/indigo v0.0.0-20250626183556-5641d3c27325 h1:Bftt2EcoLZK2Z2m12Ih5QqbReX8j29hbf4zJU/FKzaY= @@ -13,13 +15,19 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL 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.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +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/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/flosch/pongo2/v6 v6.0.0 h1:lsGru8IAzHgIAw6H2m4PCyleO58I40ow6apih0WprMU= github.com/flosch/pongo2/v6 v6.0.0/go.mod h1:CuDpFm47R0uGGE7z13/tTlt1Y6zdxvr2RLT5LJhsHEU= +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.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -34,6 +42,8 @@ github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keL github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 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.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -51,10 +61,16 @@ github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iP github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 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-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-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= @@ -63,21 +79,49 @@ github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= 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-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.2 h1:e1vOOW6MuOwG2lqxcLA+wEn93i/9laCY8sXAw76jFOY= +github.com/ipfs/go-ipfs-pq v0.0.2/go.mod h1:LWIqQpqfRG3fNc5XsnIhz/wQ2XXGyugQwls7BgUmUfY= +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-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.0 h1:JyNO144tfu9bx6Hpo119zvbEL9iQ760FHOiJYsUjqaU= +github.com/ipfs/go-peertaskqueue v0.8.0/go.mod h1:cz8hEnnARq4Du5TGqiWKgMr/BOSQ5XOgMOh1K5YYKKM= +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.2 h1:Hlnl3Awgnq8icK+ze3iRghk805lu8YNq3wlREDTF2qc= +github.com/ipld/go-car v0.6.2/go.mod h1:oEGXdwp6bmxJCZ+rARSkDliTeYnVzv3++eXajZ+Bmr8= +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/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= @@ -95,6 +139,8 @@ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2 github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 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= @@ -106,12 +152,34 @@ github.com/labstack/echo/v4 v4.11.3 h1:Upyu3olaqSHkCjs1EJJwQ3WId8b8b1hxbogyommKk github.com/labstack/echo/v4 v4.11.3/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws= github.com/labstack/gommon v0.4.1 h1:gqEff0p/hTENGMABzezPoPSRtIh1Cvw0ueMOe0/dfOk= github.com/labstack/gommon v0.4.1/go.mod h1:TyTrpPqxR5KMk8LKVtLmfMjeQ5FEkBYdxLYPw/WfrOM= +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.22.0 h1:2Tce0kHOp5zASFKJbNzRElvh0iZwdtG5uZheNW8chIw= +github.com/libp2p/go-libp2p v0.22.0/go.mod h1:UDolmweypBSjQb2f7xutPnwZ/fxioLbMBxSjRksxxU4= +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.2.0 h1:W6shmB+FeynDrUVl2dgFQvzfBZcXiyqY4VmpQLu9FqU= +github.com/libp2p/go-msgio v0.2.0/go.mod h1:dBVM1gW3Jk9XqHkU4eKdGvVHdLa51hoGfll6jMJMSlY= +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.0 h1:0FpsbsvuSnAhXFnCY0VLFbJOzaK0VnP0r1QT/o4nWRE= +github.com/libp2p/go-netroute v0.2.0/go.mod h1:Vio7LTzZ+6hoT4CMZi5/6CpY3Snzh2vgZhWgxMNwlQI= +github.com/libp2p/go-openssl v0.1.0 h1:LBkKEcUv6vtZIQLVTegAil8jbNpJErQ9AnT+bWV+Ooo= +github.com/libp2p/go-openssl v0.1.0/go.mod h1:OiOxwPpL3n4xlenjx2h7AwSGaFSC/KZvf6gNdOBQMtc= 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-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0= +github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= 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/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= @@ -124,10 +192,20 @@ github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aG 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.7.0 h1:gskHcdaCyPtp9XskVwtvEeQOG465sCohbQIirSyqxrc= +github.com/multiformats/go-multiaddr v0.7.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.3.3 h1:d5PZpjwRgVlbwfdTDjife7XszfZd8KYWfROYFlGcR8o= +github.com/multiformats/go-multistream v0.3.3/go.mod h1:ODRoqamLUsETKS9BNcII4gcRsJBU5VAwRIv7O39cEXg= 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= @@ -162,6 +240,8 @@ github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N 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/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 h1:RC6RW7j+1+HkWaX/Yh71Ee5ZHaHYt7ZP4sQgUrm6cDU= +github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc= 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= @@ -178,6 +258,8 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw 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-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4= @@ -290,6 +372,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 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= -- 2.51.2 From a497cb6767100df0b3799d4d819a97e70592d159 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Thu, 31 Jul 2025 22:28:36 +0200 Subject: [PATCH 3/3] flesh out netclient code --- atproto/{netclient => heap}/cid.go | 2 +- atproto/{netclient => heap}/examples_test.go | 42 ++++- atproto/{netclient => heap}/netclient.go | 2 +- atproto/heap/record.go | 156 +++++++++++++++++++ 4 files changed, 198 insertions(+), 4 deletions(-) rename atproto/{netclient => heap}/cid.go (95%) rename atproto/{netclient => heap}/examples_test.go (60%) rename atproto/{netclient => heap}/netclient.go (99%) create mode 100644 atproto/heap/record.go diff --git a/atproto/netclient/cid.go b/atproto/heap/cid.go similarity index 95% rename from atproto/netclient/cid.go rename to atproto/heap/cid.go index a849ac9..b029e91 100644 --- a/atproto/netclient/cid.go +++ b/atproto/heap/cid.go @@ -1,4 +1,4 @@ -package netclient +package heap import ( "github.com/ipfs/go-cid" diff --git a/atproto/netclient/examples_test.go b/atproto/heap/examples_test.go similarity index 60% rename from atproto/netclient/examples_test.go rename to atproto/heap/examples_test.go index a438df9..f433ad7 100644 --- a/atproto/netclient/examples_test.go +++ b/atproto/heap/examples_test.go @@ -1,8 +1,9 @@ -package netclient +package heap import ( "bytes" "context" + "encoding/json" "fmt" "github.com/bluesky-social/indigo/atproto/repo" @@ -66,5 +67,42 @@ func ExampleNetClient_GetAccountStatus() { } fmt.Printf("active=%t status=%s\n", active, status) - // Output: active=true status= + // active=true status= +} + +func ExampleNetClient_GetRecordUnverified() { + + ctx := context.Background() + nc := NewNetClient() + did := syntax.DID("did:plc:ewvi7nxzyoun6zhxrhs64oiz") + collection := syntax.NSID("app.bsky.actor.profile") + rkey := syntax.RecordKey("self") + + raw, _, err := nc.GetRecordUnverified(ctx, did, collection, rkey) + if err != nil { + panic("failed to fetch record: " + err.Error()) + } + var record map[string]any + _ = json.Unmarshal(*raw, &record) + + fmt.Println(record["displayName"]) + // AT Protocol Developers +} + +func ExampleNetClient_GetRecord() { + + ctx := context.Background() + nc := NewNetClient() + did := syntax.DID("did:plc:ewvi7nxzyoun6zhxrhs64oiz") + collection := syntax.NSID("app.bsky.actor.profile") + rkey := syntax.RecordKey("self") + + var record map[string]any + _, err := nc.GetRecord(ctx, did, collection, rkey, &record) + if err != nil { + panic("failed to fetch record: " + err.Error()) + } + + fmt.Println(record["displayName"]) + // Output: AT Protocol Developers } diff --git a/atproto/netclient/netclient.go b/atproto/heap/netclient.go similarity index 99% rename from atproto/netclient/netclient.go rename to atproto/heap/netclient.go index cb5f3be..7da1c03 100644 --- a/atproto/netclient/netclient.go +++ b/atproto/heap/netclient.go @@ -1,4 +1,4 @@ -package netclient +package heap import ( "bytes" diff --git a/atproto/heap/record.go b/atproto/heap/record.go new file mode 100644 index 0000000..6709774 --- /dev/null +++ b/atproto/heap/record.go @@ -0,0 +1,156 @@ +package heap + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "log/slog" + "net/http" + + "github.com/bluesky-social/indigo/atproto/data" + "github.com/bluesky-social/indigo/atproto/repo" + "github.com/bluesky-social/indigo/atproto/syntax" +) + +type repoRecordResp struct { + URI string `json:"uri"` + CID syntax.CID `json:"cid"` + Value json.RawMessage `json:"value"` +} + +// Fetches record JSON using com.atproto.repo.getRecord, and returns record as [json.RawMessage] and the CID (as string). +func (nc *NetClient) GetRecordUnverified(ctx context.Context, did syntax.DID, collection syntax.NSID, rkey syntax.RecordKey) (*json.RawMessage, syntax.CID, error) { + ident, err := nc.Dir.LookupDID(ctx, did) + if err != nil { + return nil, "", err + } + host := ident.PDSEndpoint() + if host == "" { + return nil, "", fmt.Errorf("account has no PDS host registered: %s", did.String()) + } + // TODO: validate host + // TODO: DID escaping (?) + u := fmt.Sprintf("%s/xrpc/com.atproto.repo.getRecord?repo=%s&collection=%s&rkey=%s", host, did, collection, rkey) + + slog.Debug("fetching record JSON", "did", did, "url", u) + req, err := http.NewRequestWithContext(ctx, "GET", u, nil) + if err != nil { + return nil, "", err + } + if nc.UserAgent != "" { + req.Header.Set("User-Agent", nc.UserAgent) + } + req.Header.Set("Accept", "application/json") + + resp, err := nc.Client.Do(req) + if err != nil { + return nil, "", fmt.Errorf("fetching record JSON (%s): %w", did, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, "", fmt.Errorf("HTTP error fetching record JSON (%s): %d", did, resp.StatusCode) + } + + var rrr repoRecordResp + if err := json.NewDecoder(resp.Body).Decode(&rrr); err != nil { + return nil, "", fmt.Errorf("failed decoding account status response: %w", err) + } + + return &rrr.Value, rrr.CID, nil +} + +// Fetches a record "proof" using com.atproto.sync.getRecord. Verifies signature and merkel chain. Copies record content in out 'out' parameter. +// +// If out is nil, record data is not returned. If it is [bytes.Buffer], the record CBOR is copied in. Otherwise, the record is transformed to JSON and Unmarshalled in to provided output, which could be a pointer to a struct, [json.RawMessage], `map[string]any`, etc. +// +// TODO: this might not be fully validating MST tree and record CID hashes or encoding yet +func (nc *NetClient) GetRecord(ctx context.Context, did syntax.DID, collection syntax.NSID, rkey syntax.RecordKey, out any) (syntax.CID, error) { + // TODO: "GetRecordProof" variant, which just returns CAR as io.ReadCloser? + ident, err := nc.Dir.LookupDID(ctx, did) + if err != nil { + return "", err + } + pub, err := ident.PublicKey() + if err != nil { + return "", err + } + host := ident.PDSEndpoint() + if host == "" { + return "", fmt.Errorf("account has no PDS host registered: %s", did.String()) + } + // TODO: validate host + // TODO: DID escaping (?) + u := fmt.Sprintf("%s/xrpc/com.atproto.sync.getRecord?did=%s&collection=%s&rkey=%s", host, did, collection, rkey) + + slog.Debug("fetching record proof", "did", did, "url", u) + req, err := http.NewRequestWithContext(ctx, "GET", u, nil) + if err != nil { + return "", err + } + if nc.UserAgent != "" { + req.Header.Set("User-Agent", nc.UserAgent) + } + req.Header.Set("Accept", "application/vnd.ipld.car") + + resp, err := nc.Client.Do(req) + if err != nil { + return "", fmt.Errorf("fetching record proof (%s): %w", did, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("HTTP error fetching record proof (%s): %d", did, resp.StatusCode) + } + + // TODO: re-confirm if loading tree re-checks all CIDs; or if we need to re-compute the tree data CID + commit, rp, err := repo.LoadRepoFromCAR(ctx, resp.Body) + if err != nil { + return "", fmt.Errorf("failed to parse record proof CAR (%s): %w", did, err) + } + + // NOTE: LoadRepoFromCAR calls commit.VerifyStructure() internally + + if err := commit.VerifySignature(pub); err != nil { + return "", fmt.Errorf("failed to verify record proof signature (%s): %w", did, err) + } + + rbytes, rcid, err := rp.GetRecordBytes(ctx, collection, rkey) + if err != nil { + return "", fmt.Errorf("failed to read record from proof CAR (%s): %w", did, err) + } + cidStr := syntax.CID(rcid.String()) + + // TODO: `GetRecordBytes` does not currently verify record CID, but unpacking CAR file should have done that? but need to confirm CAR implementation does this + + // check that record CBOR is valid, even if we don't return it + rdata, err := data.UnmarshalCBOR(rbytes) + if err != nil { + return "", fmt.Errorf("failed to parse record CBOR (%s): %w", did, err) + } + + switch out := out.(type) { + case nil: + // if output isn't captured, bail out early + return cidStr, nil + case *bytes.Buffer: + // simply copy data over + out.Reset() + _, err := out.Write(rbytes) + if err != nil { + return "", err + } + return cidStr, nil + default: + // attempt to unmarshal from json + jsonBytes, err := json.Marshal(rdata) + if err != nil { + return "", err + } + if err := json.Unmarshal(jsonBytes, out); err != nil { + return "", fmt.Errorf("failed unmarhsaling record (%s): %w", did, err) + } + return cidStr, nil + } +}