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

@returningai/widget-sdk

v1.1.0

Published

Shadow DOM isolated widget SDK for ReturningAI

Readme

ReturningAI Widget SDK

A Web Component SDK for embedding ReturningAI widgets on any customer website. Supports both iframe mode (Shadow DOM isolation) and bundle mode (direct DOM rendering). Drop-in replacement for the legacy widget-loader.js.


Table of Contents


Architecture

The SDK renders widgets in one of two modes, chosen automatically by the presence of the bundle-url attribute:

Iframe Mode (default)

Customer page DOM
└── <rai-store-widget>                 ← custom element (or any type-specific tag)
    └── Shadow Root [closed]
        ├── <style>                    ← all CSS scoped here, never leaks
        ├── <div class="rai-loader">   ← animated loader (hidden after ready)
        ├── <div class="rai-error">    ← error state (hidden until error)
        └── <iframe src="widget-url">  ← actual widget content

Bundle Mode (when bundle-url is set)

Customer page DOM
└── <rai-store-widget>                 ← custom element (or any type-specific tag)
    ├── Shadow Root [closed]
    │   ├── <style>                    ← SDK loader/error CSS
    │   └── <slot>                     ← renders light-DOM children through shadow
    └── <div> (light DOM)              ← widget mounts here, CSS cascade works normally
        └── [React app from IIFE bundle]

Bundle mode loads the widget's JavaScript bundle directly into the page (no iframe). CSS cascades from html[data-theme] into the widget naturally, making theme integration seamless.

Module Responsibilities

| Module | File | Purpose | |--------|------|---------| | Base Web Component | src/BaseWidget.ts | Abstract HTMLElement subclass; owns Shadow Root, auth lifecycle, iframe/bundle rendering, lazy loading, DOM events, public API | | Store Widget | src/StoreWidget.ts | Extends BaseWidget; implements buildWidgetUrl() for the store micro-frontend | | Channel Widget | src/ChannelWidget.ts | Extends BaseWidget; appends encoded channel ID to URL | | Milestone Widget | src/MilestoneWidget.ts | Extends BaseWidget; same URL strategy as ChannelWidget | | Social Widget | src/SocialWidget.ts | Extends BaseWidget; same URL strategy as ChannelWidget | | Currency Widget | src/CurrencyWidget.ts | Extends BaseWidget; same URL strategy as ChannelWidget | | Auth | src/core/auth.ts | Serverless auth with exponential backoff retry, proxy auth (authenticated embed), embed token validation (access key embed), token refresh (concurrency lock), logout, error settings fetch | | Storage | src/core/storage.ts | localStorage helpers scoped to {prefix}-{communityId}-*; access token never written to disk | | postMessage | src/core/postmessage.ts | Sends token (+ optional customData) to iframe; debounced height updates; emits DOM events; handles WIDGET_READY, WIDGET_HEIGHT_UPDATE, WIDGET_LOGOUT, RETURNINGAI_WIDGET_REQUEST_TOKEN | | Styles | src/styles/widget.css | Loader animation, error state, retry button, iframe fade-in — all scoped inside Shadow DOM | | Entry | src/index.ts | Registers all 5 custom elements; bootstraps from <script> tag for legacy embeds; routes by widget-type; exposes window.ReturningAIWidget |

Auth Flow

Page load
  │
  ├─ embed-token present? (Access Key Embed)
  │       Yes ──► validateEmbedToken()
  │                 ├─ valid   ──► continue below
  │                 └─ invalid ──► showError() — stop
  │       No  ──► skip validation, continue below
  │
  ├─ auth-url present? (Authenticated Embed)
  │       Yes ──► authenticateViaProxy()  ──► launch widget
  │       No  ──► loadFromStorage()
  │                 ├─ refresh token found ──► refreshAccessToken() ──► launch widget
  │                 └─ no token           ──► authenticateServerless() ──► launch widget
  │
  └─ launch widget
        ├─ bundle-url present? ──► mountWidget() (load IIFE, render in light DOM)
        └─ no bundle-url      ──► createIframe() (standard iframe mode)

Access Key Embed flow (credential exchange happens entirely server-side):

Customer server
  │  POST /v2/api/widget-access-keys/token
  │  Body: { accessId, accessKey }         ← stored in env vars, never in HTML
  ▼
RAI backend — validates credentials, signs 15-min JWT
  │  Response: { embedToken, expiresIn: 900 }
  ▼
Customer server injects embedToken into HTML
  │  <rai-store-widget embed-token="eyJ...">
  ▼
Browser loads page — SDK reads embed-token attribute
  │  POST /v2/api/widget-access-keys/validate
  │  Body: { embedToken }
  ▼
RAI backend — verifies JWT signature + checks key not revoked
  ├─ valid   ──► widget initialises normally
  └─ invalid ──► showError()

Token storage strategy

| Token | Location | TTL | |-------|----------|-----| | Access token | Memory only (WidgetState.accessToken) | ~5 min | | Refresh token | localStorage | 7 days |

The access token is never written to localStorage. On every page load the refresh token is exchanged for a fresh access token before the widget mounts.

postMessage Protocol (iframe mode only)

Messages the SDK sends to the widget iframe:

| Type | Payload | When | |------|---------|------| | RETURNINGAI_WIDGET_TOKEN | { widgetId, token, customData? } | After iframe load, on refresh, every 2 min |

Messages the SDK receives from the widget iframe:

| Type | Action | |------|--------| | WIDGET_READY | Hides loader, fades in iframe | | WIDGET_HEIGHT_UPDATE | Resizes iframe to payload.height | | WIDGET_ERROR | Hides loader | | WIDGET_LOGOUT | Calls logout endpoint, removes iframe | | RETURNINGAI_WIDGET_REQUEST_TOKEN | Responds immediately with a fresh token |

All messages are origin-validated against config.widgetDomain.


Embed Modes

The SDK supports three embed modes depending on your security requirements.

| | Public Embed | Access Key Embed | Authenticated Embed | |---|---|---|---| | Credentials in HTML | None | Short-lived token only | None | | Backend required | No | Yes — one call per page load | Yes — one endpoint | | Who generates auth | SDK calls RAI directly | Your server calls RAI, then token in HTML | Your server calls RAI with API key | | embed-token attribute | Absent | Required | Absent | | auth-url attribute | Absent | Absent | Required |


Shadow DOM Implementation

In iframe mode, the SDK uses a closed Shadow Root (mode: 'closed'), which means:

  • Customer page CSS cannot reach any element inside the widget
  • The widget's loader and error styles are fully encapsulated
  • No class name collisions with customer frameworks (Tailwind, Bootstrap, etc.)

In bundle mode, widget content renders in the light DOM (through a <slot>) so that html[data-theme] and Tailwind CSS cascade into the widget naturally.

Theme variables (iframe mode)

| Variable | Light | Dark | |----------|-------|------| | --rai-accent | #000000 | #ffffff | | --rai-text4 | #6b7280 | #9ca3af | | --rai-loader-bg | #ffffff | #1a1a1a | | --rai-error-bg | #ffffff | #1a1a1a |


Dependencies

Runtime

None. The SDK ships as a single self-contained IIFE with no external runtime dependencies.

Dev / Build

| Package | Version | Purpose | |---------|---------|---------| | vite | ^5.1 | Bundles TypeScript + inlines CSS into a single IIFE | | typescript | ^5.3 | Type checking and compilation | | terser | ^5.46 | Minification for production builds |

Browser requirements

| Feature | Minimum | |---------|---------| | Custom Elements v1 | Chrome 67, Firefox 63, Safari 13 | | Shadow DOM v1 | Chrome 53, Firefox 63, Safari 10 | | crypto.randomUUID() | Chrome 92, Firefox 95, Safari 15.4 | | IntersectionObserver | Chrome 58, Firefox 55, Safari 12.1 | | localStorage | All modern browsers |


Usage

Script Tag (Legacy Embed)

Zero changes required to existing embed HTML. Point src at the SDK and keep all data-* attributes as-is:

<!-- 1. Container div (unchanged from current embed) -->
<div
  id="returning-ai-widget-YOUR_COMMUNITY_ID"
  style="width: 100%; height: 600px;"
></div>

<!-- 2. SDK script tag -->
<script
  src="https://unpkg.com/@returningai/widget-sdk/dist/rai-widget.iife.js"
  data-community-id="YOUR_COMMUNITY_ID"
  data-widget-type="store"
  data-container="returning-ai-widget-YOUR_COMMUNITY_ID"
  data-theme="dark"
  data-width="100%"
  data-height="600px"
  data-api-url="YOUR_API_URL"
  data-widget-url="YOUR_WIDGET_URL"
  data-auto-refresh="true"
  data-email="[email protected]"
></script>

The SDK scans for the loader <script> tag, reads its data-* attributes, creates the appropriate widget element, and mounts it inside the container div.

Access Key Embed

Credentials live only in your server environment. Your server calls RAI to get a short-lived embed token and injects it into the page response — no secret ever reaches the browser.

Your server env:
  RAI_ACCESS_ID = rai_...
  RAI_ACCESS_KEY = pk_...    ← never in HTML

Per-request (server-side):
  POST /v2/api/widget-access-keys/token
  → { embedToken, expiresIn: 900 }

Page HTML:
  <rai-store-widget embed-token="eyJ..." ...>

Node.js / Express example:

app.get('/page', async (req, res) => {
  const { data } = await fetch(`${process.env.RAI_API_URL}/v2/api/widget-access-keys/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      accessId: process.env.RAI_ACCESS_ID,
      accessKey: process.env.RAI_ACCESS_KEY,
    }),
  }).then(r => r.json())

  res.render('page', { embedToken: data.embedToken })
})
<!-- page.html -->
<rai-store-widget
  community-id="YOUR_COMMUNITY_ID"
  api-url="YOUR_API_URL"
  embed-token="<%= embedToken %>"
  data-email="[email protected]"
></rai-store-widget>

The SDK validates the token on load. If expired (> 15 min since page render), the widget shows the error screen — users should refresh the page.

Web Component

For customers using a JavaScript framework (React, Vue, Angular), import the SDK once and use the custom element directly. Each widget type has its own tag:

| Tag | Widget type | |-----|-------------| | <rai-store-widget> | store | | <rai-channel-widget> | channel | | <rai-milestone-widget> | milestone | | <rai-social-widget> | social | | <rai-currency-widget> | currency-view | | <rai-widget> | store (deprecated alias — use <rai-store-widget>) |

<script src="https://unpkg.com/@returningai/widget-sdk/dist/rai-widget.iife.js"></script>

<!-- Store widget -->
<rai-store-widget
  community-id="YOUR_COMMUNITY_ID"
  theme="dark"
  width="100%"
  height="600px"
  api-url="YOUR_API_URL"
  widget-url="YOUR_WIDGET_URL"
  data-email="[email protected]"
></rai-store-widget>

<!-- Channel widget (requires channel-id) -->
<rai-channel-widget
  community-id="YOUR_COMMUNITY_ID"
  channel-id="YOUR_CHANNEL_ID"
  theme="dark"
  api-url="YOUR_API_URL"
  widget-url="YOUR_WIDGET_URL"
  data-email="[email protected]"
></rai-channel-widget>

In React (TypeScript):

// JSX types are included in the package — no manual declarations needed
import '@returningai/widget-sdk'

export function WidgetEmbed() {
  return (
    <rai-store-widget
      community-id="YOUR_COMMUNITY_ID"
      theme="dark"
      height="600px"
      data-email={currentUser.email}
    />
  )
}

Bundle Mode (Non-Iframe)

When you set the bundle-url attribute, the widget renders directly in the page DOM instead of inside an iframe. This gives full CSS cascade — html[data-theme] and Tailwind classes apply to the widget content naturally.

<rai-store-widget
  community-id="YOUR_COMMUNITY_ID"
  api-url="YOUR_API_URL"
  bundle-url="/path/to/store-widget.js"
  embed-token="eyJ..."
  data-email="[email protected]"
  theme="dark"
  width="100%"
  height="100vh"
  eager
></rai-store-widget>

Each widget type has its own IIFE bundle and global name:

| Widget type | Bundle global | |-------------|--------------| | store | RaiStoreWidget | | channel | RaiChannelWidget | | social | RaiSocialWidget | | milestone | RaiMilestoneWidget | | currency-view | RaiCurrencyWidget |

The bundle must export a mount(container, config) function on its global (e.g. window.RaiStoreWidget.mount).

Configuration Attributes

All attributes can be provided with or without the data- prefix.

| Attribute | Required | Default | Description | |-----------|----------|---------|-------------| | community-id | Yes | — | Your community ID — from Community Settings | | channel-id | Channel only | — | Channel ID — required when widget-type is channel | | widget-type | No | store | store, channel, milestone, social, currency-view | | theme | No | light | light or dark | | container | No | returning-ai-widget-{id} | ID of the container element | | width | No | 100% | CSS width | | height | No | 600px | Initial CSS height (auto-resized by WIDGET_HEIGHT_UPDATE in iframe mode) | | api-url | No | — | Auth API base URL — from Community Settings | | widget-url | No | — | URL served inside the iframe — from Community Settings (iframe mode only) | | bundle-url | No | — | URL to the widget IIFE bundle — triggers bundle mode (non-iframe) | | embed-token | Access Key only | — | Short-lived JWT from your server. Required for Access Key Embed; absent for Public and Authenticated Embed | | auth-url | Auth Embed only | — | Your backend auth endpoint — enables Authenticated Embed mode | | auto-refresh | No | true | Automatically refresh access token before expiry | | debug | No | false | Enable verbose console logging | | eager | No | — | Boolean — skip IntersectionObserver, init immediately on mount | | locale | No | — | BCP 47 tag appended as ?locale= to the widget URL (e.g. fr-FR) | | max-retries | No | 3 | Max auth retry attempts on network error or 5xx | | retry-delay | No | 500 | Base backoff delay in ms; doubles each attempt | | height-debounce | No | 100 | Debounce window in ms for WIDGET_HEIGHT_UPDATE | | storage-prefix | No | returning-ai-widget | localStorage key prefix — set per tenant to avoid collisions | | retry-label | No | Retry | Text for the retry button on the error screen | | custom-data | No | — | JSON string forwarded as customData in the token postMessage | | data-email | No | — | User identifier passed to auth (Public Embed only) | | data-* | No | — | Any additional data-* attributes are forwarded as userIdentifiers to the auth API |

Note: The legacy widget-id attribute is still supported as a deprecated alias for community-id.

DOM Events

All events bubble and are composed: true (cross the Shadow DOM boundary).

| Event | detail | Fired when | |-------|----------|-----------| | rai-authenticated | {} | Auth succeeded, before widget mounts | | rai-ready | {} | WIDGET_READY received, loader hidden (iframe mode) | | rai-mounted | {} | Widget bundle mounted successfully (bundle mode) | | rai-error | { message } | Auth failed after all retries | | rai-logout | {} | Widget logged out | | rai-height-change | { height } | iframe resized (after debounce) |

document.querySelector('rai-channel-widget')
  .addEventListener('rai-error', (e) => showToast(e.detail.message))

Public API

After the SDK loads, window.ReturningAIWidget is available:

// Check the loaded version
window.ReturningAIWidget.version          // e.g. "1.0.3"

// Reload the widget (re-runs auth flow)
await window.ReturningAIWidget.reload()

// Log out and remove the iframe
await window.ReturningAIWidget.logout()

// Check authentication state
window.ReturningAIWidget.isAuthenticated()  // boolean

// Inspect token metadata (no token values exposed)
window.ReturningAIWidget.getTokenInfo()
// {
//   hasAccessToken: true,
//   hasRefreshToken: true,
//   accessTokenExpiry: Date,
//   refreshTokenExpiry: Date,
//   isAccessTokenValid: true,
//   isRefreshTokenValid: true
// }

Build Process

Prerequisites

node >= 18
npm >= 9

Install

cd rai-widget-sdks
npm install

Development build (unminified, fast)

npm run build
# → dist/rai-widget.iife.js  (~17 kB, IIFE for CDN/script-tag)
# → dist/rai-widget.js       (~17 kB, ESM for bundlers/npm)

Production build (minified with Terser)

npm run build:min
# → dist/rai-widget.iife.js  (~6 kB gzip, IIFE for CDN/script-tag)
# → dist/rai-widget.js       (~6 kB gzip, ESM for bundlers/npm)

Local dev server

npm run dev
# Opens Vite dev server — open test/index.html via the dev server URL

How the build works

  1. Entry: src/index.ts — imports all widget subclasses, registers the custom elements, runs the bootstrap
  2. CSS inlining: src/styles/widget.css is imported with Vite's ?inline suffix, which converts it to a JavaScript string at build time — no separate CSS file is emitted
  3. Output formats: iife (Immediately Invoked Function Expression) for CDN/script-tag embeds → dist/rai-widget.iife.js; es (ES module) for bundlers and npm consumers → dist/rai-widget.js
  4. Version injection: vite.config.ts reads version from package.json and replaces the __WIDGET_VERSION__ placeholder at build time

To release a new version, bump version in package.json, rebuild, and upload dist/rai-widget.iife.js to the CDN under the new version path. Run npm publish to push the ESM build to the npm registry (prepublishOnly handles the rebuild automatically).


Project Structure

rai-widget-sdks/
├── src/
│   ├── types.ts              # WidgetConfig, WidgetState, TokenData interfaces
│   ├── BaseWidget.ts         # Abstract HTMLElement subclass — Shadow Root, auth, iframe/bundle
│   ├── StoreWidget.ts        # Extends BaseWidget; store micro-frontend URL builder
│   ├── ChannelWidget.ts      # Extends BaseWidget; channel URL builder
│   ├── MilestoneWidget.ts    # Extends BaseWidget; milestone URL builder
│   ├── SocialWidget.ts       # Extends BaseWidget; social URL builder
│   ├── CurrencyWidget.ts     # Extends BaseWidget; currency-view URL builder
│   ├── index.ts              # Registers all 5 custom elements + IIFE bootstrap
│   ├── jsx.d.ts              # React JSX IntrinsicElements + Vue GlobalComponents type shims
│   ├── vite-env.d.ts         # Type declarations for ?inline imports + __WIDGET_VERSION__
│   ├── core/
│   │   ├── auth.ts           # Serverless auth, refresh, logout, error settings
│   │   ├── storage.ts        # localStorage helpers (refresh token persistence)
│   │   └── postmessage.ts    # iframe token delivery + message listener
│   └── styles/
│       └── widget.css        # All loader/error/iframe CSS (inlined into Shadow DOM)
├── test/
│   └── index.html            # Local test page with config panel and verification checklist
├── dist/                     # Built output (gitignored)
│   ├── rai-widget.iife.js    # IIFE bundle for CDN / <script src> embeds
│   ├── rai-widget.js         # ESM bundle for bundlers / npm consumers
│   └── types/                # TypeScript declarations (generated by tsc)
├── package.json
├── tsconfig.json
└── vite.config.ts