# Access

inseam's first-class permission layer over Source data — envelopes resolve into durable Principals, declarative rules grant/deny per `(Principal, Source)`, and a live recheck runs on every fetch. The third leg of the Connection / Source / Access split: Connection holds reach, Source holds addresses, Access holds scope.

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

## What it does

Two surfaces, split by package:

- **Contract surface** (`@inseam/access-contract`) — `Principal`, `Identifier`, `Network`, `Host`, `Envelope`, `EnvelopeParty`, `SourceParticipant`, `AccessGrant`, the rule AST (`AccessConfig`, `Rule`, `Predicate`, `TargetSelector`), the `IdentifierKindDefinition` and `CeremonyAdapter` interfaces, event payload shapes, and the `AccessError` hierarchy. Zero runtime deps.
- **Core surface** (`@inseam/core`, Access contribution) — `openAccessRegistry`, `AccessRegistry`, the `defineAccess` builder and predicate factories (`grant`, `deny`, `identifierEquals`, `identifierMatches`, `principalHasRole`, `sourceKindIn`, `roleIn`, `all`, `any`, `not`, …), the principal-resolver and access-projection materializers, and the egress filter.

The data model rides Store tables; the read and write paths ride the Events outbox. Two core-owned materializers register against the supplied `EventsRegistry`:

- **`access.principal-resolver`** — consumes `core.envelope_indexed`, canonicalizes each party's identifier, find-or-creates the `Principal`, writes `source_participant` rows with `effectiveTrust = min(identifier.trust, party.trust)`.
- **`access.access-projection`** — consumes envelope, rule-change, identifier-trust, and merge events; JOINs participants × rules × host owners; writes `access_grant` rows. Deny-wins. Rule changes write into a shadow projection and atomically swap.

Two read paths:

- **`checkAccess`** — authoritative. Re-evaluates the current rule set against current trust state for one `(principalId, sourceId)`. Cheap, indexed. Used on every fetch.
- **`listAccessibleSources`** — projection-backed listing. Stale on a race with a rule change; callers needing authoritative state recheck per row.

Trust is a closed ladder (`claimed` < `provider-asserted` < `verified`). Plugins assert at most `provider-asserted` from envelope data; `verified` rows exist only via `principal.identifier_verified`, which only registered ceremony adapters may emit.

## How to use it

A host opens the registry, registers vocabulary, and installs the rule set:

```ts
import {
  openAccessRegistry,
  defineAccess,
  grant,
  deny,
  identifierMatches,
  principalHasRole,
} from "@inseam/core";

const access = await openAccessRegistry({ store, events });

access.registerIdentifierKind(slackUserKind);
access.registerCeremonyAdapter(magicLinkAdapter);

await access.setRules(
  defineAccess({
    rules: [
      grant({ id: "default:network-owner", when: principalHasRole("network-owner"), requires: "verified" }),
      grant({ id: "default:host-owner", when: principalHasRole("host-owner"), requires: "verified" }),
      grant({
        id: "acme-customers",
        when: identifierMatches({ kind: "email", scope: "global", domain: "acme.com" }),
        to: { kinds: ["gmail.message"], roles: ["recipient", "cc"] },
        requires: "verified",
      }),
    ],
  }),
);

const decision = await access.checkAccess({ principalId, sourceId });
```

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

## How to extend it

Plugins contribute Access vocabulary, never rules. Two extension points, both depending only on `@inseam/access-contract`:

1. **Identifier kinds** — implement `IdentifierKindDefinition` (a `kind` name, a `scopeDiscipline` description, a deterministic `canonicalize`). Register via `access.registerIdentifierKind`. The framework canonicalizes once at envelope ingest and again at predicate match; raw values are never compared.
2. **Ceremony adapters** — implement `CeremonyAdapter` (a `name`, `supportedKinds`, an async `run` that returns a signed `CeremonyAttestation`). Register via `access.registerCeremonyAdapter`. Only registered adapters may produce `principal.identifier_verified`; the events layer rejects emissions of that event type from any other origin.

Plugin-defined roles MUST be `plugin.scope`-prefixed (e.g. `"gmail.thread-owner"`); the framework's reserved role values are `sender`, `recipient`, `cc`, `bcc`, `mentioned`, `owner`. `defineAccess` rejects unprefixed values.

Plugins do not write envelope rows directly — that flows through a future ingest API which must emit `core.envelope_indexed` atomically with each envelope batch so the materializers pick the write up.

## Where the code lives

- Contract package: `pkgs/access-contract/src/index.ts`
- Core surface: `pkgs/core/src/access/`
  - `registry.ts` — `openAccessRegistry`, the registry implementation, materializers, pairing, merge, predicate evaluation, the read path, the egress filter
  - `rules.ts` — `defineAccess`, `grant`, `deny`
  - `predicates.ts` — predicate factories
  - `rule-validation.ts` — default-rule and known-kind checks, rule (de)serialization
  - `trust.ts` — trust ladder ordering
  - `ids.ts` — stable id derivation for principals / identifiers / envelopes
  - `signing.ts` — process-local signing for pairing tokens (in-tree tampering check, not strong crypto)
  - `migrations.ts` — Store tables for principals, identifiers, networks, hosts, envelopes, parties, participants, rules, grants
  - `index.ts` — public re-exports
- Test fixtures: `pkgs/core/src/access/__fixtures__/`
- Behavior tests, one file per concern: `pkgs/core/src/access/*.test.ts`

## Related

- [Spec — access](../spec/access.md) — data model, rule AST, predicate vocabulary, trust ladder, projection + recheck read path, egress filter, errors, acceptance criteria
- [Design — access](../design/access.md) — why Access is a first-class layer; the envelope→principal→grant pipeline; trust ladder rationale
- [Events](./events.md) — the outbox the materializers ride; `access.principal-resolver` and `access.access-projection` are ordinary materializers; the egress filter is owned here, called from the events layer
- [Store](./store.md) — Access tables persist here; `batch` writes Source + Envelope + outbox atomically
- [Connection](./connection.md) — Connection-owned setup is the entry point for owner / device Principals; the pairing handshake stores the peer's signing key on the inseam-node Connection
- [Monorepo package model](./monorepo-package-model.md) — package roles, naming, dependency direction
