Connections and their lifecycle¶
A connection is an owner's relationship with one account at a provider. It is a record with a name, the account the provider reported, the permissions the provider says it holds, a lifecycle state, and observed health. It holds no secret; its credential is sealed and stored separately.
State and health¶
Two things are tracked separately because they answer different questions.
| Answers | Changes when | |
|---|---|---|
| State | What was decided? | Someone connects, disconnects or reconnects, or a check finds a permanent failure |
| Health | What was last observed? | Every validation, including temporary failures |
A connected connection can show a failed last check, for example because the
provider was briefly unavailable. That does not make it unusable.
States¶
| State | Meaning | May be used |
|---|---|---|
pending |
Created, not yet validated | No |
connected |
The provider accepted the credential at the last check | Yes |
needs_attention |
A permanent failure: revoked upstream, expired, or a permission lost | No, until reconnected |
disconnected |
A person disconnected it; credentials are destroyed | No |
revoked |
Revoked at the provider; terminal | No |
pending ──validated──► connected ──► disconnected
│ ▲ ▲
failure ▼ │ success │
needs_attention ─────┘
│
▼
revoked
Every store enforces the transition table; a change it forbids returns
ErrInvalidTransition.
Health¶
| Field | Meaning |
|---|---|
LastValidatedAt |
When the provider was last asked |
LastValidation |
ok, failed or unknown |
LastError |
A classified message with the status and the provider's request ID. Never a response body |
ConsecutiveFails |
Failed checks in a row; reset by a success |
Failures are classified¶
| Provider answers | Error | Effect on a check |
|---|---|---|
| 401, other 4xx | ErrInvalidCredential |
Permanent: state becomes needs_attention |
| 403 | ErrInsufficientScope |
Permanent: state becomes needs_attention |
| 429 | ErrRateLimited |
Health only |
| 5xx, network error, unparseable response | ErrTransient |
Health only |
connectforge.Permanent(err) reports whether an error is one of the
permanent kinds.
The operations¶
All operations are on service.Service. Each checks authorization through
your Authorizer and records an audit event.
| Operation | Requires | What it does |
|---|---|---|
Create |
manage |
Checks the supplied fields against the manifest, validates with the provider, then stores the connection and its sealed credential. A rejected credential stores nothing |
Get |
view |
Returns one connection |
List |
view |
Returns the owner's connections the actor may view, optionally filtered by provider and state |
Rename |
manage |
Changes the display name |
Test |
use |
Validates again and updates health and, for a permanent failure or a recovery, state |
Disconnect |
disconnect |
Marks the connection disconnected and destroys its credentials. The record stays, so the audit trail and bindings keep their reference |
Reconnect |
manage |
Replaces the credential, keeping the connection and everything bound to it. See below |
Bindings |
view |
Lists what depends on the connection, so a person can see what a disconnect would stop |
Bind, Unbind |
use |
Records, or removes, that a consumer depends on the connection for a purpose |
Reconnect¶
A token expires, is revoked, or is rotated. Reconnect puts the connection
right without creating a new one, so the features bound to it keep working:
conn, err := svc.Reconnect(ctx, service.ReconnectInput{
Owner: owner, Actor: actor, ID: conn.ID,
Fields: map[string]cf.Secret{"token": cf.SecretString(newToken)},
})
The new credential is validated with the provider before anything is
stored, so a rejected one changes nothing. It must belong to the same account
as the connection (ErrAccountMismatch otherwise): connecting another account
is a new connection, so a feature never silently reads someone else's data.
On success the connection is connected again with fresh health, and its
credential has a new version.
Reconnect also recovers a connection whose sealed credential can no longer be
read, for example after a lost key ring: the new credential is sealed without
opening the old one. A disconnected connection can be reconnected; a
revoked one cannot.
Test, in detail¶
Test does not return the validation failure as an error. A failed check is a
normal outcome that is recorded; read it from the returned connection:
conn, err := svc.Test(ctx, owner, actor, id)
if err != nil {
return err // not found, denied, or the connection cannot be tested
}
switch {
case conn.State == cf.StateNeedsAttention:
// the person must reconnect
case conn.Health.LastValidation == cf.OutcomeFailed:
// a temporary problem; try again later
}
A connection that is disconnected or revoked cannot be tested and returns
ErrNotUsable.
Errors a caller handles¶
| Error | Meaning |
|---|---|
ErrNotFound |
No such record for this owner. A record of another owner is also not found |
ErrDenied |
The Authorizer refused |
ErrNotUsable |
The connection's state does not allow it |
ErrAccountMismatch |
A reconnect's credential belongs to a different account |
ErrConflict |
Another writer changed it first; retry |
ErrInvalidTransition |
The lifecycle forbids the change |
ErrCredentialUnavailable |
The credential could not be read or decrypted. The connection is not used; there is no fallback |
service.ErrInvalidInput |
A field is missing, unknown, or the auth method does not exist |
service.ErrNotAudited |
The change was made, but its audit event could not be recorded |
ErrNotAudited is special: the operation succeeded, and the returned
connection is valid. Treat the gap in the audit trail as an incident rather
than retrying the change.