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

@axorum/client

v0.3.0

Published

The official TypeScript client for the Axorum agent plane — submit intents, read ledger state, and recover from a stale policy pin.

Readme

@axorum/client

The official TypeScript SDK for the Axorum agent plane — submit intents, read ledger state, and recover from a stale policy pin, against an axorum-serve instance.

Axorum is a deontic financial-control ledger. An agent submits an intent envelope; the service permits or refuses it under the policy in force, and records the outcome either way. This client exists to get one thing right that a generated client cannot:

A policy refusal is not an error. It is a 200, it resolves, and the refusal record is the product.

Zero runtime dependencies — the platform's fetch, crypto.randomUUID and AbortSignal are all it needs. ESM-only, Node ≥ 20.

Install

pnpm add @axorum/client

Quickstart

import { AxorumClient } from "@axorum/client";

const axorum = new AxorumClient({
  baseUrl: "http://127.0.0.1:8080",
  attestation: paseto,                       // the PASETO v4.public capability attestation
});

const outcome = await axorum.submitPinned({
  agent: "agent://example.com/agent/clerk_01h455vb4pex5vsknk084sn02q",
  attestation: paseto,                       // the same token, carried in the envelope
  action: { action: "post", bindings: {} },
  entries: [
    { account: cash,    side: "Debit",  amount: { minor: 1_000n, currency: "USD" } },
    { account: revenue, side: "Credit", amount: { minor: 1_000n, currency: "USD" } },
  ],
  justification: "invoice 4471, approved by the purchase order",
});

submitPinned reads the policy in force, stamps it onto the envelope, submits, and — if the policy moved out from under it — re-pins and submits again. It is the loop every correct agent writes, so it ships in the client rather than in your code.

The permitted outcome

if (outcome.posted) {
  console.log(outcome.verdict);      // "Permitted"
  console.log(outcome.policy);       // the policy it committed under
  console.log(outcome.transaction);  // the id it committed as
}

The refusal — which is a value, not a throw

Point the same code at a policy that forbids the action and nothing about the control flow changes. The promise resolves:

const outcome = await axorum.submitPinned(draft);

if (!outcome.posted) {
  console.log(outcome.verdict);   // "ForbiddenRejected"
  console.log(outcome.policy);    // the policy that refused it

  // The refusal is ON THE LEDGER. Read it back like any other record.
  const record = await axorum.transaction(outcome.transaction);
}

The intent reached the commit point, was judged, and the refusal was recorded. That recording is what the ledger exists to produce. Branch on posted / verdict, never on a status code. A rejected promise means the ledger never got to answer at all.

The API

new AxorumClient({ baseUrl, attestation?, timeoutMs?, fetch? })

axorum.activePolicy(opts?)                      // → Promise<string | null>      no credential
axorum.submitIntent(envelope, opts?)            // → Promise<AgentOutcome>       in-envelope
axorum.submitPinned(draft, opts?)               // → Promise<AgentOutcome>       in-envelope
axorum.submitPinnedFrom(draft, policyId, opts?) // → Promise<AgentOutcome>       in-envelope
axorum.transaction(txnId, opts?)                // → Promise<TransactionRecord>  ATTESTATION
axorum.obligations(actor?, opts?)               // → Promise<ObligationsResponse> ATTESTATION
axorum.balance(accountId, opts?)                // → Promise<BalanceResponse>    ATTESTATION
axorum.usage(usageId, opts?)                    // → Promise<UsageBalanceReport> ATTESTATION
axorum.withAttestation(token)                   // → AxorumClient                rotate the token

Every method takes opts?: { signal?: AbortSignal; requestId?: string }. An x-request-id (UUID v4) is generated per request when you do not supply one.

Reads are credentialed

An intent authenticates in the envelope — the draft names the agent:// URI and the PASETO attestation, and the service verifies both before it proposes anything. The reads used to carry nothing at all and were answered anyway, so any party's obligations were enumerable and any observed transaction id disclosed another tenant's record.

They are credentialed now, with the same token. The four party-scoped reads present the attestation as Authorization: Bearer …; the service takes the reader's identity from that token's verified agent_uri claim and answers only within the party it is bound to. There is no second credential to obtain: an agent that can submit can already read.

const axorum = new AxorumClient({ baseUrl, attestation: paseto });

const owed = await axorum.obligations();       // your party's duties — the token decides
const record = await axorum.transaction(id);   // your party's record, or a 404

A client built without an attestation still submits and still reads activePolicy — both carry their own credential or need none — but a party-scoped read throws MissingAttestationError locally, before a byte leaves the process. The client never sent a credential, so relaying the service's 401 would blame the service for rejecting one it was never given.

Metered spend is one intent

An agent that consumes a metered resource carries the usage moves on the same draft as the journal legs. They are judged in the same commit, under the usage-balance conservation law 0 ≤ billed ≤ metered ≤ consumed:

const outcome = await axorum.submitPinned({
  ...draft,
  consume_usage: { usage, amount: { minor: 1_000n, currency: "USD" } },  // you used it
  meter_usage:   { usage, amount: { minor: 1_000n, currency: "USD" } },  // it was measured
  bill_usage:    { usage, amount: { minor: 1_000n, currency: "USD" } },  // it was billed
});

All three at once is the lawful case, not an edge case: the moves of one intent are validated together against the combined post-state. Any subset is fine too — metering something an earlier intent consumed is one move, not three — and an absent move's key is omitted from the wire entirely, never null.

Metering more than was consumed is phantom usage; billing more than was metered is billing with no meter basis. The ledger refuses both with a 422, and nothing posts — not the usage moves, and not the journal legs that rode with them. Under-recognition (billed < metered < consumed) is a lawful transient, not a fault.

Read a balance back with usage(usageId). Its counters are bigints, and here that matters most: consumed takes no ceiling, so a busy meter's total can lawfully outgrow u64. Its text is the balance narrated in compliance English with exact figures — prefer it when you have a human to show.

Usage reads are owner-scoped and fail-closed: a balance owned by another party, one opened with no owner, and one that never existed all answer the same 404 unknown_usage. The cases are deliberately indistinguishable, so that a usage id is never an existence oracle across a tenant boundary. Surface the refusal as it stands; there is nothing further in it to infer.

A usage balance is opened on the admin plane, which this SDK does not speak — an agent receives a usage id, it does not mint one.

obligations(actor?) — an assertion, not a selector

?actor= used to choose whose duties came back, which is how they were enumerable. It is now an assertion the service checks:

  • Omit it — you are answered for the party your token names.
  • Supply it (a party id party_… or a DSL party name) — the service confirms it resolves to that same party, and answers 403 if it does not.

It is there so a caller can be explicit about who it believes it is, and be told when it is wrong. It can no longer be used to ask about somebody else.

Rotating an expiring token

Attestations expire. withAttestation returns a new client over the same fetch, so re-credentialing costs a string, not a reconnect — and it is also the honest way for one process to act for several agents, since no view can see another's data.

const rotated = axorum.withAttestation(freshPaseto);

balance is authenticated, not party-scoped. The ledger records no account→party edge to scope it by, so any authenticated agent may read any account's balance. That is a known residual gap in the service, not a property of this client — do not treat a balance read as private to your party.

An intent draft is a plain object — entries, evidence, justification and transaction are optional. Use pinIntent(draft, policy) if you drive the pin yourself.

Transaction ids and idempotency

The client mints the transaction id, and it is the substrate's idempotency key: re-submitting a committed id replays its stored outcome rather than double-posting. Leave transaction off your draft and the client mints one — once, before its first attempt, so every re-pinned retry inside submitPinned carries the same id. That is the entire reason the retry loop is safe. Mint one yourself with newTransactionId().

Money is bigint

The ledger denominates money in 128-bit integer minor units, on the wire as bare JSON numbers. JSON.parse decodes those as doubles and silently loses the low bits — the worst failure a financial client can have, a wrong balance that looks right. So this client does not use JSON.parse. It ships its own parser, and the rule is one line:

Every JSON integer is a bigint. Every number with a fraction or exponent is a number.

So amount.minor, balance, anchor, deadline and the Int/Tick literals are all bigint, and the generated wire types say so. Write { minor: 50_000n, currency: "USD" }.

The error taxonomy

Everything below is a subclass of AxorumError. Catch by instanceof for the narrowed fields, or switch on the stable code — both are first-class.

| Class | code | Status | What to do | |---|---|---|---| | StalePolicyError | stale_policy / policy_pin_mismatch | 409 | Re-pin against .active and resubmit. Nothing was written. submitPinned does this for you. | | NoActivePolicyError | no_active_policy | 409 | Nothing to pin to. An operator must activate a policy. | | UnknownAgentError | unknown_agent | 401 | The agent:// URI is not bound to a party — an authentication failure. | | AttestationError | attestation | 403 | The PASETO attestation was rejected: bad signature, expired, wrong audience, untrusted issuer. | | NotPrimaryError | not_primary | 421 | You reached a follower. The primary is in .primaryClientAddr. See below. | | CommitUnavailableError | commit_unavailable | 503 | The commit is unconfirmed — it may or may not have landed. Retriable. | | ApiError | the wire code | any | A documented error with no distinct recovery: unknown_account, unknown_usage, substrate_rejected, capability_mapping, … The full body is on .body. | | MissingAttestationError | missing_attestation | — | A party-scoped read on a client holding no attestation. Raised locally — nothing was sent. Build with attestation, or withAttestation. | | TransportError | transport | — | The service could not be reached, or the request timed out. Retriable. | | ConfigError | config | — | The client could not be built. | | UnexpectedResponseError | unexpected_response | any | A non-2xx that is not an agent-plane body — a proxy, a load balancer. Bytes kept verbatim in .rawBody. | | DecodeError | decode | any | A success body that did not decode. Bytes kept verbatim. |

Two accessors say what to do next:

  • error.isRetriable — re-send the identical request. True for TransportError and CommitUnavailableError. Safe even for a submission: the transaction id is the idempotency key, so a replay returns the stored outcome. You cannot double-post by retrying.
  • error.requiresRepin — the policy moved. True exactly for StalePolicyError.

The error body's extras are load-bearing and this client never flattens them away: without pinned/active no client could re-pin, and without primary_client_addr none could find the primary.

import { CommitUnavailableError, isAxorumError, StalePolicyError } from "@axorum/client";

try {
  await axorum.submitPinned(draft);
} catch (error) {
  if (error instanceof StalePolicyError) {
    console.log(`pinned ${error.pinned}, but ${error.active} is in force`);
  } else if (isAxorumError(error) && error.isRetriable) {
    // The same envelope, the same transaction id. A replay, not a second posting.
  }
}

A 421 is never followed automatically

When a follower turns a mutation away, this client surfaces primaryClientAddr and stops. It will not re-send to the primary for you.

That is deliberate. A client that silently auto-follows a redirect hides a topology change from the operator who most needs to see it: your writes quietly start going somewhere else and nothing in your logs says so. Re-sending to another replica is an operational decision, so the address is handed to you and the move is yours.

catch (error) {
  if (error instanceof NotPrimaryError) {
    console.warn(`this replica is a follower; the primary is ${error.primaryClientAddr}`);
    // Re-target deliberately, and know that you did.
  }
}

Development

The wire types in src/generated/ are generated from the frozen OpenAPI contract at spec/openapi/agent-plane.v1.json and committed. Field names are the wire's own — snake_case stays snake_case, because a second vocabulary is a second thing to keep in sync.

pnpm install
pnpm generate    # regenerate src/generated/ from the frozen spec
pnpm typecheck   # tsc --noEmit, strict
pnpm build       # → dist/
pnpm test        # unit + conformance

The conformance suite (test/conformance/) runs all thirteen scenarios from the Rust reference client against a real, booted axorum-serve — real routes, real policy evaluation, real error bodies. It deliberately does not mock: a mock would put whatever bytes the test author believed the service emits on the wire, which is exactly the belief under test. It needs the harness binary at target/debug/examples/conformance_server, built by the Rust workspace.

pnpm test:unit          # no server required
pnpm test:conformance   # boots a real axorum-serve per scenario