Issue 527 - fix: remove static record_name from DeDI plugin config to support dynamic bap_id/bpp_id lookup

This commit is contained in:
ameersohel45
2025-10-08 12:33:46 +05:30
parent 64f5f550c0
commit 03ea297e51
4 changed files with 172 additions and 101 deletions

View File

@@ -1,16 +1,26 @@
# 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. This plugin implements the `RegistryLookup` interface, making it a specialized type of registry plugin.
## 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.
The DeDi Registry plugin is a **registry implementation** that enables Beckn-ONIX to lookup participant records from DeDi registries. It converts DeDi API responses to standard Beckn Subscription format, allowing it to work interchangeably with the standard registry plugin through the same `RegistryLookup` interface.
## Plugin Type Classification
**Registry Type Plugin**: This plugin is a **type of registry plugin**, not a standalone plugin category.
- **Interface**: Implements `RegistryLookup` interface (same as standard registry plugin)
- **Interchangeable**: Can replace or work alongside standard registry plugin
- **Manager Access**: Available via `manager.Registry()` method
- **Plugin Category**: Registry
## Features
- **RegistryLookup Interface**: Implements standard Beckn registry interface
- **Standard Registry Interface**: Implements `RegistryLookup` interface for seamless integration
- **DeDi API Integration**: GET requests to DeDi registry endpoints with Bearer authentication
- **Data Conversion**: Converts DeDi responses to Beckn Subscription format
- **Dynamic Participant Lookup**: Uses subscriber IDs from request context (not static configuration)
- **Data Conversion**: Converts DeDi responses to standard Beckn Subscription format
- **HTTP Retry Logic**: Built-in retry mechanism using retryablehttp client
- **Timeout Control**: Configurable request timeouts
@@ -26,7 +36,6 @@ plugins:
apiKey: "your-api-key"
namespaceID: "beckn-network"
registryName: "participants"
recordName: "participant-id"
timeout: "30" # seconds
```
@@ -38,7 +47,6 @@ plugins:
| `apiKey` | Yes | API key for 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,83 +58,89 @@ 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-registry.com/dedi/lookup/beckn-network/participants/bap-network`
### 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"
"record_name": "bap.example.com",
"details": {
"entity_name": "BAP Example Provider",
"entity_url": "https://bap.example.com",
"publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...",
"keyType": "RSA",
"keyFormat": "PEM"
},
"state": "active",
"created_at": "2023-01-01T00:00:00Z",
"updated_at": "2023-01-01T00:00:00Z"
"state": "live",
"created_at": "2025-09-23T07:45:10.357Z",
"updated_at": "2025-09-23T07:51:39.923Z"
}
}
```
### Converted to Beckn Format
### Converted to Standard Beckn Format
The plugin converts this to standard Beckn Subscription format:
The plugin converts DeDi responses to standard Beckn Subscription format:
```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"
"subscriber_id": "bap.example.com",
"url": "https://bap.example.com",
"signing_public_key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...",
"status": "live",
"created": "2025-09-23T07:45:10.357Z",
"updated": "2025-09-23T07:51:39.923Z"
}
```
@@ -143,11 +157,25 @@ go test ./pkg/plugin/implementation/dediregistry -v
- `github.com/hashicorp/go-retryablehttp`: HTTP client with retry logic
- Standard Go libraries for HTTP and JSON handling
## Integration Notes
## Plugin Architecture
- **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`
### Registry Type Plugin Classification
```
Plugin Manager
├── Registry Plugins (RegistryLookup interface)
│ ├── registry (standard YAML-based registry)
│ └── dediregistry (DeDi API-based registry) ← This plugin
└── Other Plugin Types...
```
### Integration Notes
- **Plugin Type**: Registry implementation
- **Interface**: Implements `RegistryLookup` interface with `Lookup(ctx, *model.Subscription) ([]model.Subscription, error)`
- **Interchangeable**: Drop-in replacement for standard registry plugin
- **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

View File

@@ -25,7 +25,6 @@ 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

View File

@@ -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 {
@@ -131,27 +135,34 @@ func (c *DeDiRegistryClient) Lookup(ctx context.Context, req *model.Subscription
return nil, fmt.Errorf("invalid response format: missing data field")
}
schema, ok := data["schema"].(map[string]interface{})
// Extract details field which contains the actual participant data
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)
// Extract values from details field
entityName, ok := details["entity_name"].(string)
if !ok || entityName == "" {
return nil, fmt.Errorf("invalid or missing entity_name in response")
}
entityURL, ok := schema["entity_url"].(string)
entityURL, ok := details["entity_url"].(string)
if !ok || entityURL == "" {
return nil, fmt.Errorf("invalid or missing entity_url in response")
}
publicKey, ok := schema["publicKey"].(string)
publicKey, ok := details["publicKey"].(string)
if !ok || publicKey == "" {
return nil, fmt.Errorf("invalid or missing publicKey in response")
}
// Extract record_name as the subscriber ID (fallback to entity_name)
recordName, _ := data["record_name"].(string)
if recordName == "" {
recordName = entityName
}
state, _ := data["state"].(string)
createdAt, _ := data["created_at"].(string)
updatedAt, _ := data["updated_at"].(string)
@@ -159,7 +170,7 @@ func (c *DeDiRegistryClient) Lookup(ctx context.Context, req *model.Subscription
// Convert to Subscription format
subscription := model.Subscription{
Subscriber: model.Subscriber{
SubscriberID: entityName,
SubscriberID: recordName, // Use record_name as subscriber ID
URL: entityURL,
Domain: req.Domain,
Type: req.Type,

View File

@@ -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,19 @@ 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",
"record_name": "bap-network",
"details": map[string]interface{}{
"entity_name": "BAP Network Provider",
"entity_url": "https://bap-network.example.com",
"publicKey": "test-public-key",
"keyType": "ed25519",
"keyFormat": "base64",
},
"state": "active",
"state": "live",
"created_at": "2023-01-01T00:00:00Z",
"updated_at": "2023-01-01T00:00:00Z",
},
@@ -174,7 +160,6 @@ func TestLookup(t *testing.T) {
ApiKey: "test-key",
NamespaceID: "test-namespace",
RegistryName: "test-registry",
RecordName: "test-record",
Timeout: 30,
}
@@ -184,7 +169,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 +186,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 != "live" {
t.Errorf("Expected status live, 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 +239,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 +247,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")
@@ -242,7 +263,7 @@ func TestLookup(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{}{
"details": map[string]interface{}{
"entity_url": "https://test.example.com",
"publicKey": "test-public-key",
},
@@ -258,7 +279,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 +287,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 details field, got nil")
}
})
@@ -287,7 +311,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 +319,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 +337,6 @@ func TestLookup(t *testing.T) {
ApiKey: "test-key",
NamespaceID: "test-namespace",
RegistryName: "test-registry",
RecordName: "test-record",
Timeout: 1,
}
@@ -320,7 +346,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 +373,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 +381,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")
}
})
}
}