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

@classic-homes/api

v1.0.1

Published

Typed client for the CHAPI API, generated from the committed OpenAPI contract.

Readme

@classic-homes/api

Typed client for the CHAPI API. Every path, query param, request body and response is typed from CHAPI's OpenAPI contract (openapi.json at the repo root), so consuming repos get autocomplete and compile-time checks against the real API — including the v2 (D1-backed) endpoints. v2 response bodies are typed per resource (e.g. LotV2, CommunityV2) from the field registry.

Install

npm install @classic-homes/api

This is a scoped, restricted package published under the @classic-homes org, so a consuming repo must authenticate npm to that scope first. Add an .npmrc at the repo root:

# .npmrc (consuming repo)
@classic-homes:registry=https://registry.npmjs.org/
//registry.npmjs.org/:_authToken=${NPM_TOKEN}

Then export a token with read access to the org before installing / in CI:

export NPM_TOKEN=xxxxxxxx   # org read token; do NOT commit it
npm install @classic-homes/api

Usage

import { createChapiClient } from '@classic-homes/api';

const chapi = createChapiClient({
  baseUrl: 'https://api.example.com',
  token: async () => getAccessToken(), // string | () => string | Promise<string>
});

// List v2 lots — params, query and response are fully typed.
const { data, error } = await chapi.GET('/v2/lots', {
  params: { query: { page: 1, limit: 25, sort: '-lotNumber' } },
});
if (error) throw new Error('request failed');
for (const lot of data.data) {
  // lot is typed from the OpenAPI schema
}

// PATCH v2 enrichment
await chapi.PATCH('/v2/filings/{id}', {
  params: { path: { id: 390 } },
  body: { webStatus: 'Production' },
});

The client is a thin wrapper over openapi-fetch; GET/POST/PATCH/… methods and the params/body shapes come from it.

Authentication

Every request is sent as Authorization: Bearer <token>. The API accepts either:

  • A JWT — obtained from CHAPI's auth flow. Use a token function to refresh short-lived JWTs per request.
  • An API key — pass the key string as token. The API checks the bearer value as an API key first, then falls back to JWT verification.
// static API key
const chapi = createChapiClient({ baseUrl, token: process.env.CHAPI_API_KEY });

// or a refreshing JWT
const chapi = createChapiClient({ baseUrl, token: () => auth.getAccessToken() });

Auth helpers

The package also ships typed auth calls, error predicates, and an auto-refreshing session that handles CHAPI's short-lived access tokens and refresh-token rotation (every refresh returns a NEW refresh token that replaces the old one).

import {
  createChapiClient,
  createAuthSession,
  login,
  isUnauthorized,
  isForbidden,
  isRateLimited,
} from '@cos/chapi-client';

// 1. Log in (typed wrapper over POST /v1/auth/login)
const bootstrap = createChapiClient({ baseUrl });
const { data, error } = await login(bootstrap, { email, password });
if (error) throw new Error('login failed');

// 2. Create a session that keeps the access token fresh and follows rotation.
//    Persist BOTH tokens on change — the previous refresh token is now invalid.
const session = createAuthSession({
  baseUrl,
  tokens: data, // { accessToken, refreshToken, sessionToken }
  onTokensChanged: (t) => saveToStorage(t),
});

// 3. Use the session's token provider — it refreshes transparently before expiry.
const chapi = createChapiClient({ baseUrl, token: session.token });

// 4. Branch on typed errors instead of string-matching codes
const res = await chapi.GET('/v2/homes', { params: { query: { page: 1 } } });
if (isUnauthorized(res.error)) redirectToLogin();
else if (isForbidden(res.error)) showNoAccess();
else if (isRateLimited(res.error)) backOff();

logout(client) and refresh(client, refreshToken) are also exported. See docs/TOKEN_LIFECYCLE.md in the API repo for the full lifecycle.

Errors, pagination & permissions

  • Errors — openapi-fetch returns { data, error } (it does not throw on non-2xx). error is the typed error body ({ error: { code, message, ... } }). Always branch on error before using data.
  • Pagination — list responses carry meta.pagination (page, limit, total, totalPages). limit max is 500. Page with params.query.page / limit.
  • Sources & staleness — meta.sources names the backing edge DB; meta.sync (lastCompleted, isStale) and the X-Data-Stale header report data freshness.
  • Permissions are exact-match — v2 hides fields your token isn't explicitly granted. A * wildcard does NOT unlock permission-scoped fields (e.g. financial fields need <resource>:read:financial). Missing fields usually mean a missing permission, not a bug.
  • PATCH write-through — meta.writeThrough is 'ok' when the edge DB was updated in-request, or 'deferred' when the write is pending the next sync (the underlying record was still updated; the read model just lags briefly).

Incremental sync (updated-since)

Each resource exposes GET /v2/<resource>/updated-since?since=<ISO> returning rows with lastUpdated >= since, ordered by lastUpdated then primary key. To pull a stable incremental feed, page until empty, then advance since to the max lastUpdated seen and dedupe by id (rows sharing a boundary timestamp can repeat across polls):

async function pullSince(chapi, since: string) {
  const seen = new Set<string>();
  let page = 1;
  let maxUpdated = since;
  for (;;) {
    const { data, error } = await chapi.GET('/v2/lots/updated-since', {
      params: { query: { since, page, limit: 500 } },
    });
    if (error) throw new Error('updated-since failed');
    for (const row of data.data) {
      if (seen.has(String(row.id))) continue;
      seen.add(String(row.id));
      if (row.lastUpdated && row.lastUpdated > maxUpdated) maxUpdated = row.lastUpdated;
      // ...upsert row into your store...
    }
    if (data.data.length < 500) break;
    page += 1;
  }
  return maxUpdated; // pass as `since` on the next poll
}

How the types stay accurate

  • openapi.json (repo root) is generated from the endpoint registry via npm run openapi:generate and committed.
  • CI runs npm run openapi:check to fail the build if the committed spec drifts from the code.
  • This package's npm run generate regenerates src/types.ts from that spec (src/types.ts is git-ignored — always built, never hand-edited).

Scripts

| Script | Purpose | | --- | --- | | npm run generate | Regenerate src/types.ts from ../../openapi.json | | npm run build | Generate types, then compile to dist/ | | npm run typecheck | Type-check without emitting |

Publishing (maintainers)

The package is published by the Publish SDK workflow on an sdk-v* tag. To cut a release:

  1. From the repo root, regenerate and verify the contract: npm run openapi:generate && npm run openapi:check.
  2. Bump version in packages/chapi-client/package.json and add a CHANGELOG.md entry.
  3. Commit, then tag: git tag sdk-v<version> && git push origin sdk-v<version>.

The workflow rebuilds src/types.ts from the committed openapi.json and runs npm publish --provenance, so the published types always match the tagged contract. Deploy tags (v*) are separate from SDK tags (sdk-v*).

Example consumer

A minimal runnable example lives at examples/sdk-consumer/ in the CHAPI repo (list lots + PATCH a filing against a local wrangler dev).