JavaScript / TypeScript SDK

npm install @openguardrails/core

@openguardrails/core is the TS counterpart of the Python package: zero dependencies, no Node built-ins in the core, runs anywhere the global fetch exists (Node ≥ 18, edge/WASM runtimes). It has the same two halves: an in-process Runtime for local evaluation, and a RuntimeClient for a hosted Runtime API.

import {
  RuntimeClient, RuntimeApiError, RateLimitedError,   // client + errors
  createNodeSigner,                                   // Ed25519 signing (Node)
  eventToWire, verdictFromWire, INGEST_BATCH_MAX,     // wire mapping
  Runtime, ConfigRulesDetector, LLMJudgeDetector,     // in-process runtime
  type GuardEvent, type Verdict,                      // camelCase models
} from "@openguardrails/core"

Models are camelCase (eventId, observationPoint, sessionId); the client does the snake_case wire mapping in both directions. Extension fields set directly on an event (run_id, turn, authz, x.ogr.*) pass through verbatim.

RuntimeClient

new RuntimeClient({ baseUrl?, apiKey?, timeoutMs?, signer? })
OptionDefaultDescription
baseUrl$OGR_RUNTIME_URLAPI root; the client appends the canonical /v1/... paths. A prefixed deployment passes the full prefix (https://host/api/public/ogr.../api/public/ogr/v1/evaluate)
apiKey$OGR_API_KEYWorkspace key, sent as Authorization: Bearer ogr_...
timeoutMs10000Per-request timeout (AbortController)
signerA Signer (sign(body: Uint8Array): string | null); adds ogr-batch-signature to evaluate/ingest bodies

Throws at construction if neither option nor environment provides a base URL / key.

evaluate(event, { partial? }) -> Promise<Verdict>

One GuardEvent to POST /v1/evaluate. { partial: true } sends ogr-partial: 1 (interim judgment on a stream — decide, record nothing). Response extension keys (x.ogr.session_id, x.ogr.unjudged, modifications, findings, …) stay verbatim keys on the returned verdict.

const verdict = await client.evaluate(event)
if (verdict.decision !== "allow") block(verdict.reasons)

ingest(events) -> Promise<IngestResult[]>

POST /v1/ingest with {"batch": [...]}. At most INGEST_BATCH_MAX (100) events per call — longer arrays throw a RangeError, so chunk long streams yourself. Per-event failures come back as results ({ id, status, error? }), not exceptions.

enroll({ publicKey, guardId?, name? }) -> Promise<{ guardId, keyId }>

POST /v1/enroll. publicKey is the base64url raw 32-byte Ed25519 public key.

heartbeat(extra?) -> Promise<{ ok: boolean }>

POST /v1/heartbeat. Pass the wire fields directly:

await client.heartbeat({ sensor: { id: "my-pep", class: "proxy" }, interval_s: 30 })

getConfig() -> Promise<RuntimeConfig>

GET /v1/config. The wire's on_unreachable arrives as config.onUnreachable; other keys pass through.

getApproval(guardId) -> Promise<ApprovalStatus>

GET /v1/approvals. Resolves to { status: "pending" | "approved" | "denied" | "expired" }; an unknown guardId rejects with a RuntimeApiError (status === 404).

Signing — createNodeSigner

Builds a Signer for the detached-JWS ogr-batch-signature scheme from an Ed25519 private key (a node:crypto KeyObject or JWK {d, x} parts) and the key_id from enrollment. node:crypto is imported lazily, so the core stays importable in edge runtimes.

import { RuntimeClient, createNodeSigner } from "@openguardrails/core"

const signer = await createNodeSigner(privateKey, keyId)
const client = new RuntimeClient({ signer })

Full enrollment bootstrap: see the enroll endpoint page.

Error handling

import { RuntimeApiError, RateLimitedError } from "@openguardrails/core"

try {
  verdict = await client.evaluate(event)
} catch (err) {
  if (err instanceof RateLimitedError) {
    // 429 — err.limit; treat like unreachable
  } else if (err instanceof RuntimeApiError) {
    // any non-2xx — err.status, err.code ("unauthorized", "invalid_event"), err.body
  }
  // timeouts reject with Error("OGR runtime request timed out after ...");
  // network failures reject with fetch's TypeError
  verdict = degradedMode(event)   // from GET /v1/config — never fail open on gated categories
}
ErrorWhenProperties
RuntimeApiErrorAny non-2xx (except ingest's 207)status, body, code
RateLimitedErrorHTTP 429 (subclass)plus limit
timeout Error / fetch TypeErrorTransport failure

Degraded mode is the caller's responsibility: apply the cached /v1/config policy when these fire.

Wire mapping

  • eventToWire(event) — camelCase GuardEvent → snake_case wire object; empty optionals dropped; unknown keys (extension fields) copied verbatim.
  • verdictFromWire(wire) — wire verdict → camelCase Verdict; keys the model doesn't name (x.ogr.*, modifications, findings) pass through unchanged.

In-process runtime

For a local PDP without any server, the package also exports Runtime, ConfigRulesDetector, LLMJudgeDetector, composition helpers, and the Detector interface — implement evaluate(GuardEvent) → Verdict and compose it like any vendor. See the package README for detector authoring.