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

@masheev/embed-sdk

v0.2.0

Published

Masheev Embed SDK — iframe, React hooks, and headless API for customer chat embedding

Readme

@masheev/embed-sdk

Embeddable chat widget for any website. Available as a script tag, React hook, or headless API with full control over the UI.

Installation

pnpm add @masheev/embed-sdk

Or load via script tag (no build step):

<script src="https://unpkg.com/@masheev/embed-sdk"></script>

Entry Points

| Import path | Use case | |---|---| | @masheev/embed-sdk/js | Vanilla JS — iframe widget with postMessage API | | @masheev/embed-sdk/react | React — hook wrapper around the iframe widget | | @masheev/embed-sdk/headless | React — direct WebSocket API, bring your own UI |

Quick Start

Script Tag

<script src="https://unpkg.com/@masheev/embed-sdk"></script>
<script>
  Masheev.init({
    inboxId: "your-inbox-id",
    mode: "chat-widget",
    position: "right",
    agentName: "Support",
  });

  Masheev.on("message", function (msg) {
    console.log(msg.role, msg.content);
  });
</script>

React (iframe)

import { useMasheev } from "@masheev/embed-sdk/react";

function App() {
  const { open, isReady, on } = useMasheev({
    inboxId: "your-inbox-id",
    mode: "chat-widget",
    position: "right",
    agentName: "Support",
  });

  useEffect(() => {
    const unsub = on("message", ({ role, content }) => {
      console.log(role, content);
    });
    return unsub;
  }, [on]);

  return (
    <button onClick={open} disabled={!isReady}>
      Chat with us
    </button>
  );
}

Headless (custom UI)

import { MasheevProvider, useMasheev } from "@masheev/embed-sdk/headless";

function Chat() {
  const { messages, sendMessage, isLoading } = useMasheev();

  return (
    <div>
      {messages.map((m) => (
        <div key={m.id} className={m.role}>
          {m.content}
          {m.isLoading && <span>Typing...</span>}
        </div>
      ))}
      <input
        onKeyDown={(e) => {
          if (e.key === "Enter") {
            sendMessage(e.currentTarget.value);
            e.currentTarget.value = "";
          }
        }}
      />
    </div>
  );
}

function App() {
  return (
    <MasheevProvider
      inboxId="your-inbox-id"
      turnstileSiteKey="your-site-key"
      customerInfo={{ name: "Jane", email: "[email protected]" }}
    >
      <Chat />
    </MasheevProvider>
  );
}

JS SDK API

When loaded via script tag the global Masheev object exposes these methods. The same functions are available from @masheev/embed-sdk/js.

init(config)

Initialize the widget. Creates an iframe and attaches it to the page.

Masheev.init({
  inboxId: "inbox_abc",       // required
  mode: "chat-widget",        // "chat-widget" | "prompt-input" | "embedded"
  position: "right",          // "left" | "right"
  baseUrl: "https://iframe.masheev.com",
  placeholder: "Ask anything...",
  agentName: "Support Bot",
  agentTitle: "AI Assistant",
  questions: ["How do I get started?", "Pricing?"],
  privacyUrl: "https://example.com/privacy",
  requireConsent: false,       // true to show consent gate (regulated industries)
  user: {
    name: "Jane Doe",
    email: "[email protected]",
    phone: "+1234567890",
    userId: "usr_123",
    company: "Acme Inc",
    customAttributes: { plan: "pro" },
  },
});

open() / close() / toggle()

Control widget visibility.

Masheev.open();
Masheev.close();
Masheev.toggle();

sendMessage(text)

Send a message programmatically.

Masheev.sendMessage("I need help with billing");

updateContext(context)

Update user context mid-conversation. This is a client-side only operation — the data is forwarded to the iframe and used for the current session, but it is not persisted to the database. Use updateContact when you need server-side persistence.

Masheev.updateContext({ name: "Jane Doe", email: "[email protected]" });

updateContact(fields)

Update the contact record associated with the current widget session. Changes are persisted to the server immediately via PATCH /api/widget/contact. Only non-empty string fields are applied; omitted or empty fields are left unchanged on the server.

This is the method to use when your app learns new information about the user (e.g. after sign-up, profile update, or form submission) and you want the contact record in Masheev to stay in sync.

Masheev.updateContact({
  name: "Jane Doe",
  email: "[email protected]",
  phone: "+14155551234",
  company: "Acme Inc",
});

Accepted fields:

| Field | Type | Max length | Notes | |---|---|---|---| | name | string | 255 chars | Trimmed before saving. | | email | string | 255 chars | Trimmed before saving. | | phone | string | — | Normalized to E.164 format on the server. Invalid numbers are silently ignored. | | company | string | 255 chars | Trimmed before saving. | | userId | string | — | Accepted by the SDK type signature but not currently persisted by the server. Use updateContext to pass userId to the session. |

Rate limiting: 5 requests per minute per session. Exceeding the limit returns 429 Too Many Requests.

Requires an active session. If called before the widget has established a session (i.e. the user has not yet started a conversation), the command is queued and sent once the iframe is ready — but the server call will only succeed once a session token exists.

updateContact vs updateContext

| | updateContact | updateContext | |---|---|---| | Persists to DB | Yes — writes to the contact table immediately | No — client-side only | | Server call | PATCH /api/widget/contact | None | | Use case | Syncing user profile data (post-login, form submit) | Passing ephemeral context to the AI (page URL, cart contents) | | Fields | name, email, phone, company (+ userId in type) | All UserContext fields including customAttributes | | Rate limited | Yes (5/min per session) | No |

How it works

  1. Your code calls masheev.updateContact({ name: "Jane" }).
  2. The SDK sends a postMessage of type "updateContact" to the widget iframe.
  3. The iframe handler receives the message and issues a PATCH request to /api/widget/contact with the session token as a Bearer header.
  4. The API validates the session token (HMAC-signed, stateless), checks the rate limit, sanitizes the fields, and updates the contact row in the database.
  5. The call is fire-and-forget from the SDK's perspective — errors are caught silently to avoid disrupting the host page.

setQuestions(questions)

Replace the list of suggested questions.

Masheev.setQuestions(["How does pricing work?", "Do you offer a free trial?"]);

on(event, callback) / off(event, callback)

Subscribe to widget events. on() returns an unsubscribe function.

const unsub = Masheev.on("message", (msg) => {
  console.log(msg.role, msg.content);
});

// later
unsub();

isReady()

Returns true once the iframe has loaded and is accepting commands.

destroy()

Remove the iframe and clean up all listeners.

version

SDK version string (e.g. "0.2.0").


Configuration

MasheevConfig

| Property | Type | Default | Description | |---|---|---|---| | inboxId | string | — | Required. Inbox to connect to. | | mode | WidgetMode | "chat-widget" | Display mode (see below). | | position | "left" \| "right" | "right" | Bubble position (chat-widget mode only). | | baseUrl | string | "https://iframe.masheev.com" | Widget iframe host. | | placeholder | string | — | Input placeholder text. | | agentName | string | — | Agent display name in the header. | | agentTitle | string | — | Agent role / title. | | questions | string[] | — | Suggested questions shown to the user. | | privacyUrl | string | — | Link to your privacy policy. | | requireConsent | boolean | false | Show a consent gate before the chat starts (see AI Disclosure & Consent). | | hideHeader | boolean | false | Hide the chat header (embedded mode only). Useful when you provide your own header. | | user | UserContext | — | Pre-fill user identity (see below). |

UserContext

| Property | Type | Description | |---|---|---| | name | string | User name | | email | string | User email | | phone | string | User phone | | userId | string | Your external user ID | | company | string | Company name | | customAttributes | Record<string, string \| number \| boolean> | Arbitrary metadata |


Widget Modes

chat-widget (default)

Floating bubble in the corner. Starts at 100 x 100 px, expands to 460 x 750 px when opened. Position controlled by the position prop.

prompt-input

Fixed input bar at the bottom of the page. Full width, 100 px tall. Expands on interaction.

embedded

Fills 100% of its parent container. Use this when you want to place the widget inside a specific element rather than floating it over the page. The parent must have defined dimensions.


AI Disclosure & Consent

Masheev automatically handles AI disclosure to comply with the EU AI Act (Art. 50), US FTC deception rules, and California SB 1001. Every channel informs end-users that they are interacting with AI.

How Disclosure Works

When an AI agent is configured on an inbox, end-users are informed they are interacting with AI. This is handled per channel:

  • Chat widget — disclosure is shown as a small text label before the conversation begins, plus a persistent "AI-powered · may make mistakes" notice below the input (similar to ChatGPT). The greeting message itself stays conversational.
  • SMS / WhatsApp — disclosure is prepended to the first AI response in a new conversation
  • Voice — disclosure is spoken as part of the first message when the call starts

Disclosure text is auto-generated from the AI agent's name and the organization name. It can be customized per channel in the AI Agent settings (Dashboard > AI Agents > Disclosure tab).

Custom templates support {{agentName}} and {{orgName}} variables.

Consent Gate

For regulated industries that require explicit data-processing consent before a conversation, set requireConsent: true:

Masheev.init({
  inboxId: "inbox_abc",
  requireConsent: true,
  privacyUrl: "https://example.com/privacy",
});

When enabled, the widget shows a consent gate before the chat UI. The user must check a consent checkbox and click "Start Chat" to proceed.

Consent is stored both client-side (localStorage, for UX persistence) and server-side on the contact record (for GDPR Art. 7(1) proof). The server stores the consent timestamp in the contact's consent.dataProcessing and consent.dataProcessingAt fields.

Session Endpoint (consent field)

When the widget sends consent data, it is included in the session creation request:

{
  "inboxId": "string",
  "visitorId": "string",
  "consent": { "timestamp": "2025-01-15T10:30:00.000Z" }
}

The server merges the consent into the contact's existing consent record without overwriting other consent fields.


Events

| Event | Payload | Fires when | |---|---|---| | ready | — | Widget iframe has loaded | | open | — | Widget panel is opened | | close | — | Widget panel is closed | | message | { role: "user" \| "ai", content: string } | A message is sent or received | | error | { message: string, code?: string } | An error occurs |

All event subscriptions are type-safe:

Masheev.on("message", (payload) => {
  // payload is typed as { role: "user" | "ai"; content: string }
});

React Hook API

useMasheev(config)

import { useMasheev } from "@masheev/embed-sdk/react";

const {
  open,            // () => void
  close,           // () => void
  toggle,          // () => void
  sendMessage,     // (text: string) => void
  updateContact,   // (fields: Partial<Pick<UserContext, "name"|"email"|"phone"|"userId"|"company">>) => void
  on,              // (event, callback) => () => void  (returns unsubscribe)
  off,             // (event, callback) => void
  containerRef,    // (node: HTMLElement | null) => void  (callback ref for embedded mode)
  isReady,         // boolean
} = useMasheev({ inboxId: "..." });

The hook creates the SDK on mount and destroys it on unmount. It re-initialises when inboxId or baseUrl change.

Updating the contact from React

Call updateContact after a user action that reveals new identity information. The method is stable (wrapped in useCallback) and safe to call from effects or event handlers.

import { useMasheev } from "@masheev/embed-sdk/react";

function ProfilePage() {
  const { updateContact } = useMasheev({
    inboxId: "inbox_abc",
    mode: "chat-widget",
  });

  async function handleProfileSave(form: FormData) {
    await saveProfile(form); // your app logic

    // Sync the updated info to the Masheev contact record
    updateContact({
      name: form.get("name") as string,
      email: form.get("email") as string,
      company: form.get("company") as string,
    });
  }

  return <form onSubmit={(e) => { e.preventDefault(); handleProfileSave(new FormData(e.currentTarget)); }}>...</form>;
}

Headless API

The headless entry point gives you direct access to the conversation over WebSocket. No iframe is created — you build the UI yourself.

<MasheevProvider>

Wrap your chat UI with the provider.

| Prop | Type | Default | Description | |---|---|---|---| | inboxId | string | — | Required. Inbox to connect to. | | apiBase | string | "https://api.masheev.com" | API base URL. | | turnstileSiteKey | string | — | Cloudflare Turnstile site key for bot protection. | | customerInfo | { name?, email?, phone? } | — | Pre-fill customer identity. |

useMasheev() (headless)

Must be called inside <MasheevProvider>.

const {
  // Session
  session,            // WidgetSession | null
  isConnected,        // boolean
  connectionStatus,   // "connecting" | "connected" | "disconnected" | "error" | "gave_up"
  error,              // string | null

  // Messages
  messages,           // Message[]
  sendMessage,        // (content: string) => Promise<void>
  isLoading,          // boolean

  // UI state
  isOpen,             // boolean
  setOpen,            // (open: boolean) => void

  // Manual session creation
  createSession,      // () => Promise<WidgetSession | null>
} = useMasheev();

Lower-level hooks

| Hook | Returns | Use case | |---|---|---| | useWidgetChat() | { messages, sendMessage, isLoading, error } | Chat operations only | | useWidgetSession() | { session, error, isLoading, createSession } | Session management | | useWidgetSocket() | { status } | Connection indicator |

Message

interface Message {
  id: string;
  role: "user" | "ai";
  content: string;
  timestamp: Date;
  isLoading?: boolean;   // true while the AI is streaming
}

WidgetSession

interface WidgetSession {
  sessionToken: string;
  conversationId: string;
  greeting: string;
}

How It Works

Iframe SDK (JS / React)

  1. init() creates a hidden <iframe> pointing to https://iframe.masheev.com/widget/{inboxId}.
  2. Communication happens via window.postMessage with strict origin validation.
  3. Commands issued before the iframe is ready are queued and flushed on the ready event.

Headless SDK

  1. A session is created lazily on the first sendMessage() call (or explicitly via createSession()).
  2. The provider opens a WebSocket to wss://api.masheev.com/ws/conversation/{conversationId}.
  3. Messages from the AI arrive in real-time over the socket.
  4. Reconnection uses exponential backoff (1 s initial, 30 s max, 10 attempts, 30% jitter).
  5. A ping is sent every 25 s to keep the connection alive.

Bot Protection

When turnstileSiteKey is provided (headless only), the SDK embeds an invisible Cloudflare Turnstile challenge. The token is sent with the session-creation request and validated server-side.


REST Endpoints (headless internals)

These are called internally by the headless provider. You only need them if you are building a non-React integration from scratch.

POST /api/widget/session

Create a chat session.

Request:

{
  "inboxId": "string",
  "visitorId": "string",
  "customerInfo": { "name": "string", "email": "string", "phone": "string" },
  "turnstileToken": "string",
  "consent": { "timestamp": "ISO-8601 string" }
}

The consent field is optional. When provided, the server records dataProcessing: true and the timestamp on the contact record for GDPR Art. 7(1) compliance.

Response:

{
  "sessionToken": "string",
  "conversationId": "string",
  "greeting": "string",
  "disclosure": "string",
  "isResumed": false
}
  • greeting — the agent's conversational greeting (e.g. "Hello! How can I help you today?"). null for resumed sessions.
  • disclosure — the AI disclosure text shown as a label before the chat begins (e.g. "You're chatting with Alex, an AI assistant for Acme Corp."). null for resumed sessions or when no AI agent is configured.
  • isResumedtrue when continuing an existing conversation.

POST /api/widget/message

Send a message. Requires Authorization: Bearer {sessionToken}.

Request:

{ "content": "string" }

Response:

{ "content": "string" }

PATCH /api/widget/contact

Update the contact record for the current session. Requires Authorization: Bearer {sessionToken}. Rate limited to 5 requests per minute per session.

Request:

{
  "name": "Jane Doe",
  "email": "[email protected]",
  "phone": "+14155551234",
  "company": "Acme Inc"
}

All fields are optional. Only non-empty string values are applied. Phone numbers are normalized to E.164 on the server. String fields are trimmed and capped at 255 characters.

Response (200):

{ "success": true }

Error responses:

| Status | Body | Cause | |---|---|---| | 400 | { "error": "No valid fields to update" } | All provided fields were empty or invalid | | 401 | { "error": "Missing session token" } | No Authorization header | | 401 | { "error": "Invalid or expired session" } | Token failed HMAC verification | | 429 | { "error": "Too many requests" } | Rate limit exceeded (5/min per session) |

WebSocket /ws/conversation/{conversationId}

Query params: ?token={sessionToken}&source=widget

Receives JSON frames:

{
  "type": "message.new",
  "payload": {
    "messageId": "string",
    "content": "string",
    "sender": { "type": "ai" }
  }
}

Constants

import { SDK_VERSION, DEFAULT_API_BASE, DEFAULT_WIDGET_BASE } from "@masheev/embed-sdk/js";

SDK_VERSION          // "0.2.0"
DEFAULT_API_BASE     // "https://api.masheev.com"
DEFAULT_WIDGET_BASE  // "https://iframe.masheev.com"