npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ojuri/sdk

v0.0.2

Published

Typed client for the Ojuri fraud-detection API — predict, decisions, investigation reports, and webhook signature verification. Zero runtime dependencies.

Readme

@ojuri/sdk

Typed Node client for the predict path of Ojuri, plus verification for the webhooks it sends you.

Zero runtime dependencies. Ships ESM and CommonJS. Node 18+.

npm install @ojuri/sdk

Scope

This package covers the two places an adopter's code meets Ojuri: the synchronous POST /v1/predict call on the authorization path, and the inbound webhook. It does not wrap the audit reads, the review queue, the override endpoint, or the FIA report API — those are the Sentinel dashboard's surface and it calls them directly. Query decisionAuditLog, or use Sentinel.

Quickstart

import { OjuriClient } from "@ojuri/sdk";

const ojuri = new OjuriClient({
  baseUrl: "https://rda.example.com",
  apiKey: process.env.OJURI_API_KEY,
});

const { decision } = await ojuri.predict({
  transaction_id: "your-own-id-min-10-chars",
  sender_id: "acct-123",
  receiver_id: "acct-456",
  amount: 50000,
  transaction_type: "TRANSFER",
  timestamp: Date.now(),
}, { idempotencyKey: "your-own-id-min-10-chars" });

if (decision.decision === "DECLINE") {
  block(decision.reason_codes);
}

Those six fields are the only required ones. Every optional field in docs/PREDICT-API.md — identity, device, geography, channel — only sharpens the score; the feature catalogue substitutes defaults for anything you omit.

The decision is ACCEPT, DECLINE, or REVIEW. Not APPROVE.

Degraded decisions

decision_source tells you how the decision was reached. One value needs handling of its own:

if (decision.decision_source === "BREAKER_FALLBACK") {
  // Inference never ran — the circuit breaker opened. This REVIEW carries
  // no model signal, so fall back to your own check rather than trusting it.
}

Configuration

new OjuriClient({
  baseUrl: "https://rda.example.com",  // required, absolute http(s)
  apiKey: "fdk_<prefix>_<secret>",
  tenantId: "acme",
  timeoutMs: 10_000,
  deadlineMs: 30_000,
  maxRetries: 2,
  retryBaseDelayMs: 200,
  autoIdempotencyKey: false,
  fetch: customFetch,                  // defaults to global fetch
});

deadlineMs caps total wall-clock time per call including every retry and backoff, defaulting to timeoutMs * (maxRetries + 1). Without it a client reading as "10 seconds" can block far longer once backoff is counted.

Idempotency and retries

Pass your own idempotencyKey — your transaction_id, or your PSP reference. A key's whole value is that you reuse it across your own retries, which the SDK cannot do on your behalf. With a key, a retry replays the original decision. Without one, the server's per-transaction guard rejects a duplicate transaction_id with a 409, which is also a correct answer.

autoIdempotencyKey: true generates a UUID per call. It protects only this client's internal retry loop and defeats the server's duplicate detection for your retries, so it is off by default.

A call is retried only when it carries an idempotency key. Retryable failures are 408, 429, 502, 503, 504, network failures, timeouts, and any 4xx carrying Retry-After (which is how RDA marks a key still in flight). Backoff is exponential with full jitter; a Retry-After value is honoured as given, and if it does not fit inside the remaining deadline the call fails with the directive attached rather than retrying earlier than instructed.

500 is never retried, even with a Retry-After header: the server handled the request and failed inside it, so a blind retry can duplicate effects the client cannot see.

Pass an AbortSignal to cancel. A signal already aborted stops the call before it is sent, and aborting during a backoff wait ends the retry loop immediately. Cancellation surfaces as the runtime's own AbortError, not an OjuriError.

Errors

Every failure the SDK raises is an OjuriError subclass. Cancellation is the one exception, as above.

| Class | Raised when | |---|---| | OjuriApiError | Non-2xx response. Carries status, code, errors[], correlationId, retryAfterSeconds, body. | | OjuriResponseError | The server answered with something unusable — a non-JSON 200, or a body with no decision. | | OjuriTimeoutError | The per-attempt timeout or the overall deadline elapsed. | | OjuriNetworkError | The request never reached the server. | | OjuriValidationError | Client-side input rejected before sending. | | OjuriConfigurationError | A baseUrl that is not an absolute http(s) URL, or no global fetch. |

Use the static predicates rather than instanceof. Publishing both ESM and CJS means an app can load two copies of this package, and instanceof fails across them:

import { OjuriApiError } from "@ojuri/sdk";

try {
  await ojuri.predict(request, { idempotencyKey: txn.reference });
} catch (err) {
  if (!OjuriApiError.isOjuriApiError(err)) throw err;
  // Two conditions return 409 and mean opposite things, so branch on `code`
  // rather than the status or the message text.
  if (err.code === "duplicate_transaction") return readExistingDecision(txn);
  if (err.code === "idempotency_in_flight") return retryShortly();
  throw err;
}

code is null on responses that carry none, so treat its absence as "read the status". It is never derived from the message.

Client-side validation

Rejected before a request is sent, so you get a named field instead of an opaque 400: transaction_id outside 10-255 characters, an idempotencyKey over 128 characters, a correlationId over 251 (the server prefixes req- and stores it in a varchar(255)), any of those containing control characters, and a request body that is not JSON-serialisable.

Webhook verification

Verify against the raw request body. A re-serialised object will not match.

import { verifyWebhookSignature } from "@ojuri/sdk";
import type { WebhookEnvelope } from "@ojuri/sdk";

app.post("/hooks/ojuri", express.raw({ type: "application/json" }), (req, res) => {
  const ok = verifyWebhookSignature({
    secret: process.env.OJURI_WEBHOOK_SECRET!,
    signatureHeader: req.header("X-Webhook-Signature"),
    rawBody: req.body,
  });
  if (!ok) return res.sendStatus(401);

  const envelope: WebhookEnvelope = JSON.parse(req.body.toString("utf8"));
  switch (envelope.event) {
    case "decision.created":    return handle(envelope.data.audit_id), res.sendStatus(204);
    case "decision.overridden": return handle(envelope.data.reviewer), res.sendStatus(204);
    default:                    return res.sendStatus(204);
  }
});

WebhookEnvelope is a discriminated union over event, so narrowing on it types data for you. Deliveries older than 300 s are rejected; widen with toleranceSeconds.

Note that X-Webhook-Delivery is currently regenerated per delivery attempt server-side, so it cannot be used to deduplicate a redelivery. Key on the payload's own identifiers (transaction_id, audit_id) instead.

Enums

Decision, DecisionSource, TransactionType, CustomerType, RuleStage, ReasonBasis, and WebhookEvent are exported as string enums. Request and response fields accept plain strings too, so importing them is optional.

Development

npm install
npm run lint     # eslint + tsc --noEmit
npm test         # jest
npm run build    # dual ESM + CJS build into dist/

Response types are hand-copied from the server. test/contracts/sdk-types.contract.test.ts in the repo root compares them field by field against the server declarations and fails the server's CI on drift in either direction.

Tests stub fetch, so they cover this client's behaviour and not the server's. The request and response shapes are verified against server source by the contract test above, not against live responses.