# Connection

inseam's plugin-defined port for *reaching* data endpoints — a Gmail mailbox, a Twilio account, a local filesystem, a paired Apple Watch, another inseam node. A plugin contributes a `ConnectionDefinition` per kind; core registers it, drives interactive setup, persists the credential opaquely, runs liveness verification, and tears down. Plugins compile against `@inseam/connection-contract` alone.

**See also:** [design](../design/connection.md) · [spec](../spec/connection.md)

## What it does

Two surfaces, split by package:

- **Contract surface** (`@inseam/connection-contract`) — `ConnectionDefinition`, `SetupStep`, `SetupContext`, `VerifyContext`, `VerifyResult`, `InboundHttpService`, `CallbackHandle`, the error hierarchy. Zero runtime deps. The stable surface plugin authors compile against.
- **Core surface** (`@inseam/core`, Connection contribution) — `openConnectionRegistry`, `Connection`, `ConnectionRegistry`, `SetupSession`. Owns the in-memory kind table, the host-services context, persistence of rows, atomic liveness updates, and the process-local liveness subscription pool.

A setup flow is host-driven: the plugin yields an `AsyncIterable<SetupStep>` (`prompt`, `redirect`, terminal `done` / `failed`); the host renders each step, gathers input via `advanceSetup`, and resumes the iterator. A `redirect` resumes itself when its `awaitCallback()` fires against a host-provided `InboundHttpService`. Core never decodes the credential — it calls `serializeCredential` once on `done` and `parseCredential` per `verify` / `teardown`.

Liveness is coarse: `live` / `offline` / `failing` / `unknown`. A failing endpoint is data (`liveness: "failing"`), not an exception. Subscribers see process-local change events via `Connection.onLivenessChange`.

## How to use it

A host wires services + a Store-backed registry:

```ts
import { openStore, openConnectionRegistry } from "@inseam/core";
import { bunSqliteAdapter } from "@inseam/adapter-store-bun-sqlite";
import { gmailConnection } from "inseam-plugin-gmail";

const store = await openStore(bunSqliteAdapter("./inseam.sqlite"));
const registry = await openConnectionRegistry({
  store,
  services: { inboundHttp: myInboundHttp },
});

registry.registerConnectionKind(gmailConnection);

const session = await registry.beginSetup("gmail");
// host renders session.currentStep, calls advanceSetup for prompts,
// awaits the redirect's callback for OAuth flows, etc.
const done = await session.terminal; // resolves once a row is persisted

const conns = await registry.listConnections();
await registry.preflight(conns.map((c) => c.id));
```

Full type signatures: [spec — Public API](../spec/connection.md#public-api).

## How to extend it

A new Connection kind is a standalone package depending on `@inseam/connection-contract` (and optionally `@inseam/connection-oauth2`). First-party plugins live at `pkgs/plugin-<name>/`; third-party plugins publish as `inseam-plugin-<name>`. They are interchangeable in `registry.registerConnectionKind`.

The minimum a plugin author needs:

1. Pick a stable `kind` string and declare `displayName` + `capabilities` (what the *integration* supports, not what the host can wire).
2. Implement `setup(ctx): AsyncIterable<SetupStep>`. Yield prompts and at most one `redirect` per step, terminate with exactly one `done` or `failed`. Guard `ctx.inboundHttp` — yield `failed` when undefined or when `allocateCallback()` returns `null`.
3. Implement `serializeCredential` / `parseCredential` (any encoding; core treats the blob as opaque bytes).
4. Implement `verify(credential, ctx)`. Return `VerifyResult`; throw only for programmer errors. Return a fresh `credential` to silently rotate — core persists atomically with the liveness update.
5. Implement `teardown` idempotently. A throw is logged; the row is deleted regardless.

The OAuth2 case is one-liner config via the [OAuth2 helper](./connection-oauth2.md). For API-key or webhook-secret flows, write `ConnectionDefinition` directly — the [spec's Twilio sketch](../spec/connection.md#plugin-author-perspective) is the worked example.

## Where the code lives

- Contract package: `pkgs/connection-contract/src/index.ts`
- Core surface (registry, lifecycle, liveness): `pkgs/core/src/connection/registry.ts`
- Setup-session driver: `pkgs/core/src/connection/setup-session.ts`
- Row storage + migrations: `pkgs/core/src/connection/rows.ts`, `migrations.ts`
- Default id generator (UUIDv7-class): `pkgs/core/src/connection/ids.ts`
- Test fixtures (fake plugins, fake inbound HTTP): `pkgs/core/src/connection/__fixtures__/`
- Behavior tests, one file per concern: `pkgs/core/src/connection/*.test.ts`

## Related

- [Spec — connection](../spec/connection.md) — types, behavior, errors, acceptance criteria
- [Design — connection](../design/connection.md) — why Connection is decoupled from Source and Permissions
- [Connection OAuth2 helper](./connection-oauth2.md) — `oauth2(config)` factory for the common case
- [Store](./store.md) — Connection rows and credential blobs persist through this port
- [Monorepo package model](./monorepo-package-model.md) — package roles, naming, dependency direction
