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

@conapps/cms-sdk

v0.3.2

Published

TypeScript SDK for the platform Content Delivery API. Works in browsers, Node, Nuxt and Vue.

Readme

@conapps/cms-sdk

Typed client for the platform's Content Delivery API — the read-only side of the CMS, the one your website or app talks to. Runs in Node 18+, Nuxt (SSR + client), Vue and the browser.

npm install @conapps/cms-sdk

Before you write any code: three values

Every call needs these, and every one of them comes from the platform UI. Getting them is the whole setup — there is no key file to download and no dashboard to generate a long-lived token in.

| What | Where it comes from | Looks like | |---|---|---| | Endpoint | the CMS delivery host (or the gateway, see below) | https://cms.conapps.net | | Workspace id | the address bar of your workspace in the backoffice | ws_4a7d… | | Client id + secret | Settings → API integrations → New integration, scope content-management:read | cms-prod / shown once |

Two endpoints work, and the bare gateway host is not one of them.

| endpoint | Result | |---|---| | https://cms.conapps.net | ✅ the delivery host — a CDN can sit in front of it, no gateway hop | | https://api.conapps.net/api/v1/workspaces/<ws>/services/content-management | ✅ through the gateway, same responses | | https://api.conapps.net | ❌ 404 on every call |

Prefer the delivery host: it is what the CDN caching is built around. The gateway route is there when your network only allows one platform host, and it needs the full service prefix — the SDK appends /delivery/v1/spaces/… to whatever you give it, so the workspace id legitimately appears twice.

The third row is the trap, and it is silent: the gateway answers 404 for a path it does not route, which reads exactly like content that does not exist.

There are no CMS "delivery tokens" to create. Earlier versions of this document sent people looking for Settings → Content → Delivery Tokens; that screen does not exist and never did. Machine access on this platform is always an API integration exchanged for a short-lived token — see below.


Quick start

import { createCmsClient } from '@conapps/cms-sdk';

const cms = createCmsClient({
  endpoint:    'https://cms.conapps.net',
  workspaceId: 'ws_4a7d',
  auth: {
    issuer:       'https://auth.conapps.net/realms/platform',
    clientId:     process.env.CMS_CLIENT_ID!,
    clientSecret: process.env.CMS_CLIENT_SECRET!,
  },
  defaultLocale: 'de-DE',
});

const posts = await cms.getEntries({ contentType: 'blogPost', take: 10 });
const post  = await cms.getEntry('abc123', { include: 2 });
const found = await cms.getEntryBy('blogPost', 'slug', 'hello-world');

That is the complete setup. The SDK fetches the token, caches it, refreshes it before it expires and retries once if the server rejects it anyway.

Authentication, and the mistake everybody makes once

An access token from this platform lives about five minutes. Handing the SDK a token string therefore works exactly as long as your first test run and then fails forever, with a 401 that looks like a wrong secret:

// ✗ Works for five minutes. Then 401, in production, at 3am.
createCmsClient({ …, token: process.env.CMS_TOKEN! })

Three supported shapes, in the order you should reach for them:

// ✓ 1. Let the SDK own the token. Server-side.
createCmsClient({ …, auth: { issuer, clientId, clientSecret } })

// ✓ 2. Your own provider, if the token comes from somewhere else
//      (a secrets broker, another service, a test double).
createCmsClient({ …, token: async () => myBroker.currentCmsToken() })

// ✓ 3. A fixed string — only for a script that finishes in minutes.
createCmsClient({ …, token: 'eyJhbGciOi…' })

clientSecret is a password for the workspace. Never ship it to a browser. The browser calls your backend, your backend calls this SDK. The Nuxt example below is built that way.

You can also build the provider yourself and share one token across several clients — it is exported from this package, so there is no second package to install:

import { createTokenProvider, createCmsClient } from '@conapps/cms-sdk';

const token = createTokenProvider({ issuer, clientId, clientSecret });
const master = createCmsClient({ endpoint, workspaceId, token, environment: 'master' });
const staging = createCmsClient({ endpoint, workspaceId, token, environment: 'staging' });

When a call is rejected

CmsAuthError carries a reason that says which of the usual four causes it is, read out of the token that was just sent:

CmsAuthError: Delivery API rejected the request (403): the token is issued for
workspace ws_other, but the request addresses ws_4a7d — an API integration
belongs to exactly one workspace

Failures from the token endpoint are a PlatformAuthError and are translated the same way — invalid_client becomes a sentence about a rotated secret rather than an OAuth error code.


API

createCmsClient(opts) / new CmsClient(opts)

| Option | Type | Description | |---|---|---| | endpoint | string | Delivery host, e.g. https://cms.conapps.net. Required. | | workspaceId | string | Workspace ("space") id. Required. | | auth | ClientCredentialsOptions | Client credentials; the SDK manages the token. Pass this or token. | | token | string \| () => string \| Promise<string> | A token, or a function returning one. | | environment | string | Default "master". | | defaultLocale | string | Used when a call omits locale. | | assetBaseUrl | string | If set, Asset.url becomes ${assetBaseUrl}/${storageKey}. | | fetch | typeof fetch | Custom fetch — used for content and for the token request. | | cacheTtlMs | number | In-memory GET cache TTL. Default 0 (off). |

Methods

client.getContentTypes(): Promise<Collection<ContentType>>;   // apiIdentifier is what `contentType` wants
client.getEntries<TFields>(query?): Promise<Collection<Entry<TFields>>>;
client.getEntry<TFields>(id, query?): Promise<Entry<TFields> | null>;
client.getEntryBy<TFields>(contentType, field, value, query?): Promise<Entry<TFields> | null>;
client.getAsset(id): Promise<Asset | null>;
client.withPreview(context): CmsClient;
client.clearCache(): void;

include: number resolves linked entries and assets up to that depth — the SDK fetches each id and inlines the resolved object back into the field.

getEntryBy is a scan, and says so

The Delivery API has no server-side filter on arbitrary fields. getEntryBy therefore pages through the content type and compares locally:

await cms.getEntryBy('blogPost', 'slug', 'hello-world')            // walks all pages
await cms.getEntryBy('blogPost', 'slug', 'x', { maxScan: 1000 })   // throws past 1000

Cheap for a few hundred entries, a real cost for thousands. On a hot path, keep your own slug → id map and use getEntry.

Until 0.3.0 this helper read a single page of 100 and returned null for everything after it. A site launched fine and began 404ing its older posts months later, with nothing in any log. It now walks to the end, and throws at maxScan rather than answering null to a question it did not finish asking.

Typed fields

interface BlogPost {
  title: string;
  slug: string;
  body: string;
  coverImage?: { id: string; url?: string; fileName: string };
}

const post = await cms.getEntry<BlogPost>('abc123', { include: 1 });
post?.fields.title; // string

Errors

import {
  CmsError, CmsAuthError, CmsNotFoundError, CmsScanLimitError, PlatformAuthError,
} from '@conapps/cms-sdk';

Everything this SDK throws extends CmsError. One catch on the base class is complete.

| Class | When | |---|---| | CmsError | base class, and any other non-2xx — .status, .url, .body | | CmsConnectionError | the request never arrived (DNS, refused, timeout, TLS) — .code, .endpoint | | CmsTokenError | the token endpoint refused the credentials — .tokenUrl | | CmsAuthError | 401/403 — .reason says which cause it is | | CmsNotFoundError | 404 | | CmsScanLimitError | getEntryBy reached maxScan — .scanned, .maxScan |

status is the HTTP status — 0 when there was no response: the request never left (scan limit) or never arrived (connection, token endpoint).

PlatformAuthError stays exported for callers who use createTokenProvider directly; when the client fetches the token, it arrives wrapped as CmsTokenError.


Preview

withPreview(context) returns a second client that reads the same space with an editor's context — the draft revision, a chosen instant, a chosen audience. Preview tickets are verified with @conapps/cms-preview; this package only carries the resulting context into the query.

const ticket = await previewHandler.read(cookieHeader)   // @conapps/cms-preview
const client = ticket ? cms.withPreview(ticket.context) : cms

const posts = await client.getEntries({ contentType: 'blogPost' })

A separate instance, not a flag: the public client keeps its cache and its published view, so a preview can never write a draft into an entry the public path reads back. The preview client cannot cache — cacheTtlMs is forced to 0, because the cache key is the URL and two contexts differing only in a forgotten parameter would share an entry.

| Context field | Effect | |---|---| | state: 'draft' (default) | ?preview=true — drafts, and the current draft of a changed entry | | state: 'published' | no preview parameter; what the public sees | | at | ISO instant the visibility window is evaluated against | | audience | audiences of the simulated visitor | | sourceMap | ask for the field-origin map that powers click-to-edit |

?preview=true needs a token carrying content-management:preview. Put that scope on a separate API integration, never on the public one: with it on the public client one bug makes drafts public, and without it the Delivery API silently ignores the parameter.

audience: [] and an absent audience are different requests — the first simulates a visitor nothing is known about (restricted content is withheld, as in public), the second does not narrow by audience at all.


Vue / Nuxt

// main.ts (Vue)
import { createCmsClient } from '@conapps/cms-sdk';
import { cmsPlugin } from '@conapps/cms-sdk/vue';

app.use(cmsPlugin, createCmsClient({ endpoint, workspaceId, token }));
<script setup lang="ts">
import { useEntries } from '@conapps/cms-sdk/vue';

const { data, pending, error } = useEntries({ contentType: 'blogPost', take: 5 });
</script>

Nuxt: the client stays on the server

// server/utils/cms.ts — one client, one token, all server routes
import { createCmsClient, type CmsClient } from '@conapps/cms-sdk';

let cms: CmsClient | undefined;

export function getCms(): CmsClient {
  cms ??= createCmsClient({
    endpoint:    process.env.CMS_ENDPOINT!,       // https://cms.conapps.net
    workspaceId: process.env.CMS_WORKSPACE_ID!,
    auth: {
      issuer:       process.env.KEYCLOAK_ISSUER!, // https://auth.conapps.net/realms/platform
      clientId:     process.env.CMS_CLIENT_ID!,
      clientSecret: process.env.CMS_CLIENT_SECRET!,
    },
    environment: process.env.CMS_ENVIRONMENT ?? 'master',
    cacheTtlMs: 15_000,
    fetch: $fetch as unknown as typeof fetch,
  });
  return cms;
}
// server/api/posts.get.ts — the browser never sees the secret
export default defineEventHandler(async () => {
  const { items } = await getCms().getEntries({ contentType: 'blogPost' });
  return items;
});

Put CMS_CLIENT_SECRET in the server runtime config only. Anything under runtimeConfig.public is shipped to the browser.

A complete working example lives under ../examples/nuxt-demo.


Troubleshooting

| Symptom | Cause | |---|---| | Every call 404s | endpoint is the bare gateway host. Use the cms. host, or the gateway with the full /api/v1/workspaces/…/services/content-management prefix. | | CmsConnectionError … does not resolve | The host name is wrong, or its DNS record is missing. The error names the host. | | Works locally, 401 in production after a few minutes | A token string instead of auth / a provider function. | | 403 mentioning another workspace | The integration belongs to a different workspace than workspaceId. | | invalid_client from the token endpoint | Secret rotated, integration revoked, or the env var is empty. | | preview=true returns published content | The token lacks content-management:preview; the parameter is ignored, not refused. | | getEntryBy returns null for old entries | Fixed in 0.3.0 — upgrade. | | Linked entries come back as { sys: { type: 'Link' } } | Pass include: 1 (or more). | | getEntries({ contentType }) returns entries of other types | A service older than 2026-09-18 silently ignored an unknown content_type and answered with everything. Check the identifier against getContentTypes()[].apiIdentifier. | | You cannot find the value for contentType | It is apiIdentifier on a content type — not its sys.id, which is the row id. On an entry it appears as contentType.sys.id. |


Build & test

npm install
npm run build              # tsup → dist/{index,vue}.{js,cjs,d.ts}
npm test                   # vitest — 92 unit tests
npx vitest run --coverage  # ≥ 98% line coverage on src/

The suite covers the HTTP client (URL composition, locale defaulting, paging, encoding, all error mappings, in-memory cache TTL + invalidation, asset URL decoration, single-level + array + recursive link resolution, broken-link tolerance), the token provider (caching, skew, single-flight, invalidation, every OAuth rejection, blank-credential guards), the 401 refresh-and-retry path, and every Vue composable.