From 31fcca1847c9ca50e1e7e11a29b29e482e4e3b46 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 10:13:00 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=A6=8B=20New=20version=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/identity-persistence-consent.md | 21 --------------------- CHANGELOG.md | 22 ++++++++++++++++++++++ package.json | 2 +- 3 files changed, 23 insertions(+), 22 deletions(-) delete mode 100644 .changeset/identity-persistence-consent.md diff --git a/.changeset/identity-persistence-consent.md b/.changeset/identity-persistence-consent.md deleted file mode 100644 index d7e94d6..0000000 --- a/.changeset/identity-persistence-consent.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@smooai/chat-widget': minor ---- - -Identity, persistence & consent client layer (ADR-048) — same-session resume, cross-device "restore my chats", marketing consent, and a stable browser fingerprint. - -**Persisted state (Zustand).** A framework-agnostic `zustand/vanilla` store with the `persist` middleware now keeps a small per-agent blob in localStorage (`smoo-chat-widget:`): the session **pointer**, visitor identity (name/email/phone), marketing consent, a verified email, and the browser fingerprint. The **transcript is never persisted** — the smooth-operator server stays the source of truth and history is re-hydrated via `get_conversation_messages`. A `version` field drives `persist.migrate` so future shape changes upgrade old blobs in place; the storage adapter tolerates missing/locked-down localStorage (SSR, privacy mode) and never throws on boot. - -**Browser fingerprint.** Every `create_conversation_session` now carries a stable `browserFingerprint` for anonymous-visitor correlation (and server-side CRM matching). Computed once and cached in the persisted store. Rather than pull in ThumbmarkJS — tens of KB and async device-probing, too heavy for an embed whose whole point is staying out of the host's LCP/TBT budget — the fingerprint is a persisted random UUID (the exact same-browser correlator) suffixed with a small FNV hash of a few non-invasive, stable signals (UA, language, timezone, screen). No canvas/WebGL/audio probes, no network, XSS-safe. Tradeoff: weaker cross-storage matching than a full device fingerprint, deferred to the server resolver, in exchange for a tiny, transparent, privacy-light token. - -**Same-session resume.** On load, if a session pointer is persisted the widget calls `get_session`; when the session isn't `ended` it reuses the `sessionId`, replays history (`get_conversation_messages`, newest-first → reversed to chronological), skips the pre-chat form, and continues the conversation. An ended/404 session clears **only** the pointer (identity & consent survive) and starts fresh. - -**Returning-visitor resume by fingerprint (HTTP).** When there is no persisted pointer, the widget first `POST`s `/internal/resume-by-fingerprint` on the chat-ws wrapper with the browser fingerprint; if the wrapper resolves (and primes) a recent session it returns `{ resumable: true, sessionId, … }` and the widget adopts that session (then `get_session` + `get_conversation_messages` to hydrate) instead of creating a new one. `{ resumable: false }` falls through to a normal create. - -**Pre-chat form: phone + marketing consent.** The phone field is now shown by default (optional; rides session `metadata.userPhone`). Two explicit, default-unchecked consent checkboxes (email + SMS) capture marketing opt-in; ticking one stamps a `consentAt` ISO timestamp, and the consent record (`{ emailOptIn, smsOptIn, consentSource: 'chat-widget-prechat', consentAt }`) threads into the session metadata. New config flags: `collectPhone`, `collectConsent`, `allowChatRestore` (all default `true`). - -**Cross-device "Restore my chats."** An explicit footer affordance (not a mid-turn agent pause) runs the identity OTP flow over the chat-ws wrapper's HTTP routes — `POST /internal/identity/request-otp` → `verify-otp` → `resolve` — reusing the existing OTP UI. On a resolved list the visitor picks a conversation to replay (`get_session` + `get_conversation_messages`); the verified email is persisted. - -**HTTP, not WS frames.** The smooth-operator engine (1.8.0) owns the `/ws` dispatch and rejects unknown verbs, so the cross-device identity flow and fingerprint resume are `fetch()` (POST, JSON) calls to the chat-ws wrapper, with the HTTP base derived from the WS endpoint (`wss://ai.smoo.ai/ws` → `https://ai.smoo.ai`). The browser sends `Origin` automatically (origin-allowlisted server-side) and each request carries `agentId`/`agentName` plus an optional pre-auth `authContext` (`{ userId, signature, timestamp }`) from the new `authContext` config option. - -All server-supplied strings (masked destinations, conversation previews, history) are rendered via `textContent`, keeping the 0.6.0 XSS guarantees intact, and the new UI follows the Aurora-Glass styling. diff --git a/CHANGELOG.md b/CHANGELOG.md index c8ff749..ce187b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## 0.7.0 + +### Minor Changes + +- 9ea1dfc: Identity, persistence & consent client layer (ADR-048) — same-session resume, cross-device "restore my chats", marketing consent, and a stable browser fingerprint. + + **Persisted state (Zustand).** A framework-agnostic `zustand/vanilla` store with the `persist` middleware now keeps a small per-agent blob in localStorage (`smoo-chat-widget:`): the session **pointer**, visitor identity (name/email/phone), marketing consent, a verified email, and the browser fingerprint. The **transcript is never persisted** — the smooth-operator server stays the source of truth and history is re-hydrated via `get_conversation_messages`. A `version` field drives `persist.migrate` so future shape changes upgrade old blobs in place; the storage adapter tolerates missing/locked-down localStorage (SSR, privacy mode) and never throws on boot. + + **Browser fingerprint.** Every `create_conversation_session` now carries a stable `browserFingerprint` for anonymous-visitor correlation (and server-side CRM matching). Computed once and cached in the persisted store. Rather than pull in ThumbmarkJS — tens of KB and async device-probing, too heavy for an embed whose whole point is staying out of the host's LCP/TBT budget — the fingerprint is a persisted random UUID (the exact same-browser correlator) suffixed with a small FNV hash of a few non-invasive, stable signals (UA, language, timezone, screen). No canvas/WebGL/audio probes, no network, XSS-safe. Tradeoff: weaker cross-storage matching than a full device fingerprint, deferred to the server resolver, in exchange for a tiny, transparent, privacy-light token. + + **Same-session resume.** On load, if a session pointer is persisted the widget calls `get_session`; when the session isn't `ended` it reuses the `sessionId`, replays history (`get_conversation_messages`, newest-first → reversed to chronological), skips the pre-chat form, and continues the conversation. An ended/404 session clears **only** the pointer (identity & consent survive) and starts fresh. + + **Returning-visitor resume by fingerprint (HTTP).** When there is no persisted pointer, the widget first `POST`s `/internal/resume-by-fingerprint` on the chat-ws wrapper with the browser fingerprint; if the wrapper resolves (and primes) a recent session it returns `{ resumable: true, sessionId, … }` and the widget adopts that session (then `get_session` + `get_conversation_messages` to hydrate) instead of creating a new one. `{ resumable: false }` falls through to a normal create. + + **Pre-chat form: phone + marketing consent.** The phone field is now shown by default (optional; rides session `metadata.userPhone`). Two explicit, default-unchecked consent checkboxes (email + SMS) capture marketing opt-in; ticking one stamps a `consentAt` ISO timestamp, and the consent record (`{ emailOptIn, smsOptIn, consentSource: 'chat-widget-prechat', consentAt }`) threads into the session metadata. New config flags: `collectPhone`, `collectConsent`, `allowChatRestore` (all default `true`). + + **Cross-device "Restore my chats."** An explicit footer affordance (not a mid-turn agent pause) runs the identity OTP flow over the chat-ws wrapper's HTTP routes — `POST /internal/identity/request-otp` → `verify-otp` → `resolve` — reusing the existing OTP UI. On a resolved list the visitor picks a conversation to replay (`get_session` + `get_conversation_messages`); the verified email is persisted. + + **HTTP, not WS frames.** The smooth-operator engine (1.8.0) owns the `/ws` dispatch and rejects unknown verbs, so the cross-device identity flow and fingerprint resume are `fetch()` (POST, JSON) calls to the chat-ws wrapper, with the HTTP base derived from the WS endpoint (`wss://ai.smoo.ai/ws` → `https://ai.smoo.ai`). The browser sends `Origin` automatically (origin-allowlisted server-side) and each request carries `agentId`/`agentName` plus an optional pre-auth `authContext` (`{ userId, signature, timestamp }`) from the new `authContext` config option. + + All server-supplied strings (masked destinations, conversation previews, history) are rendered via `textContent`, keeping the 0.6.0 XSS guarantees intact, and the new UI follows the Aurora-Glass styling. + ## 0.6.0 ### Minor Changes diff --git a/package.json b/package.json index cce553c..9d393a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@smooai/chat-widget", - "version": "0.6.0", + "version": "0.7.0", "description": "Embeddable AI chat as a framework-light web component — the Aurora Glass design, streaming replies, grounded sources, and per-brand theming. Speaks the smooth-operator WebSocket protocol.", "license": "MIT", "author": "SmooAI",