# Credential Lifecycle (/docs/connectors/credential-lifecycle)



[Authentication](/docs/connectors/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 [#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:

```mermaid
stateDiagram-v2
    [*] --> Unknown
    Unknown --> Healthy
    Healthy --> Degraded
    Healthy --> ReauthRequired
    Degraded --> ReauthRequired
    ReauthRequired --> Healthy: reconnect / rotation
    Healthy --> Revoked: revoked / deleted
    ReauthRequired --> Revoked: revoked / deleted
    Revoked --> [*]
```

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`:

```js
connection.credential_health.ready_for_routing &&
!("crm.objects.contacts.write" in connection.credential_health.missing_scopes)
```

<Callout type="info" title="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`).
</Callout>

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 [#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 [#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_connect` creates a new connection; the service mints a linked connect
  session and returns the hosted link **once** in the create response.
* `reconnect` refreshes an existing connection and must include
  `existing_connection_id`, so OAuth completion updates that connection instead of
  creating a parallel one.

<Callout type="warn" title="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`.
</Callout>

## Auto-resume on reconnect [#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:

```yaml
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 SLA
```

When 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.

<Callout type="info" title="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.
</Callout>

## Where to go next [#where-to-go-next]

* [Authentication](/docs/connectors/authentication) — how a resolved credential is decrypted at dispatch and encrypted at rest.
* [Key Custody](/docs/auth/key-custody) — how the encryption keys behind credentials are held and rotated.
* [Connections](/docs/connectors/connections) — creating connections and direct vs. route mode.
