diff --git a/toolkit/client/client.go b/toolkit/client/client.go new file mode 100644 index 0000000..3611ef2 --- /dev/null +++ b/toolkit/client/client.go @@ -0,0 +1,78 @@ +package client + +import ( + "context" + "fmt" + + "github.com/ipfs/go-cid" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/ucan-wg/go-ucan/did" + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/pkg/container" + "github.com/ucan-wg/go-ucan/token/invocation" +) + +type Client struct { + did did.DID + privKey crypto.PrivKey + + pool *Pool + requester DelegationRequester +} + +func NewClient(privKey crypto.PrivKey, requester DelegationRequester) (*Client, error) { + d, err := did.FromPrivKey(privKey) + if err != nil { + return nil, err + } + return &Client{ + did: d, + privKey: privKey, + pool: NewPool(), + requester: requester, + }, nil +} + +// PrepareInvoke returns an invocation and the proof delegation, bundled in a container.Writer. +func (c *Client) PrepareInvoke(ctx context.Context, cmd command.Command, subject did.DID, opts ...invocation.Option) (container.Writer, error) { + var proof []cid.Cid + + // do we already have a valid proof? + if proof = c.pool.FindProof(cmd, c.did, subject); len(proof) == 0 { + // we need to request a new proof + proofBundles, err := c.requester.RequestDelegation(ctx, cmd, c.did, subject) + if err != nil { + return nil, fmt.Errorf("requesting delegation: %w", err) + } + + // cache the new proofs + for bundle, err := range proofBundles { + if err != nil { + return nil, err + } + proof = append(proof, bundle.Cid) + c.pool.AddBundle(bundle) + } + } + + inv, err := invocation.New(c.did, subject, cmd, proof, opts...) + if err != nil { + return nil, err + } + + invSealed, invCid, err := inv.ToSealed(c.privKey) + if err != nil { + return nil, err + } + + cont := container.NewWriter() + cont.AddSealed(invCid, invSealed) + for bundle, err := range c.pool.GetBundles(proof) { + if err != nil { + return nil, err + } + cont.AddSealed(bundle.Cid, bundle.Sealed) + } + + return cont, nil +} diff --git a/toolkit/client/pool.go b/toolkit/client/pool.go index 6926eb7..de2b526 100644 --- a/toolkit/client/pool.go +++ b/toolkit/client/pool.go @@ -1,6 +1,7 @@ package client import ( + "fmt" "iter" "sync" "time" @@ -39,6 +40,7 @@ func (p *Pool) AddBundles(bundles iter.Seq[*delegation.Bundle]) { // Note: the returned delegation(s) don't have to match exactly the parameters, as long as they allow them. // Note: the implemented algorithm won't perform well with a large number of delegations. func (p *Pool) FindProof(cmd command.Command, iss did.DID, aud did.DID) []cid.Cid { + // TODO: move to some kind of background trim job? p.trim() p.mu.RLock() @@ -55,6 +57,24 @@ func (p *Pool) FindProof(cmd command.Command, iss did.DID, aud did.DID) []cid.Ci }, cmd, iss, aud) } +func (p *Pool) GetBundles(cids []cid.Cid) iter.Seq2[*delegation.Bundle, error] { + p.mu.RLock() + defer p.mu.RUnlock() + + return func(yield func(*delegation.Bundle, error) bool) { + for _, c := range cids { + if b, ok := p.dlgs[c]; ok { + if !yield(b, nil) { + return + } + } else { + yield(nil, fmt.Errorf("bundle not found")) + return + } + } + } +} + // trim removes expired tokens func (p *Pool) trim() { p.mu.Lock() diff --git a/toolkit/client/requester.go b/toolkit/client/requester.go new file mode 100644 index 0000000..cf28bc4 --- /dev/null +++ b/toolkit/client/requester.go @@ -0,0 +1,19 @@ +package client + +import ( + "context" + "iter" + + "github.com/ucan-wg/go-ucan/did" + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/token/delegation" +) + +type DelegationRequester interface { + // RequestDelegation retrieve a delegation or chain of delegation for the given parameters. + // - cmd: the command to execute + // - issuer: the DID of the client, also the issuer of the invocation token + // - audience: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token + // Note: the returned delegation(s) don't have to match exactly the parameters, as long as they allow them. + RequestDelegation(ctx context.Context, cmd command.Command, audience did.DID, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) +}