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

@noggn/checklists

v0.3.2

Published

Official TypeScript SDK for the Checklist-as-a-Service API

Readme

@noggn/checklists

Official TypeScript SDK for the Checklist-as-a-Service API. Zero runtime dependencies — native fetch.

Install

npm install @noggn/checklists

Setup

Create a .env in your project root (copy .env.example):

CHECKLIST_API_KEY=sk_your_key_here

Load it — no extra dependency needed:

  • Node 20.6+: run with the native flag — node --env-file=.env your-app.js
  • Next.js / Vite / Remix: .env is loaded automatically.

The SDK reads CHECKLIST_API_KEY from the environment. CHECKLIST_BASE_URL is optional and defaults to production (https://headless-api.vercel.app).

Usage

import Checklist from "@noggn/checklists";

const client = new Checklist(); // reads CHECKLIST_API_KEY from env
// or pass it explicitly: new Checklist({ apiKey: 'sk_...' })

const tmpl = await client.templates.create({title: "Onboarding"});
const run = await client.assignments.start({checklist_id: tmpl.id, name: "Store #12"});

Resources: templates, checklists, assignments, attachments, reviews, reports, events, webhooks.

Server-side only for now — the API rejects publishable (pk_) keys, so don't ship a secret key to the browser.

HTTP clients (SDK vs fetch vs axios)

The SDK is recommended — it handles auth, Checklist-Version, auto-Idempotency-Key on POST, retries, and typed errors. If you prefer raw HTTP:

Native fetch:

const base = process.env.CHECKLIST_BASE_URL ?? "https://headless-api.vercel.app";
const headers = {
  Authorization: `Bearer ${process.env.CHECKLIST_API_KEY}`,
  "Checklist-Version": "2026-06-24",
  "Content-Type": "application/json",
};

const res = await fetch(`${base}/v1/checklists`, {
  method: "POST",
  headers: {...headers, "Idempotency-Key": crypto.randomUUID()},
  body: JSON.stringify({title: "Store Open"}),
});
if (!res.ok) throw new Error(await res.text());
const checklist = await res.json();

axios:

import axios from "axios";

const api = axios.create({
  baseURL: process.env.CHECKLIST_BASE_URL ?? "https://headless-api.vercel.app",
  headers: {
    Authorization: `Bearer ${process.env.CHECKLIST_API_KEY}`,
    "Checklist-Version": "2026-06-24",
    "Content-Type": "application/json",
  },
});

const {data: checklist} = await api.post("/v1/checklists", {title: "Store Open"}, {headers: {"Idempotency-Key": crypto.randomUUID()}});

Bodyless state actions (POST …/complete, …/submit) omit Content-Type and body. Lists paginate with ?limit=&starting_after=.

Verifying webhooks

When you register a webhook endpoint, events are delivered as a signed POST. The Checklist-Signature header is t=<unix>,v1=<hex>, where v1 is HMAC_SHA256(secret, "<t>.<rawBody>") in lowercase hex.

Use client.webhooks.constructEvent(rawBody, signatureHeader, secret) to verify and parse in one step. It recomputes the HMAC, compares it in constant time, and rejects anything older than 300s (replay guard). On any failure — malformed header, signature mismatch, wrong secret, or stale timestamp — it throws WebhookSignatureError. On success it returns the typed Event.

Pass the raw request body bytes, not a re-stringified object — JSON.stringify of a parsed body can reorder keys and break the signature.

Express:

import express from "express";
import Checklist, {WebhookSignatureError} from "@noggn/checklists";

const client = new Checklist();
const app = express();

// express.raw gives us the exact bytes that were signed
app.post("/webhooks/checklist", express.raw({type: "application/json"}), (req, res) => {
  try {
    const event = client.webhooks.constructEvent(
      req.body.toString("utf8"),
      req.header("Checklist-Signature") ?? "",
      process.env.CHECKLIST_WEBHOOK_SECRET!,
    );
    // event is a typed Event — handle it
    if (event.action === "checklist.completed") {
      // …
    }
    res.sendStatus(200);
  } catch (err) {
    if (err instanceof WebhookSignatureError) return res.sendStatus(400);
    throw err;
  }
});

Next.js (App Router):

import Checklist, {WebhookSignatureError} from "@noggn/checklists";

const client = new Checklist();

export async function POST(req: Request) {
  const rawBody = await req.text(); // raw bytes, not req.json()
  try {
    const event = client.webhooks.constructEvent(rawBody, req.headers.get("Checklist-Signature") ?? "", process.env.CHECKLIST_WEBHOOK_SECRET!);
    // …handle event.action…
    return new Response(null, {status: 200});
  } catch (err) {
    if (err instanceof WebhookSignatureError) return new Response("bad signature", {status: 400});
    throw err;
  }
}

Handling deliveries: at-least-once

Delivery is at-least-once, so the same event will occasionally arrive more than once — a retry, a crash between your 2xx and the server recording it, or a manual Resend/Replay from the portal all re-deliver. Always dedupe by event.id before doing side effects, and make the check durable (survives restarts, shared across instances) — an in-memory Set only protects a single process. Ordering is best-effort; if you need order, sort by event.seq.

The robust pattern is a unique constraint on the event id — let the database reject the duplicate:

// once: create table processed_events (event_id text primary key, seen_at timestamptz default now());
const event = client.webhooks.constructEvent(rawBody, sig, secret);

const inserted = await db.query("insert into processed_events (event_id) values ($1) on conflict do nothing returning event_id", [event.id]);
if (inserted.rowCount === 0) return res.sendStatus(200); // already handled — ack and skip

await handle(event); // your side effects, exactly once
res.sendStatus(200);

For a minimal local reference (in-memory, dev only) see packages/webhook-receiver — its idempotency.ts + handler show the same flow. Swap the Set for a durable store in production.

Manual verification (no SDK)

import {createHmac, timingSafeEqual} from "node:crypto";

function constructEvent(rawBody, signatureHeader, secret) {
  const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec((signatureHeader ?? "").trim());
  if (!m) throw new Error("Malformed Checklist-Signature header");
  const t = Number(m[1]);
  const v1 = m[2];
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  if (!timingSafeEqual(Buffer.from(v1, "utf8"), Buffer.from(expected, "utf8"))) {
    throw new Error("Signature mismatch");
  }
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > 300) throw new Error("Replay guard");
  return JSON.parse(rawBody);
}

Full details (Hono handler, follow-up /v1 calls with fetch/axios): docs/WEBHOOKS.md.

Using with AI agents

This package ships llms.txt — a compact, agent-oriented integration guide (the same one served live at GET /llms.txt). Point your coding agent at it to learn the core model, the auth/versioning/idempotency/pagination conventions, and common recipes. The full machine-readable contract is GET /openapi.json (OpenAPI 3.1), interactive at GET /docs.

  • Compose checklists from templates — fetch a template tree with client.templates.retrieve(id), then create a checklist with an inline groups array that includes those copied groups alongside new ones, tagging each copied group metadata: { from_template: '<tmpl_id>' } for provenance. It's a copy/snapshot — later edits to the template don't affect the checklist. Use source_template_id instead when seeding from a single whole template.
  • Build a chatbot on Checklists — drop-in AI SDK v6 tools live at @noggn/checklists/tools (checklistTools(client)), and a ready-made agent at @noggn/checklists/agent (createChecklistAgent, plus checklistAgentTool to delegate to it as one tool). For the full 52-operation surface in Claude Desktop / Cursor / an MCP client, use @noggn/mcp (npx @noggn/mcp).
  • MCP server (@noggn/mcp) and the HITL chat agent (@noggn/agent, POST /v1/agent/chat) both build on this SDK — see their AGENTS.md for the tool surface and approval flow.