diff --git a/handlers/discovery/discovery.go b/handlers/discovery/discovery.go new file mode 100644 index 0000000..9f90e3d --- /dev/null +++ b/handlers/discovery/discovery.go @@ -0,0 +1,93 @@ +package discovery + +import ( + "context" + "regexp" + "strings" + + "github.com/google/uuid" + "github.com/iden3/iden3comm/v2/packers" + "github.com/iden3/iden3comm/v2/protocol" +) + +// Featurer interface for feature handlers +// # Experimental +type Featurer interface { + Handle(ctx context.Context) []protocol.DiscoverFeatureDisclosure +} + +// Discovery handler +// # Experimental +type Discovery struct { + features map[protocol.DiscoveryProtocolFeatureType]Featurer +} + +// New creates a new Discovery handler +// # Experimental +func New(features map[protocol.DiscoveryProtocolFeatureType]Featurer) *Discovery { + return &Discovery{ + features: features, + } +} + +// Handle processes a DiscoverFeatureQueriesMessage and returns a DiscoverFeatureDiscloseMessage +// # Experimental +func (d *Discovery) Handle(ctx context.Context, + discoverInputMessage protocol.DiscoverFeatureQueriesMessage) (protocol.DiscoverFeatureDiscloseMessage, error) { + queries := discoverInputMessage.Body.Queries + + var ( + disclosures []protocol.DiscoverFeatureDisclosure + err error + ) + + for _, query := range queries { + var disclosuresToAppend []protocol.DiscoverFeatureDisclosure + if featurer, ok := d.features[query.FeatureType]; ok { + disclosuresToAppend = featurer.Handle(ctx) + } + + disclosuresToAppend, err = d.handleMatch(disclosuresToAppend, query.Match) + if err != nil { + return protocol.DiscoverFeatureDiscloseMessage{}, err + } + disclosures = append(disclosures, disclosuresToAppend...) + } + + return protocol.DiscoverFeatureDiscloseMessage{ + ID: uuid.NewString(), + Typ: packers.MediaTypePlainMessage, + Type: protocol.DiscoverFeatureDiscloseMessageType, + ThreadID: discoverInputMessage.ThreadID, + Body: protocol.DiscoverFeatureDiscloseMessageBody{ + Disclosures: disclosures, + }, + From: discoverInputMessage.To, + To: discoverInputMessage.From, + }, nil +} + +func (d *Discovery) handleMatch(disclosures []protocol.DiscoverFeatureDisclosure, match string) ([]protocol.DiscoverFeatureDisclosure, error) { + if match == "" || match == "*" { + return disclosures, nil + } + + regExp, err := wildcardToRegExp(match) + if err != nil { + return nil, err + } + var filtered []protocol.DiscoverFeatureDisclosure + for _, disclosure := range disclosures { + if regExp.MatchString(disclosure.ID) { + filtered = append(filtered, disclosure) + } + } + return filtered, nil +} + +func wildcardToRegExp(match string) (*regexp.Regexp, error) { + // Escape special regex characters and replace `*` with `.*` + regexPattern := regexp.QuoteMeta(match) + regexPattern = strings.ReplaceAll(regexPattern, "\\*", ".*") + return regexp.Compile("^" + regexPattern + "$") +} diff --git a/handlers/discovery/discovery_test.go b/handlers/discovery/discovery_test.go new file mode 100644 index 0000000..54fafa9 --- /dev/null +++ b/handlers/discovery/discovery_test.go @@ -0,0 +1,316 @@ +package discovery_test + +import ( + "context" + "testing" + + "github.com/iden3/go-jwz/v2" + "github.com/iden3/iden3comm/v2" + "github.com/iden3/iden3comm/v2/handlers/discovery" + "github.com/iden3/iden3comm/v2/handlers/discovery/features" + "github.com/iden3/iden3comm/v2/packers" + "github.com/iden3/iden3comm/v2/protocol" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type want struct { + FeatureType protocol.DiscoveryProtocolFeatureType + ParsedFeatures discovery.Feature + ID string +} + +func TestDiscovery_Handle(t *testing.T) { + tests := []struct { + name string + discoveryFactory func(t *testing.T) *discovery.Discovery + discoverInputMessage protocol.DiscoverFeatureQueriesMessage + want []want + }{ + { + name: "Support only zkp packer with authV3 groth16 and authV3-8-32 groth16", + discoveryFactory: func(t *testing.T) *discovery.Discovery { + zkpPacker := packers.NewZKPPacker( + map[jwz.ProvingMethodAlg]packers.ProvingParams{}, + map[jwz.ProvingMethodAlg]packers.VerificationParams{ + jwz.AuthV3Groth16Alg: {}, + jwz.AuthV3_8_32Groth16Alg: {}, + }, + ) + + pm := iden3comm.NewPackageManager() + err := pm.RegisterPackers(zkpPacker) + require.NoError(t, err) + + return discovery.New(map[protocol.DiscoveryProtocolFeatureType]discovery.Featurer{ + protocol.DiscoveryProtocolFeatureTypeAccept: features.NewAcceptFeaturer(pm), + protocol.DiscoveryProtocolFeatureTypeProtocol: features.NewProtocolFeaturer([]iden3comm.ProtocolMessage{}), + protocol.DiscoveryProtocolFeatureTypeGoalCode: features.NewGoalCodeFeaturer(), + protocol.DiscoveryProtocolFeatureTypeHeader: features.NewHeaderFeaturer(), + }) + }, + discoverInputMessage: newDiscoverFeatureQueriesMessage([]protocol.DiscoverFeatureQuery{ + { + FeatureType: protocol.DiscoveryProtocolFeatureTypeAccept, + }, + }), + want: []want{ + { + FeatureType: protocol.DiscoveryProtocolFeatureTypeAccept, + ParsedFeatures: discovery.Feature{ + Version: "iden3comm/v1", + Env: "application/iden3-zkp-json", + Algs: []string{"groth16"}, + CircuitIds: []string{ + "authV3", + "authV3-8-32", + }, + }, + }, + }, + }, + { + name: "Support zkp, plain text packers with authV3 groth16 and authV3-8-32 groth16", + discoveryFactory: func(t *testing.T) *discovery.Discovery { + zkpPacker := packers.NewZKPPacker( + map[jwz.ProvingMethodAlg]packers.ProvingParams{}, + map[jwz.ProvingMethodAlg]packers.VerificationParams{ + jwz.AuthV3Groth16Alg: {}, + jwz.AuthV3_8_32Groth16Alg: {}, + }, + ) + + pm := iden3comm.NewPackageManager() + err := pm.RegisterPackers(zkpPacker, &packers.PlainMessagePacker{}) + require.NoError(t, err) + + return discovery.New(map[protocol.DiscoveryProtocolFeatureType]discovery.Featurer{ + protocol.DiscoveryProtocolFeatureTypeAccept: features.NewAcceptFeaturer(pm), + protocol.DiscoveryProtocolFeatureTypeProtocol: features.NewProtocolFeaturer([]iden3comm.ProtocolMessage{}), + protocol.DiscoveryProtocolFeatureTypeGoalCode: features.NewGoalCodeFeaturer(), + protocol.DiscoveryProtocolFeatureTypeHeader: features.NewHeaderFeaturer(), + }) + }, + discoverInputMessage: newDiscoverFeatureQueriesMessage([]protocol.DiscoverFeatureQuery{ + { + FeatureType: protocol.DiscoveryProtocolFeatureTypeAccept, + }, + }), + want: []want{ + { + FeatureType: protocol.DiscoveryProtocolFeatureTypeAccept, + ParsedFeatures: discovery.Feature{ + Version: "iden3comm/v1", + Env: "application/iden3-zkp-json", + Algs: []string{"groth16"}, + CircuitIds: []string{ + "authV3", + "authV3-8-32", + }, + }, + }, + { + FeatureType: protocol.DiscoveryProtocolFeatureTypeAccept, + ParsedFeatures: discovery.Feature{ + Version: "iden3comm/v1", + Env: "application/iden3comm-plain-json", + }, + }, + }, + }, + { + name: "Support message type: credential proposal request, credential fetch", + discoveryFactory: func(t *testing.T) *discovery.Discovery { + pm := iden3comm.NewPackageManager() + err := pm.RegisterPackers(&packers.PlainMessagePacker{}) + require.NoError(t, err) + + return discovery.New(map[protocol.DiscoveryProtocolFeatureType]discovery.Featurer{ + protocol.DiscoveryProtocolFeatureTypeAccept: features.NewAcceptFeaturer(pm), + protocol.DiscoveryProtocolFeatureTypeProtocol: features.NewProtocolFeaturer([]iden3comm.ProtocolMessage{ + protocol.CredentialProposalRequestMessageType, + protocol.CredentialFetchRequestMessageType, + }), + protocol.DiscoveryProtocolFeatureTypeGoalCode: features.NewGoalCodeFeaturer(), + protocol.DiscoveryProtocolFeatureTypeHeader: features.NewHeaderFeaturer(), + }) + }, + discoverInputMessage: newDiscoverFeatureQueriesMessage([]protocol.DiscoverFeatureQuery{ + { + FeatureType: protocol.DiscoveryProtocolFeatureTypeProtocol, + }, + }), + want: []want{ + { + FeatureType: protocol.DiscoveryProtocolFeatureTypeProtocol, + ID: string(protocol.CredentialProposalRequestMessageType), + }, + { + FeatureType: protocol.DiscoveryProtocolFeatureTypeProtocol, + ID: string(protocol.CredentialFetchRequestMessageType), + }, + }, + }, + { + name: "Support feature and protocol requests", + discoveryFactory: func(t *testing.T) *discovery.Discovery { + pm := iden3comm.NewPackageManager() + err := pm.RegisterPackers(&packers.PlainMessagePacker{}) + require.NoError(t, err) + + return discovery.New(map[protocol.DiscoveryProtocolFeatureType]discovery.Featurer{ + protocol.DiscoveryProtocolFeatureTypeAccept: features.NewAcceptFeaturer(pm), + protocol.DiscoveryProtocolFeatureTypeProtocol: features.NewProtocolFeaturer([]iden3comm.ProtocolMessage{ + protocol.CredentialProposalRequestMessageType, + protocol.CredentialFetchRequestMessageType, + }), + protocol.DiscoveryProtocolFeatureTypeGoalCode: features.NewGoalCodeFeaturer(), + protocol.DiscoveryProtocolFeatureTypeHeader: features.NewHeaderFeaturer(), + }) + }, + discoverInputMessage: newDiscoverFeatureQueriesMessage([]protocol.DiscoverFeatureQuery{ + { + FeatureType: protocol.DiscoveryProtocolFeatureTypeProtocol, + }, + { + FeatureType: protocol.DiscoveryProtocolFeatureTypeAccept, + }, + }), + want: []want{ + { + FeatureType: protocol.DiscoveryProtocolFeatureTypeProtocol, + ID: string(protocol.CredentialProposalRequestMessageType), + }, + { + FeatureType: protocol.DiscoveryProtocolFeatureTypeProtocol, + ID: string(protocol.CredentialFetchRequestMessageType), + }, + { + FeatureType: protocol.DiscoveryProtocolFeatureTypeAccept, + ParsedFeatures: discovery.Feature{ + Version: "iden3comm/v1", + Env: "application/iden3comm-plain-json", + }, + }, + }, + }, + { + name: "With match wildcard. Filter only application/iden3comm-plain-json packers", + discoveryFactory: func(t *testing.T) *discovery.Discovery { + zkpPacker := packers.NewZKPPacker( + map[jwz.ProvingMethodAlg]packers.ProvingParams{}, + map[jwz.ProvingMethodAlg]packers.VerificationParams{ + jwz.AuthV3Groth16Alg: {}, + jwz.AuthV3_8_32Groth16Alg: {}, + }, + ) + + pm := iden3comm.NewPackageManager() + err := pm.RegisterPackers(zkpPacker, &packers.PlainMessagePacker{}) + require.NoError(t, err) + + return discovery.New(map[protocol.DiscoveryProtocolFeatureType]discovery.Featurer{ + protocol.DiscoveryProtocolFeatureTypeAccept: features.NewAcceptFeaturer(pm), + protocol.DiscoveryProtocolFeatureTypeProtocol: features.NewProtocolFeaturer([]iden3comm.ProtocolMessage{}), + protocol.DiscoveryProtocolFeatureTypeGoalCode: features.NewGoalCodeFeaturer(), + protocol.DiscoveryProtocolFeatureTypeHeader: features.NewHeaderFeaturer(), + }) + }, + discoverInputMessage: newDiscoverFeatureQueriesMessage([]protocol.DiscoverFeatureQuery{ + { + FeatureType: protocol.DiscoveryProtocolFeatureTypeAccept, + Match: "*iden3comm-plain-json", + }, + }), + want: []want{ + { + FeatureType: protocol.DiscoveryProtocolFeatureTypeAccept, + ParsedFeatures: discovery.Feature{ + Version: "iden3comm/v1", + Env: "application/iden3comm-plain-json", + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := tt.discoveryFactory(t) + actual, err := d.Handle( + context.Background(), + tt.discoverInputMessage, + ) + testActualID(t, actual.Body.Disclosures, tt.want) + require.NoError(t, err) + + }) + } +} + +func testActualID(t *testing.T, actual []protocol.DiscoverFeatureDisclosure, want []want) { + require.Equal(t, len(actual), len(want)) + // try to find actual in want + for _, a := range actual { + found := false + for i, w := range want { + if a.FeatureType != w.FeatureType { + continue + } + + if a.FeatureType == protocol.DiscoveryProtocolFeatureTypeAccept { + isEqual := isEqualAcceptFeature(a, w.ParsedFeatures) + if isEqual { + // nullify to avoid duplicate matching + want = nullify(want, i) + found = true + break + } + } + // for another types, just match by FeatureType and ID + if a.ID == w.ID { + // nullify to avoid duplicate matching + want = nullify(want, i) + found = true + break + } + } + require.True(t, found, "actual ID not found in want: %v", a) + } +} + +type mockT struct{} + +func (m *mockT) Errorf(format string, args ...interface{}) {} + +func isEqualAcceptFeature(actual protocol.DiscoverFeatureDisclosure, want discovery.Feature) bool { + parsed := discovery.ParseFeature(actual.ID) + + if want.Version != parsed.Version { + return false + } + if want.Env != parsed.Env { + return false + } + + t := &mockT{} + return assert.ElementsMatch(t, want.Algs, parsed.Algs) && + assert.ElementsMatch(t, want.CircuitIds, parsed.CircuitIds) +} + +func nullify(w []want, index int) []want { + w[index] = want{} + return w +} + +func newDiscoverFeatureQueriesMessage(queries []protocol.DiscoverFeatureQuery) protocol.DiscoverFeatureQueriesMessage { + return protocol.DiscoverFeatureQueriesMessage{ + ID: "c0fc0f29-4f34-4bea-851b-58b7639fe29c", + ThreadID: "becfc675-b15d-4817-98a5-7fce0240a48a", + Typ: packers.MediaTypePlainMessage, + Type: protocol.DiscoverFeatureQueriesMessageType, + Body: protocol.DiscoverFeatureQueriesMessageBody{ + Queries: queries, + }, + } +} diff --git a/handlers/discovery/features/accept.go b/handlers/discovery/features/accept.go new file mode 100644 index 0000000..64e41d0 --- /dev/null +++ b/handlers/discovery/features/accept.go @@ -0,0 +1,34 @@ +package features + +import ( + "context" + + "github.com/iden3/iden3comm/v2" + "github.com/iden3/iden3comm/v2/protocol" +) + +// AcceptFeaturer implementation +type AcceptFeaturer struct { + packageManager *iden3comm.PackageManager +} + +// NewAcceptFeaturer constructor +func NewAcceptFeaturer(packageManager *iden3comm.PackageManager) *AcceptFeaturer { + return &AcceptFeaturer{ + packageManager: packageManager, + } +} + +// Handle implementation for AcceptFeaturer +func (a *AcceptFeaturer) Handle(ctx context.Context) []protocol.DiscoverFeatureDisclosure { + disclosures := []protocol.DiscoverFeatureDisclosure{} + + profiles := a.packageManager.GetSupportedProfiles() + for _, profile := range profiles { + disclosures = append(disclosures, protocol.DiscoverFeatureDisclosure{ + FeatureType: protocol.DiscoveryProtocolFeatureTypeAccept, + ID: profile, + }) + } + return disclosures +} diff --git a/handlers/discovery/features/goalcode.go b/handlers/discovery/features/goalcode.go new file mode 100644 index 0000000..5a2775e --- /dev/null +++ b/handlers/discovery/features/goalcode.go @@ -0,0 +1,50 @@ +package features + +import ( + "context" + + "github.com/iden3/iden3comm/v2/protocol" +) + +// GoalCodeFeaturer implementation +// # Experimental +type GoalCodeFeaturer struct { + goalCodes []string +} + +// GoalCodeOption configures a GoalCodeFeaturer +// # Experimental +type GoalCodeOption func(*GoalCodeFeaturer) + +// WithGoalCodes sets custom goal codes for GoalCodeFeaturer +// # Experimental +func WithGoalCodes(goalCodes ...string) GoalCodeOption { + return func(g *GoalCodeFeaturer) { + g.goalCodes = goalCodes + } +} + +// NewGoalCodeFeaturer constructor +// # Experimental +func NewGoalCodeFeaturer(opts ...GoalCodeOption) *GoalCodeFeaturer { + g := &GoalCodeFeaturer{} + for _, opt := range opts { + opt(g) + } + return g +} + +// Handle implementation for GoalCodeFeaturer +// # Experimental +func (g *GoalCodeFeaturer) Handle(ctx context.Context) []protocol.DiscoverFeatureDisclosure { + disclosures := make([]protocol.DiscoverFeatureDisclosure, 0, len(g.goalCodes)) + + for _, goalCode := range g.goalCodes { + disclosures = append(disclosures, protocol.DiscoverFeatureDisclosure{ + FeatureType: protocol.DiscoveryProtocolFeatureTypeGoalCode, + ID: goalCode, + }) + } + + return disclosures +} diff --git a/handlers/discovery/features/goalcode_test.go b/handlers/discovery/features/goalcode_test.go new file mode 100644 index 0000000..867325a --- /dev/null +++ b/handlers/discovery/features/goalcode_test.go @@ -0,0 +1,54 @@ +package features_test + +import ( + "context" + "testing" + + "github.com/iden3/iden3comm/v2/handlers/discovery/features" + "github.com/iden3/iden3comm/v2/protocol" + "github.com/stretchr/testify/require" +) + +func TestGoalCodeFeaturer_Handle(t *testing.T) { + tests := []struct { + name string + supportedGoalCodes []string + expectedDisclosures []protocol.DiscoverFeatureDisclosure + }{ + { + name: "should return default disclosures", + supportedGoalCodes: nil, + expectedDisclosures: []protocol.DiscoverFeatureDisclosure{}, + }, + { + name: "should return two goal codes", + supportedGoalCodes: []string{ + "goal-1", + "goal-2", + }, + expectedDisclosures: []protocol.DiscoverFeatureDisclosure{ + {FeatureType: protocol.DiscoveryProtocolFeatureTypeGoalCode, ID: "goal-1"}, + {FeatureType: protocol.DiscoveryProtocolFeatureTypeGoalCode, ID: "goal-2"}, + }, + }, + { + name: "should return empty disclosures", + supportedGoalCodes: []string{}, + expectedDisclosures: []protocol.DiscoverFeatureDisclosure{}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var g *features.GoalCodeFeaturer + if tc.supportedGoalCodes == nil { + g = features.NewGoalCodeFeaturer() + } else { + g = features.NewGoalCodeFeaturer(features.WithGoalCodes(tc.supportedGoalCodes...)) + } + + disclosures := g.Handle(context.Background()) + require.ElementsMatch(t, tc.expectedDisclosures, disclosures) + }) + } +} diff --git a/handlers/discovery/features/header.go b/handlers/discovery/features/header.go new file mode 100644 index 0000000..f5711ab --- /dev/null +++ b/handlers/discovery/features/header.go @@ -0,0 +1,64 @@ +package features + +import ( + "context" + + "github.com/iden3/iden3comm/v2/protocol" +) + +// HeaderFeaturer implementation +// # Experimental +type HeaderFeaturer struct { + headers []string +} + +// HeaderOption configures a HeaderFeaturer +// # Experimental +type HeaderOption func(*HeaderFeaturer) + +// WithHeaders sets custom headers for HeaderFeaturer +// # Experimental +func WithHeaders(headers ...string) HeaderOption { + return func(h *HeaderFeaturer) { + h.headers = headers + } +} + +// NewHeaderFeaturer constructor accepts functional options. +// If no headers option is provided, it falls back to the defaultHeaders. +// # Experimental +func NewHeaderFeaturer(opts ...HeaderOption) *HeaderFeaturer { + h := &HeaderFeaturer{ + headers: []string{ + "id", + "typ", + "type", + "thid", + "body", + "from", + "to", + "created_time", + "expires_time", + "attachments", + }, + } + for _, opt := range opts { + opt(h) + } + return h +} + +// Handle implementation for HeaderFeaturer +// # Experimental +func (h *HeaderFeaturer) Handle(ctx context.Context) []protocol.DiscoverFeatureDisclosure { + disclosures := make([]protocol.DiscoverFeatureDisclosure, 0, len(h.headers)) + + for _, header := range h.headers { + disclosures = append(disclosures, protocol.DiscoverFeatureDisclosure{ + FeatureType: protocol.DiscoveryProtocolFeatureTypeHeader, + ID: header, + }) + } + + return disclosures +} diff --git a/handlers/discovery/features/header_test.go b/handlers/discovery/features/header_test.go new file mode 100644 index 0000000..0f8de18 --- /dev/null +++ b/handlers/discovery/features/header_test.go @@ -0,0 +1,65 @@ +package features_test + +import ( + "context" + "testing" + + "github.com/iden3/iden3comm/v2/handlers/discovery/features" + "github.com/iden3/iden3comm/v2/protocol" + "github.com/stretchr/testify/require" +) + +func TestHeaderFeaturer_Handle(t *testing.T) { + tests := []struct { + name string + supportedHeaders []string + expectedDisclosures []protocol.DiscoverFeatureDisclosure + }{ + { + name: "should return default disclosures", + supportedHeaders: nil, + expectedDisclosures: []protocol.DiscoverFeatureDisclosure{ + {FeatureType: protocol.DiscoveryProtocolFeatureTypeHeader, ID: "id"}, + {FeatureType: protocol.DiscoveryProtocolFeatureTypeHeader, ID: "typ"}, + {FeatureType: protocol.DiscoveryProtocolFeatureTypeHeader, ID: "type"}, + {FeatureType: protocol.DiscoveryProtocolFeatureTypeHeader, ID: "thid"}, + {FeatureType: protocol.DiscoveryProtocolFeatureTypeHeader, ID: "body"}, + {FeatureType: protocol.DiscoveryProtocolFeatureTypeHeader, ID: "from"}, + {FeatureType: protocol.DiscoveryProtocolFeatureTypeHeader, ID: "to"}, + {FeatureType: protocol.DiscoveryProtocolFeatureTypeHeader, ID: "created_time"}, + {FeatureType: protocol.DiscoveryProtocolFeatureTypeHeader, ID: "expires_time"}, + {FeatureType: protocol.DiscoveryProtocolFeatureTypeHeader, ID: "attachments"}, + }, + }, + { + name: "should return id typ only", + supportedHeaders: []string{ + "id", + "typ", + }, + expectedDisclosures: []protocol.DiscoverFeatureDisclosure{ + {FeatureType: protocol.DiscoveryProtocolFeatureTypeHeader, ID: "id"}, + {FeatureType: protocol.DiscoveryProtocolFeatureTypeHeader, ID: "typ"}, + }, + }, + { + name: "should return empty disclosures", + supportedHeaders: []string{}, + expectedDisclosures: []protocol.DiscoverFeatureDisclosure{}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var headerFeaturer *features.HeaderFeaturer + if tc.supportedHeaders == nil { + headerFeaturer = features.NewHeaderFeaturer() + } else { + headerFeaturer = features.NewHeaderFeaturer(features.WithHeaders(tc.supportedHeaders...)) + } + + disclosures := headerFeaturer.Handle(context.Background()) + require.ElementsMatch(t, tc.expectedDisclosures, disclosures) + }) + } +} diff --git a/handlers/discovery/features/protocol.go b/handlers/discovery/features/protocol.go new file mode 100644 index 0000000..6d024ca --- /dev/null +++ b/handlers/discovery/features/protocol.go @@ -0,0 +1,35 @@ +package features + +import ( + "context" + + "github.com/iden3/iden3comm/v2" + "github.com/iden3/iden3comm/v2/protocol" +) + +// ProtocolFeaturer implementation +// # Experimental +type ProtocolFeaturer struct { + supportedProtocols []iden3comm.ProtocolMessage +} + +// NewProtocolFeaturer constructor +// # Experimental +func NewProtocolFeaturer(supportedProtocols []iden3comm.ProtocolMessage) *ProtocolFeaturer { + return &ProtocolFeaturer{ + supportedProtocols: supportedProtocols, + } +} + +// Handle implementation for ProtocolFeaturer +// # Experimental +func (p *ProtocolFeaturer) Handle(ctx context.Context) []protocol.DiscoverFeatureDisclosure { + disclosures := []protocol.DiscoverFeatureDisclosure{} + for _, protocolMessage := range p.supportedProtocols { + disclosures = append(disclosures, protocol.DiscoverFeatureDisclosure{ + FeatureType: protocol.DiscoveryProtocolFeatureTypeProtocol, + ID: string(protocolMessage), + }) + } + return disclosures +} diff --git a/handlers/discovery/utils.go b/handlers/discovery/utils.go new file mode 100644 index 0000000..db82920 --- /dev/null +++ b/handlers/discovery/utils.go @@ -0,0 +1,48 @@ +package discovery + +import "strings" + +// Feature represents a parsed feature with its components +// # Experimental +type Feature struct { + Version string + Env string + Algs []string + CircuitIds []string +} + +// ParseFeature parses a feature ID string into a Feature struct +// # Experimental +func ParseFeature(id string) Feature { + id = strings.TrimSpace(id) + if id == "" { + return Feature{} + } + parts := strings.Split(id, ";") + + f := Feature{ + Version: parts[0], + } + + for _, part := range parts[1:] { + kv := strings.SplitN(part, "=", 2) + if len(kv) != 2 { + continue + } + key := strings.TrimSpace(kv[0]) + value := strings.TrimSpace(kv[1]) + if value == "" { + continue + } + + switch key { + case "env": + f.Env = value + case "alg": + f.Algs = strings.Split(value, ",") + case "circuitIds": + f.CircuitIds = strings.Split(value, ",") + } + } + return f +} diff --git a/handlers/discovery/utils_test.go b/handlers/discovery/utils_test.go new file mode 100644 index 0000000..897ad83 --- /dev/null +++ b/handlers/discovery/utils_test.go @@ -0,0 +1,49 @@ +package discovery_test + +import ( + "testing" + + "github.com/iden3/iden3comm/v2/handlers/discovery" + "github.com/stretchr/testify/require" +) + +func TestParseFeature(t *testing.T) { + tests := []struct { + name string + id string + want discovery.Feature + }{ + { + name: "full feature", + id: "iden3comm/v1;env=application/iden3-zkp-json;alg=groth16;circuitIds=authV3,authV3-8-32", + want: discovery.Feature{ + Version: "iden3comm/v1", + Env: "application/iden3-zkp-json", + Algs: []string{"groth16"}, + CircuitIds: []string{"authV3", "authV3-8-32"}, + }, + }, + { + name: "multiple algs", + id: "iden3comm/v1;alg=alg1,alg2", + want: discovery.Feature{ + Version: "iden3comm/v1", + Algs: []string{"alg1", "alg2"}, + }, + }, + { + name: "empty alg list", + id: "iden3comm/v1;alg=", + want: discovery.Feature{ + Version: "iden3comm/v1", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := discovery.ParseFeature(tt.id) + require.Equal(t, tt.want, got) + }) + } +}