connectors 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +35 -0
  3. data/CONNECTORS_FRAMEWORK.md +799 -0
  4. data/CONTRIBUTING.md +60 -0
  5. data/MCP_CLIENT.md +168 -0
  6. data/MIT-LICENSE +20 -0
  7. data/README.md +146 -0
  8. data/app/connectors/clickup/connector.rb +13 -0
  9. data/app/connectors/gmail/api.rb +114 -0
  10. data/app/connectors/gmail/connector.rb +921 -0
  11. data/app/connectors/gmail/mime_builder.rb +261 -0
  12. data/app/connectors/gmail/mime_parser.rb +106 -0
  13. data/app/connectors/gmail/polling.rb +154 -0
  14. data/app/connectors/remote_mcp/connector.rb +18 -0
  15. data/app/connectors/resend/connector.rb +218 -0
  16. data/app/controllers/concerns/connectors/grant_access.rb +43 -0
  17. data/app/controllers/connectors/actions_controller.rb +66 -0
  18. data/app/controllers/connectors/application_controller.rb +5 -0
  19. data/app/controllers/connectors/credentials_controller.rb +245 -0
  20. data/app/controllers/connectors/grants_controller.rb +123 -0
  21. data/app/controllers/connectors/mcp_controller.rb +88 -0
  22. data/app/controllers/connectors/oauth_controller.rb +134 -0
  23. data/app/controllers/connectors/types_controller.rb +144 -0
  24. data/app/controllers/connectors/webhooks_controller.rb +105 -0
  25. data/app/jobs/connectors/application_job.rb +4 -0
  26. data/app/jobs/connectors/deliver_webhook_job.rb +27 -0
  27. data/app/jobs/connectors/poll_job.rb +49 -0
  28. data/app/models/connectors/application_record.rb +5 -0
  29. data/app/models/connectors/credential_share.rb +31 -0
  30. data/app/models/connectors/grant.rb +103 -0
  31. data/app/models/connectors/mcp_authorization.rb +6 -0
  32. data/app/models/connectors/mcp_interaction.rb +6 -0
  33. data/app/models/connectors/poll_state.rb +17 -0
  34. data/app/models/connectors/webhook_event.rb +19 -0
  35. data/config/routes.rb +68 -0
  36. data/db/migrate/20260518210324_create_connectors_grants.rb +49 -0
  37. data/db/migrate/20260518214609_create_connectors_webhook_events.rb +35 -0
  38. data/db/migrate/20260521140000_create_connectors_credential_shares.rb +26 -0
  39. data/db/migrate/20260922120000_create_connectors_poll_states.rb +12 -0
  40. data/db/migrate/20260922130000_create_connectors_mcp_transactions.rb +18 -0
  41. data/docs/adding-connectors.md +92 -0
  42. data/docs/architecture.md +71 -0
  43. data/docs/releasing.md +60 -0
  44. data/lib/connectors/action.rb +90 -0
  45. data/lib/connectors/action_builder.rb +125 -0
  46. data/lib/connectors/action_runner.rb +99 -0
  47. data/lib/connectors/auth/scheme/api_key.rb +51 -0
  48. data/lib/connectors/auth/scheme/oauth2.rb +23 -0
  49. data/lib/connectors/auth/scheme.rb +35 -0
  50. data/lib/connectors/auth.rb +4 -0
  51. data/lib/connectors/auth_injection.rb +65 -0
  52. data/lib/connectors/client_builder.rb +87 -0
  53. data/lib/connectors/configuration.rb +138 -0
  54. data/lib/connectors/connector.rb +565 -0
  55. data/lib/connectors/credential_schema.rb +219 -0
  56. data/lib/connectors/credential_tester.rb +85 -0
  57. data/lib/connectors/credential_type_registry.rb +162 -0
  58. data/lib/connectors/credential_types/http_auth.rb +171 -0
  59. data/lib/connectors/engine.rb +123 -0
  60. data/lib/connectors/errors.rb +88 -0
  61. data/lib/connectors/grant_policy.rb +13 -0
  62. data/lib/connectors/mcp/access.rb +50 -0
  63. data/lib/connectors/mcp/authorization.rb +177 -0
  64. data/lib/connectors/mcp/authorization_context.rb +25 -0
  65. data/lib/connectors/mcp/authorization_discovery.rb +42 -0
  66. data/lib/connectors/mcp/cancellation.rb +36 -0
  67. data/lib/connectors/mcp/client.rb +137 -0
  68. data/lib/connectors/mcp/connection_config.rb +48 -0
  69. data/lib/connectors/mcp/http.rb +108 -0
  70. data/lib/connectors/mcp/interaction.rb +82 -0
  71. data/lib/connectors/mcp/pending_transaction.rb +26 -0
  72. data/lib/connectors/mcp/protocol/2026-07-28.json +3963 -0
  73. data/lib/connectors/mcp/protocol/LICENSE +216 -0
  74. data/lib/connectors/mcp/protocol/README.md +8 -0
  75. data/lib/connectors/mcp/protocol_schema.rb +30 -0
  76. data/lib/connectors/mcp/schema.rb +45 -0
  77. data/lib/connectors/mcp/settings.rb +27 -0
  78. data/lib/connectors/mcp/token_endpoint.rb +48 -0
  79. data/lib/connectors/mcp/transport.rb +105 -0
  80. data/lib/connectors/mcp.rb +69 -0
  81. data/lib/connectors/middleware/authenticate_generic.rb +79 -0
  82. data/lib/connectors/middleware/auto_refresh.rb +71 -0
  83. data/lib/connectors/middleware/error_normalization.rb +45 -0
  84. data/lib/connectors/middleware/grant_status.rb +19 -0
  85. data/lib/connectors/middleware/pre_authentication.rb +54 -0
  86. data/lib/connectors/middleware/rate_limit.rb +40 -0
  87. data/lib/connectors/oauth/authorize_url.rb +78 -0
  88. data/lib/connectors/oauth/client_authentication.rb +24 -0
  89. data/lib/connectors/oauth/client_credentials.rb +43 -0
  90. data/lib/connectors/oauth/grant_writer.rb +73 -0
  91. data/lib/connectors/oauth/pkce.rb +32 -0
  92. data/lib/connectors/oauth/revoke.rb +72 -0
  93. data/lib/connectors/oauth/state.rb +41 -0
  94. data/lib/connectors/oauth/token_exchange.rb +69 -0
  95. data/lib/connectors/oauth/token_response.rb +40 -0
  96. data/lib/connectors/oauth.rb +4 -0
  97. data/lib/connectors/oauth1.rb +151 -0
  98. data/lib/connectors/permission_check.rb +33 -0
  99. data/lib/connectors/poll_runner.rb +50 -0
  100. data/lib/connectors/pre_authentication_helpers.rb +76 -0
  101. data/lib/connectors/registry.rb +38 -0
  102. data/lib/connectors/version.rb +3 -0
  103. data/lib/connectors/webhook_context.rb +62 -0
  104. data/lib/connectors/webhook_lifecycle.rb +69 -0
  105. data/lib/connectors/webhook_methods.rb +48 -0
  106. data/lib/connectors/webhooks/verifier.rb +31 -0
  107. data/lib/connectors/webhooks.rb +5 -0
  108. data/lib/connectors.rb +50 -0
  109. data/lib/tasks/connectors_mcp.rake +9 -0
  110. data/lib/tasks/connectors_tasks.rake +4 -0
  111. data/openapi.yaml +1099 -0
  112. metadata +263 -0
data/openapi.yaml ADDED
@@ -0,0 +1,1099 @@
1
+ openapi: 3.1.0
2
+ info:
3
+ title: Connectors Engine API
4
+ version: "0.1.0"
5
+ description: |
6
+ Rails engine exposing n8n's connections / credentials layer as a host-mountable API.
7
+ Every endpoint is namespaced under the engine's mount path (default `/connectors`).
8
+
9
+ **Auth model.** The engine itself doesn't authenticate requests — the host wires
10
+ `Connectors.configuration.current_owner_resolver` to extract the requester from the
11
+ controller (session, JWT, API key — host's choice). Endpoints that mutate
12
+ credentials require an `Owner` to be resolvable; those that read enforce
13
+ role-based visibility via `principal_resolver` (Phase 9).
14
+
15
+ **Source of truth.** This document is hand-maintained alongside the engine.
16
+ Cross-check against `connectors/config/routes.rb` and the controller files —
17
+ they are the implementation.
18
+
19
+ **Mount path.** The host picks where the engine lives. Reference deployment
20
+ (flow-api) uses `/api/v1/connectors`. The engine introspects its actual
21
+ mount via `Connectors::Engine.mount_path`, so OAuth callback URLs and
22
+ webhook subscribe URLs in responses always reflect reality.
23
+ contact:
24
+ name: Connectors framework
25
+ license:
26
+ name: MIT
27
+ servers:
28
+ - url: "{host}{mount}"
29
+ description: Engine mount in the host app
30
+ variables:
31
+ host:
32
+ default: "http://localhost:3000"
33
+ description: Scheme + host of the host application
34
+ mount:
35
+ default: "/api/v1/connectors"
36
+ description: Path where the host mounts Connectors::Engine
37
+
38
+ tags:
39
+ - name: MCP
40
+ description: External MCP 2026-07-28 tools and authorization
41
+ - name: Catalog
42
+ description: List of registered connector types
43
+ - name: Credentials
44
+ description: CRUD + sharing + revocation
45
+ - name: Grants
46
+ description: "@deprecated alias surface + manual harnesses (test, subscribe, poll)"
47
+ - name: OAuth
48
+ description: OAuth1 + OAuth2 (all grant types) authorize / callback / revoke
49
+ - name: Webhooks
50
+ description: Inbound webhook delivery
51
+ - name: External Secrets
52
+ description: Managed credentials with vault-resolved values
53
+
54
+ paths:
55
+ /credentials/{id}/mcp/tools:
56
+ parameters:
57
+ - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
58
+ get:
59
+ tags: [MCP]
60
+ operationId: listMcpTools
61
+ summary: Discover native MCP tool descriptors for a visible connection
62
+ responses:
63
+ "200":
64
+ description: Complete paginated tool catalog, with original schemas and metadata
65
+ content:
66
+ application/json:
67
+ schema:
68
+ type: object
69
+ required: [tools]
70
+ properties:
71
+ tools: { type: array, items: { type: object, additionalProperties: true } }
72
+ "401": { $ref: "#/components/responses/McpFailure" }
73
+ "403": { $ref: "#/components/responses/McpFailure" }
74
+ "429": { $ref: "#/components/responses/McpRateLimited" }
75
+ "502": { $ref: "#/components/responses/McpFailure" }
76
+
77
+ /credentials/{id}/mcp/tools/call:
78
+ parameters:
79
+ - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
80
+ post:
81
+ tags: [MCP]
82
+ operationId: callMcpTool
83
+ summary: Invoke an MCP tool with editor access
84
+ description: No automatic replay after a timeout or disconnected response. Tool execution errors remain MCP results with isError=true.
85
+ requestBody:
86
+ required: true
87
+ content:
88
+ application/json:
89
+ schema:
90
+ type: object
91
+ required: [name, arguments]
92
+ properties:
93
+ name: { type: string }
94
+ arguments: { type: object, additionalProperties: true }
95
+ responses:
96
+ "200":
97
+ description: Native tool result, or input_required with a durable interaction_id
98
+ content:
99
+ application/json:
100
+ schema: { $ref: "#/components/schemas/McpToolResult" }
101
+ "401": { $ref: "#/components/responses/McpFailure" }
102
+ "403": { $ref: "#/components/responses/McpFailure" }
103
+ "422": { $ref: "#/components/responses/McpFailure" }
104
+ "429": { $ref: "#/components/responses/McpRateLimited" }
105
+ "502": { $ref: "#/components/responses/McpFailure" }
106
+
107
+ /credentials/{id}/mcp/interactions/resume:
108
+ parameters:
109
+ - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
110
+ post:
111
+ tags: [MCP]
112
+ operationId: resumeMcpInteraction
113
+ summary: Resume a pending interaction once as the initiating actor
114
+ requestBody:
115
+ required: true
116
+ content:
117
+ application/json:
118
+ schema:
119
+ type: object
120
+ required: [interaction_id, responses]
121
+ properties:
122
+ interaction_id: { type: string, format: uuid }
123
+ responses:
124
+ type: object
125
+ additionalProperties:
126
+ type: object
127
+ required: [action]
128
+ properties:
129
+ action: { type: string, enum: [accept, decline, cancel] }
130
+ content: { type: object, additionalProperties: true }
131
+ responses:
132
+ "200":
133
+ description: Native completed result or next interaction
134
+ content:
135
+ application/json:
136
+ schema: { $ref: "#/components/schemas/McpToolResult" }
137
+ "401": { $ref: "#/components/responses/McpFailure" }
138
+ "403": { $ref: "#/components/responses/McpFailure" }
139
+ "422": { $ref: "#/components/responses/McpFailure" }
140
+ "429": { $ref: "#/components/responses/McpRateLimited" }
141
+ "502": { $ref: "#/components/responses/McpFailure" }
142
+
143
+ /credentials/{id}/mcp/authorize:
144
+ parameters:
145
+ - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
146
+ post:
147
+ tags: [MCP]
148
+ operationId: authorizeMcpConnection
149
+ summary: Start owner-only OAuth consent or scope upgrade
150
+ requestBody:
151
+ content:
152
+ application/json:
153
+ schema:
154
+ type: object
155
+ properties:
156
+ authorization_context: { type: string, description: Encrypted context returned by a failed MCP operation }
157
+ responses:
158
+ "200":
159
+ description: Browser authorization URL with durable PKCE state
160
+ content:
161
+ application/json:
162
+ schema:
163
+ type: object
164
+ required: [authorization_url]
165
+ properties:
166
+ authorization_url: { type: string, format: uri }
167
+ "403": { $ref: "#/components/responses/McpFailure" }
168
+ "422": { $ref: "#/components/responses/McpFailure" }
169
+ "429": { $ref: "#/components/responses/McpRateLimited" }
170
+ "502": { $ref: "#/components/responses/McpFailure" }
171
+
172
+ /mcp/oauth/callback:
173
+ get:
174
+ tags: [MCP]
175
+ operationId: completeMcpAuthorizationRedirect
176
+ summary: Complete OAuth as the authenticated initiating owner
177
+ parameters:
178
+ - { name: state, in: query, required: true, schema: { type: string } }
179
+ - { name: code, in: query, schema: { type: string } }
180
+ - { name: iss, in: query, schema: { type: string } }
181
+ - { name: error, in: query, schema: { type: string } }
182
+ responses:
183
+ "200": { description: Authorization completed }
184
+ "403": { $ref: "#/components/responses/McpFailure" }
185
+ "404": { $ref: "#/components/responses/NotFound" }
186
+ "429": { $ref: "#/components/responses/McpRateLimited" }
187
+ "502": { $ref: "#/components/responses/McpFailure" }
188
+ post:
189
+ tags: [MCP]
190
+ operationId: completeMcpAuthorizationExchange
191
+ summary: Complete OAuth from a frontend callback
192
+ requestBody:
193
+ required: true
194
+ content:
195
+ application/json:
196
+ schema:
197
+ type: object
198
+ required: [state]
199
+ properties:
200
+ state: { type: string }
201
+ code: { type: string }
202
+ iss: { type: string }
203
+ error: { type: string }
204
+ responses:
205
+ "200": { description: Authorization completed }
206
+ "403": { $ref: "#/components/responses/McpFailure" }
207
+ "404": { $ref: "#/components/responses/NotFound" }
208
+ "429": { $ref: "#/components/responses/McpRateLimited" }
209
+ "502": { $ref: "#/components/responses/McpFailure" }
210
+
211
+ # =========================================================================
212
+ # CATALOG
213
+ # =========================================================================
214
+ /types:
215
+ get:
216
+ tags: [Catalog]
217
+ summary: List every registered connector type
218
+ operationId: listTypes
219
+ responses:
220
+ "200":
221
+ description: Catalog
222
+ content:
223
+ application/json:
224
+ schema:
225
+ type: object
226
+ properties:
227
+ types:
228
+ type: array
229
+ items: { $ref: "#/components/schemas/CredentialType" }
230
+
231
+ /types/{name}:
232
+ get:
233
+ tags: [Catalog]
234
+ summary: Full description of one connector type
235
+ operationId: getType
236
+ parameters:
237
+ - { name: name, in: path, required: true, schema: { type: string } }
238
+ responses:
239
+ "200":
240
+ description: Single type
241
+ content:
242
+ application/json:
243
+ schema: { $ref: "#/components/schemas/CredentialType" }
244
+ "404": { $ref: "#/components/responses/NotFound" }
245
+
246
+ # =========================================================================
247
+ # CREDENTIALS CRUD
248
+ # =========================================================================
249
+ /credentials:
250
+ get:
251
+ tags: [Credentials]
252
+ summary: List credentials visible to the requester (owned + shared)
253
+ operationId: listCredentials
254
+ parameters:
255
+ - { name: type, in: query, schema: { type: string } }
256
+ - { name: include_data, in: query, description: "Owner-only decrypted data; MCP returns public configuration for all visible connections", schema: { type: boolean, default: false } }
257
+ responses:
258
+ "200":
259
+ description: Visible credentials
260
+ content:
261
+ application/json:
262
+ schema:
263
+ type: object
264
+ properties:
265
+ credentials:
266
+ type: array
267
+ items: { $ref: "#/components/schemas/Credential" }
268
+ "401": { $ref: "#/components/responses/Unauthorized" }
269
+ post:
270
+ tags: [Credentials]
271
+ summary: Create a credential (paste-the-key flow)
272
+ operationId: createCredential
273
+ requestBody:
274
+ required: true
275
+ content:
276
+ application/json:
277
+ schema:
278
+ type: object
279
+ required: [type]
280
+ properties:
281
+ type: { type: string, description: "connector key" }
282
+ name: { type: string }
283
+ data: { type: object, additionalProperties: true, description: "credential values" }
284
+ external_ref: { type: string, description: "vault reference (Phase 10)" }
285
+ is_managed: { type: boolean, default: false }
286
+ responses:
287
+ "201":
288
+ description: Created
289
+ content: { application/json: { schema: { $ref: "#/components/schemas/Credential" } } }
290
+ "401": { $ref: "#/components/responses/Unauthorized" }
291
+ "404": { $ref: "#/components/responses/NotFound" }
292
+
293
+ /credentials/for-workflow:
294
+ get:
295
+ tags: [Credentials]
296
+ summary: List credentials available to a workflow editor
297
+ operationId: listCredentialsForWorkflow
298
+ parameters:
299
+ - { name: type, in: query, schema: { type: string } }
300
+ responses:
301
+ "200":
302
+ description: Visible credentials
303
+ content:
304
+ application/json:
305
+ schema:
306
+ type: object
307
+ properties:
308
+ credentials:
309
+ type: array
310
+ items: { $ref: "#/components/schemas/Credential" }
311
+
312
+ /credentials/new:
313
+ get:
314
+ tags: [Credentials]
315
+ summary: Server-generated unique default name ("Resend account 3")
316
+ operationId: newCredentialName
317
+ parameters:
318
+ - { name: type, in: query, required: true, schema: { type: string } }
319
+ responses:
320
+ "200":
321
+ description: Suggested name
322
+ content:
323
+ application/json:
324
+ schema:
325
+ type: object
326
+ properties: { name: { type: string } }
327
+ "404": { $ref: "#/components/responses/NotFound" }
328
+
329
+ /credentials/test:
330
+ post:
331
+ tags: [Credentials]
332
+ summary: Test an unsaved credential
333
+ operationId: testUnsavedCredential
334
+ requestBody:
335
+ required: true
336
+ content:
337
+ application/json:
338
+ schema:
339
+ type: object
340
+ required: [type]
341
+ properties:
342
+ type: { type: string }
343
+ data: { type: object, additionalProperties: true }
344
+ responses:
345
+ "200":
346
+ description: Test passed
347
+ content: { application/json: { schema: { $ref: "#/components/schemas/TestResult" } } }
348
+ "422":
349
+ description: Test failed (provider rejected)
350
+ content: { application/json: { schema: { $ref: "#/components/schemas/TestResult" } } }
351
+
352
+ /credentials/{id}:
353
+ parameters:
354
+ - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
355
+ get:
356
+ tags: [Credentials]
357
+ summary: Show one credential (viewer-or-better)
358
+ operationId: getCredential
359
+ parameters:
360
+ - { name: include_data, in: query, description: "Owner-only decrypted data; MCP returns public configuration for all visible connections", schema: { type: boolean, default: false } }
361
+ responses:
362
+ "200":
363
+ description: Credential
364
+ content: { application/json: { schema: { $ref: "#/components/schemas/Credential" } } }
365
+ "404": { $ref: "#/components/responses/NotFound" }
366
+ patch:
367
+ tags: [Credentials]
368
+ summary: Update a credential (editor-or-owner)
369
+ operationId: updateCredential
370
+ requestBody:
371
+ required: true
372
+ content:
373
+ application/json:
374
+ schema:
375
+ type: object
376
+ properties:
377
+ name: { type: string }
378
+ data: { type: object, additionalProperties: true }
379
+ responses:
380
+ "200":
381
+ description: Updated
382
+ content: { application/json: { schema: { $ref: "#/components/schemas/Credential" } } }
383
+ "401": { $ref: "#/components/responses/Unauthorized" }
384
+ put:
385
+ tags: [Credentials]
386
+ summary: Alias for PATCH (back-compat)
387
+ operationId: replaceCredential
388
+ requestBody:
389
+ required: true
390
+ content:
391
+ application/json:
392
+ schema:
393
+ type: object
394
+ properties:
395
+ name: { type: string }
396
+ data: { type: object, additionalProperties: true }
397
+ responses:
398
+ "200":
399
+ description: Updated
400
+ content: { application/json: { schema: { $ref: "#/components/schemas/Credential" } } }
401
+ delete:
402
+ tags: [Credentials]
403
+ summary: Delete a credential (owner only)
404
+ operationId: deleteCredential
405
+ responses:
406
+ "204": { description: Deleted }
407
+ "401": { $ref: "#/components/responses/Unauthorized" }
408
+
409
+ # =========================================================================
410
+ # ACTIONS — what a credential can DO
411
+ # =========================================================================
412
+ /credentials/{id}/actions:
413
+ parameters:
414
+ - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
415
+ get:
416
+ tags: [Actions]
417
+ summary: List actions available on this credential's connector
418
+ operationId: listCredentialActions
419
+ responses:
420
+ "200":
421
+ description: Actions
422
+ content:
423
+ application/json:
424
+ schema:
425
+ type: object
426
+ properties:
427
+ actions:
428
+ type: array
429
+ items: { $ref: "#/components/schemas/Action" }
430
+ "404": { $ref: "#/components/responses/NotFound" }
431
+
432
+ /credentials/{id}/actions/{name}:
433
+ parameters:
434
+ - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
435
+ - { name: name, in: path, required: true, schema: { type: string } }
436
+ post:
437
+ tags: [Actions]
438
+ summary: Invoke a connector action with this credential
439
+ description: |
440
+ Validates `data` against the action's param schema, then runs the
441
+ action's execute block in the connector instance's context. Returns
442
+ a normalized envelope — `status: "ok"` with `data`, or `status: "error"`
443
+ with a typed `error` object (`invalid_params`, `authentication_failed`,
444
+ `forbidden`, `rate_limited`, `api_error`).
445
+ operationId: invokeCredentialAction
446
+ requestBody:
447
+ required: true
448
+ content:
449
+ application/json:
450
+ schema:
451
+ type: object
452
+ properties:
453
+ data: { type: object, additionalProperties: true, description: "action input params" }
454
+ responses:
455
+ "200":
456
+ description: Action succeeded
457
+ content:
458
+ application/json:
459
+ schema: { $ref: "#/components/schemas/ActionResult" }
460
+ "401": { $ref: "#/components/responses/Unauthorized" }
461
+ "403":
462
+ description: Provider permission denied
463
+ content:
464
+ application/json:
465
+ schema: { $ref: "#/components/schemas/ActionResult" }
466
+ "404": { $ref: "#/components/responses/NotFound" }
467
+ "422":
468
+ description: Invalid params
469
+ content:
470
+ application/json:
471
+ schema: { $ref: "#/components/schemas/ActionResult" }
472
+ "429":
473
+ description: Provider rate-limited the request
474
+ content:
475
+ application/json:
476
+ schema: { $ref: "#/components/schemas/ActionResult" }
477
+ "502":
478
+ description: Provider returned an error
479
+ content:
480
+ application/json:
481
+ schema: { $ref: "#/components/schemas/ActionResult" }
482
+
483
+ /credentials/{id}/revoke:
484
+ parameters:
485
+ - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
486
+ post:
487
+ tags: [Credentials, OAuth]
488
+ summary: RFC 7009 token revocation
489
+ operationId: revokeCredential
490
+ responses:
491
+ "200":
492
+ description: Revoked
493
+ content:
494
+ application/json:
495
+ schema:
496
+ type: object
497
+ properties:
498
+ grant_id: { type: string, format: uuid }
499
+ status: { type: string, example: "revoked" }
500
+ "401": { $ref: "#/components/responses/Unauthorized" }
501
+
502
+ /credentials/{id}/share:
503
+ parameters:
504
+ - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
505
+ put:
506
+ tags: [Credentials]
507
+ summary: Share a credential with a principal (owner only, idempotent)
508
+ operationId: shareCredential
509
+ requestBody:
510
+ required: true
511
+ content:
512
+ application/json:
513
+ schema: { $ref: "#/components/schemas/SharePayload" }
514
+ responses:
515
+ "200":
516
+ description: Share record
517
+ content: { application/json: { schema: { $ref: "#/components/schemas/CredentialShare" } } }
518
+ "401": { $ref: "#/components/responses/Unauthorized" }
519
+ delete:
520
+ tags: [Credentials]
521
+ summary: Revoke a share
522
+ operationId: unshareCredential
523
+ requestBody:
524
+ required: true
525
+ content:
526
+ application/json:
527
+ schema:
528
+ type: object
529
+ required: [principal_type, principal_id]
530
+ properties:
531
+ principal_type: { type: string }
532
+ principal_id: { type: string, format: uuid }
533
+ responses:
534
+ "204": { description: Unshared }
535
+ "401": { $ref: "#/components/responses/Unauthorized" }
536
+ "404": { $ref: "#/components/responses/NotFound" }
537
+
538
+ /credentials/{id}/transfer:
539
+ parameters:
540
+ - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
541
+ put:
542
+ tags: [Credentials]
543
+ summary: Transfer ownership to a different owner (owner only)
544
+ operationId: transferCredential
545
+ requestBody:
546
+ required: true
547
+ content:
548
+ application/json:
549
+ schema:
550
+ type: object
551
+ required: [owner_id]
552
+ properties: { owner_id: { type: string, format: uuid } }
553
+ responses:
554
+ "200":
555
+ description: Reassigned
556
+ content: { application/json: { schema: { $ref: "#/components/schemas/Credential" } } }
557
+ "401": { $ref: "#/components/responses/Unauthorized" }
558
+
559
+ # =========================================================================
560
+ # GRANTS — manual harnesses
561
+ # =========================================================================
562
+ /grants:
563
+ get:
564
+ tags: [Grants]
565
+ summary: "@deprecated alias for /credentials index"
566
+ operationId: listGrants
567
+ parameters:
568
+ - { name: connector_key, in: query, schema: { type: string } }
569
+ responses:
570
+ "200":
571
+ description: Grants
572
+ content:
573
+ application/json:
574
+ schema:
575
+ type: object
576
+ properties:
577
+ grants:
578
+ type: array
579
+ items: { $ref: "#/components/schemas/Credential" }
580
+
581
+ /grants/{id}/test:
582
+ parameters:
583
+ - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
584
+ post:
585
+ tags: [Grants]
586
+ summary: Run the connector's declared test_request against the live provider
587
+ operationId: testGrant
588
+ responses:
589
+ "200":
590
+ description: Test passed
591
+ content: { application/json: { schema: { $ref: "#/components/schemas/TestResult" } } }
592
+ "422":
593
+ description: Test failed
594
+ content: { application/json: { schema: { $ref: "#/components/schemas/TestResult" } } }
595
+
596
+ /grants/{id}/webhook_subscribe:
597
+ parameters:
598
+ - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
599
+ post:
600
+ tags: [Grants, Webhooks]
601
+ summary: Run the connector's webhook_methods subscribe lifecycle
602
+ operationId: subscribeWebhook
603
+ requestBody:
604
+ required: false
605
+ content:
606
+ application/json:
607
+ schema:
608
+ type: object
609
+ properties:
610
+ webhook_name: { type: string, default: "default" }
611
+ hook_url: { type: string, format: uri }
612
+ responses:
613
+ "200":
614
+ description: Subscribed or already-exists
615
+ content:
616
+ application/json:
617
+ schema:
618
+ type: object
619
+ properties:
620
+ status: { type: string, enum: [created, exists] }
621
+ webhook_name: { type: string }
622
+ hook_url: { type: string }
623
+ static_data: { type: object, additionalProperties: true }
624
+ delete:
625
+ tags: [Grants, Webhooks]
626
+ summary: Unsubscribe — run the delete callback
627
+ operationId: unsubscribeWebhook
628
+ requestBody:
629
+ required: false
630
+ content:
631
+ application/json:
632
+ schema:
633
+ type: object
634
+ properties: { webhook_name: { type: string, default: "default" } }
635
+ responses:
636
+ "200":
637
+ description: Deleted
638
+ content:
639
+ application/json:
640
+ schema:
641
+ type: object
642
+ properties:
643
+ status: { type: string, example: "deleted" }
644
+ webhook_name: { type: string }
645
+ static_data: { type: object, additionalProperties: true }
646
+
647
+ /grants/{id}/poll:
648
+ parameters:
649
+ - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
650
+ post:
651
+ tags: [Grants]
652
+ summary: Fire the polling block once
653
+ operationId: pollGrant
654
+ requestBody:
655
+ required: false
656
+ content:
657
+ application/json:
658
+ schema:
659
+ type: object
660
+ properties:
661
+ instance_key:
662
+ type: string
663
+ minLength: 1
664
+ description: Stable consumer identifier; isolates cursor and filter state. Omit for the legacy per-grant cursor.
665
+ responses:
666
+ "400":
667
+ description: Invalid instance key
668
+ "404": { $ref: "#/components/responses/NotFound" }
669
+ "200":
670
+ description: Items + new cursor
671
+ content:
672
+ application/json:
673
+ schema:
674
+ type: object
675
+ properties:
676
+ items: { type: array, items: { type: object, additionalProperties: true } }
677
+ static_data: { type: object, additionalProperties: true }
678
+
679
+ # =========================================================================
680
+ # OAUTH
681
+ # =========================================================================
682
+ /{connector_key}/authorize:
683
+ parameters:
684
+ - { name: connector_key, in: path, required: true, schema: { type: string } }
685
+ get:
686
+ tags: [OAuth]
687
+ summary: Start OAuth authorize flow
688
+ operationId: oauthAuthorize
689
+ description: |
690
+ Creates a new connection unless grant_id selects an owned connection to reconnect.
691
+ Reconnection must retain the provider and known account identity.
692
+ Dispatches by connector configuration:
693
+ - `oauth2 grant_type: "authorizationCode"|"pkce"` → 302 to provider authorize URL
694
+ - `oauth2 grant_type: "clientCredentials"` → exchange in-place, returns grant JSON
695
+ - `oauth1` → request-token leg, then 302
696
+ parameters:
697
+ - { name: grant_id, in: query, schema: { type: string, format: uuid }, description: "Reconnect this owned connection instead of creating a new one." }
698
+ - { name: return_to, in: query, schema: { type: string }, description: "URL to redirect to after callback completes" }
699
+ - { name: scope, in: query, schema: { type: string }, description: "Override the connector's declared default scope. The connector SUGGESTS scopes; the host decides." }
700
+ responses:
701
+ "302":
702
+ description: Redirect to provider (or to return_to after callback)
703
+ headers:
704
+ Location: { schema: { type: string } }
705
+ "200":
706
+ description: clientCredentials grant created in-place
707
+ content:
708
+ application/json:
709
+ schema:
710
+ type: object
711
+ properties:
712
+ grant_id: { type: string, format: uuid }
713
+ connector_key: { type: string }
714
+ status: { type: string, example: "active" }
715
+ "401": { $ref: "#/components/responses/Unauthorized" }
716
+ "404": { $ref: "#/components/responses/NotFound" }
717
+
718
+ /{connector_key}/authorize.json:
719
+ parameters:
720
+ - { name: connector_key, in: path, required: true, schema: { type: string } }
721
+ get:
722
+ tags: [OAuth]
723
+ summary: Return the authorize URL as JSON for popup-based flows
724
+ operationId: oauthAuthorizeJson
725
+ description: Creates a new connection unless grant_id explicitly selects an owned connection to reconnect.
726
+ parameters:
727
+ - { name: grant_id, in: query, schema: { type: string, format: uuid }, description: "Reconnect this owned connection instead of creating a new one." }
728
+ - { name: return_to, in: query, schema: { type: string } }
729
+ - { name: scope, in: query, schema: { type: string }, description: "Override the connector's declared default scope (see /authorize for details)." }
730
+ responses:
731
+ "200":
732
+ description: Authorize URL
733
+ content:
734
+ application/json:
735
+ schema:
736
+ type: object
737
+ properties:
738
+ authorize_url: { type: string, format: uri }
739
+ connector_key: { type: string }
740
+
741
+ /{connector_key}/callback:
742
+ parameters:
743
+ - { name: connector_key, in: path, required: true, schema: { type: string } }
744
+ get:
745
+ tags: [OAuth]
746
+ summary: OAuth callback (OAuth2 + OAuth1)
747
+ operationId: oauthCallback
748
+ description: |
749
+ Accepts EITHER OAuth2 callback (`?code=&state=`) OR OAuth1 callback
750
+ (`?oauth_token=&oauth_verifier=&state=`). Validates encrypted state, exchanges
751
+ for tokens, creates a Grant or reconnects the explicitly selected Grant, and either redirects to `state.return_to` or returns the grant JSON.
752
+ parameters:
753
+ - { name: code, in: query, schema: { type: string } }
754
+ - { name: state, in: query, required: true, schema: { type: string } }
755
+ - { name: oauth_token, in: query, schema: { type: string } }
756
+ - { name: oauth_verifier, in: query, schema: { type: string } }
757
+ responses:
758
+ "302":
759
+ description: Redirect to state.return_to
760
+ headers: { Location: { schema: { type: string } } }
761
+ "200":
762
+ description: Grant JSON
763
+ content:
764
+ application/json:
765
+ schema:
766
+ type: object
767
+ properties:
768
+ grant_id: { type: string, format: uuid }
769
+ connector_key: { type: string }
770
+ status: { type: string }
771
+ "401":
772
+ description: Invalid state or token exchange failed
773
+ content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
774
+ "404":
775
+ description: Owner not found
776
+ content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
777
+
778
+ # =========================================================================
779
+ # WEBHOOKS (inbound)
780
+ # =========================================================================
781
+ /{connector_key}/webhook:
782
+ parameters:
783
+ - { name: connector_key, in: path, required: true, schema: { type: string } }
784
+ post:
785
+ tags: [Webhooks]
786
+ summary: App-level webhook (single URL per app; grant resolved from payload)
787
+ operationId: receiveAppWebhook
788
+ requestBody:
789
+ required: true
790
+ content:
791
+ application/json: { schema: { type: object, additionalProperties: true } }
792
+ application/x-www-form-urlencoded: { schema: { type: object, additionalProperties: true } }
793
+ responses:
794
+ "202": { $ref: "#/components/responses/WebhookAccepted" }
795
+ "200":
796
+ description: URL-verification challenge response
797
+ content: { application/json: { schema: { type: object, additionalProperties: true } } }
798
+ "401": { $ref: "#/components/responses/Unauthorized" }
799
+ "404": { $ref: "#/components/responses/NotFound" }
800
+
801
+ /{connector_key}/webhook/{webhook_name}:
802
+ parameters:
803
+ - { name: connector_key, in: path, required: true, schema: { type: string } }
804
+ - { name: webhook_name, in: path, required: true, schema: { type: string } }
805
+ post:
806
+ tags: [Webhooks]
807
+ summary: App-level named-group webhook (Phase 7)
808
+ operationId: receiveAppWebhookNamed
809
+ requestBody:
810
+ required: true
811
+ content:
812
+ application/json: { schema: { type: object, additionalProperties: true } }
813
+ responses:
814
+ "202": { $ref: "#/components/responses/WebhookAccepted" }
815
+ "200":
816
+ description: URL-verification challenge response
817
+ content: { application/json: { schema: { type: object, additionalProperties: true } } }
818
+
819
+ /{connector_key}/{grant_id}/webhook:
820
+ parameters:
821
+ - { name: connector_key, in: path, required: true, schema: { type: string } }
822
+ - { name: grant_id, in: path, required: true, schema: { type: string, format: uuid } }
823
+ post:
824
+ tags: [Webhooks]
825
+ summary: Per-grant webhook (grant id in URL)
826
+ operationId: receiveGrantWebhook
827
+ requestBody:
828
+ required: true
829
+ content:
830
+ application/json: { schema: { type: object, additionalProperties: true } }
831
+ responses:
832
+ "202": { $ref: "#/components/responses/WebhookAccepted" }
833
+
834
+ /{connector_key}/{grant_id}/webhook/{webhook_name}:
835
+ parameters:
836
+ - { name: connector_key, in: path, required: true, schema: { type: string } }
837
+ - { name: grant_id, in: path, required: true, schema: { type: string, format: uuid } }
838
+ - { name: webhook_name, in: path, required: true, schema: { type: string } }
839
+ post:
840
+ tags: [Webhooks]
841
+ summary: Per-grant named-group webhook
842
+ operationId: receiveGrantWebhookNamed
843
+ requestBody:
844
+ required: true
845
+ content:
846
+ application/json: { schema: { type: object, additionalProperties: true } }
847
+ responses:
848
+ "202": { $ref: "#/components/responses/WebhookAccepted" }
849
+
850
+ components:
851
+ schemas:
852
+ McpToolResult:
853
+ type: object
854
+ additionalProperties: true
855
+ properties:
856
+ resultType: { type: string, enum: [complete, input_required] }
857
+ content: { type: array, items: { type: object, additionalProperties: true } }
858
+ structuredContent: {}
859
+ isError: { type: boolean }
860
+ interaction_id: { type: string, format: uuid }
861
+ inputRequests: { type: object, additionalProperties: true }
862
+ McpError:
863
+ type: object
864
+ required: [error]
865
+ properties:
866
+ error:
867
+ type: object
868
+ required: [type, message]
869
+ properties:
870
+ type: { type: string }
871
+ message: { type: string }
872
+ retry_after: { type: string, description: "rate_limited only; valid upstream delay-seconds or HTTP date, omitted if unavailable" }
873
+ authorization_context:
874
+ type: string
875
+ description: Encrypted owner consent context; pass to the authorize endpoint for scope upgrades.
876
+ CredentialType:
877
+ type: object
878
+ properties:
879
+ name: { type: string, description: "machine key" }
880
+ display_name: { type: string }
881
+ key: { type: string, deprecated: true, description: "@deprecated — use name" }
882
+ label: { type: string, deprecated: true, description: "@deprecated — use display_name" }
883
+ icon: { type: string, nullable: true }
884
+ icon_color: { type: string, nullable: true }
885
+ documentation_url: { type: string, nullable: true }
886
+ llm_docs: { type: string, nullable: true, description: "provider's llms.txt / llms-full.txt URL (llmstxt.org)" }
887
+ instructions: { type: string, nullable: true, description: "markdown rendered at the top of the Add Connection dialog (e.g. how to generate an API key); nil when no setup guidance is needed" }
888
+ extends: { type: array, items: { type: string }, description: "base credential types inherited from" }
889
+ properties:
890
+ type: array
891
+ description: "n8n INodeProperties array"
892
+ items: { $ref: "#/components/schemas/Field" }
893
+ authenticate:
894
+ type: object
895
+ nullable: true
896
+ description: "n8n IAuthenticateGeneric"
897
+ properties:
898
+ type: { type: string, enum: [generic] }
899
+ properties: { type: object, additionalProperties: true }
900
+ generic_auth: { type: boolean }
901
+ supported_nodes: { type: array, items: { type: string } }
902
+ http_request_node:
903
+ type: object
904
+ nullable: true
905
+ properties:
906
+ name: { type: string }
907
+ docsUrl: { type: string }
908
+ apiBaseUrl: { type: string }
909
+ apiBaseUrlPlaceholder: { type: string }
910
+ hidden: { type: boolean }
911
+ __overwritten_properties: { type: array, items: { type: string }, description: "fields sourced from external secrets manager" }
912
+ __skip_managed_creation: { type: boolean }
913
+ actions:
914
+ type: array
915
+ description: "declarative manifest of what this connector can DO with a credential — design-time discovery for the picker/builder UI"
916
+ items: { $ref: "#/components/schemas/Action" }
917
+ connector:
918
+ type: object
919
+ properties:
920
+ base_url: { type: string }
921
+ webhook_style: { type: string, enum: [app_level, per_grant] }
922
+ rate_limit: { type: object, nullable: true }
923
+ authorize_url: { type: string, nullable: true }
924
+ authorize_json_url: { type: string, nullable: true }
925
+ redirect_uri: { type: string, nullable: true }
926
+ test_supported: { type: boolean }
927
+ mcp:
928
+ type: object
929
+ nullable: true
930
+ description: "Present for remote MCP connectors, including clickup. URL templates use :id for the credential ID; provider endpoint/authentication values are fixed when present."
931
+ properties:
932
+ server_url: { type: string, format: uri }
933
+ auth_mode: { type: string, enum: [none, bearer, headers, oauth] }
934
+ protocol_version: { type: string }
935
+ tools_url: { type: string }
936
+ call_url: { type: string }
937
+ resume_url: { type: string }
938
+ authorize_url: { type: string }
939
+
940
+ ActionResult:
941
+ type: object
942
+ description: "envelope returned by every action invocation — same shape on success and failure"
943
+ properties:
944
+ status: { type: string, enum: [ok, error] }
945
+ action: { type: string, description: "action key" }
946
+ data: { type: object, additionalProperties: true, description: "present when status=ok" }
947
+ error:
948
+ type: object
949
+ description: "present when status=error"
950
+ properties:
951
+ type:
952
+ type: string
953
+ enum: [invalid_params, authentication_failed, forbidden, rate_limited, api_error]
954
+ message: { type: string }
955
+ status: { type: integer, nullable: true, description: "provider HTTP status" }
956
+ missing: { type: array, items: { type: string }, description: "invalid_params: required keys not supplied" }
957
+ unknown: { type: array, items: { type: string }, description: "invalid_params: keys not on the schema" }
958
+ retry_after: { type: integer, nullable: true, description: "rate_limited: seconds until next retry" }
959
+ body: { type: object, additionalProperties: true, nullable: true, description: "api_error: raw provider response" }
960
+
961
+ Action:
962
+ type: object
963
+ description: "one thing a connector can DO with a credential (n8n operation / Activepieces action)"
964
+ properties:
965
+ name: { type: string, description: "machine key, e.g. send_email" }
966
+ display_name: { type: string }
967
+ description: { type: string }
968
+ deprecated: { type: boolean }
969
+ tags: { type: array, items: { type: string } }
970
+ properties:
971
+ type: array
972
+ description: "input schema — same shape as credential properties"
973
+ items: { $ref: "#/components/schemas/Field" }
974
+ output:
975
+ type: array
976
+ description: "shape of the value the action returns (Activepieces returns: …)"
977
+ items: { $ref: "#/components/schemas/Field" }
978
+
979
+ Field:
980
+ type: object
981
+ description: "n8n INodeProperties — frontend renders the form from this"
982
+ properties:
983
+ name: { type: string }
984
+ displayName: { type: string }
985
+ type:
986
+ type: string
987
+ enum:
988
+ - string
989
+ - number
990
+ - boolean
991
+ - options
992
+ - multiOptions
993
+ - json
994
+ - hidden
995
+ - notice
996
+ - collection
997
+ - fixedCollection
998
+ - dateTime
999
+ - color
1000
+ - resourceLocator
1001
+ - resourceMapper
1002
+ - filter
1003
+ - assignmentCollection
1004
+ default: {}
1005
+ placeholder: { type: string }
1006
+ description: { type: string }
1007
+ hint: { type: string }
1008
+ required: { type: boolean }
1009
+ noDataExpression: { type: boolean }
1010
+ typeOptions: { type: object, additionalProperties: true, description: "password, expirable, redactJsonLeaves, resolvable_field, ..." }
1011
+ displayOptions:
1012
+ type: object
1013
+ additionalProperties: true
1014
+ description: "show/hide rules with _cnd predicates (n8n IDisplayOptions)"
1015
+ options:
1016
+ type: array
1017
+ items: { type: object, additionalProperties: true }
1018
+
1019
+ Credential:
1020
+ type: object
1021
+ properties:
1022
+ id: { type: string, format: uuid }
1023
+ type: { type: string, description: "connector key (n8n vocabulary)" }
1024
+ connector_key: { type: string, deprecated: true }
1025
+ name: { type: string, description: "display name" }
1026
+ external_account_id: { type: string, nullable: true }
1027
+ status: { type: string, enum: [active, expired, revoked, errored] }
1028
+ expires_at: { type: string, format: date-time, nullable: true }
1029
+ last_used_at: { type: string, format: date-time, nullable: true }
1030
+ created_at: { type: string, format: date-time }
1031
+ updated_at: { type: string, format: date-time }
1032
+ scopes: { type: array, items: { type: string }, nullable: true }
1033
+ is_managed: { type: boolean }
1034
+ external_ref: { type: string, nullable: true }
1035
+ __overwritten_properties: { type: array, items: { type: string } }
1036
+ data:
1037
+ type: object
1038
+ additionalProperties: true
1039
+ description: "With include_data=true, decrypted data for the owner role only; shared viewers/editors omit data. MCP returns only public configuration for all visible connections."
1040
+
1041
+ SharePayload:
1042
+ type: object
1043
+ required: [principal_type, principal_id]
1044
+ properties:
1045
+ principal_type: { type: string, description: "host-defined type, e.g. User/Team/Project" }
1046
+ principal_id: { type: string, format: uuid }
1047
+ role: { type: string, enum: [viewer, editor, owner], default: viewer }
1048
+
1049
+ CredentialShare:
1050
+ type: object
1051
+ properties:
1052
+ id: { type: string, format: uuid }
1053
+ grant_id: { type: string, format: uuid }
1054
+ principal_type: { type: string }
1055
+ principal_id: { type: string, format: uuid }
1056
+ role: { type: string, enum: [viewer, editor, owner] }
1057
+
1058
+ TestResult:
1059
+ type: object
1060
+ properties:
1061
+ status: { type: string, enum: [OK, ERROR] }
1062
+ message: { type: string, nullable: true }
1063
+ detail: { type: object, additionalProperties: true, nullable: true }
1064
+
1065
+ Error:
1066
+ type: object
1067
+ properties:
1068
+ error: { type: string }
1069
+
1070
+ responses:
1071
+ McpRateLimited:
1072
+ description: Upstream rate limit; the operation is not automatically replayed
1073
+ headers:
1074
+ Retry-After:
1075
+ description: Valid upstream delay-seconds or HTTP date; omitted if unavailable
1076
+ schema: { type: string }
1077
+ content:
1078
+ application/json:
1079
+ schema: { $ref: "#/components/schemas/McpError" }
1080
+ McpFailure:
1081
+ description: Typed MCP failure; authorization_required can include an encrypted consent context.
1082
+ content:
1083
+ application/json:
1084
+ schema: { $ref: "#/components/schemas/McpError" }
1085
+ Unauthorized:
1086
+ description: Unauthorized (missing owner, role gate, or signature)
1087
+ content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
1088
+ NotFound:
1089
+ description: Resource or connector type not found
1090
+ content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
1091
+ WebhookAccepted:
1092
+ description: Event accepted and queued
1093
+ content:
1094
+ application/json:
1095
+ schema:
1096
+ type: object
1097
+ properties:
1098
+ event_id: { type: string, format: uuid }
1099
+ status: { type: string, enum: [accepted, duplicate] }