npm package: @masheev/embed-sdk
Description: Masheev Embed SDK — iframe, React hooks, and headless API for customer chat embedding
Author: Masheev
Latest version: 0.2.0

<!-- AUTO-GENERATED from apps/docs/content — do not edit directly. -->
<!-- Run `pnpm docs:sync` after editing the source in apps/docs/content/ -->

# @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

```bash
pnpm add @masheev/embed-sdk
```

Or load via script tag (no build step):

```html
<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

```html
<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)

```tsx
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)

```tsx
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: "jane@example.com" }}
    >
      <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.

```ts
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: "jane@example.com",
    phone: "+1234567890",
    userId: "usr_123",
    company: "Acme Inc",
    customAttributes: { plan: "pro" },
  },
});
```

### `open()` / `close()` / `toggle()`

Control widget visibility.

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

### `sendMessage(text)`

Send a message programmatically.

```ts
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`](#updatecontactfields) when you need server-side persistence.

```ts
Masheev.updateContext({ name: "Jane Doe", email: "jane@acme.com" });
```

### `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.

```ts
Masheev.updateContact({
  name: "Jane Doe",
  email: "jane@acme.com",
  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.

```ts
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.

```ts
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](#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`:

```ts
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:

```json
{
  "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:

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

---

## React Hook API

### `useMasheev(config)`

```ts
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.

```tsx
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>`.

```ts
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`

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

### `WidgetSession`

```ts
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:**
```json
{
  "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:**
```json
{
  "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.
- `isResumed` — `true` when continuing an existing conversation.

### `POST /api/widget/message`

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

**Request:**
```json
{ "content": "string" }
```

**Response:**
```json
{ "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:**
```json
{
  "name": "Jane Doe",
  "email": "jane@acme.com",
  "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):**
```json
{ "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:
```json
{
  "type": "message.new",
  "payload": {
    "messageId": "string",
    "content": "string",
    "sender": { "type": "ai" }
  }
}
```

---

## Constants

```ts
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"
```