@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.
Maintainers
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-clientimport { 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 iscreatePeerfold, the wire headers arePeerfold-Version/X-Peerfold-*and the browser storage keys arepeerfold.*. 0.1.x shipped the oldHubLMS-prefixed names — this is a breaking rename, so pin^0.2.0. The server still accepts the oldHubLMS-VersionandX-HubLMS-Publishable-Keyrequest 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 authAdmin / 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-Keyonprogress.record(override via{ idempotencyKey }). - Edge-safe — only web-standard globals (
fetch,Headers,crypto,AbortSignal); no Node-only APIs in the default path.
