connectors 0.1.0 → 0.1.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/CONNECTORS_FRAMEWORK.md +54 -99
- data/MCP_CLIENT.md +5 -5
- data/README.md +5 -3
- data/app/connectors/gmail/api.rb +6 -14
- data/app/connectors/gmail/connector.rb +11 -24
- data/app/connectors/gmail/mime_builder.rb +0 -3
- data/app/connectors/gmail/mime_parser.rb +1 -2
- data/app/connectors/gmail/polling.rb +7 -14
- data/app/connectors/resend/connector.rb +2 -4
- data/app/controllers/connectors/actions_controller.rb +0 -6
- data/app/controllers/connectors/credentials_controller.rb +10 -23
- data/app/controllers/connectors/grants_controller.rb +6 -11
- data/app/controllers/connectors/types_controller.rb +18 -45
- data/app/controllers/connectors/webhooks_controller.rb +2 -4
- data/app/jobs/connectors/deliver_webhook_job.rb +2 -4
- data/app/models/connectors/credential_share.rb +3 -10
- data/app/models/connectors/grant.rb +2 -4
- data/config/routes.rb +6 -13
- data/db/migrate/20260518210324_create_connectors_grants.rb +4 -11
- data/db/migrate/20260521140000_create_connectors_credential_shares.rb +2 -8
- data/docs/releasing.md +4 -4
- data/lib/connectors/action.rb +4 -7
- data/lib/connectors/action_builder.rb +5 -12
- data/lib/connectors/action_runner.rb +5 -11
- data/lib/connectors/auth_injection.rb +7 -18
- data/lib/connectors/client_builder.rb +4 -10
- data/lib/connectors/configuration.rb +9 -39
- data/lib/connectors/connector.rb +31 -83
- data/lib/connectors/credential_schema.rb +12 -29
- data/lib/connectors/credential_tester.rb +3 -4
- data/lib/connectors/credential_type_registry.rb +9 -20
- data/lib/connectors/credential_types/http_auth.rb +18 -55
- data/lib/connectors/engine.rb +3 -6
- data/lib/connectors/errors.rb +2 -5
- data/lib/connectors/middleware/authenticate_generic.rb +4 -14
- data/lib/connectors/middleware/pre_authentication.rb +2 -13
- data/lib/connectors/oauth/authorize_url.rb +3 -9
- data/lib/connectors/oauth/client_credentials.rb +0 -4
- data/lib/connectors/oauth/pkce.rb +3 -9
- data/lib/connectors/oauth/revoke.rb +2 -6
- data/lib/connectors/oauth1.rb +4 -8
- data/lib/connectors/permission_check.rb +2 -4
- data/lib/connectors/poll_runner.rb +1 -5
- data/lib/connectors/pre_authentication_helpers.rb +4 -9
- data/lib/connectors/version.rb +1 -1
- data/lib/connectors/webhook_context.rb +8 -18
- data/lib/connectors/webhook_lifecycle.rb +1 -8
- data/lib/connectors/webhook_methods.rb +2 -6
- data/openapi.yaml +16 -16
- metadata +21 -1
|
@@ -35,10 +35,8 @@ module Resend
|
|
|
35
35
|
description: "Generate at https://resend.com/api-keys."
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
# The `={{...}}` template is resolved per-request from the Grant's
|
|
41
|
-
# credentials hash by `Middleware::AuthenticateGeneric`.
|
|
38
|
+
# AuthenticateGeneric resolves this template from the grant's
|
|
39
|
+
# credentials for each outgoing request.
|
|
42
40
|
authenticate type: :generic, properties: {
|
|
43
41
|
headers: { "Authorization" => "=Bearer {{$credentials.api_key}}" }
|
|
44
42
|
}
|
|
@@ -9,12 +9,6 @@ module Connectors
|
|
|
9
9
|
# the action's param schema, then dispatches through `ActionRunner`. The
|
|
10
10
|
# response envelope is always `{ status: "ok"|"error", action:, ... }` so
|
|
11
11
|
# frontends and workflow executors consume the same shape.
|
|
12
|
-
#
|
|
13
|
-
# n8n parity: this is the moral equivalent of the executor's per-node
|
|
14
|
-
# `execute()` step (packages/core/src/node-execute-functions.ts) reachable
|
|
15
|
-
# over HTTP. The workflow engine in `automations/` will eventually call
|
|
16
|
-
# `ActionRunner.call(grant, action, input)` directly without going
|
|
17
|
-
# through HTTP — same path, no controller in the loop.
|
|
18
12
|
class ActionsController < ApplicationController
|
|
19
13
|
# Order matters: Rails `rescue_from` iterates in REVERSE declaration
|
|
20
14
|
# order, so the more-specific subclasses must be declared AFTER
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
module Connectors
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
# (matches packages/cli/src/credentials/credentials.controller.ts:67-411).
|
|
5
|
-
#
|
|
6
|
-
# The paste-the-key flow lives here: `POST /credentials` with `{type,
|
|
7
|
-
# name, data}` creates a Grant directly without the OAuth dance.
|
|
8
|
-
# OAuth-issued grants continue to flow through OAuthController#callback
|
|
9
|
-
# and show up in this list automatically.
|
|
2
|
+
# Credential CRUD backed by Grant records. POST accepts `{type, name, data}`
|
|
3
|
+
# for API-key connections. OAuth-created grants appear in the same list.
|
|
10
4
|
class CredentialsController < ApplicationController
|
|
11
5
|
# Order matters: Rails `rescue_from` iterates in REVERSE declaration
|
|
12
6
|
# order, so the more-specific subclasses must be declared AFTER
|
|
@@ -24,8 +18,7 @@ module Connectors
|
|
|
24
18
|
#
|
|
25
19
|
# Returns the union of: credentials this owner owns + credentials shared
|
|
26
20
|
# with any of the requester's principals (User, Team, Project — whatever
|
|
27
|
-
# the host's `principal_resolver` returns).
|
|
28
|
-
# enterprise/credentials.controller.ee.ts.
|
|
21
|
+
# the host's `principal_resolver` returns).
|
|
29
22
|
def index
|
|
30
23
|
grants = visible_grants
|
|
31
24
|
grants = grants.for_connector(params[:type]) if params[:type].present?
|
|
@@ -53,8 +46,7 @@ module Connectors
|
|
|
53
46
|
# When `is_managed: true` is passed (typically alongside `external_ref:
|
|
54
47
|
# "vault/path/foo"`), the DB only stores the reference + non-sensitive
|
|
55
48
|
# field defaults; the actual credential values come from the host's
|
|
56
|
-
# `secrets_resolver` at request time.
|
|
57
|
-
# module (external-secrets.controller.ee.ts).
|
|
49
|
+
# `secrets_resolver` at request time.
|
|
58
50
|
def create
|
|
59
51
|
owner = current_owner!
|
|
60
52
|
type = params.fetch(:type)
|
|
@@ -81,9 +73,7 @@ module Connectors
|
|
|
81
73
|
end
|
|
82
74
|
|
|
83
75
|
# GET /connectors/credentials/new?type=resend
|
|
84
|
-
#
|
|
85
|
-
# `credentials.controller.ts:100-111`. The frontend pre-fills the name
|
|
86
|
-
# field with the response so the user can rename or accept the default.
|
|
76
|
+
# Returns a unique suggested name, such as "Resend account 3".
|
|
87
77
|
def new
|
|
88
78
|
current_owner!
|
|
89
79
|
type = params.fetch(:type)
|
|
@@ -156,8 +146,7 @@ module Connectors
|
|
|
156
146
|
end
|
|
157
147
|
|
|
158
148
|
# PUT /connectors/credentials/:id/transfer
|
|
159
|
-
# Body: { owner_id: <uuid> } —
|
|
160
|
-
# n8n parity: enterprise/credentials.controller.ee.ts transfer endpoint.
|
|
149
|
+
# Body: { owner_id: <uuid> } — transfers ownership to another host owner.
|
|
161
150
|
def transfer
|
|
162
151
|
grant = find_visible_grant!(params[:id], min_role: :owner)
|
|
163
152
|
new_owner_klass = Connectors.configuration.owner_class_name.constantize
|
|
@@ -203,7 +192,7 @@ module Connectors
|
|
|
203
192
|
def serialize(grant, include_data: false)
|
|
204
193
|
h = {
|
|
205
194
|
id: grant.id,
|
|
206
|
-
type: grant.connector_key,
|
|
195
|
+
type: grant.connector_key,
|
|
207
196
|
connector_key: grant.connector_key, # @deprecated alias
|
|
208
197
|
name: grant.display_name || "#{grant.connector_key} ##{grant.id}",
|
|
209
198
|
external_account_id: grant.external_account_id,
|
|
@@ -213,13 +202,11 @@ module Connectors
|
|
|
213
202
|
created_at: grant.created_at,
|
|
214
203
|
updated_at: grant.updated_at,
|
|
215
204
|
scopes: extract_scopes(grant),
|
|
216
|
-
#
|
|
217
|
-
#
|
|
218
|
-
# opaque key the host's `secrets_resolver` uses to look them up.
|
|
205
|
+
# `is_managed` selects vault-sourced credentials. `external_ref` is
|
|
206
|
+
# the opaque lookup key passed to the host secrets resolver.
|
|
219
207
|
is_managed: grant.is_managed,
|
|
220
208
|
external_ref: grant.external_ref,
|
|
221
|
-
#
|
|
222
|
-
# so the editor can show "this field comes from Vault: <api_key>".
|
|
209
|
+
# Field names sourced from the vault, for display as managed inputs.
|
|
223
210
|
__overwritten_properties: grant.is_managed ? Connectors.configuration.managed_fields_for(grant.connector_key) : []
|
|
224
211
|
}
|
|
225
212
|
if include_data
|
|
@@ -21,9 +21,8 @@ module Connectors
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
# POST /connectors/grants/:id/test
|
|
24
|
-
# Runs the
|
|
25
|
-
#
|
|
26
|
-
# — n8n parity (packages/cli/src/credentials/credentials.controller.ts:141-152).
|
|
24
|
+
# Runs the declared provider test using this grant's credentials
|
|
25
|
+
# and returns `{status, message}`.
|
|
27
26
|
def test
|
|
28
27
|
owner = current_owner!
|
|
29
28
|
grant = Grant.where(owner: owner).find(params[:id])
|
|
@@ -33,13 +32,10 @@ module Connectors
|
|
|
33
32
|
end
|
|
34
33
|
|
|
35
34
|
# POST /connectors/grants/:id/webhook_subscribe
|
|
36
|
-
# Runs
|
|
37
|
-
# check_exists already returns true). Manual entry point for Phase 6 —
|
|
38
|
-
# the workflow activation manager will call into this same lifecycle
|
|
39
|
-
# primitive (`Connectors::WebhookLifecycle.subscribe`) once it ships.
|
|
35
|
+
# Runs WebhookLifecycle.subscribe, skipping creation when check_exists succeeds.
|
|
40
36
|
#
|
|
41
37
|
# Params:
|
|
42
|
-
# webhook_name=:default — group
|
|
38
|
+
# webhook_name=:default — subscription group
|
|
43
39
|
# hook_url=<override> — defaults to the per-grant or app-level URL
|
|
44
40
|
def webhook_subscribe
|
|
45
41
|
owner = current_owner!
|
|
@@ -62,9 +58,8 @@ module Connectors
|
|
|
62
58
|
end
|
|
63
59
|
|
|
64
60
|
# POST /connectors/grants/:id/poll
|
|
65
|
-
# Runs the connector's
|
|
66
|
-
#
|
|
67
|
-
# directly once it ships.
|
|
61
|
+
# Runs the connector's polling block once through PollRunner.
|
|
62
|
+
# The host controls scheduling.
|
|
68
63
|
def poll
|
|
69
64
|
owner = current_owner!
|
|
70
65
|
grant = Grant.where(owner: owner).find(params[:id])
|
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
module Connectors
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# No per-connector frontend logic.
|
|
6
|
-
#
|
|
7
|
-
# Response shape mirrors n8n's `ICredentialType` description (n8n source:
|
|
8
|
-
# packages/workflow/src/interfaces.ts:356-382) so a single frontend renderer
|
|
9
|
-
# handles every connector. Returns only static metadata — never any secrets
|
|
10
|
-
# or per-user state.
|
|
2
|
+
# Static catalog of registered connectors. Clients use its metadata to
|
|
3
|
+
# render credential forms, authorization links and action inputs.
|
|
4
|
+
# The catalog contains no secrets or per-user state.
|
|
11
5
|
class TypesController < ApplicationController
|
|
12
6
|
# GET /connectors/types
|
|
13
7
|
def index
|
|
@@ -42,10 +36,8 @@ module Connectors
|
|
|
42
36
|
schema = klass.credential_schema
|
|
43
37
|
|
|
44
38
|
{
|
|
45
|
-
# `name`
|
|
46
|
-
#
|
|
47
|
-
# with the existing /grants response — frontend should prefer the n8n
|
|
48
|
-
# names going forward.
|
|
39
|
+
# `name` identifies the connector; `display_name` is its UI label.
|
|
40
|
+
# `key` and `label` remain aliases for existing consumers.
|
|
49
41
|
name: key.to_s,
|
|
50
42
|
display_name: klass.display_name || key.to_s.humanize,
|
|
51
43
|
key: key.to_s, # @deprecated — use `name`
|
|
@@ -65,44 +57,29 @@ module Connectors
|
|
|
65
57
|
# or install an app first (e.g. Resend).
|
|
66
58
|
instructions: klass.instructions,
|
|
67
59
|
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
# schema via `/connectors/types/:parent` OR rely on the resolved
|
|
71
|
-
# `properties` array below which already merges parent + own.
|
|
60
|
+
# Credential inheritance chain. The properties below already merge
|
|
61
|
+
# inherited and local fields, with local definitions taking precedence.
|
|
72
62
|
extends: schema ? schema.extends.map(&:to_s) : [],
|
|
73
63
|
properties: schema ? schema.resolved_fields.map(&:to_property) : [],
|
|
74
64
|
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
# config (connector-level override OR inherited from the credential
|
|
78
|
-
# type via `extends`). nil when the connector still uses the
|
|
79
|
-
# imperative `api_key_in` shim AND no parent declared one.
|
|
65
|
+
# Resolved auth injection: connector override, then inherited schema.
|
|
66
|
+
# Nil when neither declares an authenticate block.
|
|
80
67
|
authenticate: klass.resolved_authenticate_config,
|
|
81
|
-
#
|
|
82
|
-
# connector itself flips `generic_auth!` OR when it extends a schema
|
|
83
|
-
# that already does (HttpBearerAuth etc.).
|
|
68
|
+
# True when the connector or an inherited schema enables generic_auth.
|
|
84
69
|
generic_auth: klass.generic_auth?,
|
|
85
70
|
|
|
86
|
-
#
|
|
87
|
-
# means "no restriction" — same default as n8n. The frontend uses
|
|
88
|
-
# this to filter the credential picker per node type.
|
|
71
|
+
# Allowed node types. An empty list imposes no node-type restriction.
|
|
89
72
|
supported_nodes: klass.supported_nodes.map(&:to_s),
|
|
90
73
|
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
# credential picker how to label + link this credential. nil when the
|
|
94
|
-
# connector hasn't declared it.
|
|
74
|
+
# Optional label, documentation link and base URL for an HTTP-request
|
|
75
|
+
# credential picker.
|
|
95
76
|
http_request_node: klass.http_request_node,
|
|
96
77
|
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
# values are sourced from the external secrets manager — the editor
|
|
100
|
-
# renders them locked / hidden. Empty array when no vault is
|
|
101
|
-
# configured for this connector type.
|
|
78
|
+
# Vault-sourced field names for the editor to display as managed.
|
|
79
|
+
# Empty when the host has not configured managed fields for this type.
|
|
102
80
|
__overwritten_properties: Connectors.configuration.managed_fields_for(key),
|
|
103
81
|
|
|
104
|
-
#
|
|
105
|
-
# true the editor hides the "Use external secret" toggle.
|
|
82
|
+
# When true, clients should hide managed-credential creation.
|
|
106
83
|
__skip_managed_creation: klass.skip_managed_creation?,
|
|
107
84
|
|
|
108
85
|
# Action manifest — what this connector can DO with a credential.
|
|
@@ -113,12 +90,8 @@ module Connectors
|
|
|
113
90
|
# connector hasn't declared any actions yet.
|
|
114
91
|
actions: klass.actions.map(&:to_manifest),
|
|
115
92
|
|
|
116
|
-
# Connector capabilities
|
|
117
|
-
#
|
|
118
|
-
# can ignore it. For OAuth connectors, `redirect_uri` is the URL the
|
|
119
|
-
# admin must paste into the provider's app console (computed from the
|
|
120
|
-
# host's base URL — n8n does the same server-side at
|
|
121
|
-
# packages/cli/src/oauth/oauth.service.ts:528,690).
|
|
93
|
+
# Connector capabilities, separate from credential form metadata.
|
|
94
|
+
# OAuth redirect_uri is the callback URL to register with the provider.
|
|
122
95
|
connector: {
|
|
123
96
|
base_url: klass.base_url,
|
|
124
97
|
webhook_style: klass.webhook_style.to_s,
|
|
@@ -83,10 +83,8 @@ module Connectors
|
|
|
83
83
|
request.headers["Signature"]
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
# so the n8n-style `ctx.headers["stripe-signature"]` access works
|
|
89
|
-
# without callers having to know about Rack's mangling.
|
|
86
|
+
# Normalize inbound HTTP_* headers to lowercase, dashed names
|
|
87
|
+
# so handlers can use `ctx.headers["stripe-signature"]`.
|
|
90
88
|
def inbound_headers
|
|
91
89
|
request.headers.env.each_with_object({}) do |(k, v), out|
|
|
92
90
|
next unless k.start_with?("HTTP_")
|
|
@@ -8,10 +8,8 @@ module Connectors
|
|
|
8
8
|
event = WebhookEvent.find(event_id)
|
|
9
9
|
return if event.processed? || event.ignored?
|
|
10
10
|
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# `.payload_hash` so handlers written against the old `event` arg
|
|
14
|
-
# keep working without changes — the new accessors are additive.
|
|
11
|
+
# Pass request data through WebhookContext. Its event and payload_hash
|
|
12
|
+
# accessors also support handlers that consume the persisted event.
|
|
15
13
|
event.grant.connector.handle_webhook(Connectors::WebhookContext.new(event))
|
|
16
14
|
event.update!(status: :processed, processed_at: Time.current, error_message: nil)
|
|
17
15
|
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
module Connectors
|
|
2
|
-
# One sharing entry per (grant, principal) pair.
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# `Connectors.configuration.principal_resolver` block.
|
|
6
|
-
#
|
|
7
|
-
# `role` mirrors n8n's enterprise model: viewer = read-only, editor =
|
|
8
|
-
# update + use, owner = full control (delete + share + transfer). Only
|
|
9
|
-
# one `:owner` per grant — enforced by the unique index on
|
|
10
|
-
# `(grant_id, principal_type, principal_id)` plus a custom uniqueness
|
|
11
|
-
# validation per role.
|
|
2
|
+
# One sharing entry per (grant, principal) pair. Principal type and ID
|
|
3
|
+
# are supplied by the host principal_resolver. Roles are viewer, editor
|
|
4
|
+
# and owner; GrantPolicy defines their permitted operations.
|
|
12
5
|
class CredentialShare < ApplicationRecord
|
|
13
6
|
self.table_name = "connectors_credential_shares"
|
|
14
7
|
|
|
@@ -73,10 +73,8 @@ module Connectors
|
|
|
73
73
|
credentials || {}
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
# cursors). Save with `save!` after mutation, or use
|
|
79
|
-
# `update_static_data!(group, &block)` for the atomic path.
|
|
76
|
+
# Scratch storage for webhook subscriptions and polling cursors.
|
|
77
|
+
# Save after mutation or use update_static_data! for an atomic update.
|
|
80
78
|
def static_data_hash
|
|
81
79
|
static_data || {}
|
|
82
80
|
end
|
data/config/routes.rb
CHANGED
|
@@ -5,9 +5,7 @@ Connectors::Engine.routes.draw do
|
|
|
5
5
|
post "/credentials/:id/mcp/authorize", to: "mcp#authorize"
|
|
6
6
|
get "/mcp/oauth/callback", to: "mcp#callback"
|
|
7
7
|
post "/mcp/oauth/callback", to: "mcp#callback"
|
|
8
|
-
# Catalog
|
|
9
|
-
# /types aren't interpreted as a :connector_key.
|
|
10
|
-
# n8n-shape credential CRUD (the canonical endpoints — frontend uses these).
|
|
8
|
+
# Catalog and credential routes precede the dynamic :connector_key routes.
|
|
11
9
|
get "/credentials", to: "credentials#index", as: :credentials
|
|
12
10
|
get "/credentials/for-workflow", to: "credentials#for_workflow", as: :credentials_for_workflow
|
|
13
11
|
post "/credentials/test", to: "credentials#test_unsaved", as: :test_unsaved_credential
|
|
@@ -26,7 +24,7 @@ Connectors::Engine.routes.draw do
|
|
|
26
24
|
post "/credentials/:id/actions/:name", to: "actions#create", as: :invoke_credential_action,
|
|
27
25
|
constraints: { name: /[^\/]+/ }
|
|
28
26
|
|
|
29
|
-
#
|
|
27
|
+
# Credential sharing and ownership transfer.
|
|
30
28
|
put "/credentials/:id/share", to: "credentials#share", as: :share_credential
|
|
31
29
|
delete "/credentials/:id/share", to: "credentials#unshare"
|
|
32
30
|
put "/credentials/:id/transfer", to: "credentials#transfer", as: :transfer_credential
|
|
@@ -40,25 +38,20 @@ Connectors::Engine.routes.draw do
|
|
|
40
38
|
get "/types", to: "types#index", as: :types
|
|
41
39
|
get "/types/:name", to: "types#show", as: :type, constraints: { name: /[^\/]+/ }
|
|
42
40
|
|
|
43
|
-
#
|
|
44
|
-
# frontend's `/oauth/callback` page POSTs `{code, state}` here after the
|
|
45
|
-
# provider redirects to it.
|
|
41
|
+
# Separate frontend callbacks POST `{code, state}` here after consent.
|
|
46
42
|
post "/oauth/exchange", to: "oauth#exchange", as: :oauth_exchange
|
|
47
43
|
|
|
48
44
|
get "/:connector_key/authorize.json", to: "oauth#authorize_json", as: :authorize_json
|
|
49
45
|
get "/:connector_key/authorize", to: "oauth#authorize", as: :authorize
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
# and POST to /oauth/exchange instead.
|
|
46
|
+
# Provider callback for hosts using the engine callback URL.
|
|
47
|
+
# Hosts with a separate frontend callback use /oauth/exchange instead.
|
|
53
48
|
get "/:connector_key/callback", to: "oauth#callback", as: :callback
|
|
54
49
|
|
|
55
50
|
# App-level webhook URL (Slack, GitHub Apps, Linear, Notion, ...): one URL
|
|
56
51
|
# per app, grant resolved from payload via Connector.resolve_grant_from_webhook.
|
|
57
52
|
post "/:connector_key/webhook", to: "webhooks#receive", as: :app_webhook
|
|
58
53
|
|
|
59
|
-
#
|
|
60
|
-
# `webhookMethods.default` and `webhookMethods.setup` get distinct URLs
|
|
61
|
-
# (mirrors n8n's `IWebhookDescription.name` at interfaces.ts:2600).
|
|
54
|
+
# Named webhook groups have distinct URLs, such as default and setup.
|
|
62
55
|
post "/:connector_key/webhook/:webhook_name", to: "webhooks#receive", as: :named_app_webhook
|
|
63
56
|
|
|
64
57
|
# Per-grant webhook URL (Stripe, Twilio, ...): grant explicit in URL.
|
|
@@ -14,19 +14,12 @@ class CreateConnectorsGrants < ActiveRecord::Migration[8.1]
|
|
|
14
14
|
t.datetime :last_used_at
|
|
15
15
|
t.string :external_account_id # provider-side user/account id, if applicable
|
|
16
16
|
|
|
17
|
-
# Per-grant scratch storage
|
|
18
|
-
#
|
|
19
|
-
# Each `webhook_methods` group writes into its own nested hash keyed
|
|
20
|
-
# by the group name, so multi-webhook providers can persist
|
|
21
|
-
# independent state per subscription type.
|
|
17
|
+
# Per-grant scratch storage. Each webhook group uses its own nested hash
|
|
18
|
+
# for independent subscription state.
|
|
22
19
|
t.jsonb :static_data, null: false, default: {}
|
|
23
20
|
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
# `external_ref` that the host's vault adapter resolves on demand.
|
|
27
|
-
# n8n parity: `__overwrittenProperties` on the credential type
|
|
28
|
-
# (interfaces.ts:382), populated by frontend.service.ts:681-705
|
|
29
|
-
# when the external-secrets EE module is active.
|
|
21
|
+
# Managed credentials use an opaque external_ref resolved by the host.
|
|
22
|
+
# Stored credential values are encrypted by the Grant model.
|
|
30
23
|
t.string :external_ref
|
|
31
24
|
t.boolean :is_managed, null: false, default: false
|
|
32
25
|
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
class CreateConnectorsCredentialShares < ActiveRecord::Migration[8.1]
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
# `Connectors.configuration.principal_resolver` block — it's almost
|
|
5
|
-
# always "User" but EE hosts may share to "Team" / "Project" / etc.
|
|
6
|
-
#
|
|
7
|
-
# n8n parity: enterprise/credentials.controller.ee.ts handles
|
|
8
|
-
# `PUT /credentials/:id/share` + `/transfer`. We give the engine the
|
|
9
|
-
# SAME storage shape without baking in host-app concepts.
|
|
2
|
+
# One credential share per (grant, principal) pair. The host defines
|
|
3
|
+
# principal types, such as User, Team or Project, through its resolver.
|
|
10
4
|
def change
|
|
11
5
|
create_table :connectors_credential_shares, id: :uuid do |t|
|
|
12
6
|
t.references :grant, type: :uuid, null: false, foreign_key: { to_table: :connectors_grants }
|
data/docs/releasing.md
CHANGED
|
@@ -29,7 +29,7 @@ Create the matching GitHub environment `release`, with a deployment tag policy a
|
|
|
29
29
|
```sh
|
|
30
30
|
bundle exec ruby script/verify_package.rb
|
|
31
31
|
bundle exec gem build connectors.gemspec --strict
|
|
32
|
-
gem specification connectors-0.1.
|
|
32
|
+
gem specification connectors-0.1.1.gem files
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
Replace the artifact version when preparing a later release. The verification script checks public metadata, required files and documentation links, installs the gem into a temporary directory, and boots a fresh Rails host outside the checkout. It checks the shipped connector registry, migration discovery and MCP schema loading. It neither publishes nor invokes a provider. RSpec runs this same check in CI.
|
|
@@ -37,7 +37,7 @@ Create the matching GitHub environment `release`, with a deployment tag policy a
|
|
|
37
37
|
4. Merge the reviewed release changes through a pull request after its required checks pass. Confirm CI on the merged `main` commit also passes.
|
|
38
38
|
5. Confirm the RubyGems account and publisher configuration above are complete before pushing a tag.
|
|
39
39
|
|
|
40
|
-
The package includes runtime code, migrations, protocol data/licenses and consumer documentation. Tests, the dummy host, development tooling, lockfile
|
|
40
|
+
The package includes runtime code, migrations, protocol data/licenses and consumer documentation. Tests, the dummy host, development tooling, lockfile and workflows remain outside the gem. Develop from the source repository, not the installed package.
|
|
41
41
|
|
|
42
42
|
## Publish the verified commit
|
|
43
43
|
|
|
@@ -45,8 +45,8 @@ Fetch the merged commit, confirm its version and green CI, then create an annota
|
|
|
45
45
|
|
|
46
46
|
```sh
|
|
47
47
|
git fetch origin --tags
|
|
48
|
-
git tag -a v0.1.
|
|
49
|
-
git push origin v0.1.
|
|
48
|
+
git tag -a v0.1.1 <verified-main-commit-sha> -m "Release 0.1.1"
|
|
49
|
+
git push origin v0.1.1
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
Replace the SHA placeholder and version. Pushing the tag starts `.github/workflows/release.yml`. It reuses the normal CI workflow to run lint and the full RSpec suite before the publishing job starts. The publishing job verifies the tag, ancestry and package, builds the gem strictly, exchanges GitHub OIDC identity for short-lived RubyGems credentials using the official credentials action, then pushes the package. Only this job has `id-token: write`; neither job needs repository write permission.
|
data/lib/connectors/action.rb
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
module Connectors
|
|
2
2
|
# Declarative description of one thing a connector can DO with a credential
|
|
3
|
-
# — Slack "post message", Resend "send email", GitHub "create issue".
|
|
4
|
-
#
|
|
5
|
-
# (packages/workflow/src/interfaces.ts NodeProperties) and Activepieces'
|
|
6
|
-
# `createAction(...)` declaration.
|
|
3
|
+
# — Slack "post message", Resend "send email", GitHub "create issue". Each
|
|
4
|
+
# action describes its inputs, outputs and execution handler.
|
|
7
5
|
#
|
|
8
6
|
# The class is a value object — `ActionBuilder` materialises it from the
|
|
9
7
|
# DSL, `Connector.actions` stores them, `ActionRunner` invokes them.
|
|
@@ -30,9 +28,8 @@ module Connectors
|
|
|
30
28
|
# end
|
|
31
29
|
# end
|
|
32
30
|
class Action
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
# because they're the user's per-execution inputs, not the auth setup.
|
|
31
|
+
# Supported action input types. Action fields include collections and
|
|
32
|
+
# other per-execution inputs beyond the credential form types.
|
|
36
33
|
ALLOWED_TYPES = %w[
|
|
37
34
|
string number boolean
|
|
38
35
|
options multiOptions
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
module Connectors
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# Reuses `CredentialSchema::Field` as the property struct because n8n's
|
|
7
|
-
# node-property shape and credential-property shape are identical (both
|
|
8
|
-
# `INodeProperties` at packages/workflow/src/interfaces.ts:1773-1812) —
|
|
9
|
-
# only the allowed `type` set differs (see `Action::ALLOWED_TYPES`).
|
|
2
|
+
# Builds an Action from field, optional output, and execute declarations.
|
|
3
|
+
# Action and credential fields share CredentialSchema::Field serialization;
|
|
4
|
+
# Action::ALLOWED_TYPES defines the action-specific type set.
|
|
10
5
|
class ActionBuilder
|
|
11
6
|
Field = CredentialSchema::Field
|
|
12
7
|
|
|
@@ -61,10 +56,8 @@ module Connectors
|
|
|
61
56
|
)
|
|
62
57
|
end
|
|
63
58
|
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
# they can reference (Activepieces calls this `returns:`). The same
|
|
67
|
-
# `field` DSL is used inside the block.
|
|
59
|
+
# Optional result schema for clients to discover available output fields.
|
|
60
|
+
# Uses the same field DSL as action inputs.
|
|
68
61
|
def output(&block)
|
|
69
62
|
raise ArgumentError, "output requires a block" if block.nil?
|
|
70
63
|
collector = OutputCollector.new
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
module Connectors
|
|
2
2
|
# Validates an Action's input params and invokes its execute block in the
|
|
3
|
-
# connector instance's context. Returns
|
|
4
|
-
#
|
|
5
|
-
# same shape.
|
|
3
|
+
# connector instance's context. Returns the same normalized response
|
|
4
|
+
# envelope to HTTP controllers and direct Ruby callers.
|
|
6
5
|
#
|
|
7
6
|
# result = ActionRunner.call(grant, :send_email, { "from" => "x", "to" => "y", ... })
|
|
8
7
|
# # => { status: "ok", action: "send_email", data: { "id" => "abc-123" } }
|
|
@@ -11,9 +10,7 @@ module Connectors
|
|
|
11
10
|
# # error: { type: "api_error", message: "...", status: 422 } }
|
|
12
11
|
#
|
|
13
12
|
# The runner does not catch arbitrary StandardError — only Connectors::
|
|
14
|
-
# errors.
|
|
15
|
-
# mirrors how the workflow executor expects "unexpected" failures to be
|
|
16
|
-
# visible in logs rather than silently absorbed.
|
|
13
|
+
# errors. Unexpected failures propagate to the caller.
|
|
17
14
|
class ActionRunner
|
|
18
15
|
OK = "ok".freeze
|
|
19
16
|
ERROR = "error".freeze
|
|
@@ -63,11 +60,8 @@ module Connectors
|
|
|
63
60
|
provided = @input.keys
|
|
64
61
|
missing = action.required_param_names.reject { |k| @input.key?(k) && !blank?(@input[k]) }
|
|
65
62
|
raise Connectors::InvalidActionParams.new(missing: missing) if missing.any?
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
# optional values under one key; the connector's execute block
|
|
69
|
-
# decides which subset to honor. Strict-mode rejection can come
|
|
70
|
-
# later as an opt-in DSL flag.
|
|
63
|
+
# Unknown keys do not fail validation; coerced_input drops them
|
|
64
|
+
# before invoking the execute block.
|
|
71
65
|
_ = provided
|
|
72
66
|
end
|
|
73
67
|
|
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
module Connectors
|
|
2
|
-
# Resolves
|
|
3
|
-
# `
|
|
4
|
-
# packages/workflow/src/interfaces.ts:269-288, 197-208).
|
|
2
|
+
# Resolves credential templates in declarative authentication properties.
|
|
3
|
+
# Only `$credentials.field` and bracket lookups are supported.
|
|
5
4
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# access). Anything richer belongs in the automations expression engine,
|
|
9
|
-
# which the connectors engine must remain independent of.
|
|
10
|
-
#
|
|
11
|
-
# A string that starts with `=` is a template — its `{{ ... }}` segments
|
|
12
|
-
# are evaluated. Strings without a leading `=` are passed through as
|
|
13
|
-
# literals (matches n8n's `={{expression}}` convention).
|
|
5
|
+
# Strings beginning with `=` interpolate their `{{ ... }}` segments.
|
|
6
|
+
# Other strings remain literal; general expressions are unsupported.
|
|
14
7
|
module AuthInjection
|
|
15
8
|
module_function
|
|
16
9
|
|
|
@@ -24,10 +17,8 @@ module Connectors
|
|
|
24
17
|
def walk(value, credentials)
|
|
25
18
|
case value
|
|
26
19
|
when Hash
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
# header *name* is user-supplied (see
|
|
30
|
-
# packages/nodes-base/credentials/HttpHeaderAuth.credentials.ts:38-45).
|
|
20
|
+
# Resolve both keys and values so header and query names can come
|
|
21
|
+
# from credential fields.
|
|
31
22
|
value.each_with_object({}) do |(k, v), out|
|
|
32
23
|
resolved_key = k.is_a?(String) ? resolve_string(k, credentials) : k
|
|
33
24
|
out[resolved_key] = walk(v, credentials)
|
|
@@ -45,9 +36,7 @@ module Connectors
|
|
|
45
36
|
str[1..].gsub(EXPR) { lookup(Regexp.last_match(1), credentials) }
|
|
46
37
|
end
|
|
47
38
|
|
|
48
|
-
# Supports
|
|
49
|
-
# Missing keys resolve to "" (n8n behavior — keeps templates from raising
|
|
50
|
-
# mid-request when the field is optional).
|
|
39
|
+
# Supports dot and quoted bracket lookups. Missing fields resolve to "".
|
|
51
40
|
def lookup(expr, credentials)
|
|
52
41
|
expr = expr.to_s.strip
|
|
53
42
|
if (m = expr.match(/\A\$credentials\.([A-Za-z_][A-Za-z0-9_]*)\z/))
|
|
@@ -15,22 +15,16 @@ module Connectors
|
|
|
15
15
|
# 4. ErrorNormalization — raises after retry exhaustion
|
|
16
16
|
# 5. Retry / JSON response parser — inspect parsed responses
|
|
17
17
|
# 6. GrantStatus — reject revoked grants on each attempt
|
|
18
|
-
# 7. PreAuthentication
|
|
18
|
+
# 7. PreAuthentication — proactive token refresh
|
|
19
19
|
# 8. Auth injection — declarative or imperative (one or the other)
|
|
20
20
|
# 9. Adapter — sends the HTTP request
|
|
21
21
|
class ClientBuilder
|
|
22
22
|
DEFAULT_OPEN_TIMEOUT = 5
|
|
23
23
|
DEFAULT_TIMEOUT = 30
|
|
24
24
|
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
# back to the imperative `auth_scheme` middleware (legacy path —
|
|
29
|
-
# `Auth::Scheme::ApiKey` / `Auth::Scheme::OAuth2`).
|
|
30
|
-
#
|
|
31
|
-
# `pre_auth_block` is the connector's `pre_authentication` block (Phase 3).
|
|
32
|
-
# When present, PreAuthentication middleware runs it before each request
|
|
33
|
-
# whose grant credentials look stale (expires_at missing or in the past).
|
|
25
|
+
# AuthenticateGeneric applies a resolved authenticate declaration when
|
|
26
|
+
# present; otherwise the client uses the configured auth_scheme.
|
|
27
|
+
# PreAuthentication runs the optional hook when expires_at is absent or past.
|
|
34
28
|
def initialize(base_url:, grant:, auth_scheme:, authenticate_config: nil, pre_auth_block: nil,
|
|
35
29
|
open_timeout: DEFAULT_OPEN_TIMEOUT, timeout: DEFAULT_TIMEOUT)
|
|
36
30
|
@base_url = base_url
|