@helpai/elements 1.9.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.
@@ -139,6 +139,7 @@ interface ChatWidgetInstance {
139
139
 
140
140
  // Lifecycle
141
141
  reset(): void; // start over as a new anonymous visitor (on logout)
142
+ login(userToken: UserTokenSource): void; // sign the visitor in as your logged-in user (a signed token, or a function returning one)
142
143
  destroy(): void;
143
144
 
144
145
  // Events
@@ -332,6 +333,20 @@ visitor, their current conversation (draft and queued messages included), prefer
332
333
  `setUserContext`, then reconnects as a first visit. Listeners added with `on()` stay, and the handle keeps working.
333
334
  On `<web-ai-chat>`, also remove a `user-context` attribute you set.
334
335
 
336
+ ### `login(userToken)`
337
+
338
+ Signs the visitor in as your logged-in user. `userToken` is a JWT your server signs with the deployment's secret key,
339
+ or a function returning a fresh one (called again when the server rejects the current token as expired). The widget
340
+ reconnects as that user: their conversations on this browser move to them, and they reach their conversations on any
341
+ device where they're signed in. Call `reset()` when they sign out; switching to another user's token starts over the
342
+ same way. Also `HelpAI('login', userToken)`, `element.login(userToken)` on `<web-ai-chat>` (not an attribute, so the
343
+ token stays out of the DOM), the `userToken` option at `init()`, and `<HelpAI userToken={…} />`. Signing and the
344
+ guarantees: [Security → Verified users](./security-privacy.md#verified-users-usertoken).
345
+
346
+ ```ts
347
+ chat.login(() => fetch('/api/help-ai-token').then((res) => res.text()));
348
+ ```
349
+
335
350
  ---
336
351
 
337
352
  ## Events
@@ -102,6 +102,19 @@ Override if you use:
102
102
  - A custom domain (`your-company.com`).
103
103
  - A local dev backend.
104
104
 
105
+ ### `userToken`
106
+
107
+ `string | () => string | Promise<string>` · default: none (anonymous)
108
+
109
+ Signs the visitor in as your logged-in user: a JWT your server signs with the deployment's secret key, or a function
110
+ returning a fresh one. Their conversations then follow them to any device, and the AI gets their name and email as
111
+ verified facts. Runtime-only: not an attribute or `data-*` option, so the token stays out of the page's markup. See
112
+ [Security → Verified users](./security-privacy.md#verified-users-usertoken) for signing it.
113
+
114
+ ```ts
115
+ init({ publicKey: 'agent_pk_...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000', userToken: () => fetch('/api/help-ai-token').then((res) => res.text()) });
116
+ ```
117
+
105
118
  ### `userContext`
106
119
 
107
120
  `object | undefined`
@@ -97,6 +97,9 @@ function PricingHelp() {
97
97
  - **One import path.** The kit carries the whole widget API (`getInstance`, `hook`, the types), so import everything
98
98
  from `@helpai/elements/react`; the package's other entries are separate copies with their own widgets.
99
99
  - **Two widgets** on one page: give each `<HelpAI widgetId='support' … />` and read it with `useHelpAI('support')`.
100
+ - **Signed-in users:** pass `userToken`, a function that fetches the token your server signs
101
+ ([Verified users](./security-privacy.md#verified-users-usertoken)). Keep it stable for one user (a module function, or
102
+ `useCallback` keyed on your user id): a different function signs in afresh, and dropping it signs out.
100
103
 
101
104
  The kit bundles the widget and imports only React from your app, so you don't need Preact installed.
102
105
 
@@ -340,7 +340,8 @@ the pinned version URL (`/v1.2.0/`) supports SRI — the floating URL
340
340
  ## Authentication
341
341
 
342
342
  Browser access is scoped by `aiAgentDeploymentId`; a `publicKey` adds
343
- workspace authorization. There is **no per-end-user token.**
343
+ workspace authorization. Visitors are anonymous unless your server signs a
344
+ [user token](#verified-users-usertoken) for your logged-in user.
344
345
 
345
346
  ### public key (default)
346
347
 
@@ -365,6 +366,64 @@ What your public key does NOT grant:
365
366
  Omit `publicKey` but keep `aiAgentDeploymentId`; the widget connects to
366
367
  that public deployment as a guest without an auth header.
367
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
+
368
427
  ### user / page context is NOT auth
369
428
 
370
429
  ```ts
@@ -406,13 +465,13 @@ flowchart TB
406
465
  direction TB
407
466
  B1["Authenticates the HOST's agent deployment<br/>(NOT the end user)"]
408
467
  B2["Treats userContext / pageContext as<br/>informational + untrusted"]
409
- B3["Never registers / authenticates end users"]
468
+ B3["Never registers end users; verifies a signed userToken when the host sends one"]
410
469
  end
411
470
 
412
471
  Host -->|"context (untrusted)"| Widget
413
472
  Widget -->|"HTTPS/TLS + headers + context field"| Backend
414
473
 
415
- note["publicKey + aiAgentDeploymentId auth the host's agent, NOT the end user — no end-user token anywhere."]
474
+ note["publicKey + aiAgentDeploymentId auth the host's agent; only a userToken signed by the host's server identifies the end user."]
416
475
  Backend -.-> note
417
476
  ```
418
477
 
@@ -430,6 +489,8 @@ and anyone can base64url-decode it. Never put a secret in context.
430
489
 
431
490
  - **public keys** — rotate from the dashboard. Old keys are deactivated
432
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.
433
494
 
434
495
  ---
435
496