feat(token) combined TokenPayload model for both Delegation and Invocation tokens

This commit is contained in:
Steve Moyer
2024-09-11 08:50:24 -04:00
parent 1c2f602f4d
commit 599c5d30b0
9 changed files with 288 additions and 129 deletions

View File

@@ -1,128 +0,0 @@
package token
import (
"errors"
"fmt"
"time"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/node/bindnode"
"github.com/ucan-wg/go-ucan/capability/command"
"github.com/ucan-wg/go-ucan/capability/policy"
"github.com/ucan-wg/go-ucan/did"
)
func BindnodeOptions() []bindnode.Option {
return []bindnode.Option{
CommandConverter(),
DIDConverter(),
MetaConverter(),
PolicyConverter(),
TimeConverter(),
}
}
var ErrTypeAssertion = errors.New("failed to assert type")
func newErrTypeAssertion(where string) error {
return fmt.Errorf("%w: %s", ErrTypeAssertion, where)
}
func CommandConverter() bindnode.Option {
return bindnode.TypedStringConverter(
(*command.Command)(nil),
func(s string) (interface{}, error) {
return command.Parse(s)
},
func(i interface{}) (string, error) {
cmd, ok := i.(*command.Command)
if !ok {
return "", newErrTypeAssertion("CommandConverter")
}
return cmd.String(), nil
},
)
}
func DIDConverter() bindnode.Option {
return bindnode.TypedStringConverter(
(*did.DID)(nil),
func(s string) (interface{}, error) {
return did.Parse(s)
},
func(i interface{}) (string, error) {
return i.(*did.DID).String(), nil
},
)
}
type Meta struct {
Keys []string
Values map[string]any
}
func MetaConverter() bindnode.Option {
return bindnode.TypedAnyConverter(
(map[string]any)(nil),
func(n datamodel.Node) (interface{}, error) {
return Meta{}, nil // TODO
},
func(i interface{}) (datamodel.Node, error) {
if i == nil {
return datamodel.Null, nil
}
meta, ok := i.(Meta)
if !ok {
return nil, newErrTypeAssertion("MetaConverter")
}
_ = meta
return datamodel.Null, nil // TODO
},
)
}
func PolicyConverter() bindnode.Option {
return bindnode.TypedAnyConverter(
(*policy.Policy)(nil),
func(n datamodel.Node) (interface{}, error) {
return policy.FromIPLD(n)
},
func(i interface{}) (datamodel.Node, error) {
if i == nil {
return datamodel.Null, nil
}
pol, ok := i.(*policy.Policy)
if !ok {
return nil, newErrTypeAssertion("PolicyConverter")
}
return pol.ToIPLD()
},
)
}
func TimeConverter() bindnode.Option {
return bindnode.TypedIntConverter(
(*time.Time)(nil),
func(i int64) (interface{}, error) {
return time.Unix(i, 0), nil
},
func(i interface{}) (int64, error) {
if i == nil {
return 0, nil
}
t, ok := i.(*time.Time)
if !ok {
return 0, newErrTypeAssertion("TimeConverter")
}
return t.Unix(), nil
},
)
}

View File

@@ -1 +0,0 @@
package token_test

9
internal/token/errors.go Normal file
View File

@@ -0,0 +1,9 @@
package token
import "errors"
var ErrFailedSchemaLoad = errors.New("failed to load IPLD Schema")
var ErrNoSchemaType = errors.New("schema does not contain type")
var ErrNodeNotToken = errors.New("IPLD node is not a Token")

46
internal/token/schema.go Normal file
View File

@@ -0,0 +1,46 @@
package token
import (
_ "embed"
"fmt"
"sync"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/node/bindnode"
"github.com/ipld/go-ipld-prime/schema"
)
const tokenTypeName = "Token"
//go:embed token.ipldsch
var schemaBytes []byte
var (
once sync.Once
ts *schema.TypeSystem
err error
)
func mustLoadSchema() *schema.TypeSystem {
once.Do(func() {
ts, err = ipld.LoadSchemaBytes(schemaBytes)
})
if err != nil {
panic(fmt.Errorf("%w: %w", ErrFailedSchemaLoad, err))
}
tknType := ts.TypeByName(tokenTypeName)
if tknType == nil {
panic(fmt.Errorf("%w: %s", ErrNoSchemaType, tokenTypeName))
}
return ts
}
func tokenType() schema.Type {
return mustLoadSchema().TypeByName(tokenTypeName)
}
func Prototype() schema.TypedPrototype {
return bindnode.Prototype((*Token)(nil), tokenType())
}

View File

@@ -0,0 +1,83 @@
package token_test
import (
_ "embed"
"fmt"
"testing"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/ipld/go-ipld-prime/codec/dagjson"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/internal/token"
)
//go:embed token.ipldsch
var schemaBytes []byte
func TestSchemaRoundTrip(t *testing.T) {
const delegationJson = `
{
"aud":"did:key:def456",
"cmd":"/foo/bar",
"exp":123456,
"iss":"did:key:abc123",
"meta":{
"bar":"baaar",
"foo":"fooo"
},
"nbf":123456,
"nonce":{
"/":{
"bytes":"c3VwZXItcmFuZG9t"
}
},
"pol":[
["==", ".status", "draft"],
["all", ".reviewer", [
["like", ".email", "*@example.com"]]
],
["any", ".tags", [
["or", [
["==", ".", "news"],
["==", ".", "press"]]
]]
]
],
"sub":""
}
`
// format: dagJson --> IPLD node --> token --> dagCbor --> IPLD node --> dagJson
// function: Unwrap() Wrap()
n1, err := ipld.DecodeUsingPrototype([]byte(delegationJson), dagjson.Decode, token.Prototype())
require.NoError(t, err)
cborBytes, err := ipld.Encode(n1, dagcbor.Encode)
require.NoError(t, err)
fmt.Println("cborBytes length", len(cborBytes))
fmt.Println("cbor", string(cborBytes))
n2, err := ipld.DecodeUsingPrototype(cborBytes, dagcbor.Decode, token.Prototype())
require.NoError(t, err)
fmt.Println("read Cbor", n2)
t1, err := token.Unwrap(n2)
require.NoError(t, err)
n3 := t1.Wrap()
readJson, err := ipld.Encode(n3, dagjson.Encode)
require.NoError(t, err)
fmt.Println("readJson length", len(readJson))
fmt.Println("json: ", string(readJson))
require.JSONEq(t, delegationJson, string(readJson))
}
func BenchmarkSchemaLoad(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = ipld.LoadSchemaBytes(schemaBytes)
}
}

30
internal/token/token.go Normal file
View File

@@ -0,0 +1,30 @@
package token
import (
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/node/bindnode"
)
//go:generate go run ../cmd/token/...
func New() (*Token, error) {
return &Token{}, nil
}
func Unwrap(node datamodel.Node) (*Token, error) {
iface := bindnode.Unwrap(node)
if iface == nil {
return nil, ErrNodeNotToken
}
tkn, ok := iface.(*Token)
if !ok {
return nil, ErrNodeNotToken
}
return tkn, nil
}
func (t *Token) Wrap() datamodel.Node {
return bindnode.Wrap(t, tokenType())
}

View File

@@ -0,0 +1,63 @@
type CID string
type Command string
type DID string
# Field requirements:
#
# | Name | Delegation | Invocation | Token |
# | | Required | Nullable | Required | Nullable | |
# | ----- | -------- | -------- | -------- | -------- | -------- |
# | iss | Yes | No | Yes | No | |
# | aud | Yes | No | No | N/A | Optional |
# | sub | Yes | Yes | Yes | No | Nullable |
# | cmd | Yes | No | Yes | No | |
# | pol | Yes | No | X | X | Optional |
# | nonce | Yes | No | No | N/A | Optional |
# | meta | No | N/A | No | N/A | Optional |
# | nbf | No | N/A | X | X | Optional |
# | exp | Yes | Yes | Yes | Yes | |
# | args | X | X | Yes | No | Optional |
# | prf | X | X | Yes | No | Optional |
# | iat | X | X | No | N/A | Optional |
# | cause | X | X | No | N/A | Optional |
type Token struct {
# Issuer DID (sender)
issuer DID (rename "iss")
# Audience DID (receiver)
audience optional DID (rename "aud")
# Principal that the chain is about (the Subject)
subject nullable DID (rename "sub")
# The Command to eventually invoke
command Command (rename "cmd")
# The delegation policy
# It doesn't seem possible to represent it with a schema.
policy optional Any (rename "pol")
# The invocation's arguments
args optional {String: Any}
# Delegations that prove the chain of authority
prf optional [CID]
# A unique, random nonce
nonce optional Bytes
# Arbitrary Metadata
meta optional {String : Any}
# "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer
notBefore optional Int (rename "nbf")
# The timestamp at which the delegation becomes invalid
expiration nullable Int (rename "exp")
# The timestamp at which the invocation was created
issuedAt optional Int
# An optional CID of the receipt that enqueued this invocation
cause optional CID
}

View File

@@ -0,0 +1,31 @@
// Code generated by internal/cmd/token DO NOT EDIT.
package token
import "github.com/ipld/go-ipld-prime/datamodel"
type Map struct {
Keys []string
Values map[string]datamodel.Node
}
type List []datamodel.Node
type Map__String__Any struct {
Keys []string
Values map[string]datamodel.Node
}
type List__CID []string
type Token struct {
Issuer string
Audience *string
Subject *string
Command string
Policy *datamodel.Node
Args *Map__String__Any
Prf *List__CID
Nonce *[]uint8
Meta *Map__String__Any
NotBefore *int
Expiration *int
IssuedAt *int
Cause *string
}

View File

@@ -0,0 +1,26 @@
package token_test
import (
"testing"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagjson"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/internal/token"
)
func TestEncode(t *testing.T) {
t.Parallel()
tkn, err := token.New()
require.NoError(t, err)
node := tkn.Wrap()
json, err := ipld.Encode(node, dagjson.Encode)
require.NoError(t, err)
t.Log(string(json))
t.Fail()
}