openzeppelin_relayer/api/routes/docs/
plugin_docs.rs

1use crate::{
2    models::{ApiResponse, PluginCallRequest, PluginModel, UpdatePluginRequest},
3    repositories::PaginatedResult,
4    services::plugins::PluginHandlerError,
5};
6
7/// Calls a plugin method.
8///
9/// Logs and traces are only returned when the plugin is configured with `emit_logs` / `emit_traces`.
10/// Plugin-provided errors are normalized into a consistent payload (`code`, `details`) and a derived
11/// message so downstream clients receive a stable shape regardless of how the handler threw.
12///
13/// The endpoint supports wildcard route routing, allowing plugins to implement custom routing logic:
14/// - `/api/v1/plugins/{plugin_id}/call` - Default endpoint (route = "")
15/// - `/api/v1/plugins/{plugin_id}/call?route=/verify` - Custom route via query parameter
16/// - `/api/v1/plugins/{plugin_id}/call/verify` - Custom route via URL path (route = "/verify")
17///
18/// The route is passed to the plugin handler via the `context.route` field.
19/// You can specify a custom route either by appending it to the URL path or by using the `route` query parameter.
20#[utoipa::path(
21    post,
22    path = "/api/v1/plugins/{plugin_id}/call",
23    tag = "Plugins",
24    operation_id = "callPlugin",
25    summary = "Execute a plugin with optional wildcard route routing",
26    security(
27        ("bearer_auth" = [])
28    ),
29    params(
30        ("plugin_id" = String, Path, description = "The unique identifier of the plugin"),
31        ("route" = Option<String>, Query, description = "Optional route suffix for custom routing (e.g., '/verify'). Alternative to appending the route to the URL path.")
32    ),
33    request_body = PluginCallRequest,
34    responses(
35        (
36            status = 200,
37            description = "Plugin call successful",
38            body = ApiResponse<serde_json::Value>,
39            example = json!({
40                "success": true,
41                "data": "done!",
42                "metadata": {
43                    "logs": [
44                        {
45                            "level": "info",
46                            "message": "Plugin started..."
47                        }
48                    ],
49                    "traces": [
50                        {
51                            "method": "sendTransaction",
52                            "relayerId": "sepolia-example",
53                            "requestId": "6c1f336f-3030-4f90-bd99-ada190a1235b"
54                        }
55                    ]
56                },
57                "error": null
58            })
59        ),
60        (
61            status = 400,
62            description = "BadRequest (plugin-provided)",
63            body = ApiResponse<PluginHandlerError>,
64            example = json!({
65                "success": false,
66                "error": "Validation failed",
67                "data": { "code": "VALIDATION_FAILED", "details": { "field": "email" } },
68                "metadata": {
69                    "logs": [
70                        {
71                            "level": "error",
72                            "message": "Validation failed for field: email"
73                        }
74                    ]
75                }
76            })
77        ),
78        (
79            status = 401,
80            description = "Unauthorized",
81            body = ApiResponse<String>,
82            example = json!({
83                "success": false,
84                "error": "Unauthorized",
85                "data": null
86            })
87        ),
88        (
89            status = 404,
90            description = "Not Found",
91            body = ApiResponse<String>,
92            example = json!({
93                "success": false,
94                "error": "Plugin with ID plugin_id not found",
95                "data": null
96            })
97        ),
98        (
99            status = 429,
100            description = "Too Many Requests",
101            body = ApiResponse<String>,
102            example = json!({
103                "success": false,
104                "error": "Too Many Requests",
105                "data": null
106            })
107        ),
108        (
109            status = 500,
110            description = "Internal server error",
111            body = ApiResponse<String>,
112            example = json!({
113                "success": false,
114                "error": "Internal Server Error",
115                "data": null
116            })
117        ),
118    )
119)]
120#[allow(dead_code)]
121fn doc_call_plugin() {}
122
123/// Calls a plugin method via GET request.
124///
125/// This endpoint is disabled by default. To enable it for a given plugin, set
126/// `allow_get_invocation: true` in the plugin configuration.
127///
128/// When invoked via GET:
129/// - `params` is an empty object (`{}`)
130/// - query parameters are passed to the plugin handler via `context.query`
131/// - wildcard route routing is supported the same way as POST (see `doc_call_plugin`)
132/// - Use the `route` query parameter or append the route to the URL path
133#[utoipa::path(
134    get,
135    path = "/api/v1/plugins/{plugin_id}/call",
136    tag = "Plugins",
137    operation_id = "callPluginGet",
138    summary = "Execute a plugin via GET (must be enabled per plugin)",
139    security(
140        ("bearer_auth" = [])
141    ),
142    params(
143        ("plugin_id" = String, Path, description = "The unique identifier of the plugin"),
144        ("route" = Option<String>, Query, description = "Optional route suffix for custom routing (e.g., '/verify'). Alternative to appending the route to the URL path.")
145    ),
146    responses(
147        (
148            status = 200,
149            description = "Plugin call successful",
150            body = ApiResponse<serde_json::Value>
151        ),
152        (
153            status = 405,
154            description = "Method Not Allowed (GET invocation disabled for this plugin)",
155            body = ApiResponse<String>,
156            example = json!({
157                "success": false,
158                "error": "GET requests are not enabled for this plugin. Set 'allow_get_invocation: true' in plugin configuration to enable.",
159                "data": null
160            })
161        ),
162        (
163            status = 401,
164            description = "Unauthorized",
165            body = ApiResponse<String>,
166            example = json!({
167                "success": false,
168                "error": "Unauthorized",
169                "data": null
170            })
171        ),
172        (
173            status = 404,
174            description = "Not Found",
175            body = ApiResponse<String>,
176            example = json!({
177                "success": false,
178                "error": "Plugin with ID plugin_id not found",
179                "data": null
180            })
181        ),
182        (
183            status = 429,
184            description = "Too Many Requests",
185            body = ApiResponse<String>,
186            example = json!({
187                "success": false,
188                "error": "Too Many Requests",
189                "data": null
190            })
191        ),
192        (
193            status = 500,
194            description = "Internal server error",
195            body = ApiResponse<String>,
196            example = json!({
197                "success": false,
198                "error": "Internal Server Error",
199                "data": null
200            })
201        )
202    )
203)]
204#[allow(dead_code)]
205fn doc_call_plugin_get() {}
206
207/// List plugins.
208#[utoipa::path(
209    get,
210    path = "/api/v1/plugins",
211    tag = "Plugins",
212    operation_id = "listPlugins",
213    security(
214        ("bearer_auth" = [])
215    ),
216    params(
217        ("page" = Option<usize>, Query, description = "Page number for pagination (starts at 1)"),
218        ("per_page" = Option<usize>, Query, description = "Number of items per page (default: 10)")
219    ),
220    responses(
221        (
222            status = 200,
223            description = "Plugins listed successfully",
224            body = ApiResponse<PaginatedResult<PluginModel>>
225        ),
226        (
227            status = 400,
228            description = "BadRequest",
229            body = ApiResponse<String>,
230            example = json!({
231                "success": false,
232                "error": "Bad Request",
233                "data": null
234            })
235        ),
236        (
237            status = 401,
238            description = "Unauthorized",
239            body = ApiResponse<String>,
240            example = json!({
241                "success": false,
242                "error": "Unauthorized",
243                "data": null
244            })
245        ),
246        (
247            status = 404,
248            description = "Not Found",
249            body = ApiResponse<String>,
250            example = json!({
251                "success": false,
252                "error": "Plugin with ID plugin_id not found",
253                "data": null
254            })
255        ),
256        (
257            status = 429,
258            description = "Too Many Requests",
259            body = ApiResponse<String>,
260            example = json!({
261                "success": false,
262                "error": "Too Many Requests",
263                "data": null
264            })
265        ),
266        (
267            status = 500,
268            description = "Internal server error",
269            body = ApiResponse<String>,
270            example = json!({
271                "success": false,
272                "error": "Internal Server Error",
273                "data": null
274            })
275        ),
276    )
277)]
278#[allow(dead_code)]
279fn doc_list_plugins() {}
280
281/// Get plugin by ID.
282#[utoipa::path(
283    get,
284    path = "/api/v1/plugins/{plugin_id}",
285    tag = "Plugins",
286    operation_id = "getPlugin",
287    summary = "Get plugin by ID",
288    security(
289        ("bearer_auth" = [])
290    ),
291    params(
292        ("plugin_id" = String, Path, description = "The unique identifier of the plugin")
293    ),
294    responses(
295        (
296            status = 200,
297            description = "Plugin retrieved successfully",
298            body = ApiResponse<PluginModel>,
299            example = json!({
300                "success": true,
301                "data": {
302                    "id": "my-plugin",
303                    "path": "plugins/my-plugin.ts",
304                    "timeout": 30,
305                    "emit_logs": false,
306                    "emit_traces": false,
307                    "raw_response": false,
308                    "allow_get_invocation": false,
309                    "config": {
310                        "featureFlag": true
311                    },
312                    "forward_logs": false
313                },
314                "error": null
315            })
316        ),
317        (
318            status = 401,
319            description = "Unauthorized",
320            body = ApiResponse<String>,
321            example = json!({
322                "success": false,
323                "error": "Unauthorized",
324                "data": null
325            })
326        ),
327        (
328            status = 404,
329            description = "Plugin not found",
330            body = ApiResponse<String>,
331            example = json!({
332                "success": false,
333                "error": "Plugin with id my-plugin not found",
334                "data": null
335            })
336        ),
337        (
338            status = 429,
339            description = "Too Many Requests",
340            body = ApiResponse<String>,
341            example = json!({
342                "success": false,
343                "error": "Too Many Requests",
344                "data": null
345            })
346        ),
347        (
348            status = 500,
349            description = "Internal server error",
350            body = ApiResponse<String>,
351            example = json!({
352                "success": false,
353                "error": "Internal Server Error",
354                "data": null
355            })
356        )
357    )
358)]
359#[allow(dead_code)]
360fn doc_get_plugin() {}
361
362/// Update plugin configuration.
363///
364/// Updates mutable plugin fields such as timeout, emit_logs, emit_traces,
365/// raw_response, allow_get_invocation, config, and forward_logs.
366/// The plugin id and path cannot be changed after creation.
367///
368/// All fields are optional - only the provided fields will be updated.
369/// To clear the `config` field, pass `"config": null`.
370#[utoipa::path(
371    patch,
372    path = "/api/v1/plugins/{plugin_id}",
373    tag = "Plugins",
374    operation_id = "updatePlugin",
375    summary = "Update plugin configuration",
376    security(
377        ("bearer_auth" = [])
378    ),
379    params(
380        ("plugin_id" = String, Path, description = "The unique identifier of the plugin")
381    ),
382    request_body(
383        content = UpdatePluginRequest,
384        description = "Plugin configuration update. All fields are optional.",
385        example = json!({
386            "timeout": 60,
387            "emit_logs": true,
388            "forward_logs": true,
389            "config": {
390                "featureFlag": true,
391                "apiKey": "xyz123"
392            }
393        })
394    ),
395    responses(
396        (
397            status = 200,
398            description = "Plugin updated successfully",
399            body = ApiResponse<PluginModel>,
400            example = json!({
401                "success": true,
402                "data": {
403                    "id": "my-plugin",
404                    "path": "plugins/my-plugin.ts",
405                    "timeout": 60,
406                    "emit_logs": true,
407                    "emit_traces": false,
408                    "raw_response": false,
409                    "allow_get_invocation": false,
410                    "config": {
411                        "featureFlag": true,
412                        "apiKey": "xyz123"
413                    },
414                    "forward_logs": true
415                },
416                "error": null
417            })
418        ),
419        (
420            status = 400,
421            description = "Bad Request (invalid timeout or other validation error)",
422            body = ApiResponse<String>,
423            example = json!({
424                "success": false,
425                "error": "Timeout must be greater than 0",
426                "data": null
427            })
428        ),
429        (
430            status = 401,
431            description = "Unauthorized",
432            body = ApiResponse<String>,
433            example = json!({
434                "success": false,
435                "error": "Unauthorized",
436                "data": null
437            })
438        ),
439        (
440            status = 404,
441            description = "Plugin not found",
442            body = ApiResponse<String>,
443            example = json!({
444                "success": false,
445                "error": "Plugin with id my-plugin not found",
446                "data": null
447            })
448        ),
449        (
450            status = 429,
451            description = "Too Many Requests",
452            body = ApiResponse<String>,
453            example = json!({
454                "success": false,
455                "error": "Too Many Requests",
456                "data": null
457            })
458        ),
459        (
460            status = 500,
461            description = "Internal server error",
462            body = ApiResponse<String>,
463            example = json!({
464                "success": false,
465                "error": "Internal Server Error",
466                "data": null
467            })
468        )
469    )
470)]
471#[allow(dead_code)]
472fn doc_update_plugin() {}