Skip to content
SDKs · BrowserIn developmentView source

@rootherald/browser

The page-side collector (Client ABI 3.0). It orchestrates the keyless client flow over the page ↔ Root Herald extension ↔ native host bridge and hands your backend an opaque evidence blob. It holds no key and never talks to Root Herald directly — your server does the appraisal.

In development

This SDK is implemented but not yet published to its package registry. Until it ships, mint attestation tokens on the device and verify them server-side with @rootherald/node (or any available server SDK). The API shown below is the planned surface and may change before release.

What this package is (and isn't)

@rootherald/browser is a keyless evidence collector. It runs in the user's page, drives the TPM through the Root Herald extension and native host, and produces an opaque blob. That blob crosses to your backend, which relays it to Root Herald with its rh_sk_ secret key using @rootherald/node.

It does not mint or sign tokens, and it does not hold a Root Herald key — neither the secret rh_sk_ nor the publishable rh_pk_. No verdict is ever rendered in the browser; the backend appraises the evidence and decides. The browser's entire job is to collect a fresh TPM quote and bootstrap the device key on first use.

Install

npmbash
npm install @rootherald/browser

The two client verbs

enroll(relay) is a one-time device-key bootstrap; its two network legs are relayed through your backend. attest(nonce) is the per-request path: it produces a fresh evidence blob over a server-issued nonce, with no elevation prompt on a device that has enrolled before.

signup.tsts
import { attest, enroll, NotEnrolledError } from "@rootherald/browser";

// 1. Ask YOUR backend for a challenge (your server called rh.issueChallenge()).
const { challengeId, nonce } = await fetch("/api/rh/challenge").then((r) => r.json());

// 2. Collect fresh evidence for that nonce. No key, no prompt on a known device.
let evidence;
try {
  evidence = await attest(nonce);
} catch (err) {
  if (err instanceof NotEnrolledError) {
    // First run on this device only: enroll once (the single elevation),
    // relaying both legs through your backend, then collect again.
    await enroll(relayThroughYourBackend);
    evidence = await attest(nonce);
  } else {
    throw err;
  }
}

// 3. Hand the OPAQUE blob to your backend, which relays it to Root Herald and
//    appraises it with rh.verify(). The browser never sees a verdict.
await fetch("/api/signup", {
  method: "POST",
  body: JSON.stringify({ challengeId, evidence, ...form }),
});

API surface

  • enroll(relay): one-time device-key bootstrap. Takes an EnrollRelay whose callbacks forward the two enroll legs (EnrollBegin / EnrollComplete) to your backend's relayEnroll / relayActivate. Runs at most once per device. See the device-enrollment guide for the relay callbacks.
  • attest(nonce): signs a fresh TPM quote over the server-issued nonce and returns the opaque EvidenceBlob to relay to your backend. Unprivileged — no prompt on an already-enrolled device. Throws NotEnrolledError when the device has no key yet.
  • getClientStatus(): local readiness signals (extension present, native host reachable, OS/browser support), never a verdict. Use it to feature-detect and route users to install steps before you attempt attest().

Error handling

Errors are typed classes exported from the package — check with instanceof, not string codes.

import {
  attest,
  enroll,
  NotEnrolledError,
  ExtensionMissingError,
  HostMissingError,
  TimeoutError,
} from "@rootherald/browser";

try {
  const evidence = await attest(nonce);
  // ...relay 'evidence' to your backend.
} catch (err) {
  if (err instanceof NotEnrolledError) {
    // No device key yet — run the one-time enroll(relay), then retry attest().
    await enroll(relay);
  } else if (err instanceof ExtensionMissingError) {
    // Root Herald browser extension isn't installed — route to install steps.
  } else if (err instanceof HostMissingError) {
    // Extension present but the native host isn't installed/running.
  } else if (err instanceof TimeoutError) {
    // The user never answered the extension/host, or the bridge stalled.
  } else {
    // Unknown — log and fall through to your existing flow.
  }
}