diff --git a/token/invocation/examples_test.go b/token/invocation/examples_test.go new file mode 100644 index 0000000..530bcc0 --- /dev/null +++ b/token/invocation/examples_test.go @@ -0,0 +1,220 @@ +package invocation_test + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/ipfs/go-cid" + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/codec/dagcbor" + "github.com/ipld/go-ipld-prime/codec/dagjson" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/fluent/qp" + "github.com/ipld/go-ipld-prime/node/basicnode" + "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/token/invocation" +) + +func ExampleNew() { + privKey, iss, sub, cmd, args, prf, meta, err := setupExampleNew() + if err != nil { + fmt.Errorf("failed to create setup: %w", err) + + return + } + + inv, err := invocation.New( + iss, + sub, + cmd, + prf, + invocation.WithArguments(args), + invocation.WithMeta("env", "development"), + invocation.WithMeta("tags", meta["tags"]), + invocation.WithExpirationIn(time.Minute), + invocation.WithoutInvokedAt()) + if err != nil { + fmt.Errorf("failed to create invocation: %w", err) + + return + } + + data, cid, err := inv.ToSealed(privKey) + if err != nil { + fmt.Errorf("failed to seal invocation: %w", err) + + return + } + + json, err := prettyDAGJSON(data) + if err != nil { + fmt.Errorf("failed to pretty DAG-JSON: %w", err) + + return + } + + fmt.Println("CID:", cid) + fmt.Println("Token (pretty DAG-JSON):") + fmt.Println(json) + + // Expected CID and DAG-JSON output: + // CID: bafyreid2n5q45vk4osned7k5huocbe3mxbisonh5vujepqftc5ftr543ae + // Token (pretty DAG-JSON): + // [ + // { + // "/": { + // "bytes": "gvyL7kdSkgmaDpDU/Qj9ohRwxYLCHER52HFMSFEqQqEcQC9qr4JCPP1f/WybvGGuVzYiA0Hx4JO+ohNz8BxUAA" + // } + // }, + // { + // "h": { + // "/": { + // "bytes": "NO0BcQ" + // } + // }, + // "ucan/inv@1.0.0-rc.1": { + // "args": { + // "headers": { + // "Content-Type": "application/json" + // }, + // "payload": { + // "body": "UCAN is great", + // "draft": true, + // "title": "UCAN for Fun and Profit", + // "topics": [ + // "authz", + // "journal" + // ] + // }, + // "uri": "https://example.com/blog/posts" + // }, + // "cmd": "/crud/create", + // "exp": 1729788921, + // "iss": "did:key:z6MkhniGGyP88eZrq2dpMvUPdS2RQMhTUAWzcu6kVGUvEtCJ", + // "meta": { + // "env": "development", + // "tags": [ + // "blog", + // "post", + // "pr#123" + // ] + // }, + // "nonce": { + // "/": { + // "bytes": "2xXPoZwWln1TfXIp" + // } + // }, + // "prf": [ + // { + // "/": "bafyreigx3qxd2cndpe66j2mdssj773ecv7tqd7wovcnz5raguw6lj7sjoe" + // }, + // { + // "/": "bafyreib34ira254zdqgehz6f2bhwme2ja2re3ltcalejv4x4tkcveujvpa" + // }, + // { + // "/": "bafyreibkb66tpo2ixqx3fe5hmekkbuasrod6olt5bwm5u5pi726mduuwlq" + // } + // ], + // "sub": "did:key:z6MktWuvPvBe5UyHnDGuEdw8aJ5qrhhwLG6jy7cQYM6ckP6P" + // } + // } + // ] +} + +func prettyDAGJSON(data []byte) (string, error) { + var node ipld.Node + + node, err := ipld.Decode(data, dagcbor.Decode) + if err != nil { + return "", err + } + + jsonData, err := ipld.Encode(node, dagjson.Encode) + if err != nil { + return "", err + } + + var out bytes.Buffer + if err := json.Indent(&out, jsonData, "", " "); err != nil { + return "", err + } + + return out.String(), nil +} + +func setupExampleNew() (privKey crypto.PrivKey, iss, sub did.DID, cmd command.Command, args map[string]datamodel.Node, prf []cid.Cid, meta map[string]datamodel.Node, errs error) { + var err error + + privKey, iss, err = did.GenerateEd25519() + if err != nil { + errs = errors.Join(errs, fmt.Errorf("failed to generate Issuer identity: %w", err)) + } + + _, sub, err = did.GenerateEd25519() + if err != nil { + errs = errors.Join(errs, fmt.Errorf("failed to generate Subject identity: %w", err)) + } + + cmd, err = command.Parse("/crud/create") + if err != nil { + errs = errors.Join(errs, fmt.Errorf("failed to parse command: %w", err)) + } + + headers, err := qp.BuildMap(basicnode.Prototype.Any, 2, func(ma datamodel.MapAssembler) { + qp.MapEntry(ma, "Content-Type", qp.String("application/json")) + }) + if err != nil { + errs = errors.Join(errs, fmt.Errorf("failed to build headers: %w", err)) + } + payload, err := qp.BuildMap(basicnode.Prototype.Any, 4, func(ma datamodel.MapAssembler) { + qp.MapEntry(ma, "title", qp.String("UCAN for Fun and Profit")) + qp.MapEntry(ma, "body", qp.String("UCAN is great")) + qp.MapEntry(ma, "topics", qp.List(2, func(la datamodel.ListAssembler) { + qp.ListEntry(la, qp.String("authz")) + qp.ListEntry(la, qp.String("journal")) + })) + qp.MapEntry(ma, "draft", qp.Bool(true)) + }) + if err != nil { + errs = errors.Join(errs, fmt.Errorf("failed to build payload: %w", err)) + } + + args = map[string]datamodel.Node{ + "uri": basicnode.NewString("https://example.com/blog/posts"), + "headers": headers, + "payload": payload, + } + + prf = make([]cid.Cid, 3) + for i, v := range []string{ + "zdpuAzx4sBrBCabrZZqXgvK3NDzh7Mf5mKbG11aBkkMCdLtCp", + "zdpuApTCXfoKh2sB1KaUaVSGofCBNPUnXoBb6WiCeitXEibZy", + "zdpuAoFdXRPw4n6TLcncoDhq1Mr6FGbpjAiEtqSBrTSaYMKkf", + } { + prf[i], err = cid.Parse(v) + if err != nil { + errs = errors.Join(errs, fmt.Errorf("failed to parse proof cid: %w", err)) + } + } + + tags, err := qp.BuildList(basicnode.Prototype.Any, 3, func(la datamodel.ListAssembler) { + qp.ListEntry(la, qp.String("blog")) + qp.ListEntry(la, qp.String("post")) + qp.ListEntry(la, qp.String("pr#123")) + }) + if err != nil { + errs = errors.Join(errs, fmt.Errorf("failed to build tags: %w", err)) + } + + meta = map[string]datamodel.Node{ + "env": basicnode.NewString("development"), + "tags": tags, + } + + return // WARNING: named return values +} diff --git a/token/invocation/invocation.go b/token/invocation/invocation.go index 3a17ea2..0d43bc5 100644 --- a/token/invocation/invocation.go +++ b/token/invocation/invocation.go @@ -69,12 +69,14 @@ func New(iss, sub did.DID, cmd command.Command, prf []cid.Cid, opts ...Option) ( } iat := time.Now() + metadata := meta.NewMeta() tkn := Token{ issuer: iss, subject: sub, command: cmd, proof: prf, + meta: metadata, nonce: nonce, invokedAt: &iat, } @@ -85,6 +87,10 @@ func New(iss, sub did.DID, cmd command.Command, prf []cid.Cid, opts ...Option) ( } } + if len(tkn.meta.Keys) == 0 { + tkn.meta = nil + } + return &tkn, nil } diff --git a/token/invocation/options.go b/token/invocation/options.go index 08c7969..0d232fe 100644 --- a/token/invocation/options.go +++ b/token/invocation/options.go @@ -124,6 +124,15 @@ func WithInvokedAtIn(after time.Duration) Option { } } +// WithoutInvokedAt clears the Token's invokedAt field. +func WithoutInvokedAt() Option { + return func(t *Token) error { + t.invokedAt = nil + + 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 {