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

@voyant-travel/max-embed

v0.5.1

Published

Embed Max — Voyant's AI travel agent — into any web app. React components (a floating launcher and an inline chat) plus a framework-agnostic <script> loader.

Readme

@voyant-travel/max-embed

Embed Max — Voyant's AI travel agent — into any web app.

Two ways to embed:

  • React<MaxLauncher> (a floating launcher + panel), <MaxChat> (an inline chat that fills its container), or <MaxApp> (the fullscreen, deep-linkable assistant).
  • Plain HTML — a <script> loader that needs no build step.

The chat UI itself runs in a sandboxed iframe hosted by Voyant; these are thin, dependency-free wrappers that mount the iframe, keep it in sync with your page's theme/language, and animate it open and closed.

Install

npm install @voyant-travel/max-embed

react and react-dom (v18 or v19) are peer dependencies for the React entry.

Tokens

Every embed needs a short-lived embed token minted by your backend (so the secret API key never reaches the browser):

POST https://api.voyant.travel/max/v1/embed/token

Tokens expire (~15 min) — fetch a fresh one from your server and refresh before expiry rather than hardcoding it.

React — floating launcher

import { MaxLauncher } from "@voyant-travel/max-embed"

export function App() {
  return <MaxLauncher token={token} />
}

React — inline chat

import { MaxChat } from "@voyant-travel/max-embed"

export function Support() {
  return (
    <div style={{ height: 600 }}>
      <MaxChat token={token} />
    </div>
  )
}

React — fullscreen app

The whole assistant as a routed app inside a full width/height iframe — a persistent conversation sidebar plus a canvas surface for what's being worked on. Conversations are deep-linkable: the in-iframe location is mirrored into your page's address bar under basePath, so refresh, share and browser back/forward all work.

import { MaxApp } from "@voyant-travel/max-embed"

// Mount this on a catch-all route, e.g. `/assistant/*`
export function Assistant() {
  return (
    <div style={{ height: "100vh" }}>
      <MaxApp token={token} basePath="/assistant" />
    </div>
  )
}

Route every path under basePath to this component (a splat/catch-all route) so a refreshed deep-link still mounts it.

Props

| Prop | Type | Default | Notes | | ------------- | ------------------------------- | ---------------------------------- | ----------------------------------------------------------- | | token | string | — | Embed JWT from your backend. | | embedOrigin | string | https://agent-embed.voyant.travel| Override the iframe origin. | | theme | "light" \| "dark" \| "system" | auto-detect | Force the iframe theme to match your page. | | lang | string | auto-detect | BCP-47 tag for the chat chrome. | | title | string | "Max by Voyant" | iframe title. | | defaultOpen | boolean (launcher) | false | Start with the panel open. | | bottom/right | number (launcher) | 20 | Launcher offset in px. | | basePath | string (app) | / | Embedder path the app is mounted under; reflected in the URL. | | onRouteChange | (path: string) => void (app)| — | Fires on in-iframe navigation with the app-relative path. | | context | MaxHostContext \| null | — | Typed host context (discovery hint). null clears; see below. | | tenant | string | — | Tenant scope enforced on inbound messages. | | audience | string | — | Audience/surface scope enforced on inbound messages. | | onContextClear | () => void | — | User pressed clear inside the iframe — drop your selection. | | defaultLayout | "normal" \| "wide" \| "expanded" (launcher) | "normal" | Initial panel layout. | | onLayoutChange | (layout) => void (launcher)| — | Fires when the panel layout changes. |

Theme and language are auto-detected from <html class="dark"> / <html data-theme> / <html lang> and tracked live — toggling your page theme keeps the iframe in sync without remounting it (chat state is preserved). Pass theme/lang to take over either axis.

Host context — a typed, tenant-safe discovery hint

Tell Max what the operator is currently looking at so it can offer relevant help without the user re-typing an identifier. The context streams to the iframe over a validated postMessage channel; changing it on host navigation updates the iframe without remounting it, so chat state is preserved.

import { MaxChat, type MaxHostContext } from "@voyant-travel/max-embed"

function BookingPage({ booking }) {
  const context: MaxHostContext = {
    type: "booking",              // product | booking | customer | departure | invoice | contract
    id: booking.reference,        // stable, tenant-scoped, opaque
    label: `Booking ${booking.reference}`,
    route: `/bookings/${booking.reference}`,
    version: booking.rev,         // bump on every change
  }
  return (
    <MaxChat
      token={token}
      tenant="acme"
      audience="agent-desktop"
      context={context}
      onContextClear={() => {/* user cleared it inside Max — drop your selection */}}
    />
  )
}

Pass context={null} to explicitly clear it (distinct from omitting the prop, which means "this host supplies no context"). The clear is always explicit — Max never silently discards a context.

Security invariant. The host context is a discovery hint only. It never authorises anything — Max re-verifies identity, auth, approval and consequence-preview for every action server-side, regardless of the supplied context. Never use it to bypass a check. Exported as CONTEXT_SECURITY_INVARIANT.

Historical conversations — the contract keeps a pinned context and represents it non-destructively with a MaxContextStatus (active / stale / archived / deleted / unauthorized) via deriveContextStatus, so a stored conversation never has to inherit the current host context. Actually persisting those snapshots and resolving them live is the platform's job — see the scope note below.

The channel is strict: inbound messages are validated by exact origin, by event.source (must be this iframe — blocks cross-tab replay), by session id (blocks cross-session replay), by tenant/audience scope, by a replay/freshness guard, and by strict per-type payload validation (max:navigate accepts only safe app-relative paths; layout messages require a valid layout); the entity type is validated too. Full spec in PROTOCOL.md.

Receiving context (iframe / consumer side)

MaxContextReceiver (alias createContextReceiver) is the portable, framework- agnostic state machine for the receiving end of the channel. Feed it raw message events and it maintains an ordered, replay-resistant, verified snapshot:

import { createContextReceiver } from "@voyant-travel/max-embed"

const receiver = createContextReceiver({
  expectedOrigin: "https://your-host.example",
  expectedSource: window.parent, // the host window (strict source check)
  scope: { sessionId, tenant: "acme", audience: "agent-desktop" },
  // Optional: resolve *display* status only — never authorization.
  verify: async (ctx) => (await stillExists(ctx)) ? { ok: true } : { ok: false, reason: "deleted" },
})

window.addEventListener("message", async (event) => {
  await receiver.ingest(event)
  render(receiver.snapshot()) // { status: "active" | "cleared" | "stale" | "degraded", context }
})

It enforces exact origin/source, the v1 channel, exact payload shape, session/tenant/audience scope, a finite strictly-positive fresh ts and a bounded non-empty msgId (a ts=0 never bypasses freshness), replay dedupe, entity normalization, monotonic version/capturedAt ordering with envelope-ts fallback (older updates are rejected out-of-order), and idempotence. The verify callback resolves display status only; it does not authorize actions (see the security invariant).

Scope: what this package is (and isn't)

@voyant-travel/max-embed ships the portable contract and state machine — the wire format, the host- and receiver-side validators, the ordering/verification logic, and the React/loader plumbing. Durable snapshot persistence, live entity resolution, and the in-iframe context/approval UI live in the Max platform (tracked in platform#1515) and are not part of this package (nor necessarily deployed yet). The runnable examples/context-demo iframe is an illustrative, hand-written protocol peer for browser demos, not the exported receiver state machine and not a production backend. Receiver behavior is covered directly by the package's context-receiver.test.ts suite.

Panel layouts (launcher)

The floating launcher supports three responsive layouts — normal (docked bubble), wide, and expanded (centred near-full-page) — with on-panel expand / restore controls. The embedded app can request a layout and the host echoes the applied layout back, so both stay in sync; layout changes never remount the iframe.

Plain HTML — <script> loader

For non-React hosts (static sites, WordPress, etc.). Served from any CDN that mirrors npm:

<script src="https://unpkg.com/@voyant-travel/max-embed/max.js" defer></script>
<script>
  Max.init({ token: "<embed-jwt>", mode: "bubble" })
  // or inline:  Max.init({ token, mode: "inline", target: "#max-host" })
</script>

Max.init(opts) · Max.open() · Max.close() · Max.setContext(ctx) · Max.clearContext() · Max.setLayout("normal" | "wide" | "expanded") · Max.destroy(). It sniffs and tracks the host theme/language the same way the React components do, and speaks the same validated context/layout protocol:

<script>
  Max.init({ token, mode: "bubble", tenant: "acme", audience: "agent-desktop" })
  Max.setContext({ type: "booking", id: "VYT-10423", label: "Booking VYT-10423" })
  // …on navigation:
  Max.setContext({ type: "customer", id: "CUS-7781", label: "Ada Lovelace" })
  Max.clearContext() // explicit clear
</script>

Migration notes (for platform consumers)

0.4.x → 0.5.0 is backwards compatible — every existing usage keeps working:

  • All new props (context, tenant, audience, onContextClear, defaultLayout, onLayoutChange) are optional. Omit them and behaviour is unchanged. Same for the loader's new tenant/audience/context options and the setContext / clearContext / setLayout methods.
  • Outbound messages now carry a versioned envelope (channel, v, sessionId, tenant, audience, msgId, ts) in addition to the existing type + payload. Iframes that only read type and their payload key are unaffected.
  • Inbound validation is stricter: messages must come from this iframe's contentWindow (not just the right origin). Already-deployed iframes that send the old un-enveloped max:close / max:setLayout / max:navigate from their own content window continue to work; new context/layout-request features require the envelope. See PROTOCOL.md.
  • To adopt the context channel end-to-end, the Max iframe side must read the session / tenant / audience query params and echo them in its envelopes, handle max:setContext / max:setLayout, and send max:requestContext / max:clearContext / max:requestLayout. A complete illustrative wire-level fixture lives in examples/context-demo/fixture/max.html; production consumers should use the exported createContextReceiver contract.

License

Apache-2.0