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

ankusa

v0.3.0

Published

Client SDK for Ankusa deployments: the claim-check gateway client, the operator (admin) client, the route-management client, and a webhook-receiving header helper.

Readme

ankusa

The client SDK for Ankusa deployments: one npm package meant to bundle everything a non-Elixir consumer needs to talk to an Ankusa deployment — the claim-check gateway client, the route-management client (routes.admin.port), the operator (admin.port) client, and a helper for receiving Ankusa's HTTP sink deliveries.

Claim-check client

Redeem a claim-check ref, verify the bytes it returns against the sha256 the queue message carries next to it, and classify failures into dead-letter vs. retry, without holding any object-store credentials. Generated from the framework's own contract, priv/openapi/claim_check.v1.yaml, via openapi-typescript + openapi-fetch. The spec is the source of truth; this package conforms to it, not the reverse.

Install

npm install ankusa

Not published yet. Until the first release, depend on it as a local path, the same way the Elixir packages in this monorepo depend on ankusa core before their first Hex release:

{
  "dependencies": {
    "ankusa": "file:../../sdks/typescript"
  }
}

A file: dependency resolves to compiled output (dist/), so run npm run build here at least once before a consumer installs it that way.

Use

import { ClaimCheckError, createClaimCheckClient } from "ankusa";

const claimCheck = createClaimCheckClient({ baseUrl: process.env.CLAIM_CHECK_URL ?? "http://localhost:4001" });

// `ref` is the queue message's `claim` field, `sha256` its `sha256` field:
//   claim:  urn:ankusa:claim:v1:<tenant>:<claim_id>   (claim_id: uppercase ULID)
//   sha256: 64 lowercase hex chars, the digest of the claim's bytes
async function resolveBody(ref: string, sha256: string): Promise<Buffer> {
  try {
    return await claimCheck.redeem(ref, sha256);
  } catch (err) {
    if (err instanceof ClaimCheckError && !err.retryable) {
      // bad ref, 404, or an integrity mismatch: dead-letter, don't requeue
      throw err;
    }
    // gateway unreachable or 5xx: safe to retry
    throw err;
  }
}

redeem() does three things GET /v1/claims/... alone doesn't:

  1. Parses the ref into its tenant id, claim id, and gateway path (parseClaimRef, also exported standalone), and checks sha256 is 64 lowercase hex chars.
  2. Fetches the bytes from GET /v1/claims/{tenant_id}/{claim_id}.
  3. Verifies them against sha256 (the gateway itself does not check this, see "Redeem a claim" in docs/claim-check.md) before ever returning them to you.

Every failure is a ClaimCheckError subclass with a retryable boolean, so a consumer needs exactly one bit to decide dead-letter vs. retry:

| Class | retryable | Cause | | --- | --- | --- | | InvalidClaimRefError | false | ref isn't a well-formed claim-check URN, or sha256 isn't 64 lowercase hex chars | | ClaimNotFoundError | false | gateway 404: expired by retention, or never written | | ClaimRejectedError | false | gateway 4xx other than 404 (.status, .body) | | ClaimIntegrityError | false | the bytes' sha256 doesn't match the expected sha256 | | ClaimCheckUnavailableError | true | gateway 5xx/503, or unreachable |

health() hits GET /health for a liveness probe.

The workers in rabbitmq-consumer and kafka-sqs-consumer depend on this package.

Routes client

Manage route definitions and the global IP rules on the route-management listener (routes.admin.port, default 4003) — the routes tag of priv/openapi/admin.v1.yaml.

import { RouteNotFoundError, createRoutesClient } from "ankusa";

const routes = createRoutesClient({ baseUrl: "http://localhost:4003" });

await routes.createRoute({ id: "stripe", path: "/webhooks/stripe" });
await routes.getIpRules();   // { default: "allow", rules: [] }
await routes.testRoute({ method: "POST", path: "/webhooks/stripe", ip: "203.0.113.7" });

Methods: health(), listRoutes(), createRoute(), getRoute(), replaceRoute(), updateRoute(), deleteRoute(), getIpRules(), putIpRules(), testRoute(). The id-taking methods reject an id that is not a string, is empty, or is ./.. with InvalidRouteIdError before making any request: a URL parser normalizes those away, so they'd address the collection endpoint instead of a route. Every other id is percent-encoded as one path segment.

Failures are RoutesError subclasses with a retryable boolean: InvalidRouteIdError and RouteNotFoundError (404), RoutesRejectedError (any other 4xx, carrying code, field, message, conflicting_id, max_routes), and RoutesUnavailableError (5xx, an unfollowed 3xx, or unreachable — retryable).

Admin client

The operator API on admin.port (default 4002): health, Prometheus metrics, the redacted config, the DLQ, and the quarantine list — the operations, dlq, and quarantine tags of admin.v1.yaml.

import { createAdminClient } from "ankusa";

const admin = createAdminClient({ baseUrl: "http://localhost:4002" });

await admin.health();                        // { status, instance, roles }
await admin.listDeadLetters({ limit: 10 });
await admin.replayDeadLetters({ source_id: "demo" });
await admin.listQuarantined();

Methods: health(), metrics() (Prometheus text), config(), listDeadLetters(), replayDeadLetters(), listQuarantined(). Failures are AdminError subclasses: RoleNotEnabledError (409 role_not_enabled, carrying role), AdminRejectedError (any other 4xx, carrying code), and AdminUnavailableError (5xx, an unfollowed 3xx, or unreachable — retryable).

Webhook helper

A worker consuming Ankusa's HTTP sink doesn't need a client — it needs the hook's identity, which Ankusa attaches as headers. parseHeaders reads them case-insensitively from any header mapping: a Headers, a plain object, or Node's IncomingHttpHeaders.

import { parseHeaders } from "ankusa";

// `req.headers` is whatever your framework hands you (`http.IncomingHttpHeaders`,
// an Express `req.headers`, a WHATWG `Headers`, ...).
const hook = parseHeaders(req.headers);
// hook = { id, source, tenant, contentType }

// Dedupe on `hook.id`: delivery is at-least-once, so a retried hook arrives twice.
// `x-ankusa-id` is the identity to dedupe on, so a delivery without it is a
// framework bug rather than a tolerable request: `parseHeaders` raises
// `MissingHookIdError` instead of returning a blank id.

Every other header is optional — tenant is null unless the source has one, and source/contentType default to ""/null. See "HTTP handoff" in docs/integrations.md for the full contract.

Layout

src/
  index.ts            # umbrella barrel: re-exports every client this package bundles
  claim-check/         # the claim-check gateway client
    index.ts           # barrel for this client
    client.ts
    ref.ts
    errors.ts
    claim-check-schema.d.ts   # generated, see "Develop"
    client.test.ts
  routes/              # the route-management client (routes.admin.port)
    index.ts
    client.ts
    errors.ts
    client.test.ts
  admin/               # the operator client (admin.port)
    index.ts
    client.ts
    errors.ts
    admin-schema.d.ts   # generated, see "Develop"
    client.test.ts
  webhook/             # the helper for receiving HTTP-sink deliveries
    index.ts
    headers.ts
    webhook.test.ts

A future client (say, an ingest helper) gets its own src/<name>/ directory with the same shape, re-exported from src/index.ts.

Develop

npm install
npm run generate:types   # regenerate every schema from the OpenAPI specs:
                         #   claim_check.v1.yaml -> src/claim-check/claim-check-schema.d.ts
                         #   admin.v1.yaml       -> src/admin/admin-schema.d.ts
                         # (the routes client is generated from admin.v1.yaml too)
npm run typecheck
npm test
npm run build            # emits dist/, what npm actually publishes ("files": ["dist"])