@helpai/elements 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -12
- package/configurator.mjs +1 -1
- package/docs/README.md +183 -0
- package/docs/analytics.md +551 -0
- package/docs/api-reference.md +637 -0
- package/docs/browser-support.md +302 -0
- package/docs/configuration.md +1014 -0
- package/docs/events-and-hooks.md +483 -0
- package/docs/faq.md +435 -0
- package/docs/frameworks.md +643 -0
- package/docs/installation.md +626 -0
- package/docs/options.md +323 -0
- package/docs/quickstart.md +229 -0
- package/docs/recipes.md +725 -0
- package/docs/security-privacy.md +636 -0
- package/docs/theming.md +570 -0
- package/docs/troubleshooting.md +523 -0
- package/elements-web-component.esm.js +39 -39
- package/elements-web-component.esm.js.map +4 -4
- package/elements.cjs.js +39 -39
- package/elements.cjs.js.map +4 -4
- package/elements.esm.js +39 -39
- package/elements.esm.js.map +4 -4
- package/elements.js +39 -39
- package/elements.js.map +4 -4
- package/index.d.ts +107 -3
- package/index.mjs +13 -13
- package/jsx-react.d.ts +15 -0
- package/jsx-svelte.d.ts +14 -0
- package/jsx-vue.d.ts +14 -0
- package/llms-full.txt +7954 -0
- package/llms.txt +37 -0
- package/loader.js +3 -3
- package/loader.min.js +1 -1
- package/package.json +36 -1
- package/react.d.ts +3575 -0
- package/react.mjs +62 -0
- package/schema.d.ts +56 -0
- package/schema.json +24 -2
- package/schema.mjs +1 -1
- package/web-component.d.ts +1583 -17
- package/web-component.mjs +14 -14
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
# Security & privacy
|
|
2
|
+
|
|
3
|
+
What the widget does with data. How it isolates itself from your
|
|
4
|
+
page. How to make it work with strict CSPs. GDPR + compliance notes.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## TL;DR
|
|
9
|
+
|
|
10
|
+
- **No cookies or fingerprinting.** The default adapter uses `localStorage`; a
|
|
11
|
+
custom synchronous adapter can replace it. Anonymous allowlisted usage events
|
|
12
|
+
use 1×1 pixel requests by default; `tracking.enabled: false`, GPC, or DNT
|
|
13
|
+
disables them.
|
|
14
|
+
- **CSP-compatible.** Small set of directives needed; shown below.
|
|
15
|
+
- **Shadow DOM isolated.** Your site's CSS can't break the widget and
|
|
16
|
+
vice versa.
|
|
17
|
+
- **Optional iframe isolation** for strict threat models.
|
|
18
|
+
- **GDPR-ready.** Your DPA with Help AI covers processing terms.
|
|
19
|
+
- **SOC 2 Type II** certified (contact for report).
|
|
20
|
+
- **No `eval`, no `innerHTML`, no dynamic code.** Source-available so
|
|
21
|
+
you can audit.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## What data the widget collects
|
|
26
|
+
|
|
27
|
+
On every mount + every message, the widget sends to the Help AI API:
|
|
28
|
+
|
|
29
|
+
| Field | Required? | Example |
|
|
30
|
+
| ---------------------- | --------- | ---------------------------------------------------------------------------- |
|
|
31
|
+
| `visitorId` | Auto | `visitor_7a8f2b...` |
|
|
32
|
+
| `conversationId` | Auto | `chat_abc123...` |
|
|
33
|
+
| `userPrefs` | Auto | `{ locale: "fr", soundMuted: false, themeMode: "dark", textSize: "normal" }` |
|
|
34
|
+
| `messages` (user text) | On send | `[{ role: "user", text: "Hello" }]` |
|
|
35
|
+
| `attachments` (if any) | On send | Upload URLs + metadata |
|
|
36
|
+
| `userContext` (if set) | Optional | `{ id, email, name, ...custom }` — informational only |
|
|
37
|
+
| `pageContext` | Auto | `{ url, ...host area/path/title/custom }` — `url` auto-filled |
|
|
38
|
+
| `locale` | Auto | `"en-US"` |
|
|
39
|
+
|
|
40
|
+
When built-in usage tracking is enabled (the default), meaningful widget events
|
|
41
|
+
also send an anonymous 1×1 pixel request to the configured data collector. Each
|
|
42
|
+
request contains an allowlisted event name and scalar dimensions: anonymous
|
|
43
|
+
visitor/conversation and deployment identifiers, page origin + path, referrer
|
|
44
|
+
origin, screen/viewport values, language, timezone, presentation mode, theme,
|
|
45
|
+
and event-specific counts or outcomes. Message text, form values, attachment
|
|
46
|
+
contents, query strings, hashes, and host-provided user identity are not
|
|
47
|
+
included. The host-controlled pathname is included and may itself contain
|
|
48
|
+
identifiers; keep sensitive values out of paths or disable tracking. Set
|
|
49
|
+
`tracking.enabled: false` to stop client-side hits; GPC and DNT override all
|
|
50
|
+
configuration. See Anonymous usage tracking.
|
|
51
|
+
|
|
52
|
+
On every receive (from the backend):
|
|
53
|
+
|
|
54
|
+
- Streaming text responses.
|
|
55
|
+
- Tool calls + results.
|
|
56
|
+
- File references.
|
|
57
|
+
|
|
58
|
+
The widget does **not** collect:
|
|
59
|
+
|
|
60
|
+
- Cookies (it doesn't set any).
|
|
61
|
+
- Explicit client IP or geolocation fields. The analytics collector may derive
|
|
62
|
+
coarse location from the request IP and then discard the IP.
|
|
63
|
+
- Keystroke telemetry.
|
|
64
|
+
- Mouse movements.
|
|
65
|
+
- Page content outside what the user types.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Where data goes
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
┌─────────────────┐
|
|
73
|
+
│ Visitor's │ HTTPS + TLS 1.3
|
|
74
|
+
│ browser │ ────────────────────────→ help.ai
|
|
75
|
+
│ (widget) │
|
|
76
|
+
└─────────────────┘
|
|
77
|
+
│
|
|
78
|
+
↓
|
|
79
|
+
┌─────────────────────┐
|
|
80
|
+
│ Help AI backend │
|
|
81
|
+
│ ↓ │
|
|
82
|
+
│ Your deployment's │
|
|
83
|
+
│ configured model │
|
|
84
|
+
│ provider │
|
|
85
|
+
└─────────────────────┘
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- **Every deployment**: data flows to `help.ai` (the `/api/agent` and `/api/data` modules) and is processed in
|
|
89
|
+
US-based infrastructure.
|
|
90
|
+
- **Self-hosted**: enterprise customers can route via a bring-your-own
|
|
91
|
+
API endpoint (`baseUrl` option).
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Persistence
|
|
96
|
+
|
|
97
|
+
### What the widget stores client-side by default
|
|
98
|
+
|
|
99
|
+
The default adapter stores values in `localStorage` using three scopes. In the
|
|
100
|
+
patterns below, `[.deploymentId]` is omitted when absent and `[.vN]` appears only
|
|
101
|
+
after a handshake-provided storage version takes effect on the next load.
|
|
102
|
+
|
|
103
|
+
| Scope | Key pattern |
|
|
104
|
+
| ------------------- | --------------------------------------------------------------- |
|
|
105
|
+
| Reset lever | `helpai.storageVersion` (stable and never versioned) |
|
|
106
|
+
| Anonymous visitor | `helpai[.vN].visitorId` (shared by every widget and deployment) |
|
|
107
|
+
| Widget + deployment | `helpai.<widgetId>[.deploymentId][.vN].<suffix>` |
|
|
108
|
+
|
|
109
|
+
Complete `ClientStorage` adapter-managed suffix inventory:
|
|
110
|
+
|
|
111
|
+
| Suffix | Value | Lifetime / behavior |
|
|
112
|
+
| ------------------------ | ------------------------------------------------------------------- | ---------------------------------------------- |
|
|
113
|
+
| `conversationId` | Active UUIDv7 | Pointer removed by "New conversation" |
|
|
114
|
+
| `draft.<conversationId>` | Exact unsent textarea text | Removed when accepted/emptied; no built-in TTL |
|
|
115
|
+
| `userPrefs` | JSON: `locale`, `widgetSize`, `soundMuted`, `themeMode`, `textSize` | Cached locally; synchronized to the backend |
|
|
116
|
+
| `panelOpen` | `"0"` / `"1"` | Per-device |
|
|
117
|
+
| `panelSize` | `normal` / `expanded` / `fullscreen` | Per-device |
|
|
118
|
+
| `calloutDismissed` | Exact dismissed callout text | Per-device |
|
|
119
|
+
| `sidebarCollapsed` | `"0"` / `"1"` | Per-device, page mode |
|
|
120
|
+
| `activeModule` | Messenger module id | Per-device |
|
|
121
|
+
| `historyCount` | Last loaded conversation-row count | Per-device skeleton sizing |
|
|
122
|
+
| `formsDone` | JSON map of form id to completion epoch milliseconds | Per-device; survives "New conversation" |
|
|
123
|
+
|
|
124
|
+
`helpai.storageVersion` stores the next namespace version. A changed value
|
|
125
|
+
abandons the prior versioned namespace on the following load; it does not
|
|
126
|
+
migrate or delete old keys.
|
|
127
|
+
|
|
128
|
+
The runtime logger is separate from `ClientStorage`: explicitly changing the
|
|
129
|
+
debug level persists the brand-global `helpai.debug` key directly in
|
|
130
|
+
`localStorage` so diagnostics survive reloads. The default warning level does
|
|
131
|
+
not create that key. A custom storage adapter does not intercept it.
|
|
132
|
+
|
|
133
|
+
Adapter-managed persistence does not use `Conversation`, `IndexedDB`, `Cache`
|
|
134
|
+
storage, or a `ServiceWorker`. It sets no cookies.
|
|
135
|
+
|
|
136
|
+
The default adapter stores unsent draft text as plaintext readable by other
|
|
137
|
+
same-origin scripts. The widget does not include that text in Help AI network
|
|
138
|
+
requests until the visitor sends it, and it applies no draft TTL. Customers with
|
|
139
|
+
stricter requirements should pass an encrypted or in-memory adapter; an
|
|
140
|
+
in-memory adapter intentionally gives up draft restoration after reload.
|
|
141
|
+
|
|
142
|
+
### Default-adapter scope
|
|
143
|
+
|
|
144
|
+
| Data | Per-device | Cross-device |
|
|
145
|
+
| ---------------------- | --------------------------- | ----------------------------------------------------------- |
|
|
146
|
+
| `visitorId` | ✓ (minted once per browser) |
|
|
147
|
+
| `conversationId` | ✓ | |
|
|
148
|
+
| Unsent text draft | ✓ | |
|
|
149
|
+
| Messages | | ✓ (server is authoritative; `loadConversation` re-hydrates) |
|
|
150
|
+
| Locale preference | | ✓ (via `userPrefs`) |
|
|
151
|
+
| Theme preference | | ✓ |
|
|
152
|
+
| Sound mute state | | ✓ |
|
|
153
|
+
| Text size preference | | ✓ |
|
|
154
|
+
| Drag-resized size | | ✓ |
|
|
155
|
+
| Panel open / closed | ✓ | |
|
|
156
|
+
| Panel size tier | ✓ | |
|
|
157
|
+
| Hint dismissed | ✓ | |
|
|
158
|
+
| Sidebar state | ✓ | |
|
|
159
|
+
| Active module | ✓ | |
|
|
160
|
+
| History skeleton count | ✓ | |
|
|
161
|
+
| One-time form markers | ✓ | |
|
|
162
|
+
|
|
163
|
+
Cross-device sync uses the same `visitorId` — so if the user has the
|
|
164
|
+
widget on multiple tabs of the same browser, they sync. Across
|
|
165
|
+
different browsers / devices, the visitor is separate. `userContext.id`
|
|
166
|
+
is informational only — it does not tie conversations together server-side.
|
|
167
|
+
|
|
168
|
+
### Custom storage
|
|
169
|
+
|
|
170
|
+
You can swap `localStorage` for any synchronous adapter:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
const storage = {
|
|
174
|
+
get: (key: string) => sessionStorage.getItem(key),
|
|
175
|
+
set: (key: string, value: string) => sessionStorage.setItem(key, value),
|
|
176
|
+
remove: (key: string) => sessionStorage.removeItem(key),
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
init({
|
|
180
|
+
publicKey: '...',
|
|
181
|
+
aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000',
|
|
182
|
+
storage,
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Custom adapters receive the dynamic `draft.<conversationId>` keys and exact
|
|
187
|
+
unsent text values in addition to identity and UI state, so adapter code owns
|
|
188
|
+
their storage destination, retention, and privacy behavior. Use cases include
|
|
189
|
+
encrypted `localStorage`, `sessionStorage`, and in-memory consent fallbacks.
|
|
190
|
+
|
|
191
|
+
[Recipes § Custom storage](./recipes.md#custom-storage-adapter).
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Shadow DOM + iframe
|
|
196
|
+
|
|
197
|
+
### Shadow DOM (always on)
|
|
198
|
+
|
|
199
|
+
Every widget renders inside an open shadow root attached to a custom
|
|
200
|
+
element (`<helpai-chat-host>` or `<web-ai-chat>`). Consequences:
|
|
201
|
+
|
|
202
|
+
- **Your CSS can't reach in.** `body * { ... !important }` won't
|
|
203
|
+
style the widget's internals.
|
|
204
|
+
- **The widget's CSS can't leak out.** We don't pollute your `<body>`.
|
|
205
|
+
- **Events bubble through** (by W3C spec). Your `document.addEventListener('click', ...)`
|
|
206
|
+
handlers still see clicks that happen inside the widget — useful
|
|
207
|
+
for analytics, doesn't affect isolation.
|
|
208
|
+
|
|
209
|
+
The widget sets `all: initial` on the shadow host element to neutralize
|
|
210
|
+
inherited styles from hostile host pages.
|
|
211
|
+
|
|
212
|
+
### Iframe isolation (opt-in)
|
|
213
|
+
|
|
214
|
+
For host pages with aggressive JS (monkey-patching `window.fetch`,
|
|
215
|
+
replacing `console.log`, etc.):
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
init({ publicKey: '...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000', presentation: { iframe: true } });
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
This wraps the widget in a same-origin iframe. The widget's JS runs
|
|
222
|
+
in the iframe's realm — parent-page globals don't affect it.
|
|
223
|
+
|
|
224
|
+
Composes with every `presentation.mode`:
|
|
225
|
+
|
|
226
|
+
- `presentation: { mode: "floating", iframe: true }` → floating iframe in the corner.
|
|
227
|
+
- `presentation: { mode: "inline", iframe: true }` → iframe fills your container.
|
|
228
|
+
- `presentation: { mode: "standalone", iframe: true }` → fullscreen iframe.
|
|
229
|
+
|
|
230
|
+
When to enable:
|
|
231
|
+
|
|
232
|
+
- Host page monkey-patches `window.fetch` or `XMLHttpRequest`.
|
|
233
|
+
- Host page attaches listeners that swallow widget events.
|
|
234
|
+
- Compliance review requires "no access to parent page globals".
|
|
235
|
+
- You embed the widget on pages you don't fully control.
|
|
236
|
+
|
|
237
|
+
Not needed for 95% of deployments — shadow DOM alone is sufficient.
|
|
238
|
+
|
|
239
|
+
### In-widget content iframe sandbox (messenger modules)
|
|
240
|
+
|
|
241
|
+
This is a **different** iframe from the runtime-isolation wrapper above.
|
|
242
|
+
When the messenger renders remote help articles, status pages, or any
|
|
243
|
+
`iframe`-action / `embed` block, it shows that remote URL inside a
|
|
244
|
+
**heavily sandboxed iframe within the widget** (`src/ui/iframe-view.tsx`):
|
|
245
|
+
|
|
246
|
+
- **Article / status drill-ins** (`IframeView`):
|
|
247
|
+
```
|
|
248
|
+
sandbox="allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads"
|
|
249
|
+
referrerpolicy="no-referrer"
|
|
250
|
+
```
|
|
251
|
+
- **`embed` blocks** use a **narrower** sandbox — the same set **minus**
|
|
252
|
+
`allow-downloads`.
|
|
253
|
+
|
|
254
|
+
The critical omission in both is `allow-same-origin`. Without it, a
|
|
255
|
+
sandboxed page is forced into an opaque origin and **cannot script the
|
|
256
|
+
embedder, read its DOM, or touch its storage** — even if the remote URL
|
|
257
|
+
happens to be same-origin with the host. `referrerpolicy="no-referrer"`
|
|
258
|
+
also strips the referrer from those requests.
|
|
259
|
+
|
|
260
|
+
**Article / status bodies are never fetched or inlined.** The widget only
|
|
261
|
+
ever renders them as sandboxed _remote URLs_ — it does not download HTML
|
|
262
|
+
and inject it. (Rich text that the backend controls travels as a content row's
|
|
263
|
+
markdown `content` and is rendered DOM-safely, never via `innerHTML`.) A row
|
|
264
|
+
opens as an iframe only when it has a `url`; otherwise its markdown renders in
|
|
265
|
+
the native reader. See the content model.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Content Security Policy
|
|
270
|
+
|
|
271
|
+
The widget needs these directives to run under a strict CSP:
|
|
272
|
+
|
|
273
|
+
```
|
|
274
|
+
default-src 'self';
|
|
275
|
+
script-src 'self' https://cdn.help.ai;
|
|
276
|
+
connect-src 'self' https://help.ai;
|
|
277
|
+
style-src 'self' 'unsafe-inline';
|
|
278
|
+
img-src 'self' data: blob: https:;
|
|
279
|
+
font-src 'self' https:;
|
|
280
|
+
media-src 'self' blob:;
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Why each is needed
|
|
284
|
+
|
|
285
|
+
- `script-src https://cdn.help.ai` — the widget JS is hosted on
|
|
286
|
+
our CDN. If you self-host, use `'self'` or your CDN.
|
|
287
|
+
- `connect-src https://help.ai` — the widget fetches from our API
|
|
288
|
+
(`/api/agent` + `/api/data` on the main origin).
|
|
289
|
+
- `style-src 'unsafe-inline'` — the widget's CSS is adopted via
|
|
290
|
+
constructable stylesheets + an inline fallback. `'unsafe-inline'`
|
|
291
|
+
is safe when scoped inside shadow DOM — host pages can't inject
|
|
292
|
+
new styles there.
|
|
293
|
+
- `img-src data: blob: https:` — inline SVG icons (`data:`), uploaded
|
|
294
|
+
attachment previews (`blob:`), and avatar / attachment URLs.
|
|
295
|
+
- `font-src https:` — if you use a custom `fontFamily` that loads
|
|
296
|
+
web fonts.
|
|
297
|
+
- `media-src blob:` — voice recording uses `MediaRecorder` + `blob:`
|
|
298
|
+
URLs.
|
|
299
|
+
|
|
300
|
+
### Strict CSP + nonces
|
|
301
|
+
|
|
302
|
+
If your CSP uses nonces, the widget's shadow DOM CSS doesn't need one
|
|
303
|
+
(it's not injected into the main document). But the bootstrap
|
|
304
|
+
`<script>` tag does:
|
|
305
|
+
|
|
306
|
+
```html
|
|
307
|
+
<script src="https://cdn.help.ai/sdk/js/elements/latest/elements.js" data-public-key="agent_pk_..." data-ai-agent-deployment-id="01900000-0000-7000-8000-000000000000" nonce="{{ csp_nonce }}" defer></script>
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Adjust `script-src` to allow the nonce instead of `'unsafe-inline'`.
|
|
311
|
+
|
|
312
|
+
### CSP report-only testing
|
|
313
|
+
|
|
314
|
+
Before enforcing:
|
|
315
|
+
|
|
316
|
+
```
|
|
317
|
+
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' https://cdn.help.ai; ...
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Reports land in your DevTools console + your `report-uri` endpoint
|
|
321
|
+
without blocking anything.
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## Subresource Integrity (SRI)
|
|
326
|
+
|
|
327
|
+
Pin the bundle to a specific hash:
|
|
328
|
+
|
|
329
|
+
```html
|
|
330
|
+
<script src="https://cdn.help.ai/v1.2.0/elements.js" integrity="sha384-..." crossorigin="anonymous" data-public-key="agent_pk_..." data-ai-agent-deployment-id="01900000-0000-7000-8000-000000000000" defer></script>
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
We publish SRI hashes with each release in the
|
|
334
|
+
[changelog](https://github.com/help-ai/elements/releases). Only
|
|
335
|
+
the pinned version URL (`/v1.2.0/`) supports SRI — the floating URL
|
|
336
|
+
(`/elements.js`) doesn't, by design.
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## Authentication
|
|
341
|
+
|
|
342
|
+
Browser access is scoped by `aiAgentDeploymentId`; a `publicKey` adds
|
|
343
|
+
workspace authorization. Visitors are anonymous unless your server signs a
|
|
344
|
+
[user token](#verified-users-usertoken) for your logged-in user.
|
|
345
|
+
|
|
346
|
+
### public key (default)
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
init({ publicKey: 'agent_pk_...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000' });
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Sent as the `x-public-key` header. Your public key authorizes access
|
|
353
|
+
to your **public** widget configuration — it's designed to be safe to
|
|
354
|
+
expose client-side. Think of it like a Stripe publishable key
|
|
355
|
+
(`pk_live_...`).
|
|
356
|
+
|
|
357
|
+
What your public key does NOT grant:
|
|
358
|
+
|
|
359
|
+
- Access to other customers' data.
|
|
360
|
+
- Write access to your deployment's config (that's gated by your
|
|
361
|
+
dashboard conversation).
|
|
362
|
+
- Server-side admin APIs.
|
|
363
|
+
|
|
364
|
+
### guest
|
|
365
|
+
|
|
366
|
+
Omit `publicKey` but keep `aiAgentDeploymentId`; the widget connects to
|
|
367
|
+
that public deployment as a guest without an auth header.
|
|
368
|
+
|
|
369
|
+
### Verified users (`userToken`)
|
|
370
|
+
|
|
371
|
+
To show a logged-in user their own conversations on any device, and to let the AI rely on who they are, your
|
|
372
|
+
server signs a short-lived JWT for them with the deployment's **secret key** (on the deployment's Install page;
|
|
373
|
+
never put it in the browser):
|
|
374
|
+
|
|
375
|
+
| Claim | Required | What it is |
|
|
376
|
+
| ------- | -------- | ---------------------------------------------------------- |
|
|
377
|
+
| `sub` | yes | Your own id for the user (a string, up to 255 characters) |
|
|
378
|
+
| `exp` | yes | Expiry, in seconds since the epoch; keep it short (1 hour) |
|
|
379
|
+
| `name` | no | Their name, given to the AI as a verified fact |
|
|
380
|
+
| `email` | no | Their email, given to the AI as a verified fact |
|
|
381
|
+
|
|
382
|
+
Sign it with HS256:
|
|
383
|
+
|
|
384
|
+
```js
|
|
385
|
+
// Node (jose)
|
|
386
|
+
import { SignJWT } from 'jose';
|
|
387
|
+
|
|
388
|
+
const secret = new TextEncoder().encode(process.env.HELPAI_SECRET_KEY);
|
|
389
|
+
const userToken = await new SignJWT({ name: user.name, email: user.email }).setProtectedHeader({ alg: 'HS256' }).setSubject(String(user.id)).setExpirationTime('1h').sign(secret);
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
```python
|
|
393
|
+
# Python (PyJWT)
|
|
394
|
+
import os, time, jwt
|
|
395
|
+
|
|
396
|
+
user_token = jwt.encode({"sub": str(user.id), "exp": int(time.time()) + 3600, "name": user.name, "email": user.email}, os.environ["HELPAI_SECRET_KEY"], algorithm="HS256")
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
```php
|
|
400
|
+
// PHP (firebase/php-jwt)
|
|
401
|
+
use Firebase\JWT\JWT;
|
|
402
|
+
|
|
403
|
+
$userToken = JWT::encode(['sub' => (string) $user->id, 'exp' => time() + 3600, 'name' => $user->name, 'email' => $user->email], getenv('HELPAI_SECRET_KEY'), 'HS256');
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Serve it from an endpoint only your signed-in user can reach, and hand the widget a function that fetches it:
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
const userToken = () => fetch('/api/help-ai-token').then((res) => res.text());
|
|
410
|
+
|
|
411
|
+
init({ publicKey: 'agent_pk_...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000', userToken }); // signed in at load
|
|
412
|
+
chat.login(userToken); // or after your sign-in (`HelpAI('login', userToken)`, `<HelpAI userToken={userToken} />`)
|
|
413
|
+
chat.reset(); // on sign-out (`HelpAI('reset')`)
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
- The widget sends the token only to the agent API, as the `x-user-token` header. The function is called when a token
|
|
417
|
+
is first needed and again when the server answers `401 USER_TOKEN_INVALID` (expired or rejected); the request is
|
|
418
|
+
then sent once more. A fixed string can't be refreshed, so an expired one fails until you call `login()` again.
|
|
419
|
+
- A token the server can't verify is refused; it never falls back to an anonymous visitor.
|
|
420
|
+
- Signing in moves the visitor's conversations on that browser to the user. From then on they reach them on any device
|
|
421
|
+
where they're signed in, and nobody else does, the signed-out browser included.
|
|
422
|
+
- Signing out (`reset()`, or dropping the token) or switching to another user starts over as a new visitor: nothing of
|
|
423
|
+
the previous user stays in the browser.
|
|
424
|
+
- Keep the function's identity stable for one user (a module function, or `useCallback` keyed on your user id): a
|
|
425
|
+
different function counts as a different user.
|
|
426
|
+
|
|
427
|
+
### user / page context is NOT auth
|
|
428
|
+
|
|
429
|
+
```ts
|
|
430
|
+
init({
|
|
431
|
+
publicKey: 'agent_pk_...',
|
|
432
|
+
aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000',
|
|
433
|
+
userContext: { id: '42', email: 'jane@help.ai', name: 'Jane' },
|
|
434
|
+
pageContext: { area: 'support' },
|
|
435
|
+
});
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
`userContext` and `pageContext` are **untrusted, host-asserted
|
|
439
|
+
information for the AI** — never an authentication credential. The
|
|
440
|
+
backend does not register or authenticate these end users. The `id`
|
|
441
|
+
field is a string you provide, not a cryptographic identity. Both
|
|
442
|
+
objects are sanitized client-side before they leave the page (reserved
|
|
443
|
+
auth-like keys such as `jwt` / `token` / `authorization` / `password`
|
|
444
|
+
are stripped). Use them for personalization and context only.
|
|
445
|
+
|
|
446
|
+
#### Trust zones
|
|
447
|
+
|
|
448
|
+
```mermaid
|
|
449
|
+
flowchart TB
|
|
450
|
+
subgraph Host["Host site — example.com (untrusted by backend)"]
|
|
451
|
+
direction TB
|
|
452
|
+
H1["Asserts userContext / pageContext<br/>(who/where the visitor is)"]
|
|
453
|
+
H2["Has its OWN auth — some sites log users in,<br/>some are guest-only"]
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
subgraph Widget["Widget — runs in the visitor's browser"]
|
|
457
|
+
direction TB
|
|
458
|
+
W1["Mints + owns anonymous visitorId / conversationId"]
|
|
459
|
+
W2["Sanitizes context client-side<br/>(strips jwt/token/password/…)"]
|
|
460
|
+
W3["Encodes the context field<br/>base64url({ user, page }) on every request"]
|
|
461
|
+
W4["Authenticates to backend via<br/>x-public-key + x-ai-agent-deployment-id"]
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
subgraph Backend["Help AI backend"]
|
|
465
|
+
direction TB
|
|
466
|
+
B1["Authenticates the HOST's agent deployment<br/>(NOT the end user)"]
|
|
467
|
+
B2["Treats userContext / pageContext as<br/>informational + untrusted"]
|
|
468
|
+
B3["Never registers end users; verifies a signed userToken when the host sends one"]
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
Host -->|"context (untrusted)"| Widget
|
|
472
|
+
Widget -->|"HTTPS/TLS + headers + context field"| Backend
|
|
473
|
+
|
|
474
|
+
note["publicKey + aiAgentDeploymentId auth the host's agent; only a userToken signed by the host's server identifies the end user."]
|
|
475
|
+
Backend -.-> note
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
#### The `context` field — base64url, obfuscation not encryption
|
|
479
|
+
|
|
480
|
+
`userContext` + `pageContext` ride **every** request as one `context` field
|
|
481
|
+
= `base64url(JSON.stringify({ user, page }))` (JSON body for POSTs, form field
|
|
482
|
+
for multipart POSTs, `?context=` for GETs) — alongside the required `visitorId`
|
|
483
|
+
/ `conversationId` identity. base64url keeps PII (email / phone) out of plaintext server
|
|
484
|
+
access logs, the devtools network tab, and analytics proxies — but it is
|
|
485
|
+
**obfuscation, not encryption**: TLS already protects the bytes in transit,
|
|
486
|
+
and anyone can base64url-decode it. Never put a secret in context.
|
|
487
|
+
|
|
488
|
+
### Key rotation
|
|
489
|
+
|
|
490
|
+
- **public keys** — rotate from the dashboard. Old keys are deactivated
|
|
491
|
+
instantly; update your embeds.
|
|
492
|
+
- **secret key** — user tokens signed with a replaced secret key stop verifying, so every verified user is signed out
|
|
493
|
+
until your server signs new ones with the new key.
|
|
494
|
+
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
## GDPR + regional compliance
|
|
498
|
+
|
|
499
|
+
### Data processing agreement (DPA)
|
|
500
|
+
|
|
501
|
+
Help AI is the data processor; you are the data controller. Our DPA
|
|
502
|
+
is available on request and covers:
|
|
503
|
+
|
|
504
|
+
- Processing purposes.
|
|
505
|
+
- Sub-processor list.
|
|
506
|
+
- Data transfer mechanisms (SCCs).
|
|
507
|
+
- Deletion / return on contract termination.
|
|
508
|
+
|
|
509
|
+
### Consent management
|
|
510
|
+
|
|
511
|
+
The widget doesn't set cookies, but depending on your jurisdiction +
|
|
512
|
+
how you use `userContext`, you may need consent. Common patterns:
|
|
513
|
+
|
|
514
|
+
#### Pattern A — only mount after consent
|
|
515
|
+
|
|
516
|
+
```ts
|
|
517
|
+
window.addEventListener('consent-accepted', () => {
|
|
518
|
+
init({ publicKey: '...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000' });
|
|
519
|
+
});
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
No chat available until consent. Simple, strict.
|
|
523
|
+
|
|
524
|
+
#### Pattern B — memory-only without consent
|
|
525
|
+
|
|
526
|
+
```ts
|
|
527
|
+
const memoryAdapter = /* see recipes */;
|
|
528
|
+
|
|
529
|
+
init({
|
|
530
|
+
publicKey: '...',
|
|
531
|
+
aiAgentDeploymentId: "01900000-0000-7000-8000-000000000000",
|
|
532
|
+
storage: consentGiven ? undefined : memoryAdapter,
|
|
533
|
+
});
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
Widget works without consent but state evaporates on tab close.
|
|
537
|
+
|
|
538
|
+
#### Pattern C — annotate visitors as consented
|
|
539
|
+
|
|
540
|
+
```ts
|
|
541
|
+
init({
|
|
542
|
+
publicKey: '...',
|
|
543
|
+
aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000',
|
|
544
|
+
userContext: consentGiven ? { id: userId } : undefined,
|
|
545
|
+
});
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
Anonymous (no user context) when no consent; context attached with
|
|
549
|
+
consent.
|
|
550
|
+
|
|
551
|
+
### Data subject requests
|
|
552
|
+
|
|
553
|
+
- **Access**: visitors can view their chat thread in the widget
|
|
554
|
+
itself. For server-side exports, contact [support@help.ai](mailto:support@help.ai)
|
|
555
|
+
with the `visitorId` or `userContext.id`.
|
|
556
|
+
- **Deletion**: clicking "New conversation" orphans the old thread
|
|
557
|
+
from the client. For permanent deletion: contact support.
|
|
558
|
+
- **Portability**: export via server-side API. See dashboard → API docs.
|
|
559
|
+
|
|
560
|
+
### Region
|
|
561
|
+
|
|
562
|
+
Every deployment uses one data plane: `help.ai`, in the US (AWS us-east-1, us-west-2).
|
|
563
|
+
|
|
564
|
+
---
|
|
565
|
+
|
|
566
|
+
## Security audits
|
|
567
|
+
|
|
568
|
+
- **SOC 2 Type II** — annual. Report available on request
|
|
569
|
+
(NDA required).
|
|
570
|
+
- **Pen testing** — quarterly external pen tests.
|
|
571
|
+
- **Dependency audit** — `bun audit` on every CI run. No
|
|
572
|
+
high-severity vulns allowed in releases.
|
|
573
|
+
- **Bug bounty** — [help.ai/security](https://help.ai/security) — up
|
|
574
|
+
to $10,000 for critical reports.
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
## What the widget can / can't do on your page
|
|
579
|
+
|
|
580
|
+
**Can do:**
|
|
581
|
+
|
|
582
|
+
- Render UI in its own shadow root.
|
|
583
|
+
- Listen to its own keyboard + pointer events.
|
|
584
|
+
- Store data under `helpai.*` localStorage keys.
|
|
585
|
+
- Fetch from the configured `baseUrl`.
|
|
586
|
+
- Record audio (with user permission) for voice input.
|
|
587
|
+
- Upload files the user attaches.
|
|
588
|
+
|
|
589
|
+
**Can't do:**
|
|
590
|
+
|
|
591
|
+
- Read or set cookies.
|
|
592
|
+
- Modify your page's DOM outside its own shadow root.
|
|
593
|
+
- Read your localStorage keys that don't start with `helpai.`.
|
|
594
|
+
- Intercept your form submissions.
|
|
595
|
+
- Mine cryptocurrency / run unrelated scripts.
|
|
596
|
+
- Navigate your page.
|
|
597
|
+
- Access `iframe` content across origins.
|
|
598
|
+
|
|
599
|
+
The source is available for audit — `dist/help-ai/elements.js`
|
|
600
|
+
is the production bundle; `src/*` is the un-minified source.
|
|
601
|
+
|
|
602
|
+
---
|
|
603
|
+
|
|
604
|
+
## Supply chain
|
|
605
|
+
|
|
606
|
+
- **Bundled runtime dependencies** are pinned in `package.json` and `bun.lock`.
|
|
607
|
+
- **No third-party analytics** in the widget itself. Anything you
|
|
608
|
+
wire via hooks is your choice.
|
|
609
|
+
- **No CDN tracking**. We don't use CloudFlare Workers, Google
|
|
610
|
+
Tag Manager, or any third-party on our CDN path. Direct from our
|
|
611
|
+
infrastructure.
|
|
612
|
+
|
|
613
|
+
---
|
|
614
|
+
|
|
615
|
+
## Outage + incident response
|
|
616
|
+
|
|
617
|
+
- **Status page**: [help.ai/status](https://help.ai/status).
|
|
618
|
+
- **Incident notifications**: subscribe via the status page.
|
|
619
|
+
- **Degraded mode**: if the API is down, the widget still renders
|
|
620
|
+
(the bundle is cached). Attempting to send shows an error. Cached
|
|
621
|
+
chats remain viewable.
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
## Reporting a security issue
|
|
626
|
+
|
|
627
|
+
Please don't file public issues for vulnerabilities. Email
|
|
628
|
+
[security@help.ai](mailto:security@help.ai) with:
|
|
629
|
+
|
|
630
|
+
- The affected version.
|
|
631
|
+
- Steps to reproduce.
|
|
632
|
+
- The potential impact.
|
|
633
|
+
- PGP key: `0xCAFEBABE...` (download from
|
|
634
|
+
[help.ai/.well-known/security.asc](https://help.ai/.well-known/security.asc)).
|
|
635
|
+
|
|
636
|
+
We respond within 24 hours and coordinate disclosure on your schedule.
|