Skip to content
SDKs · ServerAvailableView source

@rootherald/node

Node 18+, built-in fetch, no dependencies. Holds the rh_sk_ key and does every Root Herald call.

terminalbash
npm install @rootherald/node

The calls

  • new RootHeraldClient({ secretKey, baseUrl? })baseUrl defaults to https://rootherald.io; only https or loopback is accepted.
  • issueChallenge({ ask?, policy?, keyPurpose?, deviceHint? }){ challengeId, challenge, nonce, expiresAt }.
  • verify(evidence, { challengeId, policy?, requestedDisclosureClass? }) → verdict plus assuranceClaimsMet, enrollmentRequired, key?.
  • relayEnroll(enrollRequestBlob, { challengeId? }){ deviceId, challenge }; relayActivate(activationResponse){ deviceId }.
  • verifyMobileEvidence(body) — the companion-app bridge; wraps verify.
  • verifyKeySignature(jwk, message, signature)boolean. ECDSA P-256/SHA-256 with Node crypto; raw r||s or DER; never throws.
  • Errors: InvalidSecretKeyError, ChallengeError, InvalidEvidenceError, UnknownPolicyError, PolicyDowngradeError, AdmissionRefusedError, QuotaExceededError. A failing device is a verdict, not an error.

Example

app/api/signup/route.tsts
import { RootHeraldClient, QuotaExceededError } from "@rootherald/node";

const rh = new RootHeraldClient({ secretKey: process.env.RH_SECRET_KEY! });

export async function GET() {
  const { challengeId, challenge } = await rh.issueChallenge({
    ask: ["identity"],                               // signup needs "same chip as before", nothing more
    policy: "rootherald:builtin:strict-hardware",
  });
  return Response.json({ challengeId, challenge }); // relay `challenge` verbatim
}

export async function POST(req: Request) {
  const { challengeId, evidence, email } = await req.json();
  let result;
  try {
    result = await rh.verify(evidence, { challengeId });
  } catch (err) {
    if (err instanceof QuotaExceededError) return Response.json({ error: "rate_limited" }, { status: 429 });
    throw err;
  }
  if (result.enrollmentRequired) return Response.json({ error: "enrollment_required" }, { status: 409 });
  if (result.device.verdict !== "pass") return Response.json({ error: "device_rejected" }, { status: 403 });
  if (await users.deviceRegistered(result.device.ueid)) return Response.json({ error: "device_already_registered" }, { status: 409 });

  const user = await users.create({ email, deviceId: result.device.ueid });
  return Response.json({ userId: user.id });
}