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

@peerfold/api-client

v0.2.0

Published

Typed TypeScript client for the Peerfold public API — browser/Node/edge, zero runtime deps. Types derive from the @peerfold/api-spec zod schemas so they can never drift from the OpenAPI contract.

Readme

@peerfold/api-client

Typed TypeScript client for the Peerfold public API (E14). Works in the browser, Node, and edge runtimes; zero runtime dependencies (a thin wrapper over fetch).

npm install @peerfold/api-client
import { PeerfoldClient } from "@peerfold/api-client";

const client = new PeerfoldClient({
  baseUrl: "https://acme.site.hublms.com",
  learnerToken: myLearnerJwt,
});

const catalog = await client.catalog.list({ limit: 25 });

A note on naming (0.2.0). The classes are PeerfoldClient / PeerfoldError, the factory is createPeerfold, the wire headers are Peerfold-Version / X-Peerfold-* and the browser storage keys are peerfold.*. 0.1.x shipped the old HubLMS-prefixed names — this is a breaking rename, so pin ^0.2.0. The server still accepts the old HubLMS-Version and X-HubLMS-Publishable-Key request headers through the 0.x window; they are removed at 1.0.

Spec-first types (drift-impossible)

Every request/response type is z.infer<> of the exact zod schema @peerfold/api-spec uses to validate the live API — the same schemas the spec:check drift gate keeps welded to openapi.json. There is no hand-written mirror of the shapes in this package. The trick (src/types.ts):

import type { z } from "zod";
type Registry = typeof import("@peerfold/api-spec/schemas").schemaRegistry;
export type Member = z.infer<Registry["Member"]>;

typeof import(...) and import type are fully erased at compile time, so no value import of zod or api-spec survives into the emitted JS — the runtime keeps its zero-dependency promise while the types stay pinned to the OpenAPI contract. If a field changes in the spec, this type changes automatically and stale call sites fail to typecheck. (@peerfold/api-spec and zod are therefore devDependencies — compile-time only.)

Install & construct

import { PeerfoldClient } from "@peerfold/api-client";

// Browser / learner plane — configured with a short-lived learner JWT.
const client = new PeerfoldClient({
  baseUrl: "https://acme.site.hublms.com",
  learnerToken: myLearnerJwt, // string, or a () => string | Promise<string> provider
});

// Server / admin plane — configured with a secret key (NEVER ship to a browser).
const admin = new PeerfoldClient({
  baseUrl: "https://app.hublms.com",
  secretKey: process.env.PEERFOLD_SK, // sk_live_… / sk_test_…
});

fetch is injectable (fetch: myFetch) for edge runtimes or testing. Pin an API version with version: "2026-07-01" (sent as the Peerfold-Version header).

Learner plane

const me = await client.me.get();
await client.me.update({ name: "Lee" });

const page = await client.catalog.list({ limit: 25 });
for await (const course of client.catalog.iterate()) { /* every page */ }

const detail = await client.courses.get("intro-to-widgets");
const enrollment = await client.enrollments.create({ course_slug: "intro-to-widgets" });

// Idempotency-Key is REQUIRED by the API; the client auto-generates one
// (crypto.randomUUID) unless you supply your own — so retries are always safe.
await client.progress.record(enrollment.id, {
  event_id: "evt-123",
  type: "lesson_completed",
  lesson_id: "lesson-1",
});

const quiz = await client.quizzes.submit(enrollment.id, {
  lesson_id: "lesson-2", block_id: "q1", answers: { "question-1": ["a"] },
});

for await (const cert of client.certificates.iterate()) { /* … */ }
const verified = await client.certificates.verify("SER-1234"); // public, no auth

Admin / server plane

// Mint a learner token for a browser session (PRD Flow 3).
const { access_token } = await admin.admin.auth.mintLearnerToken({ email: "[email protected]" });

for await (const learner of admin.admin.learners.iterate({ status: "active" })) { /* … */ }
const learner = await admin.admin.learners.create({ email: "[email protected]" });
await admin.admin.learners.enroll(learner.id, { course_slug: "intro-to-widgets" });

const reports = await admin.admin.reports.courses();
const { secret } = await admin.admin.webhooks.create({
  url: "https://api.example.com/hooks",
  events: ["certificate.issued", "course.published"],
});

Errors, rate limits, request ids

Every non-2xx response throws a PeerfoldError carrying the parsed RFC 7807 problem+json body:

import { PeerfoldError, getResponseMeta } from "@peerfold/api-client";

try {
  await client.progress.record(id, event);
} catch (err) {
  if (PeerfoldError.is(err)) {
    err.status;      // 429
    err.code;        // "rate_limited" (stable machine-readable code)
    err.requestId;   // echoed X-Request-Id
    err.rateLimit;   // { limit, remaining, reset }
    err.retryable;   // true for 429 / 5xx
  }
}

// Rate-limit / request-id metadata is also surfaced on successful results:
const page = await client.catalog.list();
getResponseMeta(page)?.rateLimit.remaining;
client.lastResponseMeta?.apiVersion;

Conventions baked in

  • Auth per plane — learner methods send the learner JWT; admin methods send the secret key; calling a plane whose credential is missing throws before any network I/O. Public certificate verification needs no credential.
  • Cursor pagination — every list has .list({ cursor, limit }) and a .iterate() async iterator that walks all pages.
  • Idempotency — auto Idempotency-Key on progress.record (override via { idempotencyKey }).
  • Edge-safe — only web-standard globals (fetch, Headers, crypto, AbortSignal); no Node-only APIs in the default path.