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

@bettercms-ai/component-output

v0.2.1

Published

Verify BetterCMS component preview sessions and run the preview bridge in your app.

Readme

@bettercms-ai/component-output

Render your own components inside the BetterCMS dashboard, with live content, without giving BetterCMS your code or taking our word for anything.

This package is the provider half of bcms-component-preview/2. You write one route; everything below the route is here.

Why this exists

The contract asks you to verify a signed session before rendering. In v1 that session was signed with an HMAC keyed by BetterCMS's own auth secret — verifying it would have meant holding the key that mints BetterCMS logins. Nobody could, so nobody did, and no component ever rendered.

v2 signs with Ed25519 and publishes the public key. Verification now needs nothing secret. But it is still a list of things to get exactly right — four token segments, a JWKS fetch matched on kid, a signature over specific bytes, then five claim checks — and both ways of getting it wrong are invisible from outside: refuse every session, or accept a forged one. So the platform that defines the format ships the check.

Zero dependencies. WebCrypto only. Node 18+, Bun, Deno, edge runtimes, browsers.

Your route

Two halves. The server verifies; the client renders what arrives.

// ── server ──────────────────────────────────────────────────────────────────
import { verifyComponentSession } from "@bettercms-ai/component-output";

const result = await verifyComponentSession({
  token: url.searchParams.get("bcmsSession"),
  jwksUrl: `${process.env.BCMS_API_ORIGIN}/.well-known/bcms-component-output.json`,
  componentId,                       // from YOUR route, never from the token
  expectedOrigin: url.origin,
});

if (!result.ok) {
  // Log the reason; do not return it. "Which check failed" is a probing oracle.
  console.warn(`[bcms] refused: ${result.reason} componentId=${componentId}`);
  // …but DO tell the dashboard you refused, or it reports "your route did not answer". Serve a 404
  // page that calls reportComponentRefusal (below) — it sends only a two-value cause, never the reason.
  return notFoundPageReporting(componentRefusalCause(result.reason));
}

// ── client ──────────────────────────────────────────────────────────────────
import { createComponentPreviewBridge } from "@bettercms-ai/component-output/bridge";

const bridge = createComponentPreviewBridge({
  claims: result.claims,
  dashboardOrigin: process.env.BCMS_DASHBOARD_ORIGIN,   // config, never document.referrer
  onProps: setProps,
  onStatus: setStatus,
});
// bridge.dispose() on unmount

return props ? <YourComponent {...props} /> : null;

Render nothing until props arrive. Rendering with defaults first puts a plausible-looking approximation on screen, which is the one thing this whole protocol exists to prevent.

Refusing without going silent

A plain 404 makes the dashboard wait five seconds and blame your route. On the page you serve for a refused session:

import { reportComponentRefusal } from "@bettercms-ai/component-output/bridge";

reportComponentRefusal({ dashboardOrigin: process.env.BCMS_DASHBOARD_ORIGIN, cause });

cause is keys-unreachable when you could not fetch the JWKS — almost always a wrong BCMS_API_ORIGIN — and session-refused for everything else. Only those two ever leave your server: the page body is public, and the specific reason there would help someone forge tokens. See example/refusal.ts.

The route path

BetterCMS builds the iframe URL from the route you declared for the component, under /__bettercms/component-preview/. Serve exactly that path.

If your router treats a leading underscore as a pathless layout — TanStack does, and strips one — the segment will silently vanish from your URL and the frame will 404 on a path that looks correct in your source. Escape it however your router documents ([_][_]bettercms… for TanStack).

Framing: the failure you will hit first

Your route is rendered inside an iframe on the BetterCMS dashboard. Most server frameworks forbid that by default — helmet, Rails, Django and Laravel all send X-Frame-Options: SAMEORIGIN — and the browser then refuses to render your page. It does not look like a framing error: the frame shows the browser's own "refused to connect" page, the handshake times out, and the dashboard reports that your route did not answer.

On the preview route, and only there, send:

Content-Security-Policy: frame-ancestors https://<your-bettercms-dashboard>

frame-ancestors supersedes X-Frame-Options in every current browser. Remove X-Frame-Options from this one route if your framework adds it globally, and do not relax it anywhere else.

Which component to render: claims.familyKey

Every session names the component family it is for in result.claims.familyKey. Map that to your own component:

const components = { "<familyKey>": Navigation };
const Component = components[result.claims.familyKey];
if (!Component) return notFound();   // a family you never agreed to draw

The value is BetterCMS's identifier for the family, and it is per project — a staging project and its production copy have different keys. The dashboard shows the exact value beside the route when you declare it, so copy it from there rather than guessing.

The two claims

result.claims.claim is "preview" or "validated", and it is inside the signature.

  • validated — an evidence chain pinned to a commit was verified. The claims carry the full tuple: commitSha, evidenceId, evidenceDigest, familyManifestHash and the rest.
  • preview — a route was declared and the props are live CMS values. Nothing has checked the code at that route. The eight evidence fields are absent, not empty, and a preview carrying any of them is rejected by this package rather than interpreted. A blank commitSha is not a commit.

Do not present a preview as verified. Do surface it honestly and without alarm: it is the normal state of a component nobody has validated yet, which on an imported project is every component.

Configuration

| Variable | What | |---|---| | BCMS_API_ORIGIN | Where the JWKS is published. Production is https://api.bettercms.ai. | | BCMS_DASHBOARD_ORIGIN | The origin allowed to embed you and receive the handshake. |

Give these no defaults. A wrong default does not fail where you set it — it fails at the far end as JWKS_UNAVAILABLE, then a 404, then a blank frame in somebody else's UI.

Key rotation and revocation

The JWKS is cached: an outage at BetterCMS must not stop your page rendering, which is the entire argument for signature verification over an introspection endpoint. An unknown kid triggers one refetch, so rotation needs no redeploy, and cached keys expire after an hour so a revoked key does not outlive a long-running process.

JWKS_UNAVAILABLE means retry later. KEY_NOT_FOUND means this token will never be accepted. They are deliberately different.