Skip to content
SDKs · NodeAvailableView source

@rootherald/node

The server-side SDK. Default path: appraise an opaque evidence blob server→server with rh.verify() and get the verdict back directly. Offline/badge path: verify a portable token with verifyAttestationToken().

Install

npmbash
npm install @rootherald/node

Appraise evidence (Background-Check default)

The default RATS Background-Check flow: your dumb client collects an opaque evidence blob and hands it to your server; your server calls Root Herald server→server with its rh_sk_ secret key. Two calls (mint a challenge, then appraise), and the verdict comes back directly with no token to verify.

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

const rh = new RootHerald({ secretKey: process.env.RH_SECRET_KEY! }); // rh_sk_…

// (a) GET /api/challenge — mint a nonce and relay it to your client.
export async function GET() {
  const { challengeId, nonce } = await rh.issueChallenge();
  return Response.json({ challengeId, nonce });
}

// (b) POST /api/signup — the client posts back the opaque evidence; appraise it.
export async function POST(req: Request) {
  const { challengeId, evidence, email, password } = await req.json();

  let verdict;
  try {
    verdict = await rh.verify(evidence, {
      challengeId,
      policy: "rootherald:builtin:strict-hardware",
    });
  } catch (err) {
    // Throws only on protocol/auth/quota problems, not on a failing device.
    if (err instanceof QuotaExceededError) {
      return Response.json({ error: "rate_limited" }, { status: 429 });
    }
    throw err;
  }

  // A real-but-rejected device returns a verdict; branch on device.verdict.
  if (verdict.device.verdict !== "pass") {
    return Response.json({ error: "device_rejected" }, { status: 403 });
  }

  // verdict.device.ueid is a stable device UUID. Use it for
  // one-device-one-account enforcement.
  if (await usersRepo.deviceAlreadyRegistered(verdict.device.ueid)) {
    return Response.json({ error: "device_already_registered" }, { status: 409 });
  }

  const user = await usersRepo.create({
    email,
    password,
    deviceId: verdict.device.ueid,
  });
  return Response.json({ ok: true, userId: user.id });
}

Verify a portable token (offline / badge path)

If you opt into a portable token (rh.verify(evidence, { …, returnToken: true })), or you're on the backend-less badge tier where a keyless SPA collects evidence and the RootHerald server — reached with your origin-pinned publishable rh_pk_ site key — returns the signed token (the client holds no secret and signs nothing), verify it offline with verifyAttestationToken(). It fetches the issuer's JWKS, verifies the JWT signature, checks iss / aud / expiry, and returns the same typed AttestationVerdict. It throws on a bad token rather than returning a failure verdict:

verify a tokents
import {
  verifyAttestationToken,
  TokenExpiredError,
  InvalidTokenError,
} from "@rootherald/node";

const verdict = await verifyAttestationToken(token, {
  issuer: "https://rootherald.io/acme",
  audience: process.env.RH_PROJECT_ID!,
}).catch((err) => {
  if (err instanceof TokenExpiredError) throw new Response(null, { status: 401 });
  if (err instanceof InvalidTokenError) throw new Response(null, { status: 403 });
  throw err;
});

// Same AttestationVerdict shape as rh.verify() returns.
const ueid = verdict.device.ueid;

Gate a route (Express middleware — token path)

For routes where the client carries a portable token (the badge path, or after you opt into returnToken), requireAttestation returns an Express-compatible middleware. It verifies the bearer token, enforces an assurance floor and freshness window, and attaches the verdict at req.attestation — or responds 401with an RFC 9470 step-up challenge when the token doesn't clear the bar. (Under the Background-Check default you appraise with rh.verify() instead.)

server.tsts
import express from "express";
import { requireAttestation } from "@rootherald/node";

const app = express();

// Require a fresh, high-assurance device attestation on money-movement.
app.post(
  "/api/transfer",
  requireAttestation({
    issuer: "https://rootherald.io/acme",
    audience: process.env.RH_PROJECT_ID!,
    acrValues: ["urn:rootherald:device:high"],
    maxAgeSeconds: 300,
  }),
  (req, res) => {
    // req.attestation is the verified, typed AttestationVerdict.
    res.json({ ok: true, device: req.attestation!.device.ueid });
  },
);