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

@acms-subir/client

v0.1.4

Published

Typed, dependency-free client for the ACMS public mobile API.

Readme

@acms-subir/client

Dependency-free ESM TypeScript client for ACMS public API v1. Supports Expo/React Native, browsers and Node 18+ using global fetch, URL, Headers and AbortController. Supply fetch when your environment needs an adapter.

npm install @acms-subir/client
import { createClient, isAcmsError } from '@acms-subir/client';

const client = createClient({ baseUrl: 'https://example.com', locale: 'es' });
const site = await client.site();
const home = await client.home();
const page = await client.page('about/team');
for await (const post of client.paginate.posts({ collection: 'blog', limit: 50 })) {
  console.log(post.title);
}
const english = client.withLocale('en');

baseUrl accepts a site origin or a full /api/v1 URL, with an optional hosting prefix. All methods return unwrapped data; openapi() returns the raw document. Every method accepts locale and signal in its options object. Locale is sent only for routes that support translations. withLocale shares the cache and transport configuration with the original client.

| Method | Options / behavior | | --- | --- | | site(), theme(), home() | Identity, tokens, resolved homepage | | page(slug, opts) | Resolved page including sections; nested slugs supported | | content(opts) | kind, collection, q, page, limit, cursor | | contentById(id, opts) | Optional kind to disambiguate imported IDs | | posts(opts) | Content filtered to posts; collection, tag, q, pagination | | post(slug, opts) | Scans post summaries, then loads detail by ID; pass collection for ambiguous slugs | | pages(opts) | Page summaries, with pagination; use page() for sections | | search(text, opts) | Top matches in posts, pages and events, plus more per group; types, collection, limit (1–20, default 5). Blank text resolves empty without a request | | events(opts) | Occurrences with from, to, calendar, q, pagination | | event(slug, opts) | First matching event from occurrences in the requested window | | calendars() | Public calendars | | resources(opts), resource(slug, opts) | Public resources; slug lookup scans list pages | | availability(opts) | Required resource, link; optional from, days, minutes | | book(opts) | Resource booking or RSVP input, optional idempotencyKey | | openapi() | Raw OpenAPI document | | registerDevice({ token, platform, locale?, topics? }) | Register or refresh an Expo push token (call on every app start); { created, device } | | updateDevice(token, { topics?, locale? }) | Change a device's topics or language | | unregisterDevice(token) | Opt out (idempotent) | | pushTopics() | Topics to offer as notification toggles | | links() | The links page: profile, socials and visible blocks (link, email, phone, contact, heading, text, spacer, live, embed) | | form(id), submitForm(id, { answers, version?, idempotencyKey? }) | A published form's questions; send answers (checkbox groups as arrays). Each call makes one key; retries reuse it | | manageBooking(token), cancelBooking(token) | The booker's own booking from its manageToken, and cancelling it; can says what is allowed | | rescheduleAvailability(token, { from?, days?, minutes? }), rescheduleBooking(token, { start, minutes? }) | Open times for moving it (its own time counts as free), then the move | | bookingIcs(token) | The booking as an iCalendar file (string) | | requestGuestCode(email), guestSignIn({ email, code, deviceName? }), guestBookings(guestToken, { when? }), guestSignOut(guestToken) | Guest sign-in for "My bookings" |

The server has no post-slug, event-detail or resource-detail endpoint. Those convenience methods may make multiple requests. event() only finds events with occurrences in its window (default: next 30 UTC days); an unlisted calendar requires calendar. It does not fetch arbitrary historical events. If post slugs collide and no collection is supplied, the first listed match wins.

Search needs site.features.search (on unless the site sets acms({ search: { enabled: false } })). It uses the website's search index and ranking: titles count most, then excerpts, tags and body text. Every word must match, and the last one matches as a prefix (birth finds "birthday"). q on content()/posts() returns ranked summaries, posts before pages, and pages through at most 200 matches per kind. search() is for a combined results screen: each hit carries a plain-text snippet with highlights ({ start, end } ranges) to bold, and events add their next occurrence. Use more.posts to offer "See all", then posts({ q, collection }). Matching uses the main-language text, so a translated title is shown but not searched.

Pagination uses page/nextPage, not opaque cursors. cursor is a numeric alias for page; an explicit page wins. tag filters each fetched page locally, so a page can be empty with a non-null nextPage. Iterators continue through these pages. paginate.content/posts/pages/events/resources yield individual items (events yields occurrences). They do not deduplicate changing server results or provide snapshot consistency; deduplicate IDs in your app.

Bookings, errors and cancellation

// Persist a UUID v4 with this booking intent before submitting; reuse it until
// the outcome is known. Use your platform's secure UUID/storage facilities.
const idempotencyKey = crypto.randomUUID();
try {
  const result = await client.book({
    resource: 'hall', link: 'meeting', start: '2026-10-05T14:00:00Z', minutes: 60,
    name: 'Example Guest', email: '[email protected]', idempotencyKey,
  });
  // Store result.manageToken securely; it grants booking-management access.
} catch (error) {
  if (isAcmsError(error)) {
    console.log(error.status, error.code, error.message, error.retryAfter);
    // error.idempotencyKey preserves even an automatically generated key.
  }
}

const controller = new AbortController();
const pending = client.home({ signal: controller.signal });
controller.abort();
await pending.catch(() => {});

Requests that are safe to repeat (GET, PUT, DELETE, device registration, and POSTs with an Idempotency-Key: bookings and form responses) retry network failures, 5xx and 429 responses at most twice (three attempts total), with 250/500ms backoff and at least the server's Retry-After delay (seconds or HTTP date). A wait that would outlast timeoutMs throws the 429 straight away, with retryAfter. One-shot writes (guest code requests and sign-in, cancelling and moving a booking, guest sign-out) are never repeated automatically: on a network error, check the state before trying again. Booking retries always reuse the same serialized body and UUID. By default each book() call creates a key using crypto.randomUUID, falling back to a Math.random UUID v4 where unavailable. For recovery across calls/app restarts, supply and persist your own secure UUID. Never create a new intent merely because a response timed out.

timeoutMs defaults to 30,000 and covers the entire request, including retries, cache access and body reading. A longer rate-limit delay may exhaust that budget. Explicit aborts and timeouts are not automatically retried; an uncertain booking must be retried with its existing key. AcmsError exposes status, code, message, retryAfter (seconds), optional validation issues and idempotencyKey. Local failures use status 0 and network_error, timeout, aborted, or invalid_response; non-JSON HTTP errors use http_error. Convenience lookups apply the timeout separately to each underlying request.

Push notifications

Available when the site enables push (acms({ mobile: { push: true } })); otherwise these return a 404 AcmsError. Tokens come from expo-notifications (getExpoPushTokenAsync). The site stores the token, platform, language and topics only.

const { topics } = await client.pushTopics();          // [{ id: 'news', label: 'News', kind: 'general' }, …]
await client.registerDevice({ token, platform: 'ios', locale: 'es', topics: ['news', 'events'] });
await client.updateDevice(token, { topics: ['events'] });
await client.unregisterDevice(token);

Cache and headers

ETag revalidation defaults to an in-memory URL-keyed cache. Each read contacts the server with If-None-Match; a 304 reuses the saved data. This is not an offline fallback. Treat returned cached objects as immutable. Use cache: false to disable it. Availability, booking results and errors are never cached. Responses without an ETag or with no-store invalidate an older entry.

import type { ClientCache } from '@acms-subir/client';
const cache: ClientCache = {
  async get(url) { /* retrieve { etag, body } from app storage */ return null; },
  async set(url, entry) { /* persist entry; null means delete */ },
};
const persistent = createClient({ baseUrl: 'https://example.com/api/v1', cache });

Adapters may be synchronous or asynchronous. Keep persistent caches scoped to one site's public API and account for storage limits. Cache adapter failures propagate to the caller. Custom headers are supported; do not embed privileged credentials. Requests omit browser credentials. Conditional headers are managed by the client; a per-call booking key overrides a configured header key.

Development

npm run build -w packages/client
npm run test -w packages/client
npm run check:types -w packages/client
npm run generate:types -w packages/client  # after reviewing core schema changes

The contract test imports the actual core OpenAPI generator using core's virtual module test stubs, compares all GET/POST paths to the client method registry, and checks generated request/response types byte-for-byte. HEAD and OPTIONS are transport operations and have no separate data-returning methods. No core code, Zod, or Node built-ins ship in this package's runtime.

Managing bookings and "My bookings"

// a booking the app made (result.manageToken from book()), or one from guestBookings()
const view = await client.manageBooking(manageToken);
if (view.can.reschedule) {
  const open = await client.rescheduleAvailability(manageToken, { days: 7 });
  await client.rescheduleBooking(manageToken, { start: open.days[0].slots[0].start });
}
if (view.can.cancel) await client.cancelBooking(manageToken);

// guest sign-in (check site.features.guestSignIn first)
await client.requestGuestCode('[email protected]');
const { accessToken } = await client.guestSignIn({ email: '[email protected]', code: '481203' });
const mine = await client.guestBookings(accessToken, { when: 'upcoming' });

Booking and guest tokens travel in headers (Booking-Token, Authorization: Bearer), never in URLs, and their responses are never cached. Keep both in secure storage (expo-secure-store). can.closed means the booking type's change cutoff has passed: show "Contact us" instead of the buttons. A guest token ends after 90 days unused; 401 means sign in again.

Editor apps (staff sign-in)

For sites with acms({ mobile: { editor: { redirectUris } } }). Staff sign in in the system browser (OAuth 2.0 + PKCE); the app never sees a password.

import { createEditorAuth, createEditorSession, createEditorClient } from '@acms-subir/client';

const auth = createEditorAuth({ baseUrl: 'https://example.com', clientId: 'org.example.staff', redirectUri: 'org.example.staff://auth' });
const session = createEditorSession({ auth, storage /* get/set backed by expo-secure-store */, onSignedOut });
const api = createEditorClient({ baseUrl: 'https://example.com', session });

const { url, verifier, state } = await auth.start({ deviceName: 'Ana’s iPhone' });
// open `url` with expo-web-browser openAuthSessionAsync, then:
const { code } = auth.parseRedirect(returnedUrl, state);
await session.signIn(await auth.exchangeCode({ code, codeVerifier: verifier }));

const doc = await api.getDocument('post', id);
await api.saveDocument('post', id, { revision: doc.revision, fields: { title: 'New title' } });

In React Native pass crypto: { randomBytes, sha256 } from expo-crypto to createEditorAuth (Web Crypto is the default). Refreshes are single-flight; a 401 is retried once after a refresh, then onAuthError runs. Writes carry the revision you loaded (409 when someone saved first).

Schedule and setup: schedule({ from, to, resource? }) (up to 63 days of bookings, blocked time, events and feeds), createBooking() (on someone's behalf), rescheduleBooking(id, …), listResources() / getResource() / createResource() / updateResource() / deleteResource(), createBookingLink() / updateBookingLink() / deleteBookingLink(), blockTime() / unblockTime(), listCalendars() / createCalendar() / updateCalendar() / deleteCalendar(), listForms() / listFormResponses(), getLinksPage() / saveLinksPage(). Setup writes need me().permissions.schedule.manage; form responses and HTML link blocks need an administrator or owner (403 otherwise).