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

@nominalso/vibe-host

v0.12.0

Published

Host-side SDK for embedding Nominal Vibe Apps — receives bridge requests over a typed postMessage protocol and dispatches them to Nominal APIs (used by nom-ui).

Downloads

1,821

Readme

@nominalso/vibe-host

Host-side SDK for embedding Nominal Vibe Apps. Used by the Nominal app (nom-ui) to receive requests from an embedded Vibe App over a typed postMessage protocol and dispatch them to Nominal APIs. The host ships a complete, type-checked handler map covering every protocol operation — you supply a clientConfig pointing at the Nominal API, and the SDK wires up and dispatches all operations for you.

For AI agents: you do not pass a handlers map. Provide clientConfig (where the Nominal API lives) and the SDK builds every handler. The iframe-side counterpart is @nominalso/vibe-bridge.

Install

npm install @nominalso/vibe-host

Externalizes only @hey-api/client-fetch; TypeScript types are self-contained.

Quickstart

import { VibeAppHost } from '@nominalso/vibe-host'

const host = new VibeAppHost({
  // Iframe origin(s) allowed to talk to this host. Exact origins, or glob
  // patterns (e.g. 'https://*.vercel.app') — add patterns only in non-prod.
  trustedOrigins: ['https://my-vibe-app.lovable.app'],
  // This Vibe App's base path in nom-ui (used to sync the browser URL).
  appBasePath: `/${tenant}/${subsidiary}/apps/${slug}`,
  // Context pushed to the iframe on connect.
  getContext: () => ({ tenant, subsidiaryId, subsidiaries, user, lastClosedPeriodSlug }),
  // Points the built-in handler map at the Nominal API (e.g. nom-ui's proxy route).
  clientConfig: { baseUrl: '/api/proxy', stripApiPrefix: true },
  // The iframe this host owns. Optional, but wire it whenever you have the
  // element — see "Hosts that outlive, or share a page with, an iframe".
  getIframe: () => iframeRef.current,
})

// Start listening. The iframe identifies itself via its CONNECT handshake, and
// the host replies with its context — no iframe window needed up front.
const unmount = host.mount()

API

new VibeAppHost(options)

| Option | Type | Description | | ---------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | trustedOrigins | string[] | Iframe origins allowed to talk to this host — exact origins or glob patterns (https://*.vercel.app, honoured unconditionally, so add only in non-prod). Messages from other origins are ignored. | | getContext | () => HostContext | Returns the current context to send to the iframe (hostVersion is injected automatically). | | appBasePath | string | Base URL path of this Vibe App in nom-ui, e.g. /acme/1/apps/fixed-assets. | | clientConfig | VibeApiClientOptions? | Points the built-in handler map at the Nominal API. Defaults to { baseUrl: '', stripApiPrefix: false }. | | getIframe | () => HTMLIFrameElement \| null? | The <iframe> this host owns, read live. Lets the host recognise its own frame without a handshake — see below. |

VibeApiClientOptions is { baseUrl: string; stripApiPrefix?: boolean }. Set stripApiPrefix: true when routing through a proxy whose convention expects paths without the /api prefix.

Testing preview deployments with trustedOrigins patterns

A pattern entry's * matches exactly one DNS label or port — anchored, scheme literal — so https://*.vercel.app matches https://pr-7.vercel.app but not https://a.b.vercel.app or https://pr-7.vercel.app.evil.com. Patterns are honoured unconditionally, so gate the list on your own deploy environment — keep production exact-only:

const trustedOrigins =
  process.env.NOM_ENV === 'production'
    ? ['https://app.nominal.so']
    : ['https://app.nominal.so', 'https://*.vercel.app', 'https://*.lovable.app']

Because you cannot postMessage to a pattern, proactive pushes target the exact allowlist entries plus the connected iframe's learned origin; a pattern-matched iframe is reached via the concrete origin learned from its CONNECT handshake.

Hosts that outlive, or share a page with, an iframe — getIframe

CONNECT is sent once per iframe document, and the bridge memoises the context it receives. That single handshake is all the host has to identify the app, which breaks in two ways getIframe closes:

  • The iframe outlives the host. React <Activity mode="hidden"> and Next.js's route bfcache run effect cleanups while keeping the DOM. A host built in an effect is therefore rebuilt around a still-running iframe that will never re-CONNECT, so every op it sends comes back NOT_CONNECTED — silently, since nothing failed to load. With getIframe wired, mount() adopts the live frame and the app resumes with its state intact.
  • Two hosts on one page. Every host listens on the same window, so one app's CONNECT reaches all of them, and a glob allowlist entry can't tell two preview apps apart. Without getIframe the last app to connect owns every host's connection and the others are rejected for good; with it, each host only ever talks to its own frame.

Reading the element at mount is safe because contentWindow is the frame's WindowProxy, whose identity survives navigation and reload of that frame — only the inner Window is swapped, so it still matches the event.source of everything that frame sends.

Adoption sends nothing. If a resumed app should also see the current context, call refreshContext() after mount().

mount(): () => void

Starts listening for bridge messages and sets up browser back/forward sync. Returns a cleanup function. Takes no iframe window — the iframe identifies itself via its CONNECT handshake (and re-identifies on every reload), and the host replies with its context. With getIframe wired it also adopts that frame up front, so one that outlived a previous host stays connected.

refreshContext(): void

Re-reads getContext() and pushes it to the connected iframe (e.g. after a tenant/subsidiary switch, or on resume), so the app reacts via bridge.onContextChange. No-op until the host knows its iframe.

notifyAuthChange(auth): void

Pushes a Nominal auth change (logout, or identity/tenant switch) to the connected iframe, so the app reacts via bridge.onAuthChange. No-op until the host knows its iframe.

Exports

VibeAppHost, and the types VibeAppHostOptions, HostContext, HostHandlers, RequestHandlers, ContextPayload, AuthPayload, VibeApiClientOptions.

Mounting inside Next.js (nom-ui)

Just mount() in an effect — no iframe-window handling, no load-event dance. The iframe is server-rendered and may load before React hydrates, but that's fine: its bridge re-sends CONNECT until the host (now listening) answers, so there's no race.

Do pass getIframe, though: effect cleanups run whenever the tree is hidden by <Activity> or kept in the route bfcache, and without it the rebuilt host cannot recognise the iframe that survived (see getIframe).

'use client'
import { useEffect, useRef, useState } from 'react'
import { VibeAppHost } from '@nominalso/vibe-host'

function VibeAppFrame({ src }: { src: string }) {
  const iframeRef = useRef<HTMLIFrameElement>(null)
  const [host] = useState(() => new VibeAppHost({ ...options, getIframe: () => iframeRef.current }))

  useEffect(() => host.mount(), [host]) // returns the unmount cleanup

  return (
    <iframe
      ref={iframeRef}
      src={src}
      sandbox="allow-scripts allow-forms allow-same-origin allow-popups"
    />
  )
}

How it fits together

The iframe side is @nominalso/vibe-bridge. See the repository for the full protocol and architecture.

License

UNLICENSED — proprietary. © Nominal. All rights reserved.