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,31 @@
|
|
|
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.
|
|
12
|
+
class CredentialShare < ApplicationRecord
|
|
13
|
+
self.table_name = "connectors_credential_shares"
|
|
14
|
+
|
|
15
|
+
belongs_to :grant, class_name: "Connectors::Grant"
|
|
16
|
+
|
|
17
|
+
ROLES = %w[viewer editor owner].freeze
|
|
18
|
+
validates :role, inclusion: { in: ROLES }
|
|
19
|
+
validates :principal_type, :principal_id, presence: true
|
|
20
|
+
validates :principal_id, uniqueness: { scope: %i[grant_id principal_type] }
|
|
21
|
+
|
|
22
|
+
# `principals` is an array of [type, id] tuples, as returned by the
|
|
23
|
+
# host's `principal_resolver`. Returns shares matching ANY of them.
|
|
24
|
+
scope :for_principals, ->(principals) {
|
|
25
|
+
next none if principals.blank?
|
|
26
|
+
clause = principals.map { "(principal_type = ? AND principal_id = ?)" }.join(" OR ")
|
|
27
|
+
args = principals.flat_map { |type, id| [ type.to_s, id ] }
|
|
28
|
+
where(clause, *args)
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
module Connectors
|
|
2
|
+
# A Grant is the user-authorized link between an owner (User, Workspace, etc.)
|
|
3
|
+
# and a third-party service. It stores the encrypted tokens / API keys needed
|
|
4
|
+
# to call that service's API on the owner's behalf.
|
|
5
|
+
class Grant < ApplicationRecord
|
|
6
|
+
serialize :credentials, coder: JSON, type: Hash
|
|
7
|
+
encrypts :credentials
|
|
8
|
+
|
|
9
|
+
belongs_to :owner, polymorphic: true
|
|
10
|
+
has_many :shares, class_name: "Connectors::CredentialShare", dependent: :destroy
|
|
11
|
+
has_many :webhook_events, class_name: "Connectors::WebhookEvent", dependent: :destroy
|
|
12
|
+
has_many :poll_states, class_name: "Connectors::PollState", dependent: :destroy
|
|
13
|
+
|
|
14
|
+
enum :status, { active: 0, expired: 1, revoked: 2, errored: 3 }, default: :active
|
|
15
|
+
|
|
16
|
+
validates :connector_key, presence: true
|
|
17
|
+
|
|
18
|
+
scope :for_connector, ->(key) { where(connector_key: key.to_s) }
|
|
19
|
+
scope :expiring_within, ->(window) { where(expires_at: ..window.from_now).where.not(expires_at: nil) }
|
|
20
|
+
|
|
21
|
+
# Returns an instance of the connector class registered for this grant's key.
|
|
22
|
+
def connector
|
|
23
|
+
Connectors::Registry.fetch(connector_key).new(self)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Cached clients must observe revocation by another worker. Read only the
|
|
27
|
+
# status so pending changes to credentials or polling state are preserved.
|
|
28
|
+
def ensure_not_revoked!
|
|
29
|
+
revoked_in_database = persisted? && self.class.uncached { self.class.where(id: id, status: :revoked).exists? }
|
|
30
|
+
raise Connectors::AuthenticationFailed, "connection has been revoked" if revoked? || revoked_in_database
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Atomic merge into the encrypted credentials hash. Always go through this
|
|
34
|
+
# instead of mutating the hash in-place, since serialized columns don't
|
|
35
|
+
# dirty-track mutation. Invalidates the per-instance resolved cache so a
|
|
36
|
+
# follow-up `credentials_hash` re-blends the vault values.
|
|
37
|
+
def update_credentials!(patch)
|
|
38
|
+
patch = patch.stringify_keys
|
|
39
|
+
with_persistence_lock do
|
|
40
|
+
attrs = { credentials: (credentials || {}).merge(patch) }
|
|
41
|
+
attrs[:expires_at] = patch["expires_at"] && Time.at(patch["expires_at"].to_i) if patch.key?("expires_at")
|
|
42
|
+
update!(attrs)
|
|
43
|
+
clear_resolved_credentials
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def reload(...)
|
|
48
|
+
clear_resolved_credentials
|
|
49
|
+
super
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Unsaved candidates can use the same primitives without attempting a row
|
|
53
|
+
# lock. Persisted grants always re-read under a database lock.
|
|
54
|
+
def with_persistence_lock(&block)
|
|
55
|
+
persisted? ? with_lock(&block) : yield
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Convenience accessor that always returns a hash, never nil. When the
|
|
59
|
+
# grant is `is_managed?`, the host's `secrets_resolver` is invoked and
|
|
60
|
+
# its return value is merged over the DB-stored credentials — the vault
|
|
61
|
+
# wins, so rotating a value in Vault propagates without a DB write.
|
|
62
|
+
# Cached per-instance to keep one request = one vault lookup.
|
|
63
|
+
def credentials_hash
|
|
64
|
+
return @resolved_credentials_hash if defined?(@resolved_credentials_hash)
|
|
65
|
+
base = credentials || {}
|
|
66
|
+
@resolved_credentials_hash = is_managed? ? base.merge(Connectors.configuration.resolve_secrets(self)) : base
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The raw, DB-only credentials hash (never consults the vault). Used by
|
|
70
|
+
# the index serializer to compute `__overwritten_properties` without
|
|
71
|
+
# tripping the resolver on every list response.
|
|
72
|
+
def stored_credentials_hash
|
|
73
|
+
credentials || {}
|
|
74
|
+
end
|
|
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.
|
|
80
|
+
def static_data_hash
|
|
81
|
+
static_data || {}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Yields the sub-hash for a webhook/polling group, then persists any
|
|
85
|
+
# mutation in a single UPDATE. Group defaults to "default" so single-
|
|
86
|
+
# webhook providers don't need to think about it.
|
|
87
|
+
def update_static_data!(group = "default")
|
|
88
|
+
with_persistence_lock do
|
|
89
|
+
data = static_data_hash.deep_dup
|
|
90
|
+
sub = (data[group.to_s] ||= {})
|
|
91
|
+
result = yield(sub)
|
|
92
|
+
update!(static_data: data)
|
|
93
|
+
result
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def clear_resolved_credentials
|
|
100
|
+
remove_instance_variable(:@resolved_credentials_hash) if defined?(@resolved_credentials_hash)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Connectors
|
|
2
|
+
# One cursor/configuration per consumer of a connection. instance_key is an
|
|
3
|
+
# opaque identifier supplied by the host (for example its trigger ID).
|
|
4
|
+
class PollState < ApplicationRecord
|
|
5
|
+
belongs_to :grant, class_name: "Connectors::Grant"
|
|
6
|
+
validates :instance_key, presence: true
|
|
7
|
+
|
|
8
|
+
def update_cursor!
|
|
9
|
+
with_lock do
|
|
10
|
+
cursor = data.deep_dup
|
|
11
|
+
result = yield(cursor)
|
|
12
|
+
update!(data: cursor)
|
|
13
|
+
result
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Connectors
|
|
2
|
+
# One record per inbound webhook from a provider. Persisted before
|
|
3
|
+
# processing so handlers can crash without losing the event, and so the
|
|
4
|
+
# idempotency index (connector_key, external_event_id) drops duplicates.
|
|
5
|
+
class WebhookEvent < ApplicationRecord
|
|
6
|
+
belongs_to :grant, class_name: "Connectors::Grant"
|
|
7
|
+
|
|
8
|
+
enum :status, { received: 0, processed: 1, failed: 2, ignored: 3 }, default: :received
|
|
9
|
+
|
|
10
|
+
validates :connector_key, :received_at, presence: true
|
|
11
|
+
|
|
12
|
+
scope :for_connector, ->(key) { where(connector_key: key.to_s) }
|
|
13
|
+
scope :unprocessed, -> { where(status: %i[received failed]) }
|
|
14
|
+
|
|
15
|
+
def payload_hash
|
|
16
|
+
payload || {}
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Connectors::Engine.routes.draw do
|
|
2
|
+
get "/credentials/:id/mcp/tools", to: "mcp#tools"
|
|
3
|
+
post "/credentials/:id/mcp/tools/call", to: "mcp#call_tool"
|
|
4
|
+
post "/credentials/:id/mcp/interactions/resume", to: "mcp#resume"
|
|
5
|
+
post "/credentials/:id/mcp/authorize", to: "mcp#authorize"
|
|
6
|
+
get "/mcp/oauth/callback", to: "mcp#callback"
|
|
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).
|
|
11
|
+
get "/credentials", to: "credentials#index", as: :credentials
|
|
12
|
+
get "/credentials/for-workflow", to: "credentials#for_workflow", as: :credentials_for_workflow
|
|
13
|
+
post "/credentials/test", to: "credentials#test_unsaved", as: :test_unsaved_credential
|
|
14
|
+
get "/credentials/new", to: "credentials#new", as: :new_credential
|
|
15
|
+
post "/credentials", to: "credentials#create"
|
|
16
|
+
get "/credentials/:id", to: "credentials#show", as: :credential
|
|
17
|
+
patch "/credentials/:id", to: "credentials#update"
|
|
18
|
+
put "/credentials/:id", to: "credentials#update"
|
|
19
|
+
delete "/credentials/:id", to: "credentials#destroy"
|
|
20
|
+
post "/credentials/:id/revoke", to: "oauth#revoke", as: :revoke_credential
|
|
21
|
+
|
|
22
|
+
# Action manifest + invocation. Per-credential because actions need an
|
|
23
|
+
# authenticated grant to run against; the catalog at /types/:name also
|
|
24
|
+
# includes a static `actions: [...]` array for design-time browsing.
|
|
25
|
+
get "/credentials/:id/actions", to: "actions#index", as: :credential_actions
|
|
26
|
+
post "/credentials/:id/actions/:name", to: "actions#create", as: :invoke_credential_action,
|
|
27
|
+
constraints: { name: /[^\/]+/ }
|
|
28
|
+
|
|
29
|
+
# Phase 9 — credential sharing / scoping
|
|
30
|
+
put "/credentials/:id/share", to: "credentials#share", as: :share_credential
|
|
31
|
+
delete "/credentials/:id/share", to: "credentials#unshare"
|
|
32
|
+
put "/credentials/:id/transfer", to: "credentials#transfer", as: :transfer_credential
|
|
33
|
+
|
|
34
|
+
# @deprecated `/grants` aliases — keep working until the frontend migrates.
|
|
35
|
+
get "/grants", to: "grants#index", as: :grants
|
|
36
|
+
post "/grants/:id/test", to: "grants#test", as: :test_grant
|
|
37
|
+
post "/grants/:id/webhook_subscribe", to: "grants#webhook_subscribe", as: :webhook_subscribe_grant
|
|
38
|
+
delete "/grants/:id/webhook_subscribe", to: "grants#webhook_unsubscribe"
|
|
39
|
+
post "/grants/:id/poll", to: "grants#poll", as: :poll_grant
|
|
40
|
+
get "/types", to: "types#index", as: :types
|
|
41
|
+
get "/types/:name", to: "types#show", as: :type, constraints: { name: /[^\/]+/ }
|
|
42
|
+
|
|
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.
|
|
46
|
+
post "/oauth/exchange", to: "oauth#exchange", as: :oauth_exchange
|
|
47
|
+
|
|
48
|
+
get "/:connector_key/authorize.json", to: "oauth#authorize_json", as: :authorize_json
|
|
49
|
+
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.
|
|
53
|
+
get "/:connector_key/callback", to: "oauth#callback", as: :callback
|
|
54
|
+
|
|
55
|
+
# App-level webhook URL (Slack, GitHub Apps, Linear, Notion, ...): one URL
|
|
56
|
+
# per app, grant resolved from payload via Connector.resolve_grant_from_webhook.
|
|
57
|
+
post "/:connector_key/webhook", to: "webhooks#receive", as: :app_webhook
|
|
58
|
+
|
|
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).
|
|
62
|
+
post "/:connector_key/webhook/:webhook_name", to: "webhooks#receive", as: :named_app_webhook
|
|
63
|
+
|
|
64
|
+
# Per-grant webhook URL (Stripe, Twilio, ...): grant explicit in URL.
|
|
65
|
+
# Must be declared AFTER /webhook so the bare form isn't shadowed.
|
|
66
|
+
post "/:connector_key/:grant_id/webhook", to: "webhooks#receive", as: :webhook
|
|
67
|
+
post "/:connector_key/:grant_id/webhook/:webhook_name", to: "webhooks#receive", as: :named_webhook
|
|
68
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
class CreateConnectorsGrants < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
# Everything is uuid — engine-owned primary keys AND polymorphic owner
|
|
4
|
+
# columns. The host (and any other owner type — Team, Workspace, …) is
|
|
5
|
+
# expected to have uuid PKs too. `gen_random_uuid()` is a PG13+ built-in,
|
|
6
|
+
# no extension required.
|
|
7
|
+
create_table :connectors_grants, id: :uuid do |t|
|
|
8
|
+
t.references :owner, polymorphic: true, type: :uuid, null: false
|
|
9
|
+
t.string :connector_key, null: false
|
|
10
|
+
t.string :display_name
|
|
11
|
+
t.text :credentials # encrypted at rest via ActiveRecord::Encryption
|
|
12
|
+
t.integer :status, null: false, default: 0
|
|
13
|
+
t.datetime :expires_at
|
|
14
|
+
t.datetime :last_used_at
|
|
15
|
+
t.string :external_account_id # provider-side user/account id, if applicable
|
|
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.
|
|
22
|
+
t.jsonb :static_data, null: false, default: {}
|
|
23
|
+
|
|
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.
|
|
30
|
+
t.string :external_ref
|
|
31
|
+
t.boolean :is_managed, null: false, default: false
|
|
32
|
+
|
|
33
|
+
t.timestamps
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
add_index :connectors_grants,
|
|
37
|
+
[ :owner_type, :owner_id, :connector_key ],
|
|
38
|
+
name: "index_connectors_grants_on_owner_and_connector"
|
|
39
|
+
|
|
40
|
+
add_index :connectors_grants, :connector_key
|
|
41
|
+
add_index :connectors_grants, :expires_at
|
|
42
|
+
add_index :connectors_grants, :status
|
|
43
|
+
add_index :connectors_grants,
|
|
44
|
+
[ :connector_key, :external_account_id ],
|
|
45
|
+
where: "external_account_id IS NOT NULL",
|
|
46
|
+
name: "index_connectors_grants_on_external_account"
|
|
47
|
+
add_index :connectors_grants, :external_ref, where: "external_ref IS NOT NULL"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
class CreateConnectorsWebhookEvents < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :connectors_webhook_events, id: :uuid do |t|
|
|
4
|
+
t.references :grant, type: :uuid, null: false, foreign_key: { to_table: :connectors_grants }
|
|
5
|
+
t.string :connector_key, null: false
|
|
6
|
+
t.string :external_event_id
|
|
7
|
+
t.jsonb :payload
|
|
8
|
+
t.text :signature
|
|
9
|
+
t.integer :status, null: false, default: 0
|
|
10
|
+
t.text :error_message
|
|
11
|
+
t.datetime :received_at, null: false
|
|
12
|
+
t.datetime :processed_at
|
|
13
|
+
|
|
14
|
+
# Full inbound request shape so `IWebhookFunctions`-style handlers
|
|
15
|
+
# have headers (Stripe-Signature etc.), query string, raw body (for
|
|
16
|
+
# replay / signature debugging), and the named webhook group the
|
|
17
|
+
# request hit (`webhookMethods.default` vs `.setup`).
|
|
18
|
+
t.jsonb :headers, null: false, default: {}
|
|
19
|
+
t.jsonb :query, null: false, default: {}
|
|
20
|
+
t.text :raw_body
|
|
21
|
+
t.string :webhook_name, null: false, default: "default"
|
|
22
|
+
|
|
23
|
+
t.timestamps
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
add_index :connectors_webhook_events, :connector_key
|
|
27
|
+
add_index :connectors_webhook_events, :status
|
|
28
|
+
add_index :connectors_webhook_events, :webhook_name
|
|
29
|
+
add_index :connectors_webhook_events,
|
|
30
|
+
[ :connector_key, :external_event_id ],
|
|
31
|
+
unique: true,
|
|
32
|
+
where: "external_event_id IS NOT NULL",
|
|
33
|
+
name: "idx_connectors_webhook_events_idempotency"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
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.
|
|
10
|
+
def change
|
|
11
|
+
create_table :connectors_credential_shares, id: :uuid do |t|
|
|
12
|
+
t.references :grant, type: :uuid, null: false, foreign_key: { to_table: :connectors_grants }
|
|
13
|
+
t.string :principal_type, null: false
|
|
14
|
+
t.uuid :principal_id, null: false
|
|
15
|
+
t.string :role, null: false, default: "viewer"
|
|
16
|
+
|
|
17
|
+
t.timestamps
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
add_index :connectors_credential_shares, [ :grant_id, :principal_type, :principal_id ],
|
|
21
|
+
unique: true,
|
|
22
|
+
name: "idx_connectors_credential_shares_uniq"
|
|
23
|
+
add_index :connectors_credential_shares, [ :principal_type, :principal_id ],
|
|
24
|
+
name: "idx_connectors_credential_shares_lookup"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class CreateConnectorsPollStates < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :connectors_poll_states, id: :uuid do |t|
|
|
4
|
+
t.references :grant, type: :uuid, null: false, foreign_key: { to_table: :connectors_grants }
|
|
5
|
+
t.string :instance_key, null: false
|
|
6
|
+
t.jsonb :data, null: false, default: {}
|
|
7
|
+
t.timestamps
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
add_index :connectors_poll_states, [ :grant_id, :instance_key ], unique: true
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class CreateConnectorsMcpTransactions < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
%i[mcp_authorizations mcp_interactions].each do |name|
|
|
4
|
+
create_table "connectors_#{name}", id: :uuid do |t|
|
|
5
|
+
t.references :grant, type: :uuid, null: false, foreign_key: { to_table: :connectors_grants, on_delete: :cascade }
|
|
6
|
+
t.string :actor_key, null: false
|
|
7
|
+
t.string :fingerprint, null: false
|
|
8
|
+
t.string :status, null: false, default: "pending"
|
|
9
|
+
t.text :payload, null: false
|
|
10
|
+
t.datetime :expires_at, null: false
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
add_index "connectors_#{name}", :expires_at
|
|
14
|
+
end
|
|
15
|
+
add_column :connectors_mcp_authorizations, :state_digest, :string, null: false
|
|
16
|
+
add_index :connectors_mcp_authorizations, :state_digest, unique: true
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Adding a connector
|
|
2
|
+
|
|
3
|
+
Use a REST connector when the backend should implement provider actions against an HTTP API. Use an MCP profile when the provider exposes a compatible remote MCP server and supplies its own tool schemas. See [architecture](architecture.md) for the separation.
|
|
4
|
+
|
|
5
|
+
## REST connector
|
|
6
|
+
|
|
7
|
+
Choose a unique key and place the class in the host's `app/connectors/<key>/connector.rb`. Match the Ruby namespace to the directory for Zeitwerk. The example below is a fictional CRM template, not a claim about an existing provider API:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# app/connectors/example_crm/connector.rb
|
|
11
|
+
module ExampleCrm
|
|
12
|
+
class Connector < Connectors::Connector
|
|
13
|
+
connector key: :example_crm, auth: :api_key,
|
|
14
|
+
base_url: "https://api.example.test", display_name: "Example CRM"
|
|
15
|
+
|
|
16
|
+
credentials do
|
|
17
|
+
field :api_key, type: "string", required: true, secret: true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
authenticate type: :generic, properties: {
|
|
21
|
+
headers: { "Authorization" => "=Bearer {{$credentials.api_key}}" }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
test_request method: :get, url: "me"
|
|
25
|
+
|
|
26
|
+
action :find_contact, display_name: "Find Contact" do
|
|
27
|
+
field :email, type: "string", required: true
|
|
28
|
+
execute do |input|
|
|
29
|
+
client.get("contacts", { email: input.fetch("email") }).body
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Replace the endpoint, authentication and action with the provider's documented contract. Use the shared `client` for request timeouts, authentication, revocation, retries and error handling. Do not store real keys in connector classes. Set safe fixed provider URLs; do not turn untrusted action inputs into arbitrary destinations.
|
|
37
|
+
|
|
38
|
+
`test_request` should use a cheap, read-only endpoint compatible with the credential's permissions. A successful test establishes access to that endpoint, not every possible action. Document scope restrictions.
|
|
39
|
+
|
|
40
|
+
REST actions check required values and remove undeclared keys. Add provider-specific validation when required; field types primarily describe the UI. OAuth2/OAuth1, pre-authentication, webhooks, polling and credential inheritance are documented in the [DSL reference](../CONNECTORS_FRAMEWORK.md). Declare only capabilities you actually implement.
|
|
41
|
+
|
|
42
|
+
## Test first
|
|
43
|
+
|
|
44
|
+
In the gem repository, use `rails_helper` for the dummy application, registry reset and WebMock. In a host project, configure equivalent owner, encryption and database fixtures. With the example connector loaded:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
require "rails_helper"
|
|
48
|
+
|
|
49
|
+
RSpec.describe ExampleCrm::Connector do
|
|
50
|
+
let(:owner) { Owner.create!(name: "CRM test") }
|
|
51
|
+
let(:grant) do
|
|
52
|
+
Connectors::Grant.create!(owner: owner, connector_key: "example_crm",
|
|
53
|
+
credentials: { "api_key" => "synthetic-key" })
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "finds contacts through the authenticated client" do
|
|
57
|
+
stub_request(:get, "https://api.example.test/contacts")
|
|
58
|
+
.with(query: { email: "person@example.test" },
|
|
59
|
+
headers: { "Authorization" => "Bearer synthetic-key" })
|
|
60
|
+
.to_return(status: 200, headers: { "Content-Type" => "application/json" },
|
|
61
|
+
body: '{"contacts":[]}')
|
|
62
|
+
|
|
63
|
+
result = Connectors::ActionRunner.call(grant, :find_contact,
|
|
64
|
+
{ "email" => "person@example.test" })
|
|
65
|
+
expect(result[:status]).to eq("ok")
|
|
66
|
+
expect(result[:data]).to eq("contacts" => [])
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Add cases for the provider's documented errors, scope restrictions and required input; verify write operations are not replayed unexpectedly. For webhooks test signature validation, grant routing and duplicate events. For polling test cursor isolation and rollback on failure. Avoid tests that only repeat DSL declarations without verifying behavior.
|
|
72
|
+
|
|
73
|
+
## Named MCP profile
|
|
74
|
+
|
|
75
|
+
For a server supported by the generic MCP integration, a named profile supplies fixed connection metadata. It does not copy remote tools into Ruby action classes:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
# app/connectors/example_mcp/connector.rb
|
|
79
|
+
module ExampleMcp
|
|
80
|
+
class Connector < Connectors::Connector
|
|
81
|
+
connector key: :example_mcp, auth: :api_key, base_url: nil,
|
|
82
|
+
display_name: "Example MCP"
|
|
83
|
+
mcp server_url: "https://mcp.example.test/mcp", auth_mode: "oauth"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The `auth: :api_key` field is the current base connector DSL marker; the `mcp` declaration selects OAuth for the remote connection. It does not enable an API-key fallback. For servers needing preregistered clients, declare the optional secret `client_information` credential field as the shipped ClickUp profile does.
|
|
89
|
+
|
|
90
|
+
Confirm the actual server's transport/protocol support, endpoint, OAuth discovery metadata, scopes and client registration requirements using official documentation and live read-only metadata. Do not assume all servers support dynamic registration, refresh tokens or the same scopes. Test fixed-profile configuration, consent requirements, sanitized credential responses, discovery and invocation. Use the shared MCP services; add no provider-specific token logic unless the actual contract requires it.
|
|
91
|
+
|
|
92
|
+
See [ClickUp](../MCP_CLIENT.md#clickup) for the implemented example and [contributing](../CONTRIBUTING.md) for full verification commands.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Architecture and execution contracts
|
|
2
|
+
|
|
3
|
+
Connectors is a Rails engine. It does not provide a workflow executor, account login UI, scheduler or hosted provider proxy. The [README](../README.md) describes installation; the [framework reference](../CONNECTORS_FRAMEWORK.md) describes the DSL.
|
|
4
|
+
|
|
5
|
+
## Components
|
|
6
|
+
|
|
7
|
+
| Component | Responsibility |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| Host application | Authenticated owner/principals, encryption keys, provider application secrets, job backend, scheduling and business workflows |
|
|
10
|
+
| Registry and connector classes | Credential schemas, provider metadata, REST actions, authentication declarations and optional polling/webhooks |
|
|
11
|
+
| `Connectors::Grant` | One connected account; encrypted credentials, status, ownership and provider identity |
|
|
12
|
+
| Credential shares | Grant access for UUID principals with viewer, editor or owner roles |
|
|
13
|
+
| REST execution | Declared actions and Faraday middleware for credential injection, refresh, errors and retries |
|
|
14
|
+
| MCP client | Grant-scoped remote discovery, schema validation, transport, OAuth and durable interactions |
|
|
15
|
+
| Controllers | HTTP access checks and stable request/response envelopes |
|
|
16
|
+
|
|
17
|
+
Classes under `app/connectors/*/connector.rb` register through the connector DSL when Rails loads them. The engine's `to_prepare` hook loads shipped and host connector classes. Remote MCP tools are discovered for each authorized grant; they do not become global connector classes.
|
|
18
|
+
|
|
19
|
+
## REST actions
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
Authenticated app request
|
|
23
|
+
→ engine role check (editor or owner)
|
|
24
|
+
→ declared action lookup and revocation check
|
|
25
|
+
→ required-parameter validation and input whitelist
|
|
26
|
+
→ provider action implementation
|
|
27
|
+
→ Faraday: rate limit / refresh / error normalization / retries
|
|
28
|
+
→ per-attempt revocation check / pre-authentication / credential injection
|
|
29
|
+
→ provider
|
|
30
|
+
→ normalized action result
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`ActionRunner.call(grant, name, input)` is also available to trusted host code. It checks revocation, but it does not accept an actor or perform sharing authorization: the host must authorize the selected grant before calling it. Connector methods using `client` get the same HTTP checks. Provider implementations that bypass `client` also bypass that middleware.
|
|
34
|
+
|
|
35
|
+
Action field declarations describe the form and required values; they are not a general JSON Schema validator. Unknown REST action keys are dropped. Provider actions must validate constraints beyond required values where needed.
|
|
36
|
+
|
|
37
|
+
## MCP tools
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
Authenticated app request
|
|
41
|
+
→ MCP access check for the grant and actor
|
|
42
|
+
→ discover provider tool descriptors
|
|
43
|
+
→ validate arguments against the selected tool's schema
|
|
44
|
+
→ transport with grant credentials and protocol metadata
|
|
45
|
+
→ provider tools/call
|
|
46
|
+
→ native tool result, or persisted input_required interaction
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The backend handles discovery, registration, OAuth scopes and token use. The frontend renders tool input and explicit user consent/elicitation; it does not implement provider authentication or requests. Unlike the REST Ruby runner, `MCP::Client.new(grant:, actor:, principals:)` checks access itself. HTTP tools and actions currently have separate endpoints.
|
|
50
|
+
|
|
51
|
+
OAuth and elicitation use encrypted, expiring database records bound to the actor, grant and connection fingerprint. One-time transaction claims prevent local replay. They do not guarantee exactly-once execution at the remote provider. A network interruption can leave a remote operation's outcome unknown. See [MCP support and limits](../MCP_CLIENT.md).
|
|
52
|
+
|
|
53
|
+
## Persistence and encryption
|
|
54
|
+
|
|
55
|
+
Credentials and MCP transaction payloads use Rails Active Record Encryption. Owner IDs, connection metadata, polling state and webhook bodies are not all encrypted by this feature. Webhook raw bodies/payloads and polling state can contain provider data; hosts must apply their own retention and access rules.
|
|
56
|
+
|
|
57
|
+
Credential merges and OAuth refresh coordinate with row locks. Polling can keep one cursor per `(grant, instance_key)`, so independent consumers do not share progress accidentally. Revocation checks query current persisted status to handle cached clients; in-flight network requests cannot be recalled.
|
|
58
|
+
|
|
59
|
+
## Webhooks and polling
|
|
60
|
+
|
|
61
|
+
Inbound webhook handling resolves the connector and grant, applies that connector's verification, persists a `WebhookEvent` and queues delivery. `DeliverWebhookJob` invokes the connector handler and optional host hook. Deduplication uses provider event IDs when available. Verification and extraction rules are provider-specific.
|
|
62
|
+
|
|
63
|
+
The host activates/deactivates declared webhook subscriptions and schedules polling. Polling commits cursor changes after a successful provider call; reliable delivery of returned items to downstream workflows is a host concern. No engine process runs an autonomous schedule.
|
|
64
|
+
|
|
65
|
+
## Errors and operational limits
|
|
66
|
+
|
|
67
|
+
REST actions return typed error envelopes; the HTTP layer maps validation, authentication, permission and quota failures to appropriate statuses. Other provider action failures use 502. Direct Ruby clients raise library errors where no action envelope is involved.
|
|
68
|
+
|
|
69
|
+
MCP preserves tool-level `isError` results separately from HTTP/protocol errors. Upstream HTTP 429 returns `rate_limited` with valid optional retry guidance. MCP transport bounds request duration, body size, pagination and interaction rounds; it validates and pins destinations. See the MCP guide for exact defaults and supported authentication mechanisms.
|
|
70
|
+
|
|
71
|
+
Production acceptance requires verification in the intended host and provider account. Passing package checks demonstrates that consumers can load the distributed artifact; it does not establish provider uptime, sufficient scopes, application authorization or load capacity.
|
data/docs/releasing.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Releasing Connectors
|
|
2
|
+
|
|
3
|
+
The public source repository is [jackhelio/connectors](https://github.com/jackhelio/connectors). The gemspec permits publishing only to `https://rubygems.org`. Releases use GitHub Actions and RubyGems Trusted Publishing; no permanent RubyGems API token is stored in GitHub.
|
|
4
|
+
|
|
5
|
+
## Account and repository setup
|
|
6
|
+
|
|
7
|
+
Enable [multi-factor authentication](https://guides.rubygems.org/setting-up-multifactor-authentication/) on the maintainer's RubyGems account. Save recovery codes outside the repository.
|
|
8
|
+
|
|
9
|
+
For the first release, create a [pending trusted publisher](https://rubygems.org/profile/oidc/pending_trusted_publishers):
|
|
10
|
+
|
|
11
|
+
| Field | Value |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| Gem name | `connectors` |
|
|
14
|
+
| Repository owner | `jackhelio` |
|
|
15
|
+
| Repository | `connectors` |
|
|
16
|
+
| Workflow filename | `release.yml` |
|
|
17
|
+
| GitHub environment | `release` |
|
|
18
|
+
|
|
19
|
+
Leave the optional workflow repository fields blank: publication runs directly in this repository's workflow. Pending publishers support the first upload and become regular trusted publishers after publication; an initial manual upload is unnecessary. Creating a pending publisher does not reserve a gem name. See the official [Trusted Publishing guide](https://guides.rubygems.org/trusted-publishing/).
|
|
20
|
+
|
|
21
|
+
Create the matching GitHub environment `release`, with a deployment tag policy allowing `v*`. Keep `main` protected with the `lint` and `test` checks required. Only maintainers with repository write access should create release tags. The release job additionally checks that the tagged commit is on `main` and that the tag matches the gem version.
|
|
22
|
+
|
|
23
|
+
## Verify a release candidate
|
|
24
|
+
|
|
25
|
+
1. Update `lib/connectors/version.rb` and the lockfile when changing versions. Finalize the corresponding version's changelog and compatibility notes.
|
|
26
|
+
2. Run the randomized RSpec suite and RuboCop against a disposable PostgreSQL database as described in [contributing](../CONTRIBUTING.md).
|
|
27
|
+
3. Verify the distributable package:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
bundle exec ruby script/verify_package.rb
|
|
31
|
+
bundle exec gem build connectors.gemspec --strict
|
|
32
|
+
gem specification connectors-0.1.0.gem files
|
|
33
|
+
```
|
|
34
|
+
|
|
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.
|
|
36
|
+
|
|
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
|
+
5. Confirm the RubyGems account and publisher configuration above are complete before pushing a tag.
|
|
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.
|
|
41
|
+
|
|
42
|
+
## Publish the verified commit
|
|
43
|
+
|
|
44
|
+
Fetch the merged commit, confirm its version and green CI, then create an annotated tag pointing to that exact commit:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
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
|
|
50
|
+
```
|
|
51
|
+
|
|
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.
|
|
53
|
+
|
|
54
|
+
Watch the Release workflow and verify the version, metadata and artifact on [RubyGems](https://rubygems.org/gems/connectors). Install the published version into a clean host before announcing it. A successful test run alone is not proof of publication.
|
|
55
|
+
|
|
56
|
+
If authentication fails before upload, fix the account/publisher configuration and rerun the workflow for the same tag. If upload may have succeeded, check RubyGems before retrying. Published versions cannot be overwritten; subsequent code changes require a new version and tag. Do not move a published release tag. Do not use `bundle exec rake release` as a validation command: it can tag, push and publish outside this workflow.
|
|
57
|
+
|
|
58
|
+
## Supported baseline
|
|
59
|
+
|
|
60
|
+
The gem declares Ruby 3.2+ and Rails 8.1.3+ within the 8.1 series. CI exercises Ruby 3.4.8 with PostgreSQL 16. Broader compatibility needs its own validation. Host authentication, encryption configuration, provider interoperability and production operations remain separate from package verification.
|