diff --git a/pkg/plugin/implementation/dediregistry/README.md b/pkg/plugin/implementation/dediregistry/README.md index bf6c65c..445b0b4 100644 --- a/pkg/plugin/implementation/dediregistry/README.md +++ b/pkg/plugin/implementation/dediregistry/README.md @@ -1,32 +1,22 @@ # DeDi Registry Plugin -A Beckn-ONIX registry type plugin for integrating with DeDi registry services. Implements the `RegistryLookup` interface to provide participant information and public keys. +A **registry type plugin** for Beckn-ONIX that integrates with DeDi (Decentralized Digital Infrastructure) registry services. ## Overview -The DeDi Registry plugin enables Beckn-ONIX to lookup DeDi registries for participant records, converting DeDi API responses to standard Beckn Subscription format for seamless integration with existing registry infrastructure. - -## Features - -- **RegistryLookup Interface**: Implements standard Beckn registry interface -- **DeDi API Integration**: GET requests to DeDi registry endpoints with Bearer authentication -- **Data Conversion**: Converts DeDi responses to Beckn Subscription format -- **HTTP Retry Logic**: Built-in retry mechanism using retryablehttp client -- **Timeout Control**: Configurable request timeouts - +The DeDi Registry plugin is a **registry implementation** that enables Beckn-ONIX to lookup participant records from remote DeDi registries via REST API calls. ## Configuration ```yaml plugins: - dediRegistry: + registry: id: dediregistry config: - baseURL: "https://dedi-registry.example.com" - apiKey: "your-api-key" - namespaceID: "beckn-network" - registryName: "participants" - recordName: "participant-id" + baseURL: "https://dedi-api.example.com" + apiKey: "your-bearer-token" + namespaceID: "76EU8BF1gzRGGatgw7wZZb7nEVx77XSwkKDv4UDLdxh8ztty4zmbYU" + registryName: "dedi_registry" timeout: "30" # seconds ``` @@ -35,10 +25,9 @@ plugins: | Parameter | Required | Description | Default | |-----------|----------|-------------|---------| | `baseURL` | Yes | DeDi registry API base URL | - | -| `apiKey` | Yes | API key for authentication | - | +| `apiKey` | Yes | Bearer token for API authentication | - | | `namespaceID` | Yes | DeDi namespace identifier | - | | `registryName` | Yes | Registry name to query | - | -| `recordName` | Yes | Record name/identifier | - | | `timeout` | No | Request timeout in seconds | 30 | ## Usage @@ -50,85 +39,108 @@ modules: - name: bapTxnReceiver handler: plugins: - dediRegistry: + registry: id: dediregistry config: baseURL: "https://dedi-registry.example.com" apiKey: "your-api-key" namespaceID: "beckn-network" registryName: "participants" - recordName: "participant-id" ``` ### In Code ```go -// Load DeDi registry plugin +// Load DeDi registry plugin (same as any registry plugin) dediRegistry, err := manager.Registry(ctx, &plugin.Config{ - ID: "dediregistry", + ID: "dediregistry", // Plugin ID specifies DeDi implementation Config: map[string]string{ "baseURL": "https://dedi-registry.example.com", "apiKey": "your-api-key", "namespaceID": "beckn-network", "registryName": "participants", - "recordName": "participant-id", }, }) -// Or use specific method -dediRegistry, err := manager.DeDiRegistry(ctx, config) - -// Lookup participant (returns Beckn Subscription format) -subscription := &model.Subscription{} +// Lookup participant with dynamic subscriber ID (from request context) +subscription := &model.Subscription{ + Subscriber: model.Subscriber{ + SubscriberID: "bap-network", // Extracted from Authorization header or request body + }, +} results, err := dediRegistry.Lookup(ctx, subscription) if err != nil { return err } -// Extract public key from first result +// Extract public key from result (standard Beckn format) if len(results) > 0 { publicKey := results[0].SigningPublicKey subscriberID := results[0].SubscriberID } ``` -## API Response Structure +## API Integration -The plugin expects DeDi registry responses in this format: +### DeDi API URL Pattern +``` +{baseURL}/dedi/lookup/{namespaceID}/{registryName}/{subscriberID} +``` + +**Example**: `https://dedi-api.com/dedi/lookup/76EU8BF1gzRGGatgw7wZZb7nEVx77XSwkKDv4UDLdxh8ztty4zmbYU/dedi_registry/bap-network` + +### Authentication +``` +Authorization: Bearer {apiKey} +``` + +### Expected DeDi Response Format ```json { - "message": "success", + "message": "Resource retrieved successfully", "data": { - "namespace": "beckn", - "schema": { - "entity_name": "participant.example.com", - "entity_url": "https://participant.example.com", - "publicKey": "base64-encoded-public-key", - "keyType": "ed25519", - "keyFormat": "base64" + "namespace": "dediregistry", + "namespace_id": "76EU8BF1gzRGGatgw7wZZb7nEVx77XSwkKDv4UDLdxh8ztty4zmbYU", + "registry_name": "dedi_registry", + "record_name": "bap-network", + "details": { + "key_id": "b692d295-5425-40f5-af77-d62646841dca", + "signing_public_key": "YK3Xqc83Bpobc1UT0ObAe6mBJMiAOkceIsNtmph9WTc=", + "encr_public_key": "YK3Xqc83Bpobc1UT0ObAe6mBJMiAOkceIsNtmph9WTc=", + "status": "SUBSCRIBED", + "created": "2024-01-15T10:00:00Z", + "updated": "2024-01-15T10:00:00Z", + "valid_from": "2024-01-01T00:00:00Z", + "valid_until": "2025-12-31T23:59:59Z" }, - "state": "active", - "created_at": "2023-01-01T00:00:00Z", - "updated_at": "2023-01-01T00:00:00Z" + "state": "live", + "created_at": "2025-10-09T06:09:48.295Z" } } ``` -### Converted to Beckn Format +### Field Mapping to Beckn Subscription -The plugin converts this to standard Beckn Subscription format: +| DeDi Field | Beckn Field | Description | +|------------|-------------|-------------| +| `data.record_name` | `subscriber_id` | Participant identifier | +| `data.details.key_id` | `key_id` | Unique key identifier | +| `data.details.signing_public_key` | `signing_public_key` | Public key for signature verification | +| `data.details.encr_public_key` | `encr_public_key` | Public key for encryption | +| `data.details.status` | `status` | Subscription status | +| `data.details.created` | `created` | Creation timestamp | +| `data.details.updated` | `updated` | Last update timestamp | +| `data.details.valid_from` | `valid_from` | Key validity start | +| `data.details.valid_until` | `valid_until` | Key validity end | -```json -{ - "subscriber_id": "participant.example.com", - "url": "https://participant.example.com", - "signing_public_key": "base64-encoded-public-key", - "status": "active", - "created": "2023-01-01T00:00:00Z", - "updated": "2023-01-01T00:00:00Z" -} -``` +### Lookup Flow + +1. **Request Processing**: Plugin extracts `subscriber_id` from incoming request +2. **API Call**: Makes GET request to DeDi API with static config + dynamic subscriber ID +3. **Response Parsing**: Extracts data from `data.details` object +4. **Format Conversion**: Maps DeDi fields to Beckn Subscription format +5. **Return**: Returns array of Subscription objects ## Testing @@ -138,16 +150,34 @@ Run plugin tests: go test ./pkg/plugin/implementation/dediregistry -v ``` +Test coverage includes: +- Configuration validation +- Successful API responses +- HTTP error handling +- Network failures +- Invalid JSON responses +- Missing required fields + ## Dependencies - `github.com/hashicorp/go-retryablehttp`: HTTP client with retry logic - Standard Go libraries for HTTP and JSON handling -## Integration Notes +## Error Handling -- **Registry Type Plugin**: Implements `RegistryLookup` interface, not a separate plugin category -- **Interchangeable**: Can be used alongside or instead of standard registry plugin -- **Manager Integration**: Available via `manager.Registry()` or `manager.DeDiRegistry()` methods -- **Data Conversion**: Automatically converts DeDi format to Beckn Subscription format -- **Interface Compliance**: Implements `RegistryLookup` interface with `Lookup()` method only -- **Build Integration**: Included in `build-plugins.sh` script, compiles to `dediregistry.so` \ No newline at end of file +- **Configuration Errors**: Missing required config parameters +- **Network Errors**: Connection failures, timeouts +- **HTTP Errors**: Non-200 status codes from DeDi API +- **Data Errors**: Missing required fields in response +- **Validation Errors**: Empty subscriber ID in request + + +### Integration Notes + +- **Plugin Type**: Registry implementation +- **Interface**: Implements `RegistryLookup` interface with `Lookup(ctx, *model.Subscription) ([]model.Subscription, error)` +- **Manager Access**: Available via `manager.Registry()` method (same as standard registry) +- **Dynamic Lookup**: Uses `req.SubscriberID` from request context, not static configuration +- **Data Conversion**: Automatically converts DeDi API format to Beckn Subscription format +- **Build Integration**: Included in `build-plugins.sh`, compiles to `dediregistry.so` +- **Usage Pattern**: Configure with `id: dediregistry` in registry plugin configuration \ No newline at end of file diff --git a/pkg/plugin/implementation/dediregistry/cmd/plugin.go b/pkg/plugin/implementation/dediregistry/cmd/plugin.go index 6d7af3d..ef77f1b 100644 --- a/pkg/plugin/implementation/dediregistry/cmd/plugin.go +++ b/pkg/plugin/implementation/dediregistry/cmd/plugin.go @@ -25,13 +25,14 @@ func (d dediRegistryProvider) New(ctx context.Context, config map[string]string) ApiKey: config["apiKey"], NamespaceID: config["namespaceID"], RegistryName: config["registryName"], - RecordName: config["recordName"], } // Parse timeout if provided if timeoutStr, exists := config["timeout"]; exists && timeoutStr != "" { if timeout, err := strconv.Atoi(timeoutStr); err == nil { dediConfig.Timeout = timeout + } else { + log.Warnf(ctx, "Invalid timeout value '%s', using default", timeoutStr) } } diff --git a/pkg/plugin/implementation/dediregistry/dediregistry.go b/pkg/plugin/implementation/dediregistry/dediregistry.go index 7e5646b..91857b3 100644 --- a/pkg/plugin/implementation/dediregistry/dediregistry.go +++ b/pkg/plugin/implementation/dediregistry/dediregistry.go @@ -19,7 +19,6 @@ type Config struct { ApiKey string `yaml:"apiKey" json:"apiKey"` NamespaceID string `yaml:"namespaceID" json:"namespaceID"` RegistryName string `yaml:"registryName" json:"registryName"` - RecordName string `yaml:"recordName" json:"recordName"` Timeout int `yaml:"timeout" json:"timeout"` } @@ -46,9 +45,7 @@ func validate(cfg *Config) error { if cfg.RegistryName == "" { return fmt.Errorf("registryName cannot be empty") } - if cfg.RecordName == "" { - return fmt.Errorf("recordName cannot be empty") - } + return nil } @@ -87,8 +84,15 @@ func New(ctx context.Context, cfg *Config) (*DeDiRegistryClient, func() error, e // Lookup implements RegistryLookup interface - calls the DeDi lookup endpoint and returns Subscription. func (c *DeDiRegistryClient) Lookup(ctx context.Context, req *model.Subscription) ([]model.Subscription, error) { + // Extract subscriber ID from request + subscriberID := req.SubscriberID + log.Infof(ctx, "DeDI Registry: Looking up subscriber ID: %s", subscriberID) + if subscriberID == "" { + return nil, fmt.Errorf("subscriber_id is required for DeDi lookup") + } + lookupURL := fmt.Sprintf("%s/dedi/lookup/%s/%s/%s", - c.config.BaseURL, c.config.NamespaceID, c.config.RegistryName, c.config.RecordName) + c.config.BaseURL, c.config.NamespaceID, c.config.RegistryName, subscriberID) httpReq, err := retryablehttp.NewRequest("GET", lookupURL, nil) if err != nil { @@ -116,7 +120,7 @@ func (c *DeDiRegistryClient) Lookup(ctx context.Context, req *model.Subscription return nil, fmt.Errorf("failed to read response body: %w", err) } - // Parse response using local variables + // Parse response var responseData map[string]interface{} err = json.Unmarshal(body, &responseData) if err != nil { @@ -125,49 +129,56 @@ func (c *DeDiRegistryClient) Lookup(ctx context.Context, req *model.Subscription log.Debugf(ctx, "DeDi lookup request successful") - // Extract data using local variables + // Extract data field data, ok := responseData["data"].(map[string]interface{}) if !ok { return nil, fmt.Errorf("invalid response format: missing data field") } - schema, ok := data["schema"].(map[string]interface{}) + // Extract details field + details, ok := data["details"].(map[string]interface{}) if !ok { - return nil, fmt.Errorf("invalid response format: missing schema field") + return nil, fmt.Errorf("invalid response format: missing details field") } - // Extract values using type assertions with error checking - entityName, ok := schema["entity_name"].(string) - if !ok || entityName == "" { - return nil, fmt.Errorf("invalid or missing entity_name in response") + // Extract required fields from details + keyID, _ := details["key_id"].(string) + signingPublicKey, ok := details["signing_public_key"].(string) + if !ok || signingPublicKey == "" { + return nil, fmt.Errorf("invalid or missing signing_public_key in response") } - - entityURL, ok := schema["entity_url"].(string) - if !ok || entityURL == "" { - return nil, fmt.Errorf("invalid or missing entity_url in response") + encrPublicKey, _ := details["encr_public_key"].(string) + detailsStatus, ok := details["status"].(string) + if !ok { + return nil, fmt.Errorf("missing status in response") } + detailsCreated, _ := details["created"].(string) + detailsUpdated, _ := details["updated"].(string) + validFromStr, _ := details["valid_from"].(string) + validUntilStr, _ := details["valid_until"].(string) - publicKey, ok := schema["publicKey"].(string) - if !ok || publicKey == "" { - return nil, fmt.Errorf("invalid or missing publicKey in response") + // Extract record_name as subscriber ID + recordName, _ := data["record_name"].(string) + if recordName == "" { + recordName = subscriberID } - state, _ := data["state"].(string) - createdAt, _ := data["created_at"].(string) - updatedAt, _ := data["updated_at"].(string) - // Convert to Subscription format subscription := model.Subscription{ Subscriber: model.Subscriber{ - SubscriberID: entityName, - URL: entityURL, + SubscriberID: recordName, + URL: req.URL, Domain: req.Domain, Type: req.Type, }, - SigningPublicKey: publicKey, - Status: state, - Created: parseTime(createdAt), - Updated: parseTime(updatedAt), + KeyID: keyID, + SigningPublicKey: signingPublicKey, + EncrPublicKey: encrPublicKey, + ValidFrom: parseTime(validFromStr), + ValidUntil: parseTime(validUntilStr), + Status: detailsStatus, + Created: parseTime(detailsCreated), + Updated: parseTime(detailsUpdated), } return []model.Subscription{subscription}, nil diff --git a/pkg/plugin/implementation/dediregistry/dediregistry_test.go b/pkg/plugin/implementation/dediregistry/dediregistry_test.go index d30a2ad..d9b63fd 100644 --- a/pkg/plugin/implementation/dediregistry/dediregistry_test.go +++ b/pkg/plugin/implementation/dediregistry/dediregistry_test.go @@ -28,7 +28,6 @@ func TestValidate(t *testing.T) { ApiKey: "test-key", NamespaceID: "test-namespace", RegistryName: "test-registry", - RecordName: "test-record", }, wantErr: true, }, @@ -39,7 +38,6 @@ func TestValidate(t *testing.T) { ApiKey: "", NamespaceID: "test-namespace", RegistryName: "test-registry", - RecordName: "test-record", }, wantErr: true, }, @@ -50,7 +48,6 @@ func TestValidate(t *testing.T) { ApiKey: "test-key", NamespaceID: "", RegistryName: "test-registry", - RecordName: "test-record", }, wantErr: true, }, @@ -61,18 +58,6 @@ func TestValidate(t *testing.T) { ApiKey: "test-key", NamespaceID: "test-namespace", RegistryName: "", - RecordName: "test-record", - }, - wantErr: true, - }, - { - name: "empty recordName", - config: &Config{ - BaseURL: "https://test.com", - ApiKey: "test-key", - NamespaceID: "test-namespace", - RegistryName: "test-registry", - RecordName: "", }, wantErr: true, }, @@ -83,7 +68,6 @@ func TestValidate(t *testing.T) { ApiKey: "test-key", NamespaceID: "test-namespace", RegistryName: "test-registry", - RecordName: "test-record", }, wantErr: false, }, @@ -107,7 +91,6 @@ func TestNew(t *testing.T) { ApiKey: "test-key", NamespaceID: "test-namespace", RegistryName: "test-registry", - RecordName: "test-record", Timeout: 30, } @@ -142,7 +125,7 @@ func TestLookup(t *testing.T) { if r.Method != "GET" { t.Errorf("Expected GET request, got %s", r.Method) } - if r.URL.Path != "/dedi/lookup/test-namespace/test-registry/test-record" { + if r.URL.Path != "/dedi/lookup/test-namespace/test-registry/bap-network" { t.Errorf("Unexpected path: %s", r.URL.Path) } // Verify Authorization header @@ -150,16 +133,22 @@ func TestLookup(t *testing.T) { t.Errorf("Expected Bearer test-key, got %s", auth) } - // Return mock response using map structure + // Return mock response using actual DeDI format response := map[string]interface{}{ - "message": "success", + "message": "Resource retrieved successfully", "data": map[string]interface{}{ - "schema": map[string]interface{}{ - "entity_name": "test.example.com", - "entity_url": "https://test.example.com", - "publicKey": "test-public-key", + "record_name": "bap-network", + "details": map[string]interface{}{ + "key_id": "b692d295-5425-40f5-af77-d62646841dca", + "signing_public_key": "test-public-key", + "encr_public_key": "test-encr-key", + "status": "SUBSCRIBED", + "created": "2023-01-01T00:00:00Z", + "updated": "2023-01-01T00:00:00Z", + "valid_from": "2023-01-01T00:00:00Z", + "valid_until": "2024-01-01T00:00:00Z", }, - "state": "active", + "state": "live", "created_at": "2023-01-01T00:00:00Z", "updated_at": "2023-01-01T00:00:00Z", }, @@ -174,7 +163,6 @@ func TestLookup(t *testing.T) { ApiKey: "test-key", NamespaceID: "test-namespace", RegistryName: "test-registry", - RecordName: "test-record", Timeout: 30, } @@ -184,7 +172,11 @@ func TestLookup(t *testing.T) { } defer closer() - req := &model.Subscription{} + req := &model.Subscription{ + Subscriber: model.Subscriber{ + SubscriberID: "bap-network", + }, + } results, err := client.Lookup(ctx, req) if err != nil { t.Errorf("Lookup() error = %v", err) @@ -197,14 +189,43 @@ func TestLookup(t *testing.T) { } subscription := results[0] - if subscription.Subscriber.SubscriberID != "test.example.com" { - t.Errorf("Expected subscriber_id test.example.com, got %s", subscription.Subscriber.SubscriberID) + if subscription.Subscriber.SubscriberID != "bap-network" { + t.Errorf("Expected subscriber_id bap-network, got %s", subscription.Subscriber.SubscriberID) } if subscription.SigningPublicKey != "test-public-key" { t.Errorf("Expected signing_public_key test-public-key, got %s", subscription.SigningPublicKey) } - if subscription.Status != "active" { - t.Errorf("Expected status active, got %s", subscription.Status) + if subscription.Status != "SUBSCRIBED" { + t.Errorf("Expected status SUBSCRIBED, got %s", subscription.Status) + } + }) + + // Test empty subscriber ID + t.Run("empty subscriber ID", func(t *testing.T) { + config := &Config{ + BaseURL: "https://test.com", + ApiKey: "test-key", + NamespaceID: "test-namespace", + RegistryName: "test-registry", + } + + client, closer, err := New(ctx, config) + if err != nil { + t.Fatalf("New() error = %v", err) + } + defer closer() + + req := &model.Subscription{ + Subscriber: model.Subscriber{ + SubscriberID: "", + }, + } + _, err = client.Lookup(ctx, req) + if err == nil { + t.Error("Expected error for empty subscriber ID, got nil") + } + if err.Error() != "subscriber_id is required for DeDi lookup" { + t.Errorf("Expected specific error message, got %v", err) } }) @@ -221,7 +242,6 @@ func TestLookup(t *testing.T) { ApiKey: "test-key", NamespaceID: "test-namespace", RegistryName: "test-registry", - RecordName: "test-record", } client, closer, err := New(ctx, config) @@ -230,7 +250,11 @@ func TestLookup(t *testing.T) { } defer closer() - req := &model.Subscription{} + req := &model.Subscription{ + Subscriber: model.Subscriber{ + SubscriberID: "bap-network", + }, + } _, err = client.Lookup(ctx, req) if err == nil { t.Error("Expected error for 404 response, got nil") @@ -238,13 +262,13 @@ func TestLookup(t *testing.T) { }) // Test missing required fields - t.Run("missing entity_name", func(t *testing.T) { + t.Run("missing signing_public_key", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { response := map[string]interface{}{ "data": map[string]interface{}{ - "schema": map[string]interface{}{ - "entity_url": "https://test.example.com", - "publicKey": "test-public-key", + "details": map[string]interface{}{ + "key_id": "test-key-id", + "status": "SUBSCRIBED", }, }, } @@ -258,7 +282,6 @@ func TestLookup(t *testing.T) { ApiKey: "test-key", NamespaceID: "test-namespace", RegistryName: "test-registry", - RecordName: "test-record", } client, closer, err := New(ctx, config) @@ -267,10 +290,14 @@ func TestLookup(t *testing.T) { } defer closer() - req := &model.Subscription{} + req := &model.Subscription{ + Subscriber: model.Subscriber{ + SubscriberID: "bap-network", + }, + } _, err = client.Lookup(ctx, req) if err == nil { - t.Error("Expected error for missing entity_name, got nil") + t.Error("Expected error for missing signing_public_key, got nil") } }) @@ -287,7 +314,6 @@ func TestLookup(t *testing.T) { ApiKey: "test-key", NamespaceID: "test-namespace", RegistryName: "test-registry", - RecordName: "test-record", } client, closer, err := New(ctx, config) @@ -296,7 +322,11 @@ func TestLookup(t *testing.T) { } defer closer() - req := &model.Subscription{} + req := &model.Subscription{ + Subscriber: model.Subscriber{ + SubscriberID: "bap-network", + }, + } _, err = client.Lookup(ctx, req) if err == nil { t.Error("Expected error for invalid JSON, got nil") @@ -310,7 +340,6 @@ func TestLookup(t *testing.T) { ApiKey: "test-key", NamespaceID: "test-namespace", RegistryName: "test-registry", - RecordName: "test-record", Timeout: 1, } @@ -320,7 +349,11 @@ func TestLookup(t *testing.T) { } defer closer() - req := &model.Subscription{} + req := &model.Subscription{ + Subscriber: model.Subscriber{ + SubscriberID: "bap-network", + }, + } _, err = client.Lookup(ctx, req) if err == nil { t.Error("Expected network error, got nil") @@ -343,7 +376,6 @@ func TestLookup(t *testing.T) { ApiKey: "test-key", NamespaceID: "test-namespace", RegistryName: "test-registry", - RecordName: "test-record", } client, closer, err := New(ctx, config) @@ -352,10 +384,14 @@ func TestLookup(t *testing.T) { } defer closer() - req := &model.Subscription{} + req := &model.Subscription{ + Subscriber: model.Subscriber{ + SubscriberID: "bap-network", + }, + } _, err = client.Lookup(ctx, req) if err == nil { t.Error("Expected error for missing data field, got nil") } }) -} +} \ No newline at end of file