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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/CONNECTORS_FRAMEWORK.md +54 -99
  4. data/MCP_CLIENT.md +5 -5
  5. data/README.md +5 -3
  6. data/app/connectors/gmail/api.rb +6 -14
  7. data/app/connectors/gmail/connector.rb +11 -24
  8. data/app/connectors/gmail/mime_builder.rb +0 -3
  9. data/app/connectors/gmail/mime_parser.rb +1 -2
  10. data/app/connectors/gmail/polling.rb +7 -14
  11. data/app/connectors/resend/connector.rb +2 -4
  12. data/app/controllers/connectors/actions_controller.rb +0 -6
  13. data/app/controllers/connectors/credentials_controller.rb +10 -23
  14. data/app/controllers/connectors/grants_controller.rb +6 -11
  15. data/app/controllers/connectors/types_controller.rb +18 -45
  16. data/app/controllers/connectors/webhooks_controller.rb +2 -4
  17. data/app/jobs/connectors/deliver_webhook_job.rb +2 -4
  18. data/app/models/connectors/credential_share.rb +3 -10
  19. data/app/models/connectors/grant.rb +2 -4
  20. data/config/routes.rb +6 -13
  21. data/db/migrate/20260518210324_create_connectors_grants.rb +4 -11
  22. data/db/migrate/20260521140000_create_connectors_credential_shares.rb +2 -8
  23. data/docs/releasing.md +4 -4
  24. data/lib/connectors/action.rb +4 -7
  25. data/lib/connectors/action_builder.rb +5 -12
  26. data/lib/connectors/action_runner.rb +5 -11
  27. data/lib/connectors/auth_injection.rb +7 -18
  28. data/lib/connectors/client_builder.rb +4 -10
  29. data/lib/connectors/configuration.rb +9 -39
  30. data/lib/connectors/connector.rb +31 -83
  31. data/lib/connectors/credential_schema.rb +12 -29
  32. data/lib/connectors/credential_tester.rb +3 -4
  33. data/lib/connectors/credential_type_registry.rb +9 -20
  34. data/lib/connectors/credential_types/http_auth.rb +18 -55
  35. data/lib/connectors/engine.rb +3 -6
  36. data/lib/connectors/errors.rb +2 -5
  37. data/lib/connectors/middleware/authenticate_generic.rb +4 -14
  38. data/lib/connectors/middleware/pre_authentication.rb +2 -13
  39. data/lib/connectors/oauth/authorize_url.rb +3 -9
  40. data/lib/connectors/oauth/client_credentials.rb +0 -4
  41. data/lib/connectors/oauth/pkce.rb +3 -9
  42. data/lib/connectors/oauth/revoke.rb +2 -6
  43. data/lib/connectors/oauth1.rb +4 -8
  44. data/lib/connectors/permission_check.rb +2 -4
  45. data/lib/connectors/poll_runner.rb +1 -5
  46. data/lib/connectors/pre_authentication_helpers.rb +4 -9
  47. data/lib/connectors/version.rb +1 -1
  48. data/lib/connectors/webhook_context.rb +8 -18
  49. data/lib/connectors/webhook_lifecycle.rb +1 -8
  50. data/lib/connectors/webhook_methods.rb +2 -6
  51. data/openapi.yaml +16 -16
  52. 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
- # Phase 1 declarative auth injection (mirrors n8n's HttpBearerAuth at
39
- # packages/nodes-base/credentials/HttpBearerAuth.credentials.ts:38-45).
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
- # n8n-shaped credential CRUD. Underneath, a "credential" is a Grant but
3
- # this controller exposes it under the vocabulary the frontend uses
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). n8n parity:
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. n8n parity: external-secrets EE
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
- # Server-generated unique default name ("Resend account 3"). n8n parity:
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> } — moves ownership to a different owner.
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, # n8n name
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
- # Phase 10 external secrets manager. `is_managed: true` means the
217
- # actual credential values are vault-sourced; `external_ref` is the
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
- # n8n's `__overwrittenProperties` (interfaces.ts:382) but per-grant
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 connector's declared `test_request` against the live
25
- # provider using THIS grant's credentials. Returns `{status, message}`
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 the connector's `webhook_methods` create flow (skipping when
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 name (multi-webhook providers)
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 `polling` block once. Manual harness for Phase 8 —
66
- # the workflow-side scheduler will call into `Connectors::PollRunner.run`
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
- # Catalog of every connector class registered in the engine. Read by the
3
- # frontend at startup to render the "Add credential" form, the connect-OAuth
4
- # popup, the credential picker on nodes purely by reading this metadata.
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` is the n8n vocabulary for the machine identifier; `display_name`
46
- # is the picker label. We keep `key`/`label` as aliases for back-compat
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
- # n8n inheritance chain. Empty array when the credential type stands
69
- # alone (e.g., API-key connectors). Frontend can fetch the parent's
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
- # Declarative auth injection mirrors n8n's `IAuthenticateGeneric`
76
- # at packages/workflow/src/interfaces.ts:278-288. Reads the resolved
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
- # n8n's `genericAuth: boolean` (`interfaces.ts:379`). True when the
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
- # n8n's `supportedNodes: string[]` (`interfaces.ts:381`). Empty array
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
- # n8n's `httpRequestNode: ICredentialHttpRequestNode` (interfaces.ts:380,
92
- # union shape at :350-354). Tells the generic HTTP-Request node's
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
- # n8n's `__overwrittenProperties: string[]` (`interfaces.ts:382`;
98
- # populated at `frontend.service.ts:681-705`). Field names whose
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
- # n8n's `__skipManagedCreation` (frontend.service.ts:707-711). When
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 orthogonal to credentials. Lives in its own
117
- # block so frontend code that only cares about credential rendering
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
- # Capture every HTTP_* header on the inbound request, normalized to
87
- # lowercase + dashed keys (`http_x_hub_signature` → `x-hub-signature`),
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
- # Phase 7: always hand the connector a `WebhookContext` (n8n parity
12
- # with `IWebhookFunctions`). The context still exposes `.event` /
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. The engine intentionally
3
- # doesn't know what a principal is `principal_type` / `principal_id`
4
- # are opaque tuples supplied by the host's
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
- # Mirrors n8n's `getWorkflowStaticData('node')` a small scratch hash
77
- # the connector mutates between webhook lifecycle calls (or polling
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 + connected-account introspection. Declared first so /grants and
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
- # Phase 9 credential sharing / scoping
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
- # Split frontend/backend OAuth completion (Activepieces-style). The
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
- # Legacy monolithic-host callback same-origin frontend/backend hosts
51
- # (n8n-style). Split-origin apps point the redirect_uri at the frontend
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
- # Phase 7 multi-webhook providers route by `webhook_name` segment so
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 for webhook subscriptions + polling cursors
18
- # (n8n parity: `getWorkflowStaticData('node')` at interfaces.ts:1257-1274).
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
- # External secrets manager integration. A grant either stores its
25
- # credentials inline (DB-encrypted) OR carries a lightweight
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
- # Phase 9 — credential sharing. One row per (grant, principal) pair.
3
- # `principal_type` is whatever the host wires through the
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.0.gem files
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, workflows and ignored internal planning documents remain outside the gem. Develop from the source repository, not the installed package.
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.0 <verified-main-commit-sha> -m "Release 0.1.0"
49
- git push origin v0.1.0
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.
@@ -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". The
4
- # equivalent of an n8n node's `(resource, operation)` slot
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
- # n8n's `NodePropertyTypes` (interfaces.ts:1561-1584) a superset of what
34
- # credentials accept. Action params can be richer than credential fields
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
- # DSL surface for the `action` block on a Connector subclass. Collects
3
- # `field`, optional `output`, and `execute` declarations into an Action
4
- # value object.
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
- # Declare the shape of the value the action returns. Optional but
65
- # encouraged agents and downstream nodes use it to know what fields
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 a normalized response envelope —
4
- # both the controller and the (eventual) workflow executor consume the
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. Anything else propagates so the caller can render a 500. This
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
- # Note: we intentionally don't reject `unknown:` keys. n8n nodes
67
- # frequently pass `additionalFields` collections that pack many
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 n8n-style `={{$credentials.x}}` templates inside an
3
- # `IAuthenticateGeneric` properties block (n8n source:
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
- # Scope on purpose: this is NOT a general-purpose expression engine. It
7
- # only understands `$credentials.<field>` lookups (with optional bracket
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
- # Both keys AND values can be templates. n8n's HttpHeaderAuth uses
28
- # `{ '={{$credentials.name}}': '={{$credentials.value}}' }` — the
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 `$credentials.field`, `$credentials["field"]`, `$credentials['field']`.
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 (Phase 3) — proactive token refresh
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
- # `authenticate_config` is the connector class's declarative
26
- # `authenticate type: :generic, properties: {...}` block (Phase 1). When
27
- # present, AuthenticateGeneric middleware applies it. When absent, fall
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