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

@cef-ai/widget-runtime

v2.4.0

Published

Browser runtime for CEF widgets. It boots from `window.WidgetSandbox.manifest`, resolves the user's identity, exposes `window.WidgetRuntime` (`query` / `publish` / `subscribe` / `connect` / `connectAgent` / `agentStatus` / `identity`), and renders config-

Readme

@cef-ai/widget-runtime

Browser runtime for CEF widgets. It boots from window.WidgetSandbox.manifest, resolves the user's identity, exposes window.WidgetRuntime (query / publish / subscribe / connect / connectAgent / agentStatus / identity), and renders config-driven widget kinds (record / list / dashboard / submit / conversation / composite) plus custom widgets. It also carries the audio capture/upload path. The runtime reads a manifest; the @cef-ai/cli build tooling builds one.

The esbuild IIFE build emits dist/widget-runtime.js (the browser bundle) and dist/widget.html (the shell); tsc emits the library entry (dist/index.js) for consumers that import the helpers directly.

Following a stream

subscribe polls one stream of the widget's scope (the scope publish writes to) and hands each poll's new events to the callback as one ordered, de-duplicated batch:

const { requestId } = …;
await WidgetRuntime.publish('workflow.start', input, requestId, { target });
const stop = WidgetRuntime.subscribe(
  requestId,
  { types: ['agent.progress'], intervalMs: 2500 },
  (events) => events.forEach(render),
);
// later: stop();

It polls every 2500ms by default, backs off (doubling, up to maxBackoffMs, default 30s) while polls fail and reports each failure to onError, and stops when the returned function is called. Reading a stream needs ddc-read and a read grant on the scope, which a widget's delegation carries.

Identity (multi-source)

The runtime resolves a signing identity from the best available source:

  1. Embedded (host): when the widget is framed, it asks the embedding page for an identity over postMessage and signs through it. The host does not have to be the Cere Vault — the contract is host-agnostic and exported (see Host contract below), so a browser-extension panel, a partner app, or any page that has already authenticated the user can supply it.
  2. Standalone (interactive wallet) — currently NON-FUNCTIONAL. When the widget is not framed — opened top-level via a direct link — it was meant to connect an interactive Cere embed-wallet built from manifest.wallet.{appId,env} and sign locally. That path terminates at beta.openlogin.com, which no longer resolves, so it fails rather than opening a wallet. Tracked in CEF-AI/sdk#144 (dead endpoint) and #146 (replacement); standalone-wallet.ts is unchanged and still shipped. Until those land, treat path 1 as the only working one: frame the widget and mount a host.

Path 1 feeds the @cef-ai/vault-sdk client, so cubby reads, connect, publish, and audio uploads work the same way whichever host supplies the identity.

A framed widget never falls back to (2) (behaviour change in 1.1). If the host does not answer the handshake within 8 s, or answers malformed, the runtime surfaces WidgetSignedOutError and the identity stays anon. It used to silently open the standalone wallet instead, which looked like a hang and produced an identity the host knew nothing about. The Cere Vault host already implements the handshake, so it is unaffected; a page that frames a CEF widget without implementing the host contract now gets WidgetSignedOutError where it previously got the silent fallback. If you frame a widget in your own page, mount createWidgetHost on it.

Host contract

Four postMessage messages, widget → host and back.

Gate the inbound side first. The host holds the user's signing key, so createWidgetHost makes you name who may talk to it. allowedOrigins is required and non-empty, and it throws at mount without it — an ungated listener is a signing oracle for every other frame, opener, or popup on the page.

A frame handle does not pin a document. widget is an optional, and recommended, extra narrowing — never a replacement for allowedOrigins. A WindowProxy keeps its identity across navigation, cross-origin included: after the framed page follows an open redirect, or navigates itself, or has its src set from a query param, event.source === iframe.contentWindow still holds and only event.origin has moved. A host gated on the frame alone would sign arbitrary bytes for whatever later occupies that slot. When both options are given, both must pass.

Each allowedOrigins entry must be a canonical origin spelled exactly as event.origin reports it — scheme + host + non-default port, lower-case, no trailing slash, no path ('https://widget.example', 'http://localhost:5173'). A default port is not optional spelling: an origin omits :443 on https and :80 on http, so 'https://widget.example:443' throws too. Anything else throws at mount rather than failing silently at runtime. In particular '*' is rejected: postMessage takes * as a targetOrigin wildcard, but event.origin is never the string "*", so an allowlist of ['*'] would mount clean and then answer nobody.

Sandboxed widgets. A sandboxed frame's event.origin is the literal string "null", so pass allowedOrigins: ['null'] to frame one. That is an opaque origin — every sandboxed frame reports it, so pair it with widget if any other sandboxed frame can reach the page. Separately, a "null" origin relaxes only the reply targetOrigin ('*', since "null" is not a valid targetOrigin); it never relaxes the inbound decision.

Bytes on the wire are number[], not Uint8Array. Structured clone handles typed arrays everywhere; the reason is the rest of the chain. A real host usually forwards the payload over a JSON-serialising transport — chrome.runtime.sendMessage to an extension service worker, a native-messaging port — which turns a Uint8Array into {"0":1,"1":2,…} and hands the signer something the widget never sent.

| Message | Direction | Fields | | --- | --- | --- | | cef-widget:identity-request | widget → host | requestId: string, v: number | | cef-widget:identity-response | host → widget | requestId: string, v?: number, pubkey?: string, sigType?: 'ed25519' \| 'sr25519' (default ed25519), token?: string (a bearer delegation the widget uses instead of asking you to sign), context?: { runId?, recordId?, vaultId? }, error?: string, errorCode?: 'unsupported-version' | | cef-widget:sign-request | widget → host | requestId: string, v: number, bytes: number[] | | cef-widget:sign-response | host → widget | requestId: string, v?: number, signature: number[] \| null (null = user declined), error?: string, errorCode?: 'unsupported-version' |

Rules:

  • Versioning (v). Current version is 1 (PROTOCOL_VERSION). A message that carries no v is treated as v1 — that is what keeps already-deployed widgets and the existing Vault host working untouched. An unrecognised v is rejected with a named WidgetProtocolVersionError; it is never silently ignored. A peer that rejects the other side's v answers with errorCode: 'unsupported-version' — its own reply is still a v1 message, so v itself cannot carry the rejection, and without the code the receiver would just see an opaque error string and keep retrying.
  • Which vault (context.vaultId). By default a widget opens the caller's own vault (vault.ensure()), and a token does not change that — a delegation says who, not which vault. A workflow, meanwhile, runs in the vault of the org that owns its agent service, so a host showing somebody else's run must name that vault or the widget reads a different database and reports it as empty. Optional: a host that sends none gets exactly today's behaviour. A host that does send one and the widget cannot open it gets WidgetVaultUnreachableError — the runtime refuses rather than falling back, because a fallback is indistinguishable from a run that produced nothing.
  • Sign verbatim. The host must sign the exact bytes it receives with ed25519_signRaw. Do not use the high-level signMessage / wallet_signMessage, which wrap the payload in Substrate's <Bytes>…</Bytes> envelope — a signature over the wrapped form verifies nowhere (not in GAR, not at the DDC gateway).
  • Validate what you sign. Every element of bytes must be an integer 0–255, and the payload is capped at 64 KiB (MAX_SIGN_BYTES). Uint8Array.from coerces junk silently (300 → 44, 'zz' → 0, and a sparse array's holes → 0), which would produce a perfectly valid signature over the wrong payload. createWidgetHost does this check for you and answers WidgetMalformedMessageError; a hand-rolled host must do it itself — isByteArray and MAX_SIGN_BYTES are exported for exactly that, so the cap moves in one place. The widget applies the same check to signature, and refuses to post a payload over the cap in the first place.
  • Timing. The widget retries identity-request every 400 ms for up to 8 s before giving up (the host page and the iframe race to attach their listeners, so the first request is often sent before the host is ready — just answer whichever request you see). A reply slower than the retry interval still counts: every request the handshake issued stays open, which is what a cold extension service worker needs. A sign-request waits up to 60 s, the user's time to approve in the host's UI. A handshake that times out is not cached — a later action (the user connects after the widget loaded) starts a fresh one.
  • Replies are source-gated. The widget only accepts messages whose event.source === window.parent. Always reply to event.source, echoing the request's requestId.

Mounting a host (non-Vault example)

createWidgetHost is the host side of the table above — one listener, no dependencies. Mount it once on the page that frames the widget:

import { createWidgetHost } from '@cef-ai/widget-runtime';

// An extension panel that has ALREADY authenticated the user with SCP Wallet.
const dispose = createWidgetHost({
  // REQUIRED. The gate between the page and the user's key: which DOCUMENT may
  // ask. Canonical origins only — no trailing slash, no path, and no '*'.
  // Use ['null'] for a sandboxed widget.
  allowedOrigins: ['https://widget.example'],

  // Optional but recommended: narrow to one frame as well. Both must pass.
  // On its own this would NOT be safe — the handle survives navigation.
  widget: document.querySelector<HTMLIFrameElement>('iframe#cef-widget')!,

  // Return null/undefined while the user is not signed in yet — the widget
  // retries for 8 s, and a later action retries again after that.
  getIdentity: () => (scp.isConnected() ? { pubkey: scp.publicKey, sigType: 'ed25519' } : null),

  // Verbatim raw-bytes signing. Return null if the user declines; throw to
  // report a failure. Never route this to signMessage/wallet_signMessage.
  //
  // NOTE: called more than once per widget action — a first `query()` signs
  // twice (delegation-token mint, then vault claim). If you put a user prompt
  // behind this, the reader sees two prompts before the first cubby read.
  // Cache or batch consent rather than prompting per call.
  sign: async (bytes) => scp.request({ method: 'ed25519_signRaw', params: [bytes] }),
});

// On unmount:
dispose();

The widget then resolves its identity through the host, exactly as it does inside the Cere Vault, and the standalone wallet is never opened.

Chrome extension hosts: an MV3 panel must also allow the widget's origin in its own CSP, or the frame never loads and the handshake has nothing to answer:

"content_security_policy": {
  "extension_pages": "script-src 'self'; object-src 'self'; frame-src https://widget.example"
}

Verified end-to-end against a real extension: the widget resolves the host's identity, signatures cross the frame as number[] and verify, and with no host mounted the framed widget fails WidgetSignedOutError without reaching the legacy wallet.

Build

pnpm --filter @cef-ai/widget-runtime build   # esbuild bundle + tsc + fix-extensions
pnpm --filter @cef-ai/widget-runtime test