diff --git a/internal/token/converter.go b/internal/token/converter.go deleted file mode 100644 index 6c264a1..0000000 --- a/internal/token/converter.go +++ /dev/null @@ -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 - }, - ) -} diff --git a/internal/token/converter_test.go b/internal/token/converter_test.go deleted file mode 100644 index 941b99c..0000000 --- a/internal/token/converter_test.go +++ /dev/null @@ -1 +0,0 @@ -package token_test diff --git a/internal/token/errors.go b/internal/token/errors.go new file mode 100644 index 0000000..4dfbcb2 --- /dev/null +++ b/internal/token/errors.go @@ -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") diff --git a/internal/token/schema.go b/internal/token/schema.go new file mode 100644 index 0000000..bdfb165 --- /dev/null +++ b/internal/token/schema.go @@ -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()) +} diff --git a/internal/token/schema_test.go b/internal/token/schema_test.go new file mode 100644 index 0000000..06b13e7 --- /dev/null +++ b/internal/token/schema_test.go @@ -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) + } +} diff --git a/internal/token/token.go b/internal/token/token.go new file mode 100644 index 0000000..e11cd01 --- /dev/null +++ b/internal/token/token.go @@ -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()) +} diff --git a/internal/token/token.ipldsch b/internal/token/token.ipldsch new file mode 100644 index 0000000..3f919ce --- /dev/null +++ b/internal/token/token.ipldsch @@ -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 +} \ No newline at end of file diff --git a/internal/token/token_gen.go b/internal/token/token_gen.go new file mode 100644 index 0000000..c392705 --- /dev/null +++ b/internal/token/token_gen.go @@ -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 +} diff --git a/internal/token/token_test.go b/internal/token/token_test.go new file mode 100644 index 0000000..d0c3609 --- /dev/null +++ b/internal/token/token_test.go @@ -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() +}