Server-Side Enclave Routing
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.
The endpoints on this page are live on both sandbox censuses today, and the SDKs are published to npm: @phosra/provider (0.1.0), @phosra/classify (0.1.1), the conformance harness @openchildsafety/provider-harness (≥ 0.1.3 — live-enclave targeting), plus public @openchildsafety/ocss 0.1.5 and @phosra/gatekeeper 0.7.0. See SDK install.
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. |
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.
Quickstart
Both demo scripts run against the live sandbox with the default loopline + courier sim seeds:
# From the repo root (after npm install):
# Demo 1 — direct enclave loop (seal → enclave verifyAndOpen → signal → receipt)
CENSUS_URL=https://phosra-api-sandbox-production.up.railway.app \
ROOT_KEY_X_B64URL=CMHWy3vUAiEcYDdE_bDvkRuEqwxkklS0tV-TYHJTlWU \
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-production.up.railway.app \
ROOT_KEY_X_B64URL=CMHWy3vUAiEcYDdE_bDvkRuEqwxkklS0tV-TYHJTlWU \
node scripts/sandbox/verify-minimization-fanout-loop.mjsBoth 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 URL (the one canonical partner sandbox host — why one host):
https://phosra-api-sandbox-production.up.railway.app
Root key X (base64url Ed25519 pub) — pinned out-of-band, matches the census above:
CMHWy3vUAiEcYDdE_bDvkRuEqwxkklS0tV-TYHJTlWU
Simulation credentials: generate fresh 32-byte Ed25519 seeds locally and expose them as
PLATFORM_SEED_B64URL and PROVIDER_SEED_B64URL. Private seed fixtures are intentionally not
published in the docs.
SDK install
All are published to npm: @phosra/provider (0.1.0) + @phosra/classify (0.1.1), the harness @openchildsafety/provider-harness (≥ 0.1.3), alongside public @openchildsafety/ocss (0.1.5) and @phosra/gatekeeper (0.7.0).
npm install @phosra/provider @phosra/classify @openchildsafety/provider-harnessImport them from the installed packages:
import { verifyAndOpen, createEpochAccumulator, closeEpoch, submitMinimizationAttestation } from "@phosra/provider";
import { classify, classifyMulti, verifyComplianceBundle } from "@phosra/classify";
import { runHarness } from "@openchildsafety/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):
{
"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:
{
"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):
{ "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 stand-in (standin) tier
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.
Discover an active enclave
GET /api/v1/enclaves/active?provider_did=did:ocss:courier
No authentication required. Response (200):
{
"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:
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-production.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 censusWhat classify() does:
- Validates
contentTypeis"text/*"client-side — rejects non-text before any network call. GET {censusUrl}/api/v1/enclaves/active?provider_did=…— the only census call; carries no content.- Seals
contenttopayload_pubkey_jwk→ JWE. - Builds an OCSS Envelope (
inner.payload = JWE) and signs withplatformSenderKey. POST {endpoint_url}/classifydirectly to the enclave — not to the census.- 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).
Classify (fan-out — N providers in parallel)
classifyMulti seals content and delivers it in parallel to N provider enclaves:
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-production.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
Nenclave POSTs fire in parallel viaPromise.all. - Fail-partial: K failures → N-K receipts, honest
errorper failure.classifyMultinever throws on partial failure. - The
POST /routing-manifestsis fired after all enclave deliveries complete (post-hoc). A manifest failure leavesmanifestReceiptundefined 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:
{
"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:
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.
{
"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
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
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-production.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).
{
"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.
Honest limits of the minimization receipt:
conservation_check: "self_report"means: your counts are internally consistent AND match the census'sharm_contextdelivery count. It does NOT prove any specific benign message was bit-wiped.two_attestor: falsemeans: 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.
Conformance harness
@openchildsafety/provider-harness (≥ 0.1.3) runs seven behavioral assertions against your
enclave. Share the results as design-partner pilot evidence; they do not confer accreditation.
# Self-test (in-process reference enclave — no network, no setup)
npx -p @openchildsafety/provider-harness provider-harness run --enclave ref
# Against YOUR live enclave — target it by URL (the flag is `--enclave-url`,
# not `--enclave`; the harness probes GET {url}/buildinfo + POST {url}/classify)
npx -p @openchildsafety/provider-harness provider-harness \
run --enclave-url https://your-enclave.example.com
# (or `npm install @openchildsafety/provider-harness` then `npx provider-harness …`.
# Use the `-p … provider-harness` form — bare `npx @openchildsafety/provider-harness`
# is unreliable due to an npx scoped-package bin quirk.)Live-enclave targeting requires ≥ 0.1.3. Older builds are reference-only and
reject --enclave-url with v0 supports only --enclave ref. Pin the version
(@openchildsafety/[email protected]) if npx resolves a cached older copy.
There is no --census flag — the live-target lane probes your enclave directly.
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 should read "self-tested in sandbox". Passing the available assertions is pilot evidence only; Phosra does not claim a current independent production accreditor or a 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 to self-register your
did:ocss:<slug>on the sandbox Trust List. - Content monitoring architecture — see Safe Content Monitoring for the full on-device classifier design.
- Design-partner beta — see Design-partner beta and production roadmap to share harness results as pilot evidence.