From d784c92c293ce5a67e14c919bdb5c8f8fa9a7244 Mon Sep 17 00:00:00 2001 From: Steve Moyer Date: Thu, 24 Oct 2024 10:44:38 -0400 Subject: [PATCH] feat(invocation): provide New constructor and encoding to wire-format --- token/invocation/invocation.go | 103 ++++++++++++++++----- token/invocation/invocation.ipldsch | 29 +++--- token/invocation/ipld.go | 47 ++++++++-- token/invocation/options.go | 134 ++++++++++++++++++++++++++++ token/invocation/schema.go | 34 ++++--- 5 files changed, 295 insertions(+), 52 deletions(-) create mode 100644 token/invocation/options.go diff --git a/token/invocation/invocation.go b/token/invocation/invocation.go index d48fee8..3a17ea2 100644 --- a/token/invocation/invocation.go +++ b/token/invocation/invocation.go @@ -13,6 +13,8 @@ import ( "fmt" "time" + "github.com/ipfs/go-cid" + "github.com/ipld/go-ipld-prime/datamodel" "github.com/ucan-wg/go-ucan/did" "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/pkg/meta" @@ -20,25 +22,70 @@ import ( // Token is an immutable type that holds the fields of a UCAN invocation. type Token struct { - // Issuer DID (invoker) + // The DID of the Invoker issuer did.DID - // Audience DID (receiver/executor) - audience did.DID - // Subject DID (subject being invoked) + // The DID of Subject being invoked subject did.DID - // The Command to invoke + // The DID of the intended Executor if different from the Subject + audience did.DID + + // The Command command command.Command - // TODO: args - // TODO: prf - // A unique, random nonce - nonce []byte + // The Command's Arguments + arguments map[string]datamodel.Node + // Delegations that prove the chain of authority + proof []cid.Cid + // Arbitrary Metadata meta *meta.Meta + + // A unique, random nonce + nonce []byte // The timestamp at which the Invocation becomes invalid expiration *time.Time // The timestamp at which the Invocation was created invokedAt *time.Time - // TODO: cause + + // An optional CID of the Receipt that enqueued the Task + cause *cid.Cid +} + +// New creates an invocation Token with the provided options. +// +// If no nonce is provided, a random 12-byte nonce is generated. Use the +// WithNonce or WithEmptyNonce options to specify provide your own nonce +// or to leave the nonce empty respectively. +// +// If no invokedAt is provided, the current time is used. Use the +// WithInvokedAt or WithInvokedAtIn options to specify a different time. +// +// With the exception of the WithMeta option, all other will overwrite +// the previous contents of their target field. +func New(iss, sub did.DID, cmd command.Command, prf []cid.Cid, opts ...Option) (*Token, error) { + nonce := make([]byte, 12) + + if _, err := rand.Read(nonce); err != nil { + return nil, err + } + + iat := time.Now() + + tkn := Token{ + issuer: iss, + subject: sub, + command: cmd, + proof: prf, + nonce: nonce, + invokedAt: &iat, + } + + for _, opt := range opts { + if err := opt(&tkn); err != nil { + return nil, err + } + } + + return &tkn, nil } // Issuer returns the did.DID representing the Token's issuer. @@ -46,28 +93,27 @@ func (t *Token) Issuer() did.DID { return t.issuer } +// Subject returns the did.DID representing the Token's subject. +func (t *Token) Subject() did.DID { + return t.subject +} + // Audience returns the did.DID representing the Token's audience. func (t *Token) Audience() did.DID { return t.audience } -// Subject returns the did.DID representing the Token's subject. -// -// This field may be did.Undef for delegations that are [Powerlined] but -// must be equal to the value returned by the Issuer method for root -// tokens. -func (t *Token) Subject() did.DID { - return t.subject -} - // Command returns the capability's command.Command. func (t *Token) Command() command.Command { return t.command } -// Nonce returns the random Nonce encapsulated in this Token. -func (t *Token) Nonce() []byte { - return t.nonce +func (t *Token) Arguments() map[string]datamodel.Node { + return t.arguments +} + +func (t *Token) Proof() []cid.Cid { + return t.proof } // Meta returns the Token's metadata. @@ -75,11 +121,24 @@ func (t *Token) Meta() *meta.Meta { return t.meta } +// Nonce returns the random Nonce encapsulated in this Token. +func (t *Token) Nonce() []byte { + return t.nonce +} + // Expiration returns the time at which the Token expires. func (t *Token) Expiration() *time.Time { return t.expiration } +func (t *Token) InvokedAt() *time.Time { + return t.invokedAt +} + +func (t *Token) Cause() *cid.Cid { + return t.cause +} + func (t *Token) validate() error { var errs error diff --git a/token/invocation/invocation.ipldsch b/token/invocation/invocation.ipldsch index 2acab27..ba2396a 100644 --- a/token/invocation/invocation.ipldsch +++ b/token/invocation/invocation.ipldsch @@ -1,23 +1,32 @@ + type DID string # The Invocation Payload attaches sender, receiver, and provenance to the Task. type Payload struct { - # Issuer DID (sender) + # The DID of the invoker iss DID - # Audience DID (receiver) - aud DID - # Principal that the chain is about (the Subject) - sub optional DID + # The Subject being invoked + sub DID + # The DID of the intended Executor if different from the Subject + aud optional DID - # The Command to eventually invoke + # The Command cmd String - - # A unique, random nonce - nonce Bytes + # The Command's Arguments + args { String : Any} + # Delegations that prove the chain of authority + prf [ Link ] # Arbitrary Metadata - meta {String : Any} + meta optional { String : Any } + # A unique, random nonce + nonce optional Bytes # The timestamp at which the Invocation becomes invalid exp nullable Int + # The Timestamp at which the Invocation was created + iat optional Int + + # An optional CID of the Receipt that enqueued the Task + cause optional Link } diff --git a/token/invocation/ipld.go b/token/invocation/ipld.go index 897f608..467e2be 100644 --- a/token/invocation/ipld.go +++ b/token/invocation/ipld.go @@ -193,29 +193,58 @@ func FromIPLD(node datamodel.Node) (*Token, error) { } func (t *Token) toIPLD(privKey crypto.PrivKey) (datamodel.Node, error) { - var sub *string + var aud *string - if t.subject != did.Undef { - s := t.subject.String() - sub = &s + if t.audience != did.Undef { + a := t.audience.String() + aud = &a } - // TODO - var exp *int64 if t.expiration != nil { u := t.expiration.Unix() exp = &u } + var iat *int64 + if t.invokedAt != nil { + i := t.invokedAt.Unix() + iat = &i + } + + argsKey := make([]string, len(t.arguments)) + i := 0 + + for k := range t.arguments { + argsKey[i] = k + i++ + } + + args := struct { + Keys []string + Values map[string]datamodel.Node + }{ + Keys: argsKey, + Values: t.arguments, + } + + prf := make([]cid.Cid, len(t.proof)) + for i, c := range t.proof { + prf[i] = c + } + model := &tokenPayloadModel{ Iss: t.issuer.String(), - Aud: t.audience.String(), - Sub: sub, + Aud: aud, + Sub: t.subject.String(), Cmd: t.command.String(), + Args: args, + Prf: prf, + Meta: t.meta, Nonce: t.nonce, - Meta: *t.meta, Exp: exp, + Iat: iat, + Cause: t.cause, } return envelope.ToIPLD(privKey, model) diff --git a/token/invocation/options.go b/token/invocation/options.go new file mode 100644 index 0000000..08c7969 --- /dev/null +++ b/token/invocation/options.go @@ -0,0 +1,134 @@ +package invocation + +import ( + "time" + + "github.com/ipfs/go-cid" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ucan-wg/go-ucan/did" +) + +// Option is a type that allows optional fields to be set during the +// creation of a Token. +type Option func(*Token) error + +// WithArgument adds a key/value pair to the Token's Arguments field. +func WithArgument(key string, val datamodel.Node) Option { + return func(t *Token) error { + t.arguments[key] = val + + return nil + } +} + +// WithArguments sets the Token's Arguments field to the provided map. +// +// Note that this will overwrite any existing Arguments whether provided +// by a previous call to this function or by one or more calls to +// WithArgument. +func WithArguments(args map[string]datamodel.Node) Option { + return func(t *Token) error { + t.arguments = args + + return nil + } +} + +// WithAudience sets the Token's audience to the provided did.DID. +// +// If the provided did.DID is the same as the Token's subject, the +// audience is not set. +func WithAudience(aud did.DID) Option { + return func(t *Token) error { + if t.subject.String() != aud.String() { + t.audience = aud + } + + return nil + } +} + +// WithMeta adds a key/value pair in the "meta" field. +// +// WithMeta can be used multiple times in the same call. +// Accepted types for the value are: bool, string, int, int32, int64, []byte, +// and ipld.Node. +func WithMeta(key string, val any) Option { + return func(t *Token) error { + return t.meta.Add(key, val) + } +} + +// WithNonce sets the Token's nonce with the given value. +// +// If this option is not used, a random 12-byte nonce is generated for +// this require. +func WithNonce(nonce []byte) Option { + return func(t *Token) error { + t.nonce = nonce + + return nil + } +} + +// WithEmptyNonce sets the Token's nonce to an empty byte slice as +// suggested by the UCAN spec for invocation tokens that represent +// idem +func WithEmptyNonce() Option { + return func(t *Token) error { + t.nonce = []byte{} + + return nil + } +} + +// WithExpiration set's the Token's optional "expiration" field to the +// value of the provided time.Time. +func WithExpiration(exp time.Time) Option { + return func(t *Token) error { + t.expiration = &exp + + return nil + } +} + +// WithExpirationIn set's the Token's optional "expiration" field to +// Now() plus the given duration. +func WithExpirationIn(exp time.Duration) Option { + return func(t *Token) error { + expTime := time.Now().Add(exp) + t.expiration = &expTime + + return nil + } +} + +// WithInvokedAt sets the Token's invokedAt field to the provided +// time.Time. +func WithInvokedAt(iat time.Time) Option { + return func(t *Token) error { + t.invokedAt = &iat + + return nil + } +} + +// WithInvokedAtIn sets the Token's invokedAt field to Now() plus the +// given duration. +func WithInvokedAtIn(after time.Duration) Option { + return func(t *Token) error { + iat := time.Now().Add(after) + t.invokedAt = &iat + + return nil + } +} + +// WithCause sets the Token's cause field to the provided cid.Cid. +func WithCause(cause *cid.Cid) Option { + return func(t *Token) error { + t.cause = cause + + return nil + } +} diff --git a/token/invocation/schema.go b/token/invocation/schema.go index e6941a2..f438e1f 100644 --- a/token/invocation/schema.go +++ b/token/invocation/schema.go @@ -5,7 +5,9 @@ import ( "fmt" "sync" + "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/node/bindnode" "github.com/ipld/go-ipld-prime/schema" @@ -46,26 +48,36 @@ var _ envelope.Tokener = (*tokenPayloadModel)(nil) // TODO type tokenPayloadModel struct { - // Issuer DID (sender) + // The DID of the Invoker Iss string - // Audience DID (receiver) - Aud string - // Principal that the chain is about (the Subject) - // optional: can be nil - Sub *string + // The DID of Subject being invoked + Sub string + // The DID of the intended Executor if different from the Subject + Aud *string - // The Command to eventually invoke + // The Command Cmd string + // The Command's Arguments + Args struct { + Keys []string + Values map[string]datamodel.Node + } + // Delegations that prove the chain of authority + Prf []cid.Cid + + // Arbitrary Metadata + Meta *meta.Meta // A unique, random nonce Nonce []byte - - // Arbitrary Metadata - Meta meta.Meta - // The timestamp at which the Invocation becomes invalid // optional: can be nil Exp *int64 + // The timestamp at which the Invocation was created + Iat *int64 + + // An optional CID of the Receipt that enqueued the Task + Cause *cid.Cid } func (e *tokenPayloadModel) Prototype() schema.TypedPrototype {