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

@keepcv/api

v0.7.0

Published

The KeepCV HTTP boundary: Hono routes, Zod validation, problem+json errors and a typed client.

Readme

@keepcv/api

The HTTP boundary for KeepCV: Hono routes, Zod validation, RFC 9457 problem+json errors, a generated OpenAPI document and a typed client.

This is a first-class artifact rather than an emitted side effect. A store people run themselves is one whose client and server drift out of step, so the boundary between them is designed, versioned and stable - which framework-internal RPC would not be.

Status: early development. The public API is unstable. Today it serves the whole record store - the profile, contact channels, organisations, custom sections, records, points, phrasings, tags, drafts and everything hanging off them - the composition a resume is, its history with version comparison and restore, and the boot payload and the native export and import. There is no search route and no composition route: both are pure functions over the boot payload, in @keepcv/core.

Installation

pnpm add @keepcv/api

Usage

The package builds an app; it does not open a store, choose a port or decide who the caller is. Those belong to whatever is hosting it - locally that is the keepcv launcher.

import { createApi, sessionTokenAuth } from "@keepcv/api";
import { openLocalStore, runAsOwner } from "@keepcv/db";

const store = openLocalStore({ dataDir: "./keepcv-data" });
await store.migrate();
const ownerId = await store.ensureLocalOwner();

const api = createApi({
  unitOfWork: store.unitOfWork,
  runAsOwner,
  authenticate: sessionTokenAuth(process.env.KEEPCV_TOKEN, ownerId),
});

// api.fetch is a standard fetch handler: serve it with @hono/node-server,
// or hand it straight to a runtime that speaks Request and Response.

Every content transaction receives the incoming Request's signal, including native import and writes that plan multiple rows. The host connects transport disconnects and operation deadlines to that Request; aborting a separate client promise does not establish server cancellation. The supplied unit-of-work implementation controls interruption and cleanup. The PostgreSQL adapter stops the checked-out query and rolls back before COMMIT where possible. Once COMMIT has been dispatched, cancellation cannot promise rollback; re-read after an ambiguous disconnect before deciding whether to retry. Lifecycle and erasure calls made separately on the store never inherit a Request signal.

The typed client resolves the route types inside this package, so a consumer does not re-infer them:

import { createClient } from "@keepcv/api";

const client = createClient("http://127.0.0.1:4319", { sessionToken });
const response = await client.v1.profile.$get();

createApi accepts reportUnexpectedError. Omit it to retain console.error, pass null to disable reporting, or supply an UnexpectedErrorReporter:

const api = createApi({
  unitOfWork: store.unitOfWork,
  runAsOwner,
  authenticate,
  reportUnexpectedError: () => {
    console.error("keepcv.unexpected_error");
  },
});

The callback receives the original unexpected error, whose causes may contain SQL parameters or authored content. The caller chooses what is safe to report; the API adds no request or authentication data. Expected problems do not invoke it. Reporting is best effort: synchronous throws and rejected promises are ignored, with no fallback console output. Reports do not delay the response. Every reporting choice retains the same generic 500 problem and the existing validation, conflict, stale-write, restore and export-change responses. problemFor(error, instance, reportUnexpectedError) accepts the same optional reporter for callers using the mapper directly.

What the boundary guarantees

  • The owner is never a parameter. authenticate resolves a request to an owner and the API enters that scope once; every repository call underneath is scoped by construction, so "forgot to scope this query" is not a mistake a handler is able to make.
  • Every error is a typed problem. One shape, application/problem+json, with a type the client switches on. Nothing downstream parses prose.
  • A stale write answers with the state the server holds. Mutations carry the updated_at they were based on - as { expectedUpdatedAt, patch }, beside the changes rather than among them - and a mismatch returns 409 with the current row attached, so the UI can show both sides instead of discarding one.
  • A refused write names the constraint. A taken sort key is a 409, because the caller resolves it by re-reading; a parent that does not exist, or a column the record's kind may not carry, is a 422. A driver error never reaches the client, since a caller mistake reported as 500 sends everybody looking in the wrong place.
  • DELETE archives. Nothing the user wrote is destroyed, the row stays readable by id, and ?archived=include brings it back into a list.
  • Phrasing text is append-only. PATCH /v1/phrasings/{id} has no field that could carry text; changing what a phrasing says is POST /v1/phrasings/{id}/revisions, which keeps the superseded wording and moves a pointer. It is the one write with no concurrency token, because two people writing different wordings at once must both keep their text.
  • In-progress text survives a closed tab. PUT /v1/drafts/{targetKind}/{targetId}/{field} keeps uncommitted editor state outside history, overwritable and without a concurrency token, so keystrokes never become revisions and nothing half-written is lost. Drafts arrive with the boot payload, so an editor knows one is waiting before it opens.
  • A resume is a selection, and it is composed rather than copied. /v1/resumes holds what a resume is aimed at; /v1/resume-sections, /v1/resume-entries and /v1/resume-entry-points hold what it selects, each narrowed by ?resumeId=. Toggling something off sets isVisible, so the wording chosen and the position it sat in survive, and a point can appear at most once on one resume. Reading a whole resume is composition(store, id) in @keepcv/core, not a route.
  • A version records what was sent, and the store captures it. POST /v1/resume-versions takes the resume and the trigger, never a manifest: the store freezes what the resume says, assigns the sequence, and answers 200 with the current version rather than appending a duplicate when nothing has moved. A version is immutable, so it has no PATCH; a snapshot is the label you put on one, and /v1/resume-snapshots is an ordinary collection. /v1/points/{id}/usage and /v1/records/{id}/usage answer which versions printed a row, which is what "if I archive this, what does it affect?" reads.
  • One request boots a client. GET /v1/store answers the whole store, archived rows included, so filters and counts are selectors over cached data rather than requests. It carries the wording each phrasing says now and not the ones it used to; GET /v1/export carries every wording ever written.
  • Export is never gated. Not by an account, not by a licence, not by any entitlement state.
  • The OpenAPI document comes from the same Zod schemas that validate the requests, so the description and the validator cannot disagree. It is served at /v1/openapi.json, without a token, because tooling fetches it before it has one.

Licence

MIT

createApi accepts an optional templateTypes catalog of trusted validators from @keepcv/templates. Ordinary template writes validate the installed type and payload; native import preserves bounded unavailable definitions without running them. Built-in validators cover Default and HTML.

Assistant deployment controls

createApi accepts optional agent, agentLimits and agentScope settings. agentScope(request, ownerId) asynchronously derives a trusted authorization scope for turns, grants and proposal decisions; never derive it from untrusted request body claims. Runtime authorize revalidates current authority during work. See the runtime package's public authorization and cancellation contract.

agentLimits lowers file bytes, text-file bytes, selected file bytes per turn, attachment count and message character limits. /v1/agent/capabilities reports the effective minimum of API and runtime limits, generation available and historyAvailable. File metadata includes decoded sizeBytes; original download bytes remain unchanged. Existing custom runtimes may omit the new limit fields.

Omitting agent does not disable conversation creation, attachment storage, history or native export/import routes. Those remain authenticated owner-scoped storage operations. Enforce any additional storage authorization at the consumer's request/unit-of-work boundary. Editor configuration only changes presentation. Generation refusal or provider failure leaves ordinary editing and export usable. Authentication, provider credentials, deployment rate limiting and storage accounting belong to the embedding application.

Stop is a local cancellation request. The built-in runtime returns a boolean confirming whether it held a controller; legacy void cancellation remains accepted but cannot confirm one. If no controller is confirmed and the owned conversation has an unexpired bounded running lease, POST cancel returns 409 with guidance to wait for expiry or stop the executing process. It still invalidates local task access; it never clears the stored lease. A 200 saved response does not promise completion or immediate lease release. Expired leases permit a fresh turn without restoring grants or proposal authority. Committed edits and receipts remain intact.