Skip to main content
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, @phosra/classify, @phosra/provider-harness (all 0.1.0), plus @openchildsafety/ocss 0.1.0 and @phosra/gatekeeper 0.2.0. See SDK install.

Three roles

RoleDoesSees plaintext?
Platform (e.g. Snaptr)Seals content to the enclave; calls classify()Transiently, before sealing
Provider enclaveDecrypts, classifies; sends content-free signal to censusYes — inside the enclave only, then zeroizes
Phosra (census)Enclave registry; content-free signal + manifest + minimization receiptNever. 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-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

SDK install

All three are published to npm (0.1.0), alongside @openchildsafety/ocss (0.1.0) and @phosra/gatekeeper (0.2.0).
npm install @phosra/provider @phosra/classify @phosra/provider-harness
Once built, import them by workspace path (or monorepo node_modules/@phosra/…):
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):
{
  "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
}
FieldRequiredNotes
provider_didYesdid:ocss:<slug> — must be on the OCSS Trust List
modeYes"standin" (dev/demo — no hardware) or "nitro" (AWS Nitro Enclave)
endpoint_urlYesBase URL of your enclave; the /classify path is derived from this
payload_pubkey_jwkYesEC P-256 public JWK the platform seals content to
signing_pubkeyYesEd25519 pub X (base64url) for receipt verification
attestation_documentNoNitro COSE_Sign1 attestation doc (base64url); required for attested:true
challenge_idNoUUID 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 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 timetrue 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-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:trueRevokedEnclaveError (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-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:
{
  "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-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).
{
  "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’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.

Conformance harness

@phosra/provider-harness runs 7 behavioral assertions against your enclave. Pass it to Phosra as part of your accreditation request.
# 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:
IDNameLive status
A1closed-enum fail-closedPASS (census rejects out-of-enum harm class)
A2content-free signal lanePASS (mock mode — requires in-process inspector)
A3sealed-to-consent-recipient onlyPENDING — requires consent infra
A4parent-sole-control + monitoring_active indicatorPENDING — requires GET /capabilities on enclave
A5minimization attestation wired (HMAC-salted leaves)PASS — labels receipt conservation_check: "self_report"
A6abuse-at-home → advocate routingPENDING — advocate lane (Tier 2.5) not built
A7attestation-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 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.
  • Production accreditation — see Production Accreditation to submit your harness results for Trust List accreditation.