Skip to content

Commit 7ad8bee

Browse files
authored
feat: Plugin routing support (#587)
* feat: Add route info to plugin context * chore(plugins): Add raw_response and config object * chore: Add x402-facilitator plugin example * chore: Tests fixes * chore: Improvements * chore: Improve docs * chore: Improvements * chore: Improvements * chore: Test improvements * chore: Ignore examples lock files * chore: Test improvements * chore: Remove x402 docs and guide * chore: Improve plugin model * chore: Example improvements
1 parent 0ac8b4c commit 7ad8bee

File tree

24 files changed

+3370
-66
lines changed

24 files changed

+3370
-66
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ node_modules
6363
# Exclude generated OpenAPI files from the docs directory
6464
!./openapi.json
6565

66+
!examples/**/pnpm-lock.yaml
67+
!examples/**/package-lock.json
68+
6669
# Integration test artifacts
6770
test-results/
6871
**/*lcov.info

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ k256 = { version = "0.13", features = ["ecdsa-core"]}
9999
solana-system-interface = { version = "2.0.0", features = ["bincode"] }
100100
cdp-sdk = "0.1.0"
101101
reqwest-middleware = { version = "0.4.2", default-features = false, features = ["json"] }
102+
url = "2"
102103

103104
[dev-dependencies]
104105
cargo-llvm-cov = "0.6"

docs/plugins/index.mdx

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ The plugin system features:
1515
- **_Plugin API_**: Clean interface for interacting with relayers
1616
- **_Key-Value Storage_**: Persistent state and locking for plugins
1717
- **_HTTP Headers_**: Access request headers for authentication and metadata
18+
- **_Route-Based Invocation_**: Optional route suffix (`/call/<route>`) for multi-endpoint plugins
1819
- **_Docker Integration_**: Seamless development and deployment
1920
- **_Comprehensive Error Handling_**: Detailed logging and debugging capabilities
2021

@@ -98,6 +99,39 @@ export async function handler(context: PluginContext): Promise<MyPluginResult> {
9899
}
99100
```
100101

102+
#### Handler Context (`PluginContext`)
103+
104+
All modern plugins export a single function with the signature:
105+
106+
```typescript
107+
import type { PluginContext } from '@openzeppelin/relayer-sdk';
108+
109+
export async function handler(context: PluginContext) {
110+
const { api, params, kv, headers, route, method, query, config } = context;
111+
112+
// ...your logic
113+
return { ok: true };
114+
}
115+
```
116+
117+
The `handler` receives a `PluginContext` object with these commonly-used fields:
118+
119+
| Field | Type | Description |
120+
| --- | --- | --- |
121+
| `context.api` | `PluginAPI` | Client for interacting with Relayers (e.g. `api.useRelayer(...)`). |
122+
| `context.params` | `any` | The input parameters for the plugin invocation. For `POST`, this comes from the request body; for `GET`, it is `{}`. |
123+
| `context.kv` | KV store client | Persistent key/value store (namespaced per plugin). Only available in the modern context handler. |
124+
| `context.headers` | `Record<string, string[]>` | Incoming HTTP headers (lowercased keys; values are arrays). |
125+
| `context.route` | `string` | The route suffix, including the leading slash, e.g. `""`, `"/verify"`, `"/settle"`. |
126+
| `context.method` | `"GET" \| "POST"` | The HTTP method used to invoke the plugin. |
127+
| `context.query` | `Record<string, string[]>` | Query parameters parsed from the request URL. Most useful for `GET` invocations. |
128+
| `context.config` | `unknown` | User-defined plugin configuration object from `plugins[].config` in the Relayer config file. |
129+
130+
<Callout>
131+
`context.params` is user-provided input. Prefer defining a `type` for your params, validating required fields, and using
132+
`pluginError(...)` to return structured 4xx errors.
133+
</Callout>
134+
101135
#### Legacy Patterns (Deprecated, but supported)
102136

103137
<Callout type="warn">
@@ -140,14 +174,18 @@ The file contains a list of plugins, each with an id, path, timeout in seconds (
140174
Example:
141175

142176
```json
143-
144177
"plugins": [
145178
{
146179
"id": "my-plugin",
147180
"path": "my-plugin.ts",
148181
"timeout": 30,
149182
"emit_logs": false,
150183
"emit_traces": false,
184+
"raw_response": false,
185+
"allow_get_invocation": false,
186+
"config": {
187+
"featureFlagExample": true
188+
}
151189
"forward_logs": false
152190
}
153191
]
@@ -231,9 +269,28 @@ console.log(result);
231269

232270
## Invocation
233271

234-
Plugins are invoked by hitting the `api/v1/plugins/plugin-id/call` endpoint.
272+
Plugins are invoked by hitting the `/api/v1/plugins/{plugin-id}/call{route}` endpoint.
235273

236-
The endpoint accepts a `POST` request. Example post request body:
274+
- `route` is optional. Use `/call` for a single endpoint, or `/call/<route>` to expose multiple routes from the same plugin.
275+
- The plugin receives the route as `context.route`.
276+
- You can implement your own routing by branching on `context.route` (for example, `"/verify"`, `"/settle"`, `""`).
277+
278+
Example (route-based invocation):
279+
280+
```bash
281+
# Calls the plugin with context.route = "/verify"
282+
curl -X POST http://localhost:8080/api/v1/plugins/my-plugin/call/verify \
283+
-H "Content-Type: application/json" \
284+
-H "Authorization: Bearer YOUR_API_KEY" \
285+
-d '{"params":{}}'
286+
```
287+
288+
The endpoint accepts a `POST` request.
289+
290+
- If the request body contains a top-level `params` field, that value is used as `context.params`.
291+
- If the request body does not contain `params`, the entire JSON body is treated as `params`.
292+
293+
Example `POST` request body:
237294

238295
```json
239296
{
@@ -247,9 +304,18 @@ The endpoint accepts a `POST` request. Example post request body:
247304

248305
The parameters are passed directly to your plugin’s `handler` function.
249306

307+
### GET invocation (optional)
308+
309+
By default, plugins can only be called with `POST`. To allow `GET` requests, set `allow_get_invocation: true` in the plugin configuration.
310+
311+
- `GET` requests invoke the plugin with an empty params object (`context.params = {}`).
312+
- Query parameters are available to plugins via `context.query` as `Record<string, string[]>`.
313+
250314
## Responses
251315

252-
API responses use the `ApiResponse` envelope: `success, data, error, metadata`.
316+
By default, API responses use the `ApiResponse` envelope: `success, data, error, metadata`.
317+
318+
If `raw_response: true` is set for a plugin, the Relayer bypasses the envelope and returns the plugin result (or plugin error) directly.
253319

254320
### Success responses (HTTP 200)
255321

@@ -266,6 +332,12 @@ API responses use the `ApiResponse` envelope: `success, data, error, metadata`.
266332
- `metadata` follows the same visibility rules (`emit_logs` / `emit_traces`).
267333
- Plugin error logs are also forwarded to the Relayer's tracing system if `forward_logs` is enabled.
268334

335+
### Raw responses (`raw_response: true`)
336+
337+
- **Success**: the response body is the value returned by your plugin handler.
338+
- **Error**: the response body is the plugin error payload (typically `{ "code": string | null, "details": any | null }`) and the HTTP status is taken from the error.
339+
- **No metadata envelope**: when `raw_response` is enabled, the response does not include `metadata` even if `emit_logs` / `emit_traces` are enabled.
340+
269341
### Complete Example
270342

271343
1. **_Plugin Code_** (`plugins/example.ts`):

0 commit comments

Comments
 (0)