Credential Lifecycle
The credential control plane — health facts, the requirement scanner, hosted Connect-Link authorization requests, and auto-resume on reconnect.
Authentication covers how a credential is resolved and decrypted at dispatch. This page covers everything around that: the control plane that decides whether a connection is even safe to route to, the preflight that answers which credential fields a provider needs, the durable handles for asking an end-user to authorize a connection, and how a workflow recovers when a token dies mid-run.
Every surface here shares one invariant: it summarizes credential health and names credential fields, but it never returns, logs, or resolves credential material.
Credential lifecycle
The credential lifecycle (docs/connectors/credential-lifecycle.md) is the
tenant-scoped control plane deciding whether a connection is safe to use for
routing, workflows, syncs, proxy requests, and function runs. It is stored as
three first-class records:
CredentialLifecyclePolicy— required OAuth scopes, refresh-failure thresholds, stale-credential windows, and whether degraded credentials stay eligible for routing.CredentialHealthFact— one current routing-safe readiness snapshot per connection: grade, readiness booleans, missing scopes, refresh exhaustion, connection status, policy version, redacted timestamps.CredentialLifecycleEvent— append-only, redacted history for auth checks, OAuth authorization, refresh success/failure, reauth requirements, policy updates, and revocation.
The health grade moves through an explicit state machine:
Routing receives connection.credential_health as a read-only CEL input, so
a route predicate can filter on ready_for_routing, grade, requires_reauth,
refresh_exhausted, missing_scopes, or policy_version:
connection.credential_health.ready_for_routing &&
!("crm.objects.contacts.write" in connection.credential_health.missing_scopes)Health facts are eligibility signals, not a credential source
Workflow steps still resolve credentials through ConnectionService.Resolve.
Health facts only decide whether a connection is eligible. OAuth callback,
inline refresh, manual TestAuth, reconnect recovery, and the daily health
sweep all project facts through the same lifecycle service, and event metadata
is rejected if it contains credential-like keys (token, refresh_token,
client_secret, api_key, authorization, password, private_key).
Deployment activation and promotion call PreviewCredentialLifecycleImpact first;
if every affected connection would be excluded from routing, the promotion is
blocked so operators can stage a reauth or rotation plan before retrying.
The credential requirement scanner
Before a connector is tested, canaried, or run live, the credential requirement
scanner (domain/connector/credential_scan.go) answers a narrower preflight
question: which secret/config field names must exist for this provider in
this auth mode, and where are they supplied? It never returns values — only field
names and a present/missing status.
DeriveCredentialRequirements(provider, authType) is the single source of truth,
mirroring the dispatch-time handlers. Connection-form fields come from
provider.CredentialSchema; platform fields are added per auth mode:
| Auth mode | Platform requirements |
|---|---|
none | (none) |
secret_text, basic, custom, two_step | schema fields only |
oauth2, oauth2_client_credentials, mcp_oauth2 | client_id (public), client_secret (secret) |
oauth1 | consumer_key (public), consumer_secret (secret) |
jwt | signing_key (secret) |
Each scan produces a redacted CredentialScanReceipt — missing requirements,
present field names, non-blocking warnings — and feeds the plan-091 setup-readiness
engine, so a missing required credential blocks the real setup-readiness surfaces
with a typed NotReadyError rather than being downgraded to a warning. The
read-only MCP tool scan_connector_credentials returns the requirement set plus
that receipt; field names appear, values never do.
Authorization requests and hosted Connect-Link
An authorization request (docs/connectors/authorization-requests.md) is a
durable wait handle for asking an end-user to authorize a provider connection. It
sits above a short-lived connect session: the session mints the hosted link and
one-time token, while the request records tenant/environment, provider, mode,
source operation, status, and redacted lifecycle evidence.
initial_connectcreates a new connection; the service mints a linked connect session and returns the hosted link once in the create response.reconnectrefreshes an existing connection and must includeexisting_connection_id, so OAuth completion updates that connection instead of creating a parallel one.
The hosted link is returned exactly once
Create returns the hosted Connect-Link once. Subsequent get/list/wait responses
expose only request state and redacted evidence — never the plaintext
connect-session token or a reusable link. Workflow and routing steps should park
on the request's wait_correlation_key and resume from its lifecycle event
(completed / expired / revoked / failed) rather than polling
ListConnections.
Auto-resume on reconnect
When a long-lived OAuth connection's refresh token is revoked, a naive workflow
step fails the run and waits for a human. Auto-resume
(application/connector/connection_recovery.go) replaces that with a step that
pauses and resumes itself once the user reconnects.
Place a waitForEvent step upstream of any connector action that depends on a
long-lived connection:
nodes:
- id: wait_until_connection_healthy
type: waitForEvent
config:
event_name: connector.connection.recovered # exact name the recovery service publishes
correlation_key: "{{ connection.id }}" # must be the connection UUID
timeout_ms: 604800000 # 7 days — tune to your SLAWhen the end-user completes the hosted reconnect, OAuthCallback updates the
existing connection row, RecoverConnection re-validates with TestAuth, resets
the refresh-failure count, flips the connection back to active, and — when
publish_resume_events: true — publishes connector.connection.recovered. The
event-pause substrate matches the registered pause and the coordinator drains the
resume signal on its next tick.
Recovery never writes run state
Recovery is default-off (connector.connection_recovery.enabled) and, when on,
only publishes events — the coordinator remains the sole writer of
eec_workflow_run (the eec_ prefix marks Ductor's durable Enterprise
Eventing Core tables). After max_refresh_attempts failures the refresh path
short-circuits with ErrTokenExpired without contacting the IdP, protecting
it from hammering a dead token. Fully automatic parking on ErrConnectionExpired
without an upstream waitForEvent is deferred; the explicit wait pattern is the
supported path today.
Where to go next
- Authentication — how a resolved credential is decrypted at dispatch and encrypted at rest.
- Key Custody — how the encryption keys behind credentials are held and rotated.
- Connections — creating connections and direct vs. route mode.
Action Connections
Creating and resolving connections, direct vs. route mode, connect sessions, and the management API with real request/response payloads.
Triggers & Polling
Webhook ingress vs. polling, the three trigger modes, signature verification, provider-webhook reconciliation, and event coalescing.