The core read of the data-plane. The {endpoint_id} path segment is the high-entropy
bound-resolver label from the bind response, and
presenting it under your RFC 9421 signatureis the §9.3(b) authentication. The response is a SignedDocument whose documentstring parses to an EnforcementProfile.
document is a JSON string on the wire, not an object. The signature covers exactly those
UTF-8 bytes — verify the signature over the raw string, then JSON.parse it. Never
re-stringify an object before verifying. The canonical profile field is categories[];
profile.rules[] does not exist.
A wrong or unknown label returns 404, indistinguishable from a wrong-resolver dereference —
fail-closed, no existence leak (§9.3(a)). The endpoint supports ETag / If-None-Match for
304 Not Modified.
Worked example
The @phosra/gatekeeper SDK is the supported path: it polls, verifies to
the root, and exposes the decision. The raw signed GET is shown alongside it.
ts
import { signRequest } from "@openchildsafety/ocss"const BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1"const seed = new Uint8Array(Buffer.from(process.env.OCSS_SANDBOX_TEST_SEED_B64URL!, "base64url"))const keyID = process.env.OCSS_SANDBOX_KEY_ID!const label = process.env.OCSS_SANDBOX_ENDPOINT_ID_LABEL!const targetURI = `${BASE}/enforcement-profiles/${label}`const headers = signRequest({ method: "GET", targetURI, keyID, seed, created: Math.floor(Date.now() / 1000) })const res = await fetch(targetURI, { method: "GET", headers })const signed = await res.json() // { document: "<json string>", key_id, alg, sig }const profile = JSON.parse(signed.document) // parse AFTER verifying the signatureconsole.log(res.status, profile.categories)
The signer is defined once in the request-signing guide. A
GET covers only @method, @target-uri, and ocss-spec-version — nocontent-digest.
Always verify signed.sig to the router key and root before you trust categories[].
Example synthetic sandbox 200 response (router-signed fixture, categories[]
empty here because the sandbox self-serve child carries no compiled rules yet):
Once the child has active rules (see the rule-write flow), each
categories[] row carries a category, a decision (allow / warn / block), a
fail_mode, and a per-child rule_ref you echo when you
confirm enforcement.