Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion cmd/nodecore/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/drpcorg/nodecore/internal/caches"
"github.com/drpcorg/nodecore/internal/config"
"github.com/drpcorg/nodecore/internal/dimensions"
"github.com/drpcorg/nodecore/internal/integration"
"github.com/drpcorg/nodecore/internal/ratelimiter"
"github.com/drpcorg/nodecore/internal/rating"
"github.com/drpcorg/nodecore/internal/server"
Expand Down Expand Up @@ -50,7 +51,9 @@ func main() {

mainCtx, mainCtxCancel := context.WithCancel(context.Background())

authProcessor, err := auth.NewAuthProcessor(appConfig.AuthConfig)
integrationResolver := integration.NewIntegrationResolver(appConfig.IntegrationConfig)

authProcessor, err := auth.NewAuthProcessor(mainCtx, appConfig.AuthConfig, integrationResolver)
if err != nil {
log.Panic().Err(err).Msg("unable to create the auth processor")
}
Expand Down
97 changes: 80 additions & 17 deletions docs/nodecore/03-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ JWT should be passed via the `Authorization` header with the `Bearer` prefix.

```yaml
key-management:
- id: "drpc-super-key"
type: drpc
drpc:
owner:
id: "id"
api-token: "apiToken"
- id: "my-first-key"
type: local
local:
Expand All @@ -112,26 +118,28 @@ This is especially useful for multi-tenant environments or when you want to enfo

`key-management` fields:
* `id` - Unique identifier for the key. **_Required_**, **_Unique_**
* `type` - Defines the backend that manages this key. Currently supported: `local`. **_Required_**
* `type` - Defines the backend that manages this key. Currently supported: `local`, `drpc`. **_Required_**

if `type: local`, you mush provide:
#### Local keys

If `type: local`, you mush provide:
```yaml
local:
key: "bXkta2V5"
settings:
cors-origins:
- "https://example.com"
allowed-ips:
- "192.0.0.1"
- "127.0.0.1"
methods:
allowed:
- 'eth_getBlockByNumber'
forbidden:
- "eth_syncing"
contracts:
allowed:
- "0xfde26a190bfd8c43040c6b5ebf9bc7f8c934c80a"
key: "bXkta2V5"
settings:
cors-origins:
- "https://example.com"
allowed-ips:
- "192.0.0.1"
- "127.0.0.1"
methods:
allowed:
- 'eth_getBlockByNumber'
forbidden:
- "eth_syncing"
contracts:
allowed:
- "0xfde26a190bfd8c43040c6b5ebf9bc7f8c934c80a"
```

The `local` key type is the simplest form of key management. It allows you to define access keys directly in the configuration file, without relying on an external service. This is useful for quick setups and internal environments.
Expand All @@ -146,6 +154,61 @@ The `local` key type is the simplest form of key management. It allows you to de
* `settings.contracts.allowed` - Restricts interaction to a specific set of contract addresses for `eth_call` and `eth_getLogs` methods
* `settings.cors-origins` - The list of allowed CORS origins for this key. If present, nodecore will include the appropriate `Access-Control-Allow-Origin` header only for the origins explicitly listed here. If the incoming request’s Origin header does not match any entry, the request will be rejected by the CORS layer.

#### DRPC keys

DRPC keys are owned and maintained on the DRPC platform and fetched by nodecore through the DRPC integration API. Such keys allow you to offload key lifecycle operations to the external platform, specifically to DRPC. A single DRPC account may contain multiple owners (teams), and each owner can maintain its own set of NodeCore keys. Nodecore always treats DRPC keys identically to local keys during request validation.

Use DRPC-managed keys if you want:
- Centralized management of all nodecore keys across multiple nodecore instances.
- Separation of keys by project/team (each owner has its own namespace).
- Automatic synchronization: once an owner updates/deletes a key in DRPC, nodecore sees the change without requiring configuration edits.
- (**_Future feature_**) Aggregated analytics in DRPC: dashboards with nodecore stats: request counts, latency, error rate, per-key insights, etc.

If `type: drpc`, you mush provide:
```yaml
drpc:
owner:
id: "you-owner-id"
api-token: "apiToken"
```

* `owner.id` - The Owner ID from your DRPC nodecore workspace. Keys belonging to this owner will be accessible in nodecore. **_Required_**
* `owner.api-token` - API token used to authenticate this nodecore instance when calling DRPC. It can be generated and regenerated on the DRPC website. **_Required_**

Each DRPC key entry corresponds to one owner on DRPC.
If your account manages multiple owners, list them all under `key-management`.

All key-level restrictions — IP whitelists, method filters, contract filters, CORS origins, etc. — are configured on the DRPC website, not in `nodecore.yaml`. Nodecore automatically fetches these attributes from DRPC and enforces them exactly the same way as for locally defined keys.

Supported attributes:
* **IP whitelist** - Restrict which client IPs may use this key. Nodecore rejects requests from any IP not included.
* **Allowed RPC methods** - Allow or deny specific RPC methods (e.g., allow only eth_call / block eth_sendRawTransaction).
* **Allowed contract addresses** - Restrict access to smart contracts by address
* **CORS origins** - Restrict browser origins allowed to use this key.

How DRPC key integration works:
1. **Integration Configuration Check**. On startup, nodecore verifies that the `integration.drpc` section is present in the configuration.
* If the section is missing, nodecore fails to start with an error. DRPC-managed keys cannot function without a valid integration endpoint.
2. **DRPC Key Configuration Validation**. Nodecore inspects each `auth.key-management` entry of type `drpc`. For every such key, the following fields are required: `owner.id`, `owner.api-token`. If either field is missing, empty, or malformed, nodecore terminates with a configuration error.
3. **Initial Key Load from DRPC**. Once configuration is validated, nodecore attempts to load the owner’s keys from DRPC:
* The request is authenticated using the owner’s api-token.
* All key definitions and attributes (IP whitelist, method filters, etc.) are fetched at once.
4. **Key Registration and Periodic Sync**. When the initial load completes successfully:
* nodecore registers all DRPC-managed keys locally
* a periodic polling loop begins (every 1 minute):
* updates key attributes (IP whitelist, methods, CORS, contracts, enabled/disabled state)
* adds new keys created on DRPC
* removes keys deleted on DRPC
5. **Handling Initial Load Failures**. If the initial load fails, nodecore distinguishes between retryable and non-retryable errors:
* Retryable Errors. Nodecore retries the initial load every 10 seconds until successful. Once a retry succeeds, NodeCore proceeds to step 4. Errors:
* 429 Too Many Requests - Rate limit on DRPC side — safe to retry.
* 500 Internal Server Error - Temporary server issue — safe to retry.
* Connection errors / timeouts - Network or availability issue — safe to retry.
* Non-Retryable Errors. Nodecore immediately disables DRPC key integration for the affected owner.
DRPC keys belonging to that owner will not be available. Errors:
* 404 Not Found - The provided Owner ID does not exist on DRPC.
* 403 Forbidden - API token missing, invalid, or belonging to another owner.

> **⚠️ Important**:
> 1. If at least one key is defined in the `key-management` section, then every request must include a valid nodecore key. If no key is provided, or the provided key does not match the configured rules, the request will be rejected.
> 2. If you want to pass your key via the URL you have to use another endpoint path - `/queries/{chain}/api-key/{your-key}`
36 changes: 36 additions & 0 deletions docs/nodecore/09-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Integration config guide

Nodecore supports integration with external platforms that provide a wide range of features. The `integration` section helps you set all of this up.

```yaml
integration:
drpc:
url: http://localhost:9090
request-timeout: 35s
```

## DRPC integration

DRPC integration allows nodecore to fetch and maintain authentication keys that are managed in the DRPC platform instead of being defined locally. This enables centralized key management across multiple nodecore instances and unlocks future analytics features provided by DRPC.

* `url` - The DRPC integration endpoint. **_Required_**
* `request-timeout` - Timeout for communication with the DRPC integration API. **_Default_**: `10s`

To enable NodeCore ↔ DRPC integration, you must complete the Quickstart guide on the DRPC website:

1. Visit https://drpc.org/.
2. Sign in or create a new account.
3. Select the Nodecore product.
4. Complete the Quickstart guide for Nodecore.
5. Generate your owner's API token and get the integration endpoint.
6. Copy this endpoint into your nodecore configuration file.

### Current DRPC Features

1. DRPC-managed API keys — create and maintain nodecore keys directly in DRPC, and have nodecore automatically fetch them and enforce all associated restrictions (see [DRPC Key Management](03-auth.md#drpc-keys))

**Future updates on the DRPC side will introduce:**

1. Request stats (request counts, latency, error rate, etc).
2. Error stats.
3. Tracing stats.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
github.com/failsafe-go/failsafe-go v0.6.9
github.com/go-redis/redismock/v9 v9.2.0
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/grafana/pyroscope-go v1.2.7
Expand Down
11 changes: 8 additions & 3 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"net/http"

"github.com/drpcorg/nodecore/internal/config"
"github.com/drpcorg/nodecore/internal/integration"
"github.com/drpcorg/nodecore/internal/key_management"
"github.com/drpcorg/nodecore/internal/protocol"
)

Expand All @@ -14,7 +16,7 @@ const (
XNodecoreKey = "X-Nodecore-Key"
)

func NewAuthProcessor(authCfg *config.AuthConfig) (AuthProcessor, error) {
func NewAuthProcessor(ctx context.Context, authCfg *config.AuthConfig, integrationResolver *integration.IntegrationResolver) (AuthProcessor, error) {
if authCfg == nil || !authCfg.Enabled {
return newNoopAuthProcessor(), nil
}
Expand All @@ -27,7 +29,10 @@ func NewAuthProcessor(authCfg *config.AuthConfig) (AuthProcessor, error) {
if len(authCfg.KeyConfigs) == 0 {
authProcessor = newSimpleAuthProcessor(authRequestStrategy)
} else {
keyResolver := NewKeyResolver(authCfg.KeyConfigs)
keyResolver, err := NewKeyResolver(ctx, authCfg.KeyConfigs, integrationResolver)
if err != nil {
return nil, err
}
authProcessor = newBasicAuthProcessor(keyResolver, authRequestStrategy)
}

Expand Down Expand Up @@ -81,7 +86,7 @@ func (b *basicAuthProcessor) PostKeyValidate(ctx context.Context, payload AuthPa
return key.PostCheckSetting(ctx, request)
}

func (b *basicAuthProcessor) getKey(payload AuthPayload) (Key, error) {
func (b *basicAuthProcessor) getKey(payload AuthPayload) (keymanagement.Key, error) {
keyStr := getPayloadKey(payload)
if keyStr == "" {
return nil, errors.New("api-key must be provided")
Expand Down
25 changes: 7 additions & 18 deletions internal/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (

"github.com/drpcorg/nodecore/internal/auth"
"github.com/drpcorg/nodecore/internal/config"
"github.com/drpcorg/nodecore/internal/protocol"
"github.com/drpcorg/nodecore/pkg/test_utils"
"github.com/stretchr/testify/assert"
)
Expand All @@ -24,7 +23,7 @@ func newBasicProcessor(t *testing.T, token string, allowedIps []string, methods
KeyConfigs: []*config.KeyConfig{
{
Id: "k1",
Type: config.Local,
Type: config.LocalKey,
LocalKeyConfig: &config.LocalKeyConfig{
Key: "secret-key",
KeySettingsConfig: &config.KeySettingsConfig{
Expand All @@ -37,7 +36,7 @@ func newBasicProcessor(t *testing.T, token string, allowedIps []string, methods
},
}

p, err := auth.NewAuthProcessor(appCfg)
p, err := auth.NewAuthProcessor(context.Background(), appCfg, nil)
if err != nil {
t.Fatalf("NewAuthProcessor error: %v", err)
}
Expand Down Expand Up @@ -149,7 +148,7 @@ func TestBasicAuthProcessor_PostKeyValidate_Success(t *testing.T) {
})

// Build a real RequestHolder for eth_call with 'to'
req := newUpstreamRequest(t, "eth_call", []any{map[string]any{"to": "0xabc"}, "latest"})
req := test_utils.NewUpstreamRequest(t, "eth_call", []any{map[string]any{"to": "0xabc"}, "latest"})

err := processor.PostKeyValidate(context.Background(), payload, req)
assert.NoError(t, err)
Expand All @@ -165,7 +164,7 @@ func TestBasicAuthProcessor_PostKeyValidate_MethodNotAllowed_Error(t *testing.T)
auth.XNodecoreToken: "tok-123",
})

req := newUpstreamRequest(t, "eth_call", []any{map[string]any{"to": "0xabc"}, "latest"})
req := test_utils.NewUpstreamRequest(t, "eth_call", []any{map[string]any{"to": "0xabc"}, "latest"})

err := processor.PostKeyValidate(context.Background(), payload, req)
assert.ErrorContains(t, err, "method 'eth_call' is not allowed")
Expand All @@ -181,7 +180,7 @@ func TestBasicAuthProcessor_PostKeyValidate_ContractNotAllowed_Error(t *testing.
auth.XNodecoreToken: "tok-123",
})

req := newUpstreamRequest(t, "eth_call", []any{map[string]any{"to": "0xabc"}, "latest"})
req := test_utils.NewUpstreamRequest(t, "eth_call", []any{map[string]any{"to": "0xabc"}, "latest"})

err := processor.PostKeyValidate(context.Background(), payload, req)
assert.ErrorContains(t, err, "'0xabc' address is not allowed")
Expand All @@ -197,7 +196,7 @@ func TestBasicAuthProcessor_PostKeyValidate_MissingHeader_Error(t *testing.T) {
auth.XNodecoreToken: "tok-123",
})

req := newUpstreamRequest(t, "eth_call", []any{map[string]any{"to": "0xabc"}, "latest"})
req := test_utils.NewUpstreamRequest(t, "eth_call", []any{map[string]any{"to": "0xabc"}, "latest"})

err := processor.PostKeyValidate(context.Background(), payload, req)
assert.ErrorContains(t, err, "api-key must be provided")
Expand All @@ -213,18 +212,8 @@ func TestBasicAuthProcessor_PostKeyValidate_KeyNotFound_Error(t *testing.T) {
auth.XNodecoreToken: "tok-123",
})

req := newUpstreamRequest(t, "eth_call", []any{map[string]any{"to": "0xabc"}, "latest"})
req := test_utils.NewUpstreamRequest(t, "eth_call", []any{map[string]any{"to": "0xabc"}, "latest"})

err := processor.PostKeyValidate(context.Background(), payload, req)
assert.ErrorContains(t, err, "specified api-key not found")
}

// helper to create a real UpstreamJsonRpcRequest with realistic params
func newUpstreamRequest(t *testing.T, method string, params any) protocol.RequestHolder {
t.Helper()
req, err := protocol.NewInternalUpstreamJsonRpcRequest(method, params)
if err != nil {
t.Fatalf("failed to build UpstreamJsonRpcRequest: %v", err)
}
return req
}
Loading