connectionforge

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 13, 2026 License: MIT Imports: 8 Imported by: 0

README

ConnectionForge

Go CI Go Lint Go SAST Docs Docs Visualization License

Manage a tenant's connections to external services in Go: provider definitions, connections, encrypted credentials, grants and bindings, and a broker that hands out authenticated HTTP clients so callers never handle a raw secret.

Status: in development. Static-token connections work end to end: the model, the in-memory and SQL stores, the egress policy, the broker and the service. GitHub App and OAuth follow. See the specifications.

What it does

  • One model for every connection: provider, connection, credential, grant, binding, with a lifecycle that separates what was decided from what was observed.
  • Owns the credential lifecycle, not the API clients. Provider SDKs stay in their own modules and receive an authenticated http.Client.
  • Safe refresh and rotation: single-flight refresh that persists a rotated refresh token atomically.
  • Tested connections: validation with account discovery on create, on demand and on a schedule.
  • Ports and adapters: storage, encryption, authorization, audit and egress policy are interfaces the host supplies.
  • Declarative provider manifests so a UI or an agent can render and repair a connection.

What it is not

  • Not a provider API client library.
  • Not a cryptography library or a secret manager; encryption and secret retrieval are ports.
  • Not a workflow engine.

Design

License

MIT

Documentation

Overview

Package connectionforge manages a tenant's connections to external services: the provider, the connection, its encrypted credential, who may use it and what depends on it.

It owns the credential lifecycle, not the provider API clients. A caller asks the broker for an authenticated net/http.Client and never handles a raw secret. Storage, encryption, authorization, audit and egress policy are ports that the host application supplies.

The design is in docs/specs/initiatives/INIT-CONNECTIONFORGE-001.

Index

Constants

View Source
const CredentialPurpose = "connectionforge.credential"

CredentialPurpose is the purpose under which credentials are sealed.

Variables

View Source
var (
	// ErrNotFound is returned when a record does not exist for the owner. A
	// record that belongs to another owner is also not found.
	ErrNotFound = errors.New("connectionforge: not found")
	// ErrExists is returned when creating a record that already exists.
	ErrExists = errors.New("connectionforge: already exists")
	// ErrConflict is returned when a compare-and-swap finds a different version
	// than expected: another writer got there first.
	ErrConflict = errors.New("connectionforge: version conflict")
	// ErrInvalidTransition is returned for a lifecycle change the transition
	// table does not allow.
	ErrInvalidTransition = errors.New("connectionforge: invalid state transition")
	// ErrLeaseHeld is returned when a lease is held by someone else.
	ErrLeaseHeld = errors.New("connectionforge: lease held")
)

Store errors.

View Source
var (
	// ErrDenied is returned when the host's authorizer refuses an action.
	ErrDenied = errors.New("connectionforge: denied")
	// ErrNotUsable is returned when a connection's state does not allow use.
	ErrNotUsable = errors.New("connectionforge: connection not usable")
	// ErrCredentialUnavailable is returned when a credential cannot be read or
	// decrypted. The connection is not used: there is no fallback.
	ErrCredentialUnavailable = errors.New("connectionforge: credential unavailable")
)

Use errors.

View Source
var (
	// ErrInvalidCredential means the provider rejected the credential: it is
	// wrong, expired or revoked. It is a permanent failure.
	ErrInvalidCredential = errors.New("connectionforge: invalid credential")
	// ErrInsufficientScope means the credential is valid but lacks a
	// permission the connection needs. It is a permanent failure.
	ErrInsufficientScope = errors.New("connectionforge: insufficient scope")
	// ErrRateLimited means the provider is limiting requests. It is transient.
	ErrRateLimited = errors.New("connectionforge: rate limited")
	// ErrTransient means a network failure or provider error that may succeed
	// on retry.
	ErrTransient = errors.New("connectionforge: transient failure")
)

Provider errors, as classified by a provider adapter. An adapter wraps one of these; it never puts a request or response body in an error.

Functions

func CredentialAAD

func CredentialAAD(owner OwnerID, connectionID string, version int) []byte

CredentialAAD returns the additional data a credential is sealed with. It binds the ciphertext to its owner, connection and version, so it cannot be moved to another record.

func Permanent

func Permanent(err error) bool

Permanent reports whether err is a provider failure that retrying will not fix, so the connection needs attention.

Types

type Action

type Action string

Action is something a principal may do with a connection.

const (
	ActionView       Action = "view"
	ActionUse        Action = "use"
	ActionManage     Action = "manage"
	ActionShare      Action = "share"
	ActionDisconnect Action = "disconnect"
)

Actions on a connection.

type Auditor

type Auditor interface {
	Record(ctx context.Context, e Event) error
}

Auditor records events. An event never carries a secret or a response body.

type Authorizer

type Authorizer interface {
	Allowed(ctx context.Context, p Principal, a Action, c Connection) error
}

Authorizer decides whether a principal may act on a connection. A refusal is an error wrapping ErrDenied.

type Binding

type Binding struct {
	Owner        OwnerID   `json:"owner"`
	ConnectionID string    `json:"connectionId"`
	Consumer     Consumer  `json:"consumer"`
	Purpose      string    `json:"purpose"`
	CreatedAt    time.Time `json:"createdAt"`
}

Binding is dependency: a consumer that uses a connection for a purpose.

type BindingStore

type BindingStore interface {
	// PutBinding records that a consumer uses a connection for a purpose. It
	// is idempotent.
	PutBinding(ctx context.Context, b Binding) error
	// RemoveBinding removes a binding. Removing none is not an error.
	RemoveBinding(ctx context.Context, owner OwnerID, connectionID string, c Consumer, purpose string) error
	// ListBindings returns a connection's bindings.
	ListBindings(ctx context.Context, owner OwnerID, connectionID string) ([]Binding, error)
}

BindingStore holds bindings.

type Clock

type Clock interface{ Now() time.Time }

Clock tells the time. Tests supply a fake one.

type Connection

type Connection struct {
	ID                string            `json:"id"`
	Owner             OwnerID           `json:"owner"`
	Provider          string            `json:"provider"`
	AuthMethod        string            `json:"authMethod"`
	DisplayName       string            `json:"displayName"`
	ExternalAccountID string            `json:"externalAccountId,omitempty"`
	ExternalLabel     string            `json:"externalLabel,omitempty"`
	Scopes            []string          `json:"scopes,omitempty"`
	State             State             `json:"state"`
	Health            Health            `json:"health"`
	Metadata          map[string]string `json:"metadata,omitempty"`
	CreatedBy         Principal         `json:"createdBy"`
	CreatedAt         time.Time         `json:"createdAt"`
	UpdatedAt         time.Time         `json:"updatedAt"`
	// Revision increases on every update and drives compare-and-swap.
	Revision uint64 `json:"revision"`
}

Connection is an owner's relationship with one account at a provider. It holds no secret.

func (Connection) Clone

func (c Connection) Clone() Connection

Clone returns a deep copy of c.

type ConnectionStore

type ConnectionStore interface {
	// CreateConnection stores a new connection with revision 1. It returns
	// [ErrExists] if the ID is taken.
	CreateConnection(ctx context.Context, c Connection) (Connection, error)
	// GetConnection returns one connection.
	GetConnection(ctx context.Context, owner OwnerID, id string) (Connection, error)
	// ListConnections returns the owner's connections that match f, ordered
	// by ID.
	ListConnections(ctx context.Context, owner OwnerID, f Filter) ([]Connection, error)
	// UpdateConnection replaces a connection if its stored revision equals
	// c.Revision, and returns it with the next revision. Otherwise it returns
	// [ErrConflict]. A state change the transition table forbids returns
	// [ErrInvalidTransition].
	UpdateConnection(ctx context.Context, c Connection) (Connection, error)
	// DeleteConnection removes a connection with its credentials, grants and
	// bindings.
	DeleteConnection(ctx context.Context, owner OwnerID, id string) error
}

ConnectionStore holds connections. Every method is scoped by owner: a record of another owner is ErrNotFound. Returned records are copies.

type Consumer

type Consumer struct {
	Kind string `json:"kind"`
	ID   string `json:"id"`
}

Consumer is something that depends on a connection: an application, a workflow or an agent.

type Credential

type Credential struct {
	Owner        OwnerID `json:"owner"`
	ConnectionID string  `json:"connectionId"`
	// Version starts at 1 and increases by one with each replacement or
	// refresh.
	Version int `json:"version"`
	// Kind names the plaintext's structure, which its provider defines, for
	// example "api_token" or "oauth_tokens".
	Kind string `json:"kind"`
	// Ciphertext is the sealed material, from the [Cryptographer].
	Ciphertext []byte `json:"-"`
	// ExpiresAt is when the access token expires, if known.
	ExpiresAt *time.Time `json:"expiresAt,omitempty"`
	CreatedAt time.Time  `json:"createdAt"`
}

Credential is the encrypted authorization material for a connection, one version of it. Plaintext never appears here.

func (Credential) Clone

func (c Credential) Clone() Credential

Clone returns a deep copy of c.

type CredentialStore

type CredentialStore interface {
	// SwapCredential installs next as the active version if the active
	// version is expectActive (0 for none) and next.Version is
	// expectActive+1. Otherwise it returns [ErrConflict]. The previous
	// version is kept until destroyed.
	SwapCredential(ctx context.Context, expectActive int, next Credential) error
	// ActiveCredential returns the active version.
	ActiveCredential(ctx context.Context, owner OwnerID, connectionID string) (Credential, error)
	// DestroyCredentials removes every version for a connection.
	DestroyCredentials(ctx context.Context, owner OwnerID, connectionID string) error
}

CredentialStore holds sealed credential versions.

type Cryptographer

type Cryptographer interface {
	Seal(ctx context.Context, purpose string, plaintext, aad []byte) ([]byte, error)
	Open(ctx context.Context, purpose string, ciphertext, aad []byte) ([]byte, error)
}

Cryptographer seals and opens credential material. A key-management library implements it; the host supplies the adapter. Open with a different purpose or aad must fail.

type Event

type Event struct {
	Time         time.Time         `json:"time"`
	Type         EventType         `json:"type"`
	Owner        OwnerID           `json:"owner"`
	Actor        Principal         `json:"actor"`
	OnBehalfOf   *Principal        `json:"onBehalfOf,omitempty"`
	ConnectionID string            `json:"connectionId,omitempty"`
	Provider     string            `json:"provider,omitempty"`
	Success      bool              `json:"success"`
	Reason       string            `json:"reason,omitempty"`
	Details      map[string]string `json:"details,omitempty"`
}

Event is an audit record.

type EventType

type EventType string

EventType names what happened.

const (
	EventConnectionCreated      EventType = "connection.created"
	EventConnectionValidated    EventType = "connection.validated"
	EventConnectionStateChanged EventType = "connection.state_changed"
	EventConnectionUpdated      EventType = "connection.updated"
	EventConnectionDisconnected EventType = "connection.disconnected"
	EventConnectionDeleted      EventType = "connection.deleted"
	EventCredentialReplaced     EventType = "credential.replaced"  //nolint:gosec // G101: Enum/constant identifier matches the credential-name heuristic, but the value is a public tag, not a secret
	EventCredentialRefreshed    EventType = "credential.refreshed" //nolint:gosec // G101: Enum/constant identifier matches the credential-name heuristic, but the value is a public tag, not a secret
	EventCredentialRevoked      EventType = "credential.revoked"   //nolint:gosec // G101: Enum/constant identifier matches the credential-name heuristic, but the value is a public tag, not a secret
	EventGrantChanged           EventType = "grant.changed"
	EventAccessDenied           EventType = "access.denied"
)

Event types.

type Filter

type Filter struct {
	Provider string
	State    State
}

Filter narrows a connection listing. Zero fields match everything.

func (Filter) Match

func (f Filter) Match(c Connection) bool

Match reports whether c passes the filter.

type Grant

type Grant struct {
	Owner        OwnerID   `json:"owner"`
	ConnectionID string    `json:"connectionId"`
	Principal    Principal `json:"principal"`
	Actions      []Action  `json:"actions"`
}

Grant is authority: what a principal may do with a connection.

func (Grant) Clone

func (g Grant) Clone() Grant

Clone returns a deep copy of g.

type GrantStore

type GrantStore interface {
	// PutGrant creates or replaces the grant for a principal on a connection.
	PutGrant(ctx context.Context, g Grant) error
	// RemoveGrant removes a principal's grant. Removing none is not an error.
	RemoveGrant(ctx context.Context, owner OwnerID, connectionID string, p Principal) error
	// ListGrants returns a connection's grants.
	ListGrants(ctx context.Context, owner OwnerID, connectionID string) ([]Grant, error)
}

GrantStore holds grants.

type Health

type Health struct {
	LastValidatedAt  time.Time `json:"lastValidatedAt,omitzero"`
	LastValidation   Outcome   `json:"lastValidation,omitempty"`
	LastError        string    `json:"lastError,omitempty"`
	ConsecutiveFails int       `json:"consecutiveFails,omitempty"`
}

Health is what was last observed about a connection, kept apart from its lifecycle State.

type Lease

type Lease interface {
	// Release gives the lease up before it expires.
	Release(ctx context.Context) error
}

Lease is a held lease.

type Leaser

type Leaser interface {
	// Acquire takes the lease named key for ttl, or returns [ErrLeaseHeld].
	Acquire(ctx context.Context, key string, ttl time.Duration) (Lease, error)
}

Leaser grants short exclusive leases, used to make refresh single-flight across processes.

type Outcome

type Outcome string

Outcome is the result of the last validation.

const (
	OutcomeUnknown Outcome = "unknown"
	OutcomeOK      Outcome = "ok"
	OutcomeFailed  Outcome = "failed"
)

Validation outcomes.

type OwnerID

type OwnerID string

OwnerID is the tenant scope that owns a record. It is opaque to the library; a host typically uses an organization ID. Every store method is scoped by it.

type Principal

type Principal struct {
	Kind PrincipalKind `json:"kind"`
	ID   string        `json:"id"`
}

Principal is who or what performs an action, as the host identifies it.

type PrincipalKind

type PrincipalKind string

PrincipalKind distinguishes who or what acts.

const (
	PrincipalUser    PrincipalKind = "user"
	PrincipalService PrincipalKind = "service"
	PrincipalAgent   PrincipalKind = "agent"
)

Principal kinds.

type Secret

type Secret struct {
	// contains filtered or unexported fields
}

Secret holds credential material. It redacts itself wherever a value is printed, logged or encoded, so a secret placed in a struct, an error or a log record does not leak. Only Secret.Reveal returns the value, and it is called only by the broker and provider adapters.

func NewSecret

func NewSecret(b []byte) Secret

NewSecret returns a secret holding a copy of b.

func SecretString

func SecretString(s string) Secret

SecretString returns a secret holding s.

func (Secret) Clear

func (s Secret) Clear()

Clear overwrites the secret's value in place, on a best-effort basis.

func (Secret) Format

func (Secret) Format(f fmt.State, _ rune)

Format prints a fixed placeholder for every verb. Format has no way to report a failed write to fmt, which records write errors itself, so the result of Write is deliberately not used.

func (Secret) GoString

func (Secret) GoString() string

GoString returns a fixed placeholder.

func (Secret) IsZero

func (s Secret) IsZero() bool

IsZero reports whether the secret is empty.

func (Secret) LogValue

func (Secret) LogValue() slog.Value

LogValue logs a fixed placeholder.

func (Secret) MarshalJSON

func (Secret) MarshalJSON() ([]byte, error)

MarshalJSON encodes a fixed placeholder.

func (Secret) MarshalText

func (Secret) MarshalText() ([]byte, error)

MarshalText encodes a fixed placeholder.

func (Secret) Reveal

func (s Secret) Reveal() []byte

Reveal returns a copy of the secret's value. The caller clears it when done.

func (Secret) String

func (Secret) String() string

String returns a fixed placeholder.

type SecretSource

type SecretSource interface {
	Secret(ctx context.Context, ref string) (Secret, error)
}

SecretSource reads secrets that belong to the deployment rather than to an owner, such as an OAuth client secret or a GitHub App private key. A secret manager implements it; the host supplies the adapter.

type State

type State string

State is a connection's lifecycle state: what was decided. What was last observed is Health; a transient failure changes health, never state.

const (
	// StatePending is a connection created but not yet authorized or validated.
	StatePending State = "pending"
	// StateConnected is a connection whose credential the provider accepted at
	// the last check.
	StateConnected State = "connected"
	// StateNeedsAttention is a connection with a permanent failure: revoked
	// upstream, expired, or a scope lost. It is not used until reconnected.
	StateNeedsAttention State = "needs_attention"
	// StateDisconnected is a connection a person disconnected. Its credentials
	// are destroyed.
	StateDisconnected State = "disconnected"
	// StateRevoked is a connection whose credential was revoked at the
	// provider. It is terminal.
	StateRevoked State = "revoked"
)

Lifecycle states.

func States

func States() []State

States returns every lifecycle state.

func (State) CanTransition

func (s State) CanTransition(next State) bool

CanTransition reports whether s may move to next. Staying in the same state is always allowed.

func (State) CheckTransition

func (s State) CheckTransition(next State) error

CheckTransition returns an error wrapping ErrInvalidTransition if s may not move to next.

func (State) Next

func (s State) Next() []State

Next returns the states s may move to.

func (State) Usable

func (s State) Usable() bool

Usable reports whether a connection in state s may be used.

func (State) Valid

func (s State) Valid() bool

Valid reports whether s is a known state.

type Store

Store is every store the library needs. A host may implement it with one type, as the packages memstore and sqlstore do.

type SystemClock

type SystemClock struct{}

SystemClock is the real clock.

func (SystemClock) Now

func (SystemClock) Now() time.Time

Now returns the current time.

type Use

type Use struct {
	Consumer Consumer `json:"consumer"`
	// Purpose is the feature using the connection, for example "code-health".
	Purpose string `json:"purpose"`
	// OnBehalfOf is the principal an agent or service acts for, if any.
	OnBehalfOf *Principal `json:"onBehalfOf,omitempty"`
}

Use describes why a connection is being used, for authorization and for use counters.

Directories

Path Synopsis
Package broker is the only path from a stored credential to an authenticated request.
Package broker is the only path from a stored credential to an authenticated request.
Package cryptotest is the conformance suite for a [connectionforge.Cryptographer], and an in-memory cryptographer for tests.
Package cryptotest is the conformance suite for a [connectionforge.Cryptographer], and an in-memory cryptographer for tests.
Package egress restricts where a provider request may go: only to the hosts a provider's manifest names, over HTTPS, without following a redirect to another host, and never to a private, loopback or link-local address, which is checked after DNS resolution so a rebinding name cannot reach one.
Package egress restricts where a provider request may go: only to the hosts a provider's manifest names, over HTTPS, without following a redirect to another host, and never to a private, loopback or link-local address, which is checked after DNS resolution so a rebinding name cannot reach one.
internal
fakeapi
Package fakeapi is a token-authenticated HTTPS API for tests: a stand-in for a provider such as a CDN or analytics service.
Package fakeapi is a token-authenticated HTTPS API for tests: a stand-in for a provider such as a CDN or analytics service.
Package provider defines what a provider adapter implements: a declarative Manifest, validation with account discovery, and authentication of outgoing requests.
Package provider defines what a provider adapter implements: a declarative Manifest, validation with account discovery, and authentication of outgoing requests.
apikey
Package apikey is a provider for services that issue a static API key or token, driven by configuration: where the token goes in a request, which endpoint verifies it, and where the account is in the response.
Package apikey is a provider for services that issue a static API key or token, driven by configuration: where the token goes in a request, which endpoint verifies it, and where the account is in the response.
Package service holds connectionforge's operations: create, get, list, rename, test and disconnect.
Package service holds connectionforge's operations: create, get, list, rename, test and disconnect.
Package sqlstore is connectionforge's [connectionforge.Store] and [connectionforge.Leaser] on SQLite or PostgreSQL, through Ent.
Package sqlstore is connectionforge's [connectionforge.Store] and [connectionforge.Leaser] on SQLite or PostgreSQL, through Ent.
ent
ent/schema
Package schema is the Ent schema of connectionforge's SQL store.
Package schema is the Ent schema of connectionforge's SQL store.
store
memstore
Package memstore is an in-memory [connectionforge.Store] and [connectionforge.Leaser].
Package memstore is an in-memory [connectionforge.Store] and [connectionforge.Leaser].
storetest
Package storetest is the conformance suite every [connectionforge.Store] and [connectionforge.Leaser] must pass.
Package storetest is the conformance suite every [connectionforge.Store] and [connectionforge.Leaser] must pass.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL