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? })
| Option | Default | Description |
|---|---|---|
baseUrl | $OGR_RUNTIME_URL | API 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_KEY | Workspace key, sent as Authorization: Bearer ogr_... |
timeoutMs | 10000 | Per-request timeout (AbortController) |
signer | — | A 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
}
| Error | When | Properties |
|---|---|---|
RuntimeApiError | Any non-2xx (except ingest's 207) | status, body, code |
RateLimitedError | HTTP 429 (subclass) | plus limit |
timeout Error / fetch TypeError | Transport failure | — |
Degraded mode is the caller's responsibility: apply the cached
/v1/config policy when these fire.
Wire mapping
eventToWire(event)— camelCaseGuardEvent→ snake_case wire object; empty optionals dropped; unknown keys (extension fields) copied verbatim.verdictFromWire(wire)— wire verdict → camelCaseVerdict; 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.