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

@kanera/sdk

v1.10.0

Published

TypeScript client for the Kanera public API

Readme

@kanera/sdk

TypeScript client for the Kanera public API. No runtime dependencies; built on global fetch, so it runs unchanged on Node 18+, Bun, Deno, Cloudflare Workers, and browsers.

npm install @kanera/sdk
import { Kanera } from "@kanera/sdk";

const kanera = new Kanera({ apiKey: process.env.KANERA_API_KEY! });

const session = await kanera.session();
if (session.scope === "read") throw new Error("this credential cannot write");

for await (const card of kanera.work.iterateCards()) {
  console.log(card.key, card.title);
}

await kanera.cards.setCompletion("MKT-42", true);
await kanera.comments.create("MKT-42", { body: "Shipped in 1.4.0." });

Getting an API key

Create a key in the Kanera web app under Settings → API keys. Two kinds exist:

  • Personal keys (kanera_u_…) act as you, across everything you can access. Each key is scoped read-only or read-write at creation — read-only is the safe choice for reporting jobs and AI agents.
  • Workspace keys (created in workspace settings) are tied to a workspace rather than a person, so they survive people leaving and are the right shape for long-lived integrations.

Keys are shown once at creation. Keep them out of source control — read from the environment:

const kanera = new Kanera({ apiKey: process.env.KANERA_API_KEY! });

Self-hosting? Point the client at your deployment:

const kanera = new Kanera({ apiKey, baseUrl: "https://api.your-kanera.example" });

Bootstrapping a workspace

One request creates a workspace with its shared lists, custom fields, labels, checklist templates, starter cards, automations, and a first board. Seed content refers to lists, labels, and fields by name. This needs an organisation admin using a personal key; workspace keys are refused.

const workspace = await kanera.workspaces.create({
  name: "Marketing",
  lists: [{ name: "Ideas" }, { name: "In progress" }, { name: "Done" }],
  labels: [{ name: "Campaign", color: "blue" }],
  initialBoard: { name: "Q4 launch" },
});

// A standalone board is a hidden one-board workspace with its own lists and fields.
const solo = await kanera.workspaces.create({
  kind: "board",
  name: "Reading list",
  initialBoard: { name: "Reading list" },
  lists: [{ name: "To read" }, { name: "Read" }],
});

await kanera.boards.create(workspace.id, { name: "Events" });

Board separators

Separators are first-class, board-owned items in a workflow list's ordering lane. They can carry a title and color, and their anchors can target either cards or other separators.

const separator = await kanera.separators.create(boardId, listId, {
  title: "This week",
  color: "blue",
  anchor: { side: "before", item: { type: "card", id: cardId } },
});

await kanera.cards.move("MKT-42", {
  listId,
  anchor: { side: "after", item: { type: "separator", id: separator.id } },
});

Dependencies

Cards in the same workspace can block each other or simply be related. Direction reads from the first card's side, and a relation that would create a blocking cycle is rejected.

await kanera.cards.addRelation("API-12", "WEB-40", "blocks"); // WEB-40 is blocked by API-12

const { relations, hiddenBlockerCount } = await kanera.cards.listRelations("WEB-40");
const blocker = relations.find((relation) => relation.direction === "blocked_by");
if (blocker) await kanera.cards.removeRelation("WEB-40", blocker.id);

hiddenBlockerCount counts open blockers on boards you cannot open; they are never listed.

To see whole chains at once, across boards, read the dependency map for one card, a board or a workspace:

const map = await kanera.cards.dependencyGraph({ cards: ["WEB-40"] }); // or { boardId } / { workspaceId }
const card = map.cards.find((entry) => entry.key === "WEB-40");
const blockerIds = map.edges
  .filter((edge) => edge.kind === "blocks" && edge.targetCardId === card?.id)
  .map((edge) => edge.sourceCardId);

The map follows blocks relations both ways through open cards you can open, includes completed blockers where a chain reaches them, and stops at 400 cards (truncated). Cards on boards you cannot open are never included; blockedByCount still counts them.

Card references

Anywhere a card is named, you may pass a UUID, a human key such as MKT-42, or a canonical card URL. Keys are resolved once per client and cached, so repeating one across a bulk operation costs a single lookup.

A key prefix is unique only inside an organisation, and a personal credential can see several. A key visible in more than one is rejected, not guessed at — pass the canonical URL or the UUID to disambiguate.

Check the scope first

const { scope } = await kanera.session();

read means every mutation will be refused with a FORBIDDEN error. Checking once at start-up is cheaper than discovering it halfway through a migration.

Pagination

Every list endpoint is bounded — there is no "give me the whole board" call. The common integration bug is reading page one and treating it as the whole set, so iteration is the default shape:

for await (const board of kanera.boards.iterate()) { /* … */ }

const firstFifty = await kanera.comments.iterate("MKT-42").all(50);

// Or drive the pages yourself.
let cursor: string | undefined;
do {
  const page = await kanera.comments.list("MKT-42", { cursor });
  cursor = page.nextCursor ?? undefined;
} while (cursor);

Errors

import { KaneraApiError } from "@kanera/sdk";

try {
  await kanera.cards.update("MKT-42", { title: "New" });
} catch (error) {
  if (error instanceof KaneraApiError && error.isForbidden) {
    // A read-only credential, or no editor access to this board. Retrying will not help.
  }
}

KaneraApiError carries status, the API's stable code, and guards for the cases worth branching on: isUnauthenticated, isForbidden, isNotFound, isRateLimited, isRetryable. Transport failures raise KaneraConnectionError.

Retries and idempotency

Reads and DELETE/PUT are retried automatically on rate limits and transient upstream failures, with exponential backoff, full jitter, and Retry-After honoured when the server sends it.

Mutations are not retried unless you supply an idempotency key, because a retry after an ambiguous failure would create a second card or post a second comment. Supply one and the API replays the first outcome instead of repeating the write:

await kanera.comments.create("MKT-42", { body: "Deployed." }, {
  idempotencyKey: crypto.randomUUID(),
});

Webhooks

import { parseWebhook } from "@kanera/sdk";

app.post("/kanera", async (req, res) => {
  const event = await parseWebhook({
    secret: process.env.KANERA_WEBHOOK_SECRET!,
    payload: req.rawBody,   // the exact bytes; re-serialised JSON will not verify
    headers: req.headers,
  });
  // event.type, event.workspaceId, event.data
});

parseWebhook throws rather than returning a boolean, so a handler cannot forget to check before using the payload. The signature covers {timestamp}.{body}, and the timestamp is checked against a 5-minute window (configurable) to bound replay.

Endpoints can be registered from the API as well as from Workspace Settings. A workspace admin manages every endpoint; any other write-capable credential that is a workspace member can register endpoints scoped to its own connection, which is how an agent subscribes without admin authority:

const endpoint = await kanera.webhookEndpoints.create(workspaceId, {
  name: "My agent",
  url: "https://agent.example.com/kanera",
  eventTypes: ["card:created", "comment:created"], // [] for everything
});
// endpoint.secret is shown once; keep it for parseWebhook.

Options

new Kanera({
  apiKey,
  baseUrl: "https://api.kanera.app", // your own origin when self-hosting
  timeoutMs: 30_000,
  maxRetries: 2,
  organisationId,                     // pin identity-wide personal credentials to one organisation
  userAgent: "acme-sync/2.1",         // appended to the SDK's own User-Agent
  onRetry: ({ attempt, delayMs }) => log.warn({ attempt, delayMs }),
  fetch: myFetch,                     // supply your own agent or proxy
});

Escape hatch

The API moves faster than the typed surface. Nothing is gated behind a wrapper:

const result = await kanera.http.post<MyType>("/api/v1/some/new/endpoint", body);

A note on browsers

The package runs in a browser, but using it there means shipping an API key to the client. Only do that in a trusted first-party context — for anything user-facing, call Kanera from your server.

License

MIT. The SDK is deliberately more permissive than the Kanera server (Elastic License 2.0), so embedding it in your application raises no license-policy questions.