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

@cloudhal/api

v0.6.2

Published

Cloudhal HTTP API (Hono): sites, pages, entries, snapshots, auth — runs on Node and Cloudflare Workers

Readme

@cloudhal/api

The Cloudhal HTTP API as a Hono app — one fetch handler that runs on Node (@cloudhal/api/node) and on Cloudflare Workers.

The core routes are below; the feature routes (taxonomy, menus, media, comments, schedules, analytics, members, approvals, …) are one module each in src/routes/.

| Route | What | |---|---| | GET /api/health | liveness | | GET /api/me | the calling actor (401 when anonymous) | | /api/auth/* | Better Auth (sign-up, sign-in, session, organization, admin) | | GET/PUT /api/sites[/:id] | sites (PUT = full document, CAS on rev) | | GET /api/sites/:id/pending | what a publish would carry, as counts (Studio's badge beside Publish) | | /api/sites/:id/pages[/:pageId] | pages (POST creates and appends to Site.pageIds atomically; DELETE needs ?rev=) | | /api/sites/:id/entries[/:entryId] | content entries (?collection=, locale, status, filter); fields is checked against the collection's typed fields (RFC-0041), and null removes a key there. RFC-0042 §4: with collection, where (a JSON list of up to 4 {field, op, value?/values?}; ops by type, value: "now" for date/datetime fields) and orderBy (JSON {field, direction: "asc"\|"desc"}) filter and sort by the collection's listFields, e.g. ?collection=events&where=[{"field":"startsAt","op":"gte","value":"now"}]&orderBy={"field":"startsAt","direction":"asc"} (URL-encoded). limit (1–100) pages the answer as {entries, total, nextCursor?}, continued with cursor=<nextCursor>; without limit the answer stays the plain array | | /api/sites/:id/collections[/:key] | the collection registry; PUT /:key {settings, rev} (?dryRun=1) declares a collection or changes its settings, typed fields included; DELETE /:key?rev= | | /api/sites/:id/collections/:key/fields/operations | RFC-0041 §6 field operations (site:settings and content:write): POST {op, …, dryRun?} with op one of rename_field {from, to}, change_field_type {key, type, options?, multiple?, onInvalid?: "clear"}, set_field_localized {key, localized}, remove_field {key} → {operationId, done, report, rewritten, skipped} (a dry run writes nothing; done: false when the 2,000-entry / 20 s budget ran out, then POST ?continue=<operationId>); GET /:operationId → state and pending entries; POST /:operationId/revert (again while done: false). Only the draft graph changes | | /api/sites/:id/snapshots | create ({label, publish}), list, :id/publish, :id/rollback, :a/diff/:b | | /api/agent-keys | API keys for agents: POST {name, scopes} (key shown once), GET, DELETE /:id — signed-in humans only | | /mcp | the Cloudhal MCP server (ADR-0011); session cookie → human, x-api-key or Authorization: Bearer <key> → agent limited to the key's scopes (REST answers agent keys with a pointer to /mcp) |

Errors: 400 validation (contract errors with ajv details), 409 conflict (expectedRev/actualRev), 409 unique, 404 not_found, 401 unauthorized. Store invariants answer with their code (src/errors.ts, INVARIANT_STATUS).

Typed fields (RFC-0041) answer with every problem and its path in details.issues ([{ path: "fields/startsAt/at", code, message }]):

| Code | Status | When | |---|---|---| | invalid_fields | 422 | values that do not fit their definitions (or several kinds of problem at once) | | unknown_field | 422 | a new key without a definition, in a collection that has definitions | | field_not_localized | 422 | a translation's own copy of a non-localized value (set it on the original) | | required_field | 422 | an entry in review or published without a requiredToPublish value | | too_large | 409 | fields over 256 KB, or collection settings over 256 KB | | field_key_reserved, field_key_duplicate, field_option_duplicate, field_bounds | 400 | definitions that can never be saved as written | | too_many_fields | 422 | over 30 fields per collection, 200 per site or 50 options per select | | field_change_needs_operation | 409 | changing a field's type, localization or multiple, or dropping a key or option that entries still hold | | operation_in_progress | 409 | another field operation is still rewriting the collection (details.operationId) | | field_values_unconvertible | 422 | a type change would drop values the conversion table cannot convert, without onInvalid: "clear" (details.report is the dry run) | | field_operation_incomplete | 409 | reverting an operation that has not finished, or was already reverted |

Collections stage 3 (RFC-0042) answers the same way (details.issues, slash paths):

| Code | Status | When | |---|---|---| | invalid_query | 422 | an entry list's where/orderBy/limit/cursor, or a contentList section's (sections/2/where/0/value), names an unlisted field, an operator or value its type does not take, or breaks a bound | | unknown_field | 422 | listFields or expiry.field names no field of the collection | | invalid_expiry | 422 | expiry.field is not a date or datetime field | | structured_data_invalid | 422 | a structuredData preset that does not fit the type's table or the collection's fields (warnings are not refused) | | list_field_in_use | 409 | unlisting, dropping or retyping a list field (or dropping a select option) that contentList sections still use (details.pages); remove_field of a field named by listFields, expiry or structuredData; a type change that would break those sections. rename_field renames those references instead |

import { createApi } from "@cloudhal/api";
import { createAuth } from "@cloudhal/api/auth";
const auth = createAuth({ database: db, baseURL, secret });
const app = createApi({
  store,
  resolveActor: auth.resolveActor,
  resolvePrincipal: auth.resolvePrincipal,
  authHandler: auth.handler,
  agentKeys: auth.agentKeys,
  mcp: { stateKey: process.env.MCP_STATE_KEY, audit, skills },
});

Auth (ADR-0010): Better Auth 1.7 with email+password, organization and admin; sessions resolve to Actor { kind: "human", id: "user_…" }. Auth tables live in storage migration 0002_auth. The shared test suite (src/testing.ts) runs on better-sqlite3 and Miniflare D1, including the article workflow draft → review → published → snapshot → publish → rollback.