Authentication
OAuth 2.1 + DPoP at spec level. Scopes, refresh, HMAC-signed bodies, and how clinic-scoped identities work.
Updated May 28, 2026
Nyra agent identities are clinic-scoped and short-lived. Every token is bound to a single clinic and a fixed patient roster. There are no long-lived bearer tokens, no shared admin keys, and no global sandbox.
The v1 SDK ships the spec-level primitives: the scope constants, the DPoP proof claim builder, and the typed errors that any future auth layer will raise. The auth server itself is not live yet. Working code that hits a real endpoint will land in a follow-up release.
The model
| Element | Value |
|---|---|
| Authorization protocol | OAuth 2.1 |
| Sender constraint | DPoP (RFC 9449), ES256 key binding |
| Nonce TTL | 120 seconds |
| Clock skew | ±60 seconds |
| Token lifetime | 1 hour, refreshable |
| Body signing | HMAC-SHA256 over body + timestamp on every write |
| Refresh | Standard OAuth 2.1 refresh token, scoped to the original clinic identifier |
Scopes
identity.read resolve the current OAuth identity
evidence_map.read read a patient's evidence map
scales.read read PHQ-9 / GAD-7 instrument definitions
audit.read read the append-only audit log
sandbox.fork fork a patient graph into a session-scoped sandbox
simulation.run walk a plan against the graph surrogate
draft.create propose a clinical edit, held until clinician commit
A token grants a subset of these scopes. The SDK's typed ScopeError carries the missing scope name, so the agent's error handler can surface it to the user verbatim.
DPoP proof, the v1 spec
import { buildDpopProofClaims, DPOP_NONCE_TTL_SECONDS } from "@humyn/nyra";
const claims = buildDpopProofClaims({
method: "POST",
url: "https://api.nyra.us.com/v1/sandbox/fork",
jti: crypto.randomUUID(),
nonce: serverNonce, // server-issued, ≤ 120s old
accessTokenHash: ath, // sha256 of the access token, base64url
});
// Sign `claims` with your ES256 private key. The SDK does not sign for you in v1.
// We expect integrators to bring their own JWS signer (jose, node:crypto, hardware-bound).
The Python signature mirrors the TS one:
from humyn_nyra import build_dpop_proof_claims, DpopProofInput
claims = build_dpop_proof_claims(DpopProofInput(
method="POST",
url="https://api.nyra.us.com/v1/sandbox/fork",
jti="...",
nonce=server_nonce,
access_token_hash=ath,
))
What lands at the auth boundary
Five typed errors map the marketing-spec refusals to runtime failures the SDK can hand back to your agent.
AuthError 401 bad or expired token
ScopeError 403 token is valid but missing a required scope
CommitNotAllowedError 403 agent attempted a clinician-only commit
ClinicianRejectedDraftError 403 commit after rejection; rejected_draft_id is on the error
InstrumentFrozenError 409 attempt to mutate PHQ-9 or GAD-7
RateLimitError 429 bucket + retry_after_seconds on the error
The marketing spec says, "the refusals are enforced at the auth layer, not at the prompt layer." The SDK error names are the contract.
Next: Function-call schemas.