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

@enruta/sdk

v0.3.0

Published

TypeScript client for the Enruta API: agents (payment requests, clarifications, identity, merchant checks, economics, recurrences, reversals, settlements, reconciliation, disputes, consents), sellers (orders, PSP connections, storefronts) and platforms (c

Readme

@enruta/sdk

TypeScript client for the Enruta API (v0). fetch only, no runtime dependencies, works in Node 18+ and any runtime with a global fetch. Version 0.3 covers agents (requests, clarifications, identity, merchant checks, the money split, recurrences, mandates, records and the objects around a payment), sellers (the seller API, PSP connections, hosted storefronts) and platforms (connected accounts, keys, feeds, brand, usage), plus billing, webhook signature verification and the UCP conformance runner. A Python client with the same surface is enruta in packages/sdk-python.

import { Enruta } from "@enruta/sdk";

const enruta = new Enruta(process.env.ENRUTA_AGENT_KEY!);

const r = await enruta.paymentRequests.create({
  agentId: "agt_procurement_02",
  payee: { name: "Ream & Rule", domain: "reamandrule.com" },
  amount: { value: "168.40", currency: "USD" },
  purpose: "20 reams A4 paper, 1 stapler",
  checkout: { protocol: "ucp", reference: "chk_8f3…" },
  intent: { text: "restock the print room, under $200", slots: { category: "office_supplies", max_total: "200.00", currency: "USD" } },
}, { idempotencyKey: "a3c9…" });

if (r.decision.result === "clarify") {
  // Ask the user every question, then send their answers; the request is decided again.
  const answered = await enruta.clarifications.answer(r.id, { answers: r.decision.questions!.map((q) => ({ question_id: q.id, answer: ask(q.question) })) });
}
if (r.decision.result === "allow" || r.decision.result === "observe") pay(r.mandate!.credential);

Surface

| Method | Endpoint | | --- | --- | | paymentRequests.create(body, { idempotencyKey }) | POST /v0/payment-requests (body may carry intent, catalog_snapshot_hash, policy_snapshot_hash, deviations) | | paymentRequests.get(id, { wait? }) | GET /v0/payment-requests/{id}?wait= (gains risk, reversals, settlement, reconciliation, disputes, fulfillment) | | paymentRequests.handOff(id, { continue_url, order? }) | POST /v0/payment-requests/{id}/handoff | | approvals.get(id) | GET /v0/approvals/{id} | | mandates.list({ agent_id?, state? }), mandates.get(id) | GET /v0/mandates, GET /v0/mandates/{id} | | records.list({ agent_id?, from?, to?, kind?, cursor?, limit? }) | GET /v0/records (kind filters payment and linked records) | | records.get(id){ id, text, record, kind } | GET /v0/records/{id} (canonical text, parsed object, record kind) | | records.bundle(id), records.verify(id) | GET /v0/records/{id}/bundle, GET /v0/records/{id}/verify | | records.linked(id) | GET /v0/records/{id}/linked (the records chained to one) | | exports.create({ from, to, principal_login_id?, kinds?, consents? }), exports.get(id), exports.download(id)Uint8Array | POST /v0/exports, GET /v0/exports/{id}, GET /v0/exports/{id}/download | | reversals.create(body, { idempotencyKey? }), .get(id), .list(q), .adjustments(id, protocol) | POST /v0/reversals, GET /v0/reversals/{id}, GET /v0/reversals, GET /v0/reversals/{id}/adjustments?protocol= | | settlements.get(recordId) | GET /v0/settlements/{record_id} | | reconciliation.lines(q), .line(mandateId), .report(period), .buildReport(period), .signReport(period) | GET /v0/reconciliation/lines, …/lines/{mandate_id}, …/reports/{period}, POST …/reports/{period}/build, POST …/reports/{period}/sign (link signature) | | disputes.list(q), .get(id), .packet(recordId, { regenerate? }), .representmentPdf(recordId)Uint8Array, .recordOutcome(id, body) | GET /v0/disputes, GET /v0/disputes/{id}, GET|POST /v0/records/{id}/dispute-packet, GET /v0/records/{id}/representment.pdf, POST /v0/disputes/{id}/outcome | | consents.create(body), .list(q), .get(id), .revoke(id, { effective_at?, cascades_scheduled? }) | POST /v0/consents, GET /v0/consents, GET /v0/consents/{id}, POST /v0/consents/{id}/revoke | | attestations.create(body), .list(q) | POST /v0/attestations, GET /v0/attestations | | agents.pause(id, reason), .resume(id) | POST /v0/agents/{id}/pause, POST /v0/agents/{id}/resume | | sandbox.createKeys(), createSandboxKeys({ baseUrl? }) | POST /v0/sandbox/keys (no key needed) | | sellers.orders.list(q), .get(id), .observe(body) | GET /v0/sellers/orders, GET /v0/sellers/orders/{id}, POST /v0/sellers/orders | | sellers.verify(body) | POST /v0/sellers/verify | | sellers.adjustments.report(body), sellers.reversals.request(body) | POST /v0/sellers/adjustments, POST /v0/sellers/reversals | | sellers.fulfillments.confirm(body) | POST /v0/sellers/fulfillments | | sellers.disputes.list(q), sellers.packet(recordId), sellers.representmentPdf(recordId) | GET /v0/sellers/disputes, GET /v0/sellers/records/{id}/dispute-packet, GET /v0/sellers/records/{id}/representment.pdf | | sellers.reconciliation(period \| { period?, order_id? }) | GET /v0/sellers/reconciliation | | sellers.conformance.run(endpoint, { domain? }), .list() | POST /v0/sellers/conformance, GET /v0/sellers/conformance | | sellers.middleware(), sellers.domains.list(), .add(domain) | GET /v0/sellers/middleware, GET /v0/sellers/domains, POST /v0/sellers/domains | | clarifications.answer(requestId, { answers, answered_by? }) | POST /v0/payment-requests/{id}/clarifications | | merchants.identity(domain) | GET /v0/merchants/identity?domain= | | attestations.nonce({ agent_id? }), attestations.create({ agent_id, anchor: { type, … } }) | POST /v0/attestations/nonce, POST /v0/attestations (identity anchors; the 0.2 registry body still works) | | agents.identity(id) | GET /v0/agents/{id}/identity | | economics.forPayment(requestId), sellers.orders.economics(orderId) | GET /v0/payment-requests/{id}/economics, GET /v0/sellers/orders/{id}/economics | | recurrences.create, .list, .get, .update(id, { action }), .pause, .resume, .end, .occurrences | POST|GET /v0/recurrences, GET|PATCH /v0/recurrences/{id}, GET /v0/recurrences/{id}/occurrences | | records.pspMetadata(id, psp) | GET /v0/records/{id}/psp-metadata?psp= | | disputes.deliver(id, { target, connection_id? }) | POST /v0/disputes/{id}/deliveries | | sellers.pspConnections.create, .list, .delete, .test | POST|GET /v0/sellers/psp-connections, DELETE /v0/sellers/psp-connections/{id}, POST …/{id}/test | | storefronts.create, .list, .get, .update, .refreshCatalog, .orders | POST|GET /v0/storefronts, GET|PATCH /v0/storefronts/{id}, POST …/{id}/catalog/refresh, GET …/{id}/orders | | platform.accounts.create, .list, .get, .update, .createKey | POST|GET /v0/platform/accounts, GET|PATCH /v0/platform/accounts/{id}, POST …/{id}/keys | | platform.usage({ period }), platform.brand.get(), .update(body) | GET /v0/platform/usage?period=, GET|PUT /v0/platform/brand | | platform.feeds.create, .list, .delete, .events(id, { after, limit }), .replay(id, { from }) | POST|GET /v0/platform/feeds, DELETE /v0/platform/feeds/{id}, GET …/{id}/events, POST …/{id}/replay | | billing.subscription(), .plans(), .usage({ period }), .invoices.list(), .invoices.get(period) | GET /v0/billing/subscription, /plans, /usage, /invoices, /invoices/{period} | | approvalDelegations.create, .list, .revoke | POST|GET /v0/approval-delegations, DELETE /v0/approval-delegations/{id} | | org.settings.get(), .update(body), org.verification.start(body), .get() | GET|PATCH /v0/org/settings, POST|GET /v0/org/verification | | policies.list(), policies.version(id, n) | GET /v0/policies, GET /v0/policies/{id}/versions/{n} | | rails.list() | GET /v0/rails | | me(){ agent_id, org_id, role } | GET /v0/me |

Options: new Enruta(key, { baseUrl = "https://api.enruta.ai", fetch, platform? }). Every request carries Authorization: Bearer <key>, User-Agent: enruta-sdk/0.3.0 and, when there is a body, Content-Type: application/json. Keys: enruta_agt_… (agent), enruta_dev_… (developer), enruta_aud_… (auditor), enruta_slr_… (seller), enruta_plt_… (platform).

Errors

Every non-2xx response throws EnrutaError { status, code, message, requestId?, details?, retryAfter? }, built from the { error: { code, message } } envelope (invalid_request, unauthenticated, forbidden, not_found, idempotency_conflict, rate_limited). A failed connection throws with status: 0 and code: "network_error".

A denied payment is not an error: create resolves with decision.result: "deny" and the reasons, and the denial has a record. The same holds for a denied reversal: reversals.create resolves with status: "denied" and decision.reasons.

Every decision says whether the same payment may be requested again: decision.retryable is true only when every reason that ended the request is remediable, and decision.retry_after says from when (over_budget: the next budget window), otherwise null. Never retry with another amount, payee or wording; a policy's retry_policy refuses attempts beyond its limit with retry_limit_reached.

Idempotency

paymentRequests.create requires idempotencyKey. Repeating a key returns the original decision with idempotent_replay added to decision.reasons; the same key with a different body is a 409 idempotency_conflict. reversals.create accepts an optional key the same way; the platform also dedupes reversals on dedupe_key.

Decisions in shadow mode

A policy with shadow: true answers decision.result: "observe" where it would have stepped up or denied: the payment goes through as an allow, and decision.shadow_result ({ result, reasons }) says what the policy would have done. Records carry both. decision.risk is the risk assessment made at decision time (action, score, checks).

Clarification

When the policy needs a detail the request did not give (how many, the most to spend, by when), create resolves with decision.result: "clarify", state: "awaiting_clarification" and decision.questions ([{ id, slot, question }]). Ask the user, then clarifications.answer(id, { answers: [{ question_id, answer }] }): the answers are recorded (answered_by: "user" unless you say otherwise) and the request is decided again. Unanswered past the policy's time limit, the attempt is sealed as expired with clarification_expired. Send the person's words in intent.text and what the agent understood in intent.slots to avoid most questions.

Identity

agents.identity(id) returns the agent's assurance level (A0 nothing verified, A1 a verified agent key, A2 plus a verified workload identity or hardware attestation, A3 plus a verified legal entity, or collateral on a crypto rail) and its anchors. Anchors are attested with attestations.create({ agent_id, anchor }): agent_key { jwk, proof } (a compact JWS by that key over { agent_id, nonce, iat }, with a nonce from attestations.nonce()), workload_identity { token } (an OIDC token for the audience https://api.enruta.ai; enruta attest workload does this inside GitHub Actions), code_digest, hardware_attestation, legal_entity, collateral. merchants.identity(domain) is the other direction: the check of a merchant before paying it.

Who earns what

economics.forPayment(requestId) (and sellers.orders.economics(orderId) for a seller) returns the money split: parties[] (user, agent platform, merchant, PSP, card network, issuer, Enruta) with pays, receives, net, how_it_earns and lines[], and flows[]. Every line has a basis: actual, estimate (with source and source_date) or plan. Estimates are never labelled actual.

Platforms: acting for a connected account

A platform key (enruta_plt_…) manages connected accounts with platform.accounts.* and acts for one with the platform option, which sets Enruta-Account (and Enruta-Agent):

const platform = new Enruta(process.env.ENRUTA_PLATFORM_KEY!);
const { account, agents } = await platform.platform.accounts.create({ kind: "agents", name: "Globex buyer", external_ref: "cust_9", agents: [{ name: "Buyer" }] });
const buyer = platform.forAccount("cust_9", { agent: agents[0]!.id }); // or new Enruta(key, { platform: { account: "cust_9", agent } })
await buyer.paymentRequests.create({ … }, { idempotencyKey });

An unknown or suspended account answers 403 account_not_connected. Records made this way name the platform as agent.host.

Webhooks and feeds

Deliveries carry Enruta-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, t + "." + body)>. await verifyWebhookSignature(rawBody, header, secret, { toleranceSeconds: 300 }) returns true or false (WebCrypto, constant-time, any v1 may match during a secret rotation); await constructWebhookEvent(rawBody, header, secret) returns the parsed event or throws EnrutaSignatureError. Verify the raw body before parsing it.

Reversals

reversals.create({ record_id, kind, amount?, reason_code, refund_to? }) asks for a void, cancel, refund, return, credit, price_adjustment or chargeback_credit against a payment record. A reversal is decided like a payment (decision.result allow, step_up or deny, with checks on the original amount, the stage, the finality class, the window and cancellable_until); the reply carries stage, mandate_effect (none, refill, refill_partial, refill_pending_settlement), status (requested, pending, completed, failed, denied) and fund_change. reversals.adjustments(id, "ucp" | "acp" | "adyen" | "shopify") renders it as that protocol's adjustments[] entry.

Linked records

Every supporting object (reversal, settlement record, reconciliation line and report, dispute packet and outcome, consent record, attestation, agent pause, seller-side agent order) is its own Evidence Record on the same chain, with kind and chain: { relation, of, of_hash } pointing at the record it supports. records.list({ kind }) filters by kind, records.linked(id) lists the records chained to one, and records.get(id).kind names what came back. exports.create({ kinds: "all", consents: true }) includes them in the zip.

Completing the checkout through Enruta

Pass complete_checkout: true with a UCP checkout and Enruta executes the checkout itself after allow, presenting the mandate and the single-use credential to the merchant; the credential stays with Enruta. The response then carries order ({ id, status, checkout_id, completed_at, permalink_url? }) and completion ({ state: "completed" | "failed" | "pending" | "not_requested", error? }); on failed the mandate stays issued.

Handing the checkout to a person

Some merchants accept no credential from an agent: Shopify completes a checkout only on its own page. Once the mandate is issued, call paymentRequests.handOff(id, { continue_url }) with the merchant's checkout link (https): the request moves to handed_off, the mandate to void, and the budget reservation is kept. When the person has paid, call it again with order: { id, status?, permalink_url?, total? }: the order becomes the receipt (self-asserted), outcome.status becomes completed and the record seals; the reservation is spent when total is given, released otherwise. Without an order the record seals 15 minutes after the hand-off. Every PaymentRequest carries outcome and handoff ({ continue_url, handed_off_at, order }); state gains handed_off.

Conformance runner

import { runConformance, CONFORMANCE_TESTS } from "@enruta/sdk";

const run = await runConformance("https://store.example.com", {
  domain: "store.example.com",
  platformProfileUrl: "https://app.enruta.ai/.well-known/agent-profile.json", // enables the two AP2 vectors
});
console.log(run.passed, "of", run.total, run.ok ? "green" : "failing");
for (const r of run.results) console.log(r.ok ? "ok  " : "FAIL", r.name, r.detail);

runConformance(endpoint, { domain?, fetch?, timeoutMs?, platformProfileUrl?, profileUrl?, productId?, cleanup? }) runs the 19 vectors of CONFORMANCE_TESTS against a UCP REST endpoint: discovery (profile, keys, capabilities, REST service), lifecycle (create, get, update with a shipping address and option, complete with the mock handler's success_token, order fetch), protocol (envelope version, version negotiation, error shape), idempotency (replay, conflict), mandate (AP2 negotiated, mandate_required without ap2.checkout_mandate; skipped unless platformProfileUrl is given), signature (ap2.merchant_authorization verifies with a profile key), receipt (payment_reference, X-Receipt-Signature when the merchant signs). Results are { name, ok, detail, skipped? }[] with passed, total (skipped vectors excluded) and ok. The runner presents a data: platform profile without AP2 so the mock handler can complete; merchants that only accept https profile URLs get profileUrl. One order is placed with the mock handler (no money moves); the other sessions are canceled. The platform's POST /v0/sellers/conformance runs the same suite and issues the badge.

CLI output schemas

CLI_OUTPUT_SCHEMAS (also @enruta/sdk/schemas) holds the JSON Schema (draft 2020-12) of what every enruta command prints with --json; enruta <command> --schema prints it.

Apache-2.0.