> ## 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.

# Server-Side Enclave Routing

> The trust layer for server-side content classification — register an enclave, classify content seal-direct, report minimization, and verify compliance bundles. All endpoints live on the sandbox today.

**The model in one line:** a platform seals a child's content **directly** to the provider's enclave — Phosra (the census) carries only the content-free abuse signal, a minimization receipt, and a routing manifest. The census never sees plaintext or ciphertext.

<Note>
  The endpoints on this page are **live** on both sandbox censuses today, and the SDKs are **published to npm**: `@phosra/provider`, `@phosra/classify`, `@phosra/provider-harness` (all 0.1.0), plus `@openchildsafety/ocss` 0.1.0 and `@phosra/gatekeeper` 0.2.0. See [SDK install](#sdk-install-preview).
</Note>

## Three roles

| Role                       | Does                                                                    | Sees plaintext?                              |
| -------------------------- | ----------------------------------------------------------------------- | -------------------------------------------- |
| **Platform** (e.g. Snaptr) | Seals content to the enclave; calls `classify()`                        | Transiently, before sealing                  |
| **Provider enclave**       | Decrypts, classifies; sends content-free signal to census               | Yes — inside the enclave only, then zeroizes |
| **Phosra (census)**        | Enclave registry; content-free signal + manifest + minimization receipt | **Never.** Carries only metadata.            |

<Warning>
  **Invariants — do not weaken:**

  * Phosra never receives content (plaintext or sealed JWE). Only content-free metadata reaches the census.
  * Classification is **fail-open on safety**: an expired attestation does not block content classification. Only Phosra-staff revocation triggers a hard fail-closed refusal.
  * The minimization receipt is labeled `conservation_check: "self_report"` in solo mode. **Never write "verifiable deletion"** — solo mode proves a count discrepancy but cannot prove any specific benign message was destroyed.
  * The routing-manifest and compliance-bundle POSTs are **post-hoc** — content is sealed and delivered to the enclave first, unconditionally. A manifest failure must never block delivery.
</Warning>

***

## Quickstart

Both demo scripts run against the live sandbox with the default loopline + courier sim seeds:

```bash theme={null}
# From the repo root (after npm install):

# Demo 1 — direct enclave loop (seal → enclave verifyAndOpen → signal → receipt)
CENSUS_URL=https://phosra-api-sandbox-staging.up.railway.app \
ROOT_KEY_X_B64URL=jDWZfB70DM8B3ZnPG8FQ0tnanqLgwJX4ZMr_E1Ugv3w \
node scripts/sandbox/verify-enclave-direct-loop.mjs

# Demo 2 — minimization (epoch accumulator + attestation receipt) + fan-out (two providers) + compliance bundle
CENSUS_URL=https://phosra-api-sandbox-staging.up.railway.app \
ROOT_KEY_X_B64URL=jDWZfB70DM8B3ZnPG8FQ0tnanqLgwJX4ZMr_E1Ugv3w \
node scripts/sandbox/verify-minimization-fanout-loop.mjs
```

Both scripts exit 0 on PASS, 1 on FAIL. Without `CENSUS_URL` they run in-process against a mock census (also exit 0 on PASS — no network required).

**Sandbox base URLs:**

* Staging: `https://phosra-api-sandbox-staging.up.railway.app`
* Production: `https://phosra-api-sandbox-production.up.railway.app`

**Root key X (base64url Ed25519 pub):**

* Staging: `jDWZfB70DM8B3ZnPG8FQ0tnanqLgwJX4ZMr_E1Ugv3w`
* Production: `CMHWy3vUAiEcYDdE_bDvkRuEqwxkklS0tV-TYHJTlWU`

**Default sim seeds** (32-byte Ed25519 seeds, base64url — match the sandbox Trust List public keys):

* `PLATFORM_SEED_B64URL` (loopline): `bG9vcGxpbmUBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE`
* `PROVIDER_SEED_B64URL` (courier): `Y291cmllcgEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE`

***

<h2 id="sdk-install-preview">
  SDK install
</h2>

<Note>
  All three are **published to npm** (0.1.0), alongside `@openchildsafety/ocss` (0.1.0) and `@phosra/gatekeeper` (0.2.0).
</Note>

```bash theme={null}
npm install @phosra/provider @phosra/classify @phosra/provider-harness
```

Once built, import them by workspace path (or monorepo `node_modules/@phosra/…`):

```ts theme={null}
import { verifyAndOpen, createEpochAccumulator, closeEpoch, submitMinimizationAttestation } from "@phosra/provider";
import { classify, classifyMulti, verifyComplianceBundle } from "@phosra/classify";
import { runHarness } from "@phosra/provider-harness";
```

***

## Register an enclave

### 1. Issue a challenge nonce (Nitro only)

For the `nitro` hardware-attestation path, request a one-shot anti-replay challenge before registering:

```
POST /api/v1/enclaves/challenge
```

Requires RFC-9421 signature. Response (`201`):

```json theme={null}
{
  "challenge_id": "<uuid>",
  "nonce_hex": "<64 hex chars>"
}
```

The nonce is single-use (5-minute TTL). Include it in the registration body as `challenge_id`.

### 2. Register the enclave

```
POST /api/v1/enclaves
```

Requires RFC-9421 signature (the `provider_did` must be on the OCSS Trust List).

**Request body:**

```json theme={null}
{
  "provider_did": "did:ocss:courier",
  "mode": "standin",
  "endpoint_url": "https://your-enclave.example.com",
  "payload_pubkey_jwk": { "kty": "EC", "crv": "P-256", "x": "…", "y": "…" },
  "signing_pubkey": "<base64url Ed25519 pub X>",
  "attestation_document": null,
  "challenge_id": null
}
```

| Field                  | Required | Notes                                                                                |
| ---------------------- | -------- | ------------------------------------------------------------------------------------ |
| `provider_did`         | Yes      | `did:ocss:<slug>` — must be on the OCSS Trust List                                   |
| `mode`                 | Yes      | `"standin"` (dev/demo — no hardware) or `"nitro"` (AWS Nitro Enclave)                |
| `endpoint_url`         | Yes      | Base URL of your enclave; the `/classify` path is derived from this                  |
| `payload_pubkey_jwk`   | Yes      | EC P-256 public JWK the platform seals content to                                    |
| `signing_pubkey`       | Yes      | Ed25519 pub X (base64url) for receipt verification                                   |
| `attestation_document` | No       | Nitro COSE\_Sign1 attestation doc (base64url); required for `attested:true`          |
| `challenge_id`         | No       | UUID from `POST /enclaves/challenge`; required when providing `attestation_document` |

**Response** (`201 Created`):

```json theme={null}
{ "enclave_id": "<uuid>" }
```

**Owner-upsert (redeploy).** If your provider already has an active enclave and you re-register with the same RFC-9421-verified `provider_did`, the census **re-points** the existing record (new endpoint + keys, attestation reset) and returns `200` with the existing `enclave_id` instead of `409`. This is the correct deploy-time pattern: call `POST /enclaves` on every deploy without deactivating first.

### Nitro attestation and the standin tier

<Note>
  `attested:true` is **hardware-gated** — it requires a real AWS Nitro Enclave build, PCR pins (`OCSS_NITRO_PCR0`/`PCR1`/`PCR2` env vars on the census), and a key generated inside the enclave. The live sandbox today returns `attested:false` for all `mode:"standin"` registrations. This is the honest live tier: enclaves operate normally with `attested:false`; only Phosra-staff revocation (`revoked:true`) triggers a hard fail-closed refusal.
</Note>

***

## Discover an active enclave

```
GET /api/v1/enclaves/active?provider_did=did:ocss:courier
```

No authentication required. Response (`200`):

```json theme={null}
{
  "enclave_id": "<uuid>",
  "provider_did": "did:ocss:courier",
  "endpoint_url": "https://your-enclave.example.com",
  "payload_pubkey_jwk": { "kty": "EC", "crv": "P-256", "x": "…", "y": "…" },
  "signing_pubkey": "<base64url Ed25519 pub X>",
  "mode": "standin",
  "attested": false,
  "revoked": false,
  "updated_at": "2026-06-29T00:00:00Z"
}
```

`attested` is **computed at read time** — `true` only when the DB row's `attested_expires_at` is in the future and `revocation_reason` is NULL. Expired (attested:false, revoked:false) is fail-open. Revoked is fail-closed in the SDK when `requireAttested:true` is set.

***

## Classify (single provider)

`@phosra/classify` seals content and sends it directly to the provider enclave:

```ts theme={null}
import { classify } from "@phosra/classify";

const result = await classify(
  "hey what time is it?",
  { child: "child-abc123", providerDid: "did:ocss:courier" },
  {
    censusUrl: "https://phosra-api-sandbox-staging.up.railway.app",
    platformSenderKey: { seed: <32-byte Uint8Array>, keyID: "did:ocss:loopline#<kid>" },
  }
);
// result.verdict — "allow" | "flag" (from your enclave)
// result.receipt — the enclave's signal_receipt from the census
```

**What classify() does:**

1. Validates `contentType` is `"text/*"` client-side — rejects non-text before any network call.
2. `GET {censusUrl}/api/v1/enclaves/active?provider_did=…` — the **only** census call; carries no content.
3. Seals `content` to `payload_pubkey_jwk` → JWE.
4. Builds an OCSS Envelope (`inner.payload = JWE`) and signs with `platformSenderKey`.
5. `POST {endpoint_url}/classify` **directly to the enclave** — not to the census.
6. Returns the enclave's `{ receipt, verdict }`.

The census never receives the JWE or plaintext. Step 5 goes to the enclave's endpoint directly.

**FIX 3 attestation policy** (when `requireAttested: true`): `revoked:true` → `RevokedEnclaveError` (fail-closed). Expired attestation → proceed (fail-open — safety preserved).

***

<h2 id="classify-multi">
  Classify (fan-out — N providers in parallel)
</h2>

`classifyMulti` seals content and delivers it in parallel to N provider enclaves:

```ts theme={null}
import { classifyMulti } from "@phosra/classify";

const result = await classifyMulti(
  "hey what's your address?",
  { child: "child-abc123", providers: ["did:ocss:loopline", "did:ocss:courier"] },
  { censusUrl: "https://phosra-api-sandbox-staging.up.railway.app", platformSenderKey }
);

// result.providerCount  — 2 (requested)
// result.authorizedCount — 2 (received receipts)
// result.verified        — true iff authorizedCount === providerCount
// result.manifestReceipt — receipt from POST /routing-manifests (post-hoc, fail-open)
// result.manifestReceiptJson — raw JSON string (use for Merkle leaf computation)
```

**Invariants:**

* Each provider gets a **separate JWE** (sealed to its own `payload_pubkey_jwk`). A single shared ciphertext is not used.
* All `N` enclave POSTs fire in parallel via `Promise.all`.
* **Fail-partial**: K failures → N-K receipts, honest `error` per failure. `classifyMulti` never throws on partial failure.
* The `POST /routing-manifests` is fired **after** all enclave deliveries complete (post-hoc). A manifest failure leaves `manifestReceipt` undefined but does not throw.

**Consent-scoped discovery.** Before calling `classifyMulti`, the platform can use the fan-out discovery endpoint to get the active enclaves for a child it has active consent standing with:

```
GET /api/v1/children/{child_ref}/enclaves
```

Requires RFC-9421 signature. Returns 403 `standing_failure` if the calling platform has no active `consent_attestation` for `(caller.DID, child_ref)`. Response is an array of `{ provider_did, endpoint_url, payload_pubkey_jwk, signing_pubkey, attested }`.

***

## Routing manifest (post-hoc fan-out record)

After `classifyMulti`, a content-free manifest is filed with the census:

```
POST /api/v1/routing-manifests
```

Requires RFC-9421 signature. This is called automatically by `classifyMulti` — you do not need to call it manually unless you are building a custom fan-out loop.

**Request body:**

```json theme={null}
{
  "child_ref_epoch_hash": "<HMAC-hashed child ref>",
  "provider_key_ids": ["did:ocss:loopline", "did:ocss:courier"],
  "scope_groups": [],
  "provider_count": 2
}
```

`provider_count` must equal `len(provider_key_ids)` — the census rejects mismatches. No content, no URLs, no plaintext — `DisallowUnknownFields` rejects any extra field.

Response (`201`): a router-signed `routing_manifest_receipt`. Store the **raw JSON string** — use it as-is for `verifyComplianceBundle`; do not re-serialize.

***

## Compliance bundle (client-side verification)

`verifyComplianceBundle` independently verifies receipt signatures and the Merkle root — no trust in the census's claims:

```ts theme={null}
import { verifyComplianceBundle } from "@phosra/classify";

const bundleReceiptWire = /* POST /api/v1/compliance-bundles response */;

const verify = verifyComplianceBundle(
  {
    receiptJsonStrings: [result.manifestReceiptJson],  // raw strings from classifyMulti
    bundleReceipt: bundleReceiptWire,
  },
  (keyId) => trustListResolver.signingKey(keyId),      // Trust-List-backed resolver
);

// verify.verified      — true iff all receipts passed Ed25519 + Merkle root matches
// verify.merkleMatch   — true iff locally-recomputed root === bundle's claimed root
// verify.timestamped   — true iff the bundle carries a non-empty tsa_token
// verify.errors        — per-receipt error strings (never suppressed)
```

**POST /api/v1/compliance-bundles** (called by your code; `verifyComplianceBundle` is purely client-side):

```
POST /api/v1/compliance-bundles
```

Requires RFC-9421 signature.

```json theme={null}
{
  "manifest_receipts": [
    {
      "id": "<receipt-id>",
      "receipt_json": { /* full receipt wire JSON object */ }
    }
  ]
}
```

Response (`201`): `compliance_bundle_receipt` with `body.verified_count`, `body.unverified_count`, `body.merkle_root`. Use `verifyComplianceBundle` to independently recompute the Merkle root — do not trust the census's `verified_count` alone.

***

## Minimization attestation

At the end of each **epoch**, the enclave self-reports how many messages it classified, how many were flagged, and how many harmful excerpts were delivered via the census. The census cross-checks `f_delivered` against its own `harm_context_routes` table and signs a receipt.

### 1. Accumulate messages during the epoch

```ts theme={null}
import { createEpochAccumulator, closeEpoch, submitMinimizationAttestation } from "@phosra/provider";

const acc = createEpochAccumulator({
  enclaveIdentityKey: <32-byte seed from enclave identity>,
  epochId: "epoch-2026-06-29-001",
  familyIdHash: "<HMAC of family id>",
  enclaveDid: "did:ocss:courier",
});

// Called after each classify decision:
acc.record({ msgRef: "msg-001", kind: "benign",  harmClass: "grooming", delivered: false });
acc.record({ msgRef: "msg-002", kind: "flagged", harmClass: "grooming", delivered: false });
```

Each leaf is `HMAC-SHA256(enclaveIdentityKey, msgRef|kind|harmClass)` — bare SHA-256 hashes are rejected. `delivered: false` means the excerpt was not forwarded via the census `harm_context` lane (the typical case for DIRECT topology; DIRECT/intra-provider deliveries are not census-visible).

### 2. Close the epoch and submit

```ts theme={null}
const attestation = closeEpoch(acc, {
  classifierBuildHash: "sha256:<your build hash>",
  epochStart: "<RFC3339>",
  epochClose: "<RFC3339>",
});
// attestation.n_classified, .f_flagged, .f_delivered, .salted_merkle_root

const receipt = await submitMinimizationAttestation(attestation, {
  censusUrl: "https://phosra-api-sandbox-staging.up.railway.app",
  enclaveSenderKey: { seed: <32-byte seed>, keyID: "did:ocss:courier#<kid>" },
});
// receipt.body.conservation_check — "self_report" (solo mode) or "fail" (discrepancy)
// receipt.body.two_attestor       — false (solo mode)
```

### Wire contract (`POST /api/v1/minimization-attestation`)

Requires RFC-9421 signature (`enclave_did` in the body must match the verified caller DID).

```json theme={null}
{
  "epoch_id": "epoch-2026-06-29-001",
  "family_id_hash": "<HMAC of family id>",
  "enclave_did": "did:ocss:courier",
  "classifier_build_hash": "sha256:<hex>",
  "n_classified": 42,
  "f_flagged": 3,
  "f_delivered": 0,
  "salted_merkle_root": "<64 hex chars>",
  "epoch_start": "2026-06-29T00:00:00Z",
  "epoch_close": "2026-06-29T01:00:00Z"
}
```

`DisallowUnknownFields` rejects any body carrying extra fields (e.g. `excerpt`, `content`, `payload`). The census returns `201` regardless of `conservation_check` — a `"fail"` label is the honest signal of a discrepancy, not an error.

<Note>
  **Honest limits of the minimization receipt:**

  * `conservation_check: "self_report"` means: your counts are internally consistent AND match the census's `harm_context` delivery count. It does NOT prove any specific benign message was bit-wiped.
  * `two_attestor: false` means: only your count was checked. The two-attestor upgrade (platform co-signs its ingestion count) is planned but requires a co-signing partner.
  * **Never** use the phrase "verifiable deletion" — it is over-strong for solo mode.
</Note>

***

## Conformance harness

`@phosra/provider-harness` runs 7 behavioral assertions against your enclave. Pass it to Phosra as part of your accreditation request.

```bash theme={null}
# Self-test (in-process mock enclave + mock census — no network, no setup)
npx -p @phosra/provider-harness provider-harness

# Against a live enclave + census
npx -p @phosra/provider-harness provider-harness \
  --enclave http://your-enclave.example.com \
  --census https://phosra-api-sandbox-staging.up.railway.app

# (or `npm install @phosra/provider-harness` then `npx provider-harness …`.
#  Use the `-p … provider-harness` form — bare `npx @phosra/provider-harness` is
#  unreliable due to an npx scoped-package bin quirk.)
```

**The 7 assertions:**

| ID | Name                                                    | Live status                                               |
| -- | ------------------------------------------------------- | --------------------------------------------------------- |
| A1 | closed-enum fail-closed                                 | PASS (census rejects out-of-enum harm class)              |
| A2 | content-free signal lane                                | PASS (mock mode — requires in-process inspector)          |
| A3 | sealed-to-consent-recipient only                        | PENDING — requires consent infra                          |
| A4 | parent-sole-control + monitoring\_active indicator      | PENDING — requires GET /capabilities on enclave           |
| A5 | minimization attestation wired (HMAC-salted leaves)     | PASS — labels receipt `conservation_check: "self_report"` |
| A6 | abuse-at-home → advocate routing                        | PENDING — advocate lane (Tier 2.5) not built              |
| A7 | attestation-fail → suspend (fail-closed before content) | PASS — forged sender\_signature → 4xx                     |

The top-level label after a full run reads: **"self-tested; production accreditor pending"** — passing all available assertions is a necessary but not sufficient condition for Trust-List accreditation. A production accreditor (external to Phosra) will run the full suite including the pending assertions before any production Trust List entry.

***

## Revoke an enclave (staff-only)

```
PUT /api/v1/enclaves/{id}/revoke
```

Requires Phosra-staff admin session. Sets `revocation_reason` so the entry reads `attested:false, revoked:true` at GET time. The SDK's `RevokedEnclaveError` then prevents any platform from sealing to that enclave when `requireAttested:true`.

Providers cannot self-revoke — Phosra staff revoke only on evidence of compromise.

***

## Next steps

* **Sandbox onboarding** — see [OCSS Onboarding](/ocss/onboarding) to self-register your `did:ocss:<slug>` on the sandbox Trust List.
* **Content monitoring architecture** — see [Safe Content Monitoring](/integration/content-monitoring) for the full on-device classifier design.
* **Production accreditation** — see [Production Accreditation](/integration/production-accreditation) to submit your harness results for Trust List accreditation.
