> ## Documentation Index
> Fetch the complete documentation index at: https://docs.phosra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Platform Quickstart · createConnectReceiver

> The entire OCSS connect receiver is one route file with @phosra/gatekeeper/next — signed-verify-to-root, a provider allowlist as the only trust config, and no shared secret to store or rotate.

`createConnectReceiver` (from `@phosra/gatekeeper/next`, v0.6.0+) collapses the OCSS connect
receiver to **one route file and one allowlist**. It verifies every delivery to the **pinned
OCSS root** — the same root your gatekeeper already pins to fetch enforcement profiles — so
the provider's signature *is* the authentication. There is no shared HMAC secret to mint,
store, sync, or rotate.

<Note>
  This is purely additive. The low-level `createGatekeeper()` and every existing gatekeeper
  export are **unchanged** — see the [Platform Quickstart (gatekeeper)](/integration/platform)
  for the enforce/confirm loop. This page covers the *connect leg* the receiver handles.
</Note>

```bash theme={null}
npm install @phosra/gatekeeper
```

***

## The entire receiver

```ts theme={null}
// app/api/ocss/connect/route.ts — the whole thing
import { createConnectReceiver } from "@phosra/gatekeeper/next"
import { store }   from "@/lib/ocss/store"   // your ConnectSessionStore over YOUR child table
import { onBound } from "@/lib/ocss/apply"   // materialize the bound profile into your product

export const { POST } = createConnectReceiver({
  env:       "production",                    // bundled census URL + PINNED trust root (no TOFU)
  did:       "did:ocss:notflix",             // your platform DID (= the RFC 8707 delivery audience)
  seed:      process.env.OCSS_SENDER_SEED_B64URL!,  // your Ed25519 seed (base64url-raw, 32B)
  authorize: ["did:ocss:custo"],             // provider-DID allowlist — the ONLY trust config
  store, onBound,
})
```

That is the complete `/api/ocss/connect` route. The provider's Ed25519 signature, verified to
the root, is the auth; the allowlist is the authorization. **Revoke a provider by removing its
DID from `authorize`** — no key exchange, no rotation.

### Config

| Field                            | Required | Meaning                                                                                                                                                                                   |
| -------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `env`                            | yes      | `"sandbox"` \| `"production"` — selects the bundled census URL **and** the pinned trust root (no trust-on-first-use).                                                                     |
| `did`                            | yes      | Your platform DID. Also the RFC 8707 `audience_did` deliveries must bind to (replay to a different receiver fails).                                                                       |
| `seed`                           | yes      | Your platform Ed25519 seed, base64url-raw (32 bytes) — the `OCSS_SENDER_SEED_B64URL` you registered.                                                                                      |
| `authorize`                      | **yes**  | The provider-DID allowlist. **Required by construction** so "signed with no allowlist" is unrepresentable — role-gating alone (any accredited enforcement-agent) is never the ship state. |
| `store`                          | yes      | Your `ConnectSessionStore` — the §3.6 state machine over your own child/session table.                                                                                                    |
| `onBound`                        | yes      | `(label, childRef, provision?) => void \| Promise<void>` — fires after each 2xx per bound label; materialize the enforcement caps here.                                                   |
| `onError`                        | no       | `(err, ctx) => void` — surfaces a throwing `onBound` without failing the delivery.                                                                                                        |
| `legacyHmacSecret`               | no       | **Opt-in** HMAC lane → posture `"both"`. Absent = signed-only (the default). Only for a migration drain window — see [Migration](/integration/link-migration).                            |
| `censusUrl` / `trustRootXB64Url` | no       | Advanced overrides for the `env` preset (pass together).                                                                                                                                  |
| `keyId`                          | no       | Writer key-id fragment; defaults to the current `YYYY-MM`.                                                                                                                                |
| `createdSkewSec`                 | no       | Signed-delivery freshness window in seconds (default `300`).                                                                                                                              |

<Warning>
  **`authorize` is not optional and it is the whole trust surface.** A signed delivery from any
  accredited enforcement-agent is *authenticated*, but only a DID on your `authorize` list is
  *authorized*. Making the field required means you cannot accidentally ship a receiver that
  trusts every accredited provider on the Trust List.
</Warning>

***

## How a delivery is verified — nothing you write

`createConnectReceiver` reads the raw request body **once** and dispatches by shape:

1. **Signed provision delivery** (`isSignedProvisionDelivery(body)`) → `verifyProvisionDelivery`
   → create-or-adopt N age-banded profiles, then `onBound` per profile. This is the
   [create-and-link](/integration/link-provider#create-and-link-one-parent-action-n-banded-profiles)
   fan-out from the provider.
2. **Signed connect envelope** / `{ endpoint_id_label, state }` → `handleConnect` → bind the
   single label, then `onBound(label, childRef)`.
3. **Legacy `ocss-ext01/provision.v1` HMAC batch** → the HMAC lane — **only** when
   `legacyHmacSecret` is set.

For (1) and (2), the SDK inherits **audience binding** (the delivery must name your `did`),
**`created`-freshness** (within `createdSkewSec`), and the **`authorize` gate** from the
verified envelope — all before your `onBound` runs. You verify nothing by hand.

```ts theme={null}
// @phosra/gatekeeper/next — the types your store and onBound implement
export type AgeBand = "under_13" | "13_15" | "16_17" | "adult"

export interface ProvisionContext {
  age_band:           AgeBand
  display_hint?:      string
  state:              string
  adult_pin_auto_set: boolean
}

export type OnBound = (
  label: string,
  childRef: string,
  provision?: ProvisionContext,   // present on the batch-provision path
) => void | Promise<void>
```

`onBound` fires **post-2xx**, per bound label. On the batch path each call carries a
`ProvisionContext` so you know the age band and whether to auto-set an adult PIN; on the
single-bind path `provision` is `undefined`. A throwing `onBound` is routed to `onError` and
does **not** fail the delivery (the binding already landed).

***

## After the connect leg — enforce

Receiving the connection is only the connect leg. The bound `endpoint_id_label` is your
profile-poll target: fetch and verify the signed enforcement profile, decide locally, and
confirm — all with the low-level `@phosra/gatekeeper`:

```ts theme={null}
// in onBound (or a background poller keyed by the stored label)
await gk.refreshProfile()                       // GET + Ed25519-verify to the Trust List root
const verdict = gk.isAllowed({ category: "addictive_pattern_block" })
if (verdict.decision === "block") blockFeed()
await verdict.confirm("applied")                // §8.3.8 receipt
```

The full profile → `isAllowed` → `confirm` loop (including the two-`endpoint_id_label` gotcha
and the fail-closed rules) is documented in the
[gatekeeper Platform Quickstart](/integration/platform).

***

## Trust-list liveness

Signed verification role-gates the signer to an **active accredited enforcement-agent** and
verifies to root, so the connect leg now depends on Trust-List availability and accreditation
freshness (the 7-day attestation TTL) — a coupling the old HMAC path never had. This is a
deliberate trade, not a surprise: cache the last-known-good Trust List so a transient census
blip doesn't reject a legitimate, still-accredited provider. Name it in your ops runbook. See
[Migration → Liveness](/integration/link-migration#the-new-dependency-trust-list-liveness).

***

## Branding is an assessed conformance item

Two parts, both checked at accreditation: (1) **co-brand your OAuth leg** — the authorize page
the parent hits during the ceremony MUST read **`Phosra Link · <Platform>`**, never a bare
auth form; (2) **provenance** — once a profile is bound, surface a persistent **"Managed via
Phosra"** label on the managed account. See the
[Phosra Link Branding Requirement](/sdks/branding).

***

## What the provider (your partner) must do

Almost nothing new lands on the provider — the signed model is symmetric with what they
already sign:

1. Add your platform DID to their `createLink` connect target (they call
   `link.connect.finish` / `link.provision` with `platformDid: "did:ocss:<you>"`).
2. Deliver with their **own** writer key — no secret from you. If their delivery `401`s, the
   fix is **you** adding their DID to `authorize`, not a secret exchange.
3. That's it. See [Provider Quickstart](/integration/link-provider).

***

## Next

* [Provider Quickstart · createLink](/integration/link-provider) — the sending side
* [Migrating HMAC → Signed](/integration/link-migration) — retire your connect secret safely
* [gatekeeper Platform Quickstart](/integration/platform) — the enforce/confirm loop
