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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +35 -0
- data/CONNECTORS_FRAMEWORK.md +799 -0
- data/CONTRIBUTING.md +60 -0
- data/MCP_CLIENT.md +168 -0
- data/MIT-LICENSE +20 -0
- data/README.md +146 -0
- data/app/connectors/clickup/connector.rb +13 -0
- data/app/connectors/gmail/api.rb +114 -0
- data/app/connectors/gmail/connector.rb +921 -0
- data/app/connectors/gmail/mime_builder.rb +261 -0
- data/app/connectors/gmail/mime_parser.rb +106 -0
- data/app/connectors/gmail/polling.rb +154 -0
- data/app/connectors/remote_mcp/connector.rb +18 -0
- data/app/connectors/resend/connector.rb +218 -0
- data/app/controllers/concerns/connectors/grant_access.rb +43 -0
- data/app/controllers/connectors/actions_controller.rb +66 -0
- data/app/controllers/connectors/application_controller.rb +5 -0
- data/app/controllers/connectors/credentials_controller.rb +245 -0
- data/app/controllers/connectors/grants_controller.rb +123 -0
- data/app/controllers/connectors/mcp_controller.rb +88 -0
- data/app/controllers/connectors/oauth_controller.rb +134 -0
- data/app/controllers/connectors/types_controller.rb +144 -0
- data/app/controllers/connectors/webhooks_controller.rb +105 -0
- data/app/jobs/connectors/application_job.rb +4 -0
- data/app/jobs/connectors/deliver_webhook_job.rb +27 -0
- data/app/jobs/connectors/poll_job.rb +49 -0
- data/app/models/connectors/application_record.rb +5 -0
- data/app/models/connectors/credential_share.rb +31 -0
- data/app/models/connectors/grant.rb +103 -0
- data/app/models/connectors/mcp_authorization.rb +6 -0
- data/app/models/connectors/mcp_interaction.rb +6 -0
- data/app/models/connectors/poll_state.rb +17 -0
- data/app/models/connectors/webhook_event.rb +19 -0
- data/config/routes.rb +68 -0
- data/db/migrate/20260518210324_create_connectors_grants.rb +49 -0
- data/db/migrate/20260518214609_create_connectors_webhook_events.rb +35 -0
- data/db/migrate/20260521140000_create_connectors_credential_shares.rb +26 -0
- data/db/migrate/20260922120000_create_connectors_poll_states.rb +12 -0
- data/db/migrate/20260922130000_create_connectors_mcp_transactions.rb +18 -0
- data/docs/adding-connectors.md +92 -0
- data/docs/architecture.md +71 -0
- data/docs/releasing.md +60 -0
- data/lib/connectors/action.rb +90 -0
- data/lib/connectors/action_builder.rb +125 -0
- data/lib/connectors/action_runner.rb +99 -0
- data/lib/connectors/auth/scheme/api_key.rb +51 -0
- data/lib/connectors/auth/scheme/oauth2.rb +23 -0
- data/lib/connectors/auth/scheme.rb +35 -0
- data/lib/connectors/auth.rb +4 -0
- data/lib/connectors/auth_injection.rb +65 -0
- data/lib/connectors/client_builder.rb +87 -0
- data/lib/connectors/configuration.rb +138 -0
- data/lib/connectors/connector.rb +565 -0
- data/lib/connectors/credential_schema.rb +219 -0
- data/lib/connectors/credential_tester.rb +85 -0
- data/lib/connectors/credential_type_registry.rb +162 -0
- data/lib/connectors/credential_types/http_auth.rb +171 -0
- data/lib/connectors/engine.rb +123 -0
- data/lib/connectors/errors.rb +88 -0
- data/lib/connectors/grant_policy.rb +13 -0
- data/lib/connectors/mcp/access.rb +50 -0
- data/lib/connectors/mcp/authorization.rb +177 -0
- data/lib/connectors/mcp/authorization_context.rb +25 -0
- data/lib/connectors/mcp/authorization_discovery.rb +42 -0
- data/lib/connectors/mcp/cancellation.rb +36 -0
- data/lib/connectors/mcp/client.rb +137 -0
- data/lib/connectors/mcp/connection_config.rb +48 -0
- data/lib/connectors/mcp/http.rb +108 -0
- data/lib/connectors/mcp/interaction.rb +82 -0
- data/lib/connectors/mcp/pending_transaction.rb +26 -0
- data/lib/connectors/mcp/protocol/2026-07-28.json +3963 -0
- data/lib/connectors/mcp/protocol/LICENSE +216 -0
- data/lib/connectors/mcp/protocol/README.md +8 -0
- data/lib/connectors/mcp/protocol_schema.rb +30 -0
- data/lib/connectors/mcp/schema.rb +45 -0
- data/lib/connectors/mcp/settings.rb +27 -0
- data/lib/connectors/mcp/token_endpoint.rb +48 -0
- data/lib/connectors/mcp/transport.rb +105 -0
- data/lib/connectors/mcp.rb +69 -0
- data/lib/connectors/middleware/authenticate_generic.rb +79 -0
- data/lib/connectors/middleware/auto_refresh.rb +71 -0
- data/lib/connectors/middleware/error_normalization.rb +45 -0
- data/lib/connectors/middleware/grant_status.rb +19 -0
- data/lib/connectors/middleware/pre_authentication.rb +54 -0
- data/lib/connectors/middleware/rate_limit.rb +40 -0
- data/lib/connectors/oauth/authorize_url.rb +78 -0
- data/lib/connectors/oauth/client_authentication.rb +24 -0
- data/lib/connectors/oauth/client_credentials.rb +43 -0
- data/lib/connectors/oauth/grant_writer.rb +73 -0
- data/lib/connectors/oauth/pkce.rb +32 -0
- data/lib/connectors/oauth/revoke.rb +72 -0
- data/lib/connectors/oauth/state.rb +41 -0
- data/lib/connectors/oauth/token_exchange.rb +69 -0
- data/lib/connectors/oauth/token_response.rb +40 -0
- data/lib/connectors/oauth.rb +4 -0
- data/lib/connectors/oauth1.rb +151 -0
- data/lib/connectors/permission_check.rb +33 -0
- data/lib/connectors/poll_runner.rb +50 -0
- data/lib/connectors/pre_authentication_helpers.rb +76 -0
- data/lib/connectors/registry.rb +38 -0
- data/lib/connectors/version.rb +3 -0
- data/lib/connectors/webhook_context.rb +62 -0
- data/lib/connectors/webhook_lifecycle.rb +69 -0
- data/lib/connectors/webhook_methods.rb +48 -0
- data/lib/connectors/webhooks/verifier.rb +31 -0
- data/lib/connectors/webhooks.rb +5 -0
- data/lib/connectors.rb +50 -0
- data/lib/tasks/connectors_mcp.rake +9 -0
- data/lib/tasks/connectors_tasks.rake +4 -0
- data/openapi.yaml +1099 -0
- metadata +263 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
module Connectors
|
|
2
|
+
# Declarative description of every field a credential type's data hash can
|
|
3
|
+
# contain. Mirrors n8n's `INodeProperties` shape exactly (n8n source:
|
|
4
|
+
# packages/workflow/src/interfaces.ts:1773-1812) so a single frontend
|
|
5
|
+
# renderer handles every connector with no per-connector logic.
|
|
6
|
+
#
|
|
7
|
+
# credentials do
|
|
8
|
+
# field :api_key,
|
|
9
|
+
# type: "string",
|
|
10
|
+
# display_name: "API Key",
|
|
11
|
+
# required: true,
|
|
12
|
+
# type_options: { password: true },
|
|
13
|
+
# placeholder: "re_...",
|
|
14
|
+
# description: "Generate at https://resend.com/api-keys"
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# Inheritance: a connector can `extends :oauth2` (or any other registered
|
|
18
|
+
# base type) and `field` calls in the block can REDECLARE inherited fields
|
|
19
|
+
# — typically as `type: "hidden"` with a fixed `default:` to lock provider
|
|
20
|
+
# endpoints (matches n8n's Slack/Google override pattern at
|
|
21
|
+
# SlackOAuth2Api.credentials.ts:38-122).
|
|
22
|
+
class CredentialSchema
|
|
23
|
+
# n8n's `NodePropertyTypes` enum (interfaces.ts:1561-1584). Wave A ships
|
|
24
|
+
# the most commonly-used subset; later waves add resourceLocator /
|
|
25
|
+
# collection / fixedCollection / dateTime as connectors demand them.
|
|
26
|
+
ALLOWED_TYPES = %w[
|
|
27
|
+
string number boolean
|
|
28
|
+
options multiOptions
|
|
29
|
+
json hidden notice
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
Field = Struct.new(
|
|
33
|
+
:name, :display_name, :type, :default, :placeholder, :description, :hint,
|
|
34
|
+
:required, :no_data_expression,
|
|
35
|
+
:type_options, :display_options, :options,
|
|
36
|
+
keyword_init: true
|
|
37
|
+
) do
|
|
38
|
+
def required?
|
|
39
|
+
required == true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def password?
|
|
43
|
+
(type_options || {}).any? { |k, v| k.to_s == "password" && v == true }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Frontend-shaped hash. n8n serializes property names in camelCase
|
|
47
|
+
# (`displayName`, `typeOptions`); we mirror that so the editor's
|
|
48
|
+
# generic form renderer eats it without translation.
|
|
49
|
+
def to_property
|
|
50
|
+
{
|
|
51
|
+
name: name.to_s,
|
|
52
|
+
displayName: display_name || name.to_s.tr("_", " ").capitalize,
|
|
53
|
+
type: type.to_s,
|
|
54
|
+
default: default,
|
|
55
|
+
placeholder: placeholder,
|
|
56
|
+
description: description,
|
|
57
|
+
hint: hint,
|
|
58
|
+
required: required == true,
|
|
59
|
+
noDataExpression: no_data_expression == true,
|
|
60
|
+
typeOptions: stringify(type_options),
|
|
61
|
+
displayOptions: stringify(display_options),
|
|
62
|
+
options: options&.map { |o| stringify(o) }
|
|
63
|
+
}.compact
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def stringify(v)
|
|
69
|
+
case v
|
|
70
|
+
when Hash then v.each_with_object({}) { |(k, vv), out| out[k.to_s] = stringify(vv) }
|
|
71
|
+
when Array then v.map { |vv| stringify(vv) }
|
|
72
|
+
else v
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.build(&block)
|
|
78
|
+
schema = new
|
|
79
|
+
schema.instance_eval(&block) if block
|
|
80
|
+
schema
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def initialize
|
|
84
|
+
@fields = {}
|
|
85
|
+
@extends = []
|
|
86
|
+
@authenticate = nil
|
|
87
|
+
@generic_auth = false
|
|
88
|
+
@display_name = nil
|
|
89
|
+
@documentation_url = nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Inherit fields from one or more registered base credential schemas
|
|
93
|
+
# (today only `:oauth2`). Inherited fields can be REDECLARED below via
|
|
94
|
+
# `field` — typically as `type: "hidden", default: "..."` to lock
|
|
95
|
+
# provider-specific endpoints. Doubles as a getter when called with
|
|
96
|
+
# no args (the serializer needs to read it back).
|
|
97
|
+
def extends(*base_names)
|
|
98
|
+
return @extends if base_names.empty?
|
|
99
|
+
@extends = base_names.flatten.map(&:to_sym)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# n8n-shape declarative auth injection (mirrors `ICredentialType.authenticate`
|
|
103
|
+
# at packages/workflow/src/interfaces.ts:367; type `IAuthenticateGeneric`
|
|
104
|
+
# at :278-288). Per-credential-type so any connector that `extends` this
|
|
105
|
+
# schema inherits the injection contract automatically.
|
|
106
|
+
def authenticate(type: nil, properties: nil)
|
|
107
|
+
return @authenticate if type.nil? && properties.nil?
|
|
108
|
+
raise ArgumentError, "authenticate type: must be :generic" unless type.to_sym == :generic
|
|
109
|
+
@authenticate = { "type" => "generic", "properties" => properties }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# n8n flag: this credential type is visible to the future generic HTTP
|
|
113
|
+
# Request node's credential picker (`interfaces.ts:379`; example
|
|
114
|
+
# consumer `HttpBearerAuth.credentials.ts:12`). Stored here so the
|
|
115
|
+
# serializer surfaces it; runtime enforcement lands in Phase 5.
|
|
116
|
+
def generic_auth!
|
|
117
|
+
@generic_auth = true
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def generic_auth?
|
|
121
|
+
@generic_auth == true || @extends.any? { |b| CredentialTypeRegistry.fetch(b).generic_auth? rescue false }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Optional metadata that lives on the credential type rather than the
|
|
125
|
+
# connector class. Set on base credential types (HttpBearerAuth's
|
|
126
|
+
# "Bearer Auth" name) — the serializer prefers these over connector-
|
|
127
|
+
# level fallbacks when present.
|
|
128
|
+
def display_name(value = nil)
|
|
129
|
+
return @display_name if value.nil?
|
|
130
|
+
@display_name = value.to_s
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def documentation_url(value = nil)
|
|
134
|
+
return @documentation_url if value.nil?
|
|
135
|
+
@documentation_url = value.to_s
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def field(name,
|
|
139
|
+
type: "string",
|
|
140
|
+
display_name: nil,
|
|
141
|
+
default: nil,
|
|
142
|
+
placeholder: nil,
|
|
143
|
+
description: nil,
|
|
144
|
+
hint: nil,
|
|
145
|
+
required: false,
|
|
146
|
+
no_data_expression: false,
|
|
147
|
+
secret: false, # convenience — sets type_options[:password] = true
|
|
148
|
+
type_options: nil,
|
|
149
|
+
display_options: nil,
|
|
150
|
+
options: nil)
|
|
151
|
+
raise ArgumentError, "unknown credential field type #{type.inspect}; allowed: #{ALLOWED_TYPES.inspect}" \
|
|
152
|
+
unless ALLOWED_TYPES.include?(type.to_s)
|
|
153
|
+
|
|
154
|
+
effective_type_options = (type_options || {}).dup
|
|
155
|
+
effective_type_options[:password] = true if secret
|
|
156
|
+
|
|
157
|
+
@fields[name.to_sym] = Field.new(
|
|
158
|
+
name: name.to_sym,
|
|
159
|
+
display_name: display_name,
|
|
160
|
+
type: type.to_s,
|
|
161
|
+
default: default,
|
|
162
|
+
placeholder: placeholder,
|
|
163
|
+
description: description,
|
|
164
|
+
hint: hint,
|
|
165
|
+
required: required,
|
|
166
|
+
no_data_expression: no_data_expression,
|
|
167
|
+
type_options: effective_type_options.empty? ? nil : effective_type_options,
|
|
168
|
+
display_options: display_options,
|
|
169
|
+
options: options
|
|
170
|
+
)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# The fields declared directly on this schema, in declaration order.
|
|
174
|
+
# For the full resolved form (parents + own), use `resolved_fields`.
|
|
175
|
+
def own_fields
|
|
176
|
+
@fields.values
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# n8n's editor walks `extends` and merges parent properties before
|
|
180
|
+
# rendering. We do the walk server-side and emit the union so the
|
|
181
|
+
# frontend doesn't need to fetch each parent type separately. Children
|
|
182
|
+
# override parents by re-declaring with the same name.
|
|
183
|
+
def resolved_fields
|
|
184
|
+
parent_fields = @extends.flat_map { |name| CredentialTypeRegistry.fetch(name).own_fields }
|
|
185
|
+
union = {}
|
|
186
|
+
(parent_fields + own_fields).each { |f| union[f.name] = f }
|
|
187
|
+
union.values
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Walk `extends` to find the inherited authenticate block; the child's
|
|
191
|
+
# own block (if declared) overrides. Returns nil when no parent in the
|
|
192
|
+
# chain declared one. Matches n8n's runtime credential-walker pattern
|
|
193
|
+
# at packages/cli/src/credential-types.ts:26-37.
|
|
194
|
+
def resolved_authenticate
|
|
195
|
+
return @authenticate if @authenticate
|
|
196
|
+
@extends.each do |name|
|
|
197
|
+
parent = CredentialTypeRegistry.fetch(name) rescue nil
|
|
198
|
+
next if parent.nil?
|
|
199
|
+
block = parent.resolved_authenticate
|
|
200
|
+
return block if block
|
|
201
|
+
end
|
|
202
|
+
nil
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Back-compat — older code referenced `.fields` and `.required_fields`.
|
|
206
|
+
alias_method :fields, :resolved_fields
|
|
207
|
+
|
|
208
|
+
def required_fields
|
|
209
|
+
resolved_fields.select(&:required?)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def validate!(credentials_hash)
|
|
213
|
+
data = (credentials_hash || {}).transform_keys(&:to_s)
|
|
214
|
+
missing = required_fields.map { |f| f.name.to_s }.reject { |k| data[k].to_s.length.positive? }
|
|
215
|
+
raise MissingCredentialFields.new(missing) if missing.any?
|
|
216
|
+
true
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
module Connectors
|
|
2
|
+
# Executes a connector's `test_request` declaration against the live
|
|
3
|
+
# provider, applying configured rules to decide pass/fail. Mirrors n8n's
|
|
4
|
+
# `CredentialsTester` (packages/cli/src/services/credentials-tester.service.ts).
|
|
5
|
+
#
|
|
6
|
+
# result = CredentialTester.run(grant)
|
|
7
|
+
# # => { status: "OK", message: "Connection successful" }
|
|
8
|
+
# # or
|
|
9
|
+
# # => { status: "Error", message: "Slack token is invalid", details: {...} }
|
|
10
|
+
#
|
|
11
|
+
# Reuses the connector's Faraday client so the same auth middleware,
|
|
12
|
+
# auto-refresh, rate-limit, and error-normalization apply — a test that
|
|
13
|
+
# passes here means a real request will pass too.
|
|
14
|
+
class CredentialTester
|
|
15
|
+
OK = "OK".freeze
|
|
16
|
+
ERROR = "Error".freeze
|
|
17
|
+
|
|
18
|
+
def self.run(grant)
|
|
19
|
+
new(grant).run
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(grant)
|
|
23
|
+
@grant = grant
|
|
24
|
+
@klass = grant.connector.class
|
|
25
|
+
@cfg = @klass.test_request_config
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def run
|
|
29
|
+
return inconclusive("No test_request declared for #{@klass.connector_key}") if @cfg.nil?
|
|
30
|
+
|
|
31
|
+
response = issue_request
|
|
32
|
+
verdict_for(response)
|
|
33
|
+
rescue Connectors::AuthenticationFailed => e
|
|
34
|
+
{ status: ERROR, message: e.message }
|
|
35
|
+
rescue Connectors::ApiError => e
|
|
36
|
+
{ status: ERROR, message: e.message, details: { status: e.respond_to?(:status) ? e.status : nil } }
|
|
37
|
+
rescue => e
|
|
38
|
+
{ status: ERROR, message: "#{e.class}: #{e.message}" }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def issue_request
|
|
44
|
+
client = @grant.connector.client
|
|
45
|
+
client.public_send(@cfg[:method], @cfg[:url], @cfg[:query]) do |req|
|
|
46
|
+
(@cfg[:headers] || {}).each { |k, v| req.headers[k] = v }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def verdict_for(response)
|
|
51
|
+
expected = @cfg[:expect_status]
|
|
52
|
+
if expected && response.status.to_i != expected.to_i
|
|
53
|
+
return { status: ERROR, message: "Expected HTTP #{expected}, got #{response.status}" }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
Array(@cfg[:rules]).each do |rule|
|
|
57
|
+
verdict = apply_rule(rule, response)
|
|
58
|
+
return verdict if verdict
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
{ status: OK, message: "Connection successful" }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Each rule short-circuits with an Error verdict when it matches the
|
|
65
|
+
# failure condition; returns nil to mean "this rule said nothing, keep
|
|
66
|
+
# going".
|
|
67
|
+
def apply_rule(rule, response)
|
|
68
|
+
case rule[:type].to_sym
|
|
69
|
+
when :response_code
|
|
70
|
+
return nil if response.status.to_i == rule[:value].to_i
|
|
71
|
+
{ status: ERROR, message: rule[:message] || "Unexpected status #{response.status}" }
|
|
72
|
+
when :response_success_body
|
|
73
|
+
actual = response.body.is_a?(Hash) ? response.body[rule[:key].to_s] : nil
|
|
74
|
+
return nil unless actual == rule[:value]
|
|
75
|
+
{ status: ERROR, message: rule[:message] || "Response body indicated failure" }
|
|
76
|
+
else
|
|
77
|
+
raise Connectors::Error, "unknown test rule type #{rule[:type].inspect}"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def inconclusive(msg)
|
|
82
|
+
{ status: ERROR, message: msg, details: { kind: "test_not_configured" } }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
module Connectors
|
|
2
|
+
# Registry of reusable base credential schemas that connectors `extends`.
|
|
3
|
+
# Mirrors n8n's pattern where every OAuth2 provider extends a single
|
|
4
|
+
# canonical `OAuth2Api` credential type (packages/nodes-base/credentials/
|
|
5
|
+
# OAuth2Api.credentials.ts:1-238). The registry is populated at engine
|
|
6
|
+
# boot — see `Connectors::Engine`.
|
|
7
|
+
class CredentialTypeRegistry
|
|
8
|
+
class << self
|
|
9
|
+
def register(name, schema)
|
|
10
|
+
store[name.to_sym] = schema
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def fetch(name)
|
|
14
|
+
store.fetch(name.to_sym) do
|
|
15
|
+
raise Error.new("unknown base credential type #{name.inspect}; known: #{store.keys.inspect}")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def all
|
|
20
|
+
store.dup
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def clear!
|
|
24
|
+
store.clear
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def store
|
|
30
|
+
@store ||= {}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Canonical OAuth2 credential schema — every OAuth2 provider connector
|
|
35
|
+
# extends this and overrides specific fields as `type: "hidden"` with a
|
|
36
|
+
# fixed `default`. Field set matches n8n's OAuth2Api credentials type
|
|
37
|
+
# (packages/nodes-base/credentials/OAuth2Api.credentials.ts:1-238).
|
|
38
|
+
OAUTH2 = CredentialSchema.build do
|
|
39
|
+
field :grant_type,
|
|
40
|
+
type: "options",
|
|
41
|
+
display_name: "Grant Type",
|
|
42
|
+
default: "authorizationCode",
|
|
43
|
+
required: true,
|
|
44
|
+
description: "OAuth2 grant flow to use.",
|
|
45
|
+
options: [
|
|
46
|
+
{ name: "Authorization Code", value: "authorizationCode" },
|
|
47
|
+
{ name: "Client Credentials", value: "clientCredentials" },
|
|
48
|
+
{ name: "Authorization Code (PKCE)", value: "pkce" }
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
field :authorization_url,
|
|
52
|
+
type: "string",
|
|
53
|
+
display_name: "Authorization URL",
|
|
54
|
+
required: true,
|
|
55
|
+
placeholder: "https://example.com/oauth/authorize",
|
|
56
|
+
display_options: { show: { grant_type: %w[authorizationCode pkce] } }
|
|
57
|
+
|
|
58
|
+
field :access_token_url,
|
|
59
|
+
type: "string",
|
|
60
|
+
display_name: "Access Token URL",
|
|
61
|
+
required: true,
|
|
62
|
+
placeholder: "https://example.com/oauth/token"
|
|
63
|
+
|
|
64
|
+
field :client_id,
|
|
65
|
+
type: "string",
|
|
66
|
+
display_name: "Client ID",
|
|
67
|
+
required: true
|
|
68
|
+
|
|
69
|
+
field :client_secret,
|
|
70
|
+
type: "string",
|
|
71
|
+
display_name: "Client Secret",
|
|
72
|
+
required: true,
|
|
73
|
+
secret: true
|
|
74
|
+
|
|
75
|
+
field :scope,
|
|
76
|
+
type: "string",
|
|
77
|
+
display_name: "Scope",
|
|
78
|
+
default: "",
|
|
79
|
+
description: "Space-separated list of OAuth scopes to request."
|
|
80
|
+
|
|
81
|
+
field :auth_query_parameters,
|
|
82
|
+
type: "string",
|
|
83
|
+
display_name: "Auth URI Query Parameters",
|
|
84
|
+
default: "",
|
|
85
|
+
description: "Additional query parameters appended to the authorize URL " \
|
|
86
|
+
"(e.g., `access_type=offline&prompt=consent`)."
|
|
87
|
+
|
|
88
|
+
field :authentication,
|
|
89
|
+
type: "options",
|
|
90
|
+
display_name: "Authentication",
|
|
91
|
+
default: "header",
|
|
92
|
+
description: "How the client_id / client_secret are sent during token exchange.",
|
|
93
|
+
options: [
|
|
94
|
+
{ name: "Header", value: "header" },
|
|
95
|
+
{ name: "Body", value: "body" }
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
# Phase 11 polish — OAuth2 advanced fields. n8n parity:
|
|
99
|
+
# `OAuth2Api.credentials.ts:208-237`. Form-only at the engine level
|
|
100
|
+
# (no runtime JWE decryption); ship the schema so the editor can
|
|
101
|
+
# render the configuration UI for enterprise SSO scenarios.
|
|
102
|
+
field :jwe_enabled,
|
|
103
|
+
type: "boolean",
|
|
104
|
+
display_name: "Encrypted Tokens (JWE)",
|
|
105
|
+
default: false,
|
|
106
|
+
description: "Whether the IdP returns tokens encrypted as JWE to the public key at this " \
|
|
107
|
+
"instance's JWKS endpoint."
|
|
108
|
+
field :jwks_uri,
|
|
109
|
+
type: "string",
|
|
110
|
+
display_name: "JWKS URI",
|
|
111
|
+
default: "",
|
|
112
|
+
description: "Provide this URL to your IdP so it can fetch the public key used to " \
|
|
113
|
+
"encrypt access and ID tokens for this instance.",
|
|
114
|
+
display_options: { show: { jwe_enabled: [ true ] } }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# n8n's canonical `OAuth1Api` credential type
|
|
118
|
+
# (packages/nodes-base/credentials/OAuth1Api.credentials.ts:1-72). Same
|
|
119
|
+
# storage shape as OAuth2's `client_id` / `client_secret` — they map to
|
|
120
|
+
# the `consumerKey` / `consumerSecret` here. Connectors that extend this
|
|
121
|
+
# type generally `field :authorization_url, type: "hidden", default: "..."`
|
|
122
|
+
# to lock the provider endpoint.
|
|
123
|
+
OAUTH1 = CredentialSchema.build do
|
|
124
|
+
display_name "OAuth1 API"
|
|
125
|
+
documentation_url "httprequest"
|
|
126
|
+
generic_auth!
|
|
127
|
+
|
|
128
|
+
field :authorization_url,
|
|
129
|
+
type: "string",
|
|
130
|
+
display_name: "Authorization URL",
|
|
131
|
+
required: true
|
|
132
|
+
field :access_token_url,
|
|
133
|
+
type: "string",
|
|
134
|
+
display_name: "Access Token URL",
|
|
135
|
+
required: true
|
|
136
|
+
field :request_token_url,
|
|
137
|
+
type: "string",
|
|
138
|
+
display_name: "Request Token URL",
|
|
139
|
+
required: true
|
|
140
|
+
field :consumer_key,
|
|
141
|
+
type: "string",
|
|
142
|
+
display_name: "Consumer Key",
|
|
143
|
+
required: true,
|
|
144
|
+
secret: true
|
|
145
|
+
field :consumer_secret,
|
|
146
|
+
type: "string",
|
|
147
|
+
display_name: "Consumer Secret",
|
|
148
|
+
required: true,
|
|
149
|
+
secret: true
|
|
150
|
+
field :signature_method,
|
|
151
|
+
type: "options",
|
|
152
|
+
display_name: "Signature Method",
|
|
153
|
+
required: true,
|
|
154
|
+
default: "HMAC-SHA1",
|
|
155
|
+
options: [
|
|
156
|
+
{ name: "HMAC-SHA1", value: "HMAC-SHA1" },
|
|
157
|
+
{ name: "HMAC-SHA256", value: "HMAC-SHA256" },
|
|
158
|
+
{ name: "HMAC-SHA512", value: "HMAC-SHA512" }
|
|
159
|
+
]
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
module Connectors
|
|
2
|
+
# Generic HTTP authentication credential types — n8n parity with the six
|
|
3
|
+
# `Http*Auth.credentials.ts` types shipped in
|
|
4
|
+
# `packages/nodes-base/credentials/`. Each is `generic_auth: true` so a
|
|
5
|
+
# future HTTP-Request-style node can offer them in its credential picker
|
|
6
|
+
# (the runtime check lands in Phase 5).
|
|
7
|
+
#
|
|
8
|
+
# Connectors inherit any of these via `extends :http_bearer_auth` etc.
|
|
9
|
+
# When a connector extends one of the runtime-supported types
|
|
10
|
+
# (basic / bearer / header / query), the AuthenticateGeneric middleware
|
|
11
|
+
# picks up the inherited `authenticate` block and injects per-request —
|
|
12
|
+
# no `api_key_in` boilerplate needed.
|
|
13
|
+
#
|
|
14
|
+
# n8n source: `packages/nodes-base/credentials/HttpBasicAuth.credentials.ts`,
|
|
15
|
+
# `HttpBearerAuth.credentials.ts`, `HttpHeaderAuth.credentials.ts`,
|
|
16
|
+
# `HttpQueryAuth.credentials.ts`, `HttpDigestAuth.credentials.ts`,
|
|
17
|
+
# `HttpCustomAuth.credentials.ts`.
|
|
18
|
+
module CredentialTypes
|
|
19
|
+
# --- :http_basic_auth -------------------------------------------------
|
|
20
|
+
# n8n: HttpBasicAuth.credentials.ts:1-33. n8n's HTTP node implements
|
|
21
|
+
# Basic auth itself (no `authenticate` block on the credential). We
|
|
22
|
+
# synthesize one using AuthenticateGeneric's `auth: { username, password }`
|
|
23
|
+
# shortcut so the declarative path works end-to-end.
|
|
24
|
+
HTTP_BASIC_AUTH = CredentialSchema.build do
|
|
25
|
+
display_name "Basic Auth"
|
|
26
|
+
documentation_url "httprequest"
|
|
27
|
+
generic_auth!
|
|
28
|
+
|
|
29
|
+
field :user,
|
|
30
|
+
type: "string",
|
|
31
|
+
display_name: "User",
|
|
32
|
+
default: ""
|
|
33
|
+
field :password,
|
|
34
|
+
type: "string",
|
|
35
|
+
display_name: "Password",
|
|
36
|
+
default: "",
|
|
37
|
+
secret: true
|
|
38
|
+
|
|
39
|
+
authenticate type: :generic, properties: {
|
|
40
|
+
auth: { username: "={{$credentials.user}}", password: "={{$credentials.password}}" }
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# --- :http_bearer_auth ------------------------------------------------
|
|
45
|
+
# n8n: HttpBearerAuth.credentials.ts:1-43.
|
|
46
|
+
HTTP_BEARER_AUTH = CredentialSchema.build do
|
|
47
|
+
display_name "Bearer Auth"
|
|
48
|
+
documentation_url "httprequest"
|
|
49
|
+
generic_auth!
|
|
50
|
+
|
|
51
|
+
field :token,
|
|
52
|
+
type: "string",
|
|
53
|
+
display_name: "Bearer Token",
|
|
54
|
+
default: "",
|
|
55
|
+
secret: true
|
|
56
|
+
|
|
57
|
+
field :_notice_custom_auth,
|
|
58
|
+
type: "notice",
|
|
59
|
+
display_name: 'This credential uses the "Authorization" header. ' \
|
|
60
|
+
'To use a custom header, use a "Header Auth" credential instead',
|
|
61
|
+
default: ""
|
|
62
|
+
|
|
63
|
+
authenticate type: :generic, properties: {
|
|
64
|
+
headers: { "Authorization" => "=Bearer {{$credentials.token}}" }
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# --- :http_header_auth ------------------------------------------------
|
|
69
|
+
# n8n: HttpHeaderAuth.credentials.ts:1-45. Both header NAME and VALUE
|
|
70
|
+
# are templated — AuthInjection.walk resolves keys as well as values
|
|
71
|
+
# (see auth_injection.rb).
|
|
72
|
+
HTTP_HEADER_AUTH = CredentialSchema.build do
|
|
73
|
+
display_name "Header Auth"
|
|
74
|
+
documentation_url "httprequest"
|
|
75
|
+
generic_auth!
|
|
76
|
+
|
|
77
|
+
field :name,
|
|
78
|
+
type: "string",
|
|
79
|
+
display_name: "Name",
|
|
80
|
+
default: ""
|
|
81
|
+
field :value,
|
|
82
|
+
type: "string",
|
|
83
|
+
display_name: "Value",
|
|
84
|
+
default: "",
|
|
85
|
+
secret: true
|
|
86
|
+
|
|
87
|
+
field :_notice_multi,
|
|
88
|
+
type: "notice",
|
|
89
|
+
display_name: 'To send multiple headers, use a "Custom Auth" credential instead',
|
|
90
|
+
default: ""
|
|
91
|
+
|
|
92
|
+
authenticate type: :generic, properties: {
|
|
93
|
+
headers: { "={{$credentials.name}}" => "={{$credentials.value}}" }
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# --- :http_query_auth -------------------------------------------------
|
|
98
|
+
# n8n: HttpQueryAuth.credentials.ts:1-30. n8n's HTTP node handles
|
|
99
|
+
# injection imperatively; we synthesize the equivalent generic block.
|
|
100
|
+
HTTP_QUERY_AUTH = CredentialSchema.build do
|
|
101
|
+
display_name "Query Auth"
|
|
102
|
+
documentation_url "httprequest"
|
|
103
|
+
generic_auth!
|
|
104
|
+
|
|
105
|
+
field :name,
|
|
106
|
+
type: "string",
|
|
107
|
+
display_name: "Name",
|
|
108
|
+
default: ""
|
|
109
|
+
field :value,
|
|
110
|
+
type: "string",
|
|
111
|
+
display_name: "Value",
|
|
112
|
+
default: "",
|
|
113
|
+
secret: true
|
|
114
|
+
|
|
115
|
+
authenticate type: :generic, properties: {
|
|
116
|
+
qs: { "={{$credentials.name}}" => "={{$credentials.value}}" }
|
|
117
|
+
}
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# --- :http_digest_auth ------------------------------------------------
|
|
121
|
+
# n8n: HttpDigestAuth.credentials.ts:1-32. n8n consumes this via axios's
|
|
122
|
+
# built-in digest challenge handling. Faraday has no equivalent
|
|
123
|
+
# middleware out of the box — we ship the SCHEMA (so the frontend can
|
|
124
|
+
# render the form + a future generic HTTP-Request node can offer the
|
|
125
|
+
# type) but the AuthenticateGeneric middleware can't perform the
|
|
126
|
+
# challenge-response handshake on its own.
|
|
127
|
+
#
|
|
128
|
+
# Runtime support requires a Faraday-Digest middleware. Tracked as a
|
|
129
|
+
# follow-up; for now the type is form-only.
|
|
130
|
+
HTTP_DIGEST_AUTH = CredentialSchema.build do
|
|
131
|
+
display_name "Digest Auth"
|
|
132
|
+
documentation_url "httprequest"
|
|
133
|
+
generic_auth!
|
|
134
|
+
|
|
135
|
+
field :user,
|
|
136
|
+
type: "string",
|
|
137
|
+
display_name: "User",
|
|
138
|
+
default: ""
|
|
139
|
+
field :password,
|
|
140
|
+
type: "string",
|
|
141
|
+
display_name: "Password",
|
|
142
|
+
default: "",
|
|
143
|
+
secret: true
|
|
144
|
+
|
|
145
|
+
# No authenticate block — requires challenge-response middleware
|
|
146
|
+
# (Faraday-Digest gem or equivalent). Phase 2 ships the schema only.
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# --- :http_custom_auth ------------------------------------------------
|
|
150
|
+
# n8n: HttpCustomAuth.credentials.ts:1-29. Single `json` field carrying
|
|
151
|
+
# `{ headers, body, qs }`. n8n's HTTP node parses this JSON at request
|
|
152
|
+
# time and applies it. Our AuthenticateGeneric middleware works on a
|
|
153
|
+
# static `properties` block; runtime injection of a user-supplied JSON
|
|
154
|
+
# is deferred until a Custom-API-Call style node lands (workflow
|
|
155
|
+
# roadmap). Phase 2 ships the schema only.
|
|
156
|
+
HTTP_CUSTOM_AUTH = CredentialSchema.build do
|
|
157
|
+
display_name "Custom Auth"
|
|
158
|
+
documentation_url "httprequest"
|
|
159
|
+
generic_auth!
|
|
160
|
+
|
|
161
|
+
field :json,
|
|
162
|
+
type: "json",
|
|
163
|
+
display_name: "JSON",
|
|
164
|
+
required: true,
|
|
165
|
+
default: "",
|
|
166
|
+
description: "Use JSON to specify authentication values for headers, body and qs.",
|
|
167
|
+
placeholder: '{ "headers": { "key" : "value" }, "body": { "key": "value" }, "qs": { "key": "value" } }',
|
|
168
|
+
type_options: { redactJsonLeaves: true }
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|