SMOODEV-2129e: chat-widget 0.7.0 — identity / persistence / consent client layer#15
Merged
Conversation
…t layer Add the ADR-048 identity, persistence, and consent client layer to @smooai/chat-widget (vanilla TS + Shadow DOM): - Persisted Zustand (zustand/vanilla + persist) store keyed per agent (smoo-chat-widget:<agentId>). Persists ONLY the session pointer + identity + consent + verifiedEmail + browserFingerprint — never the transcript (server is source of truth). version drives persist.migrate; storage adapter tolerates missing/locked-down localStorage. - browserFingerprint computed once + cached, sent on every createConversationSession. Lightweight UUID-anchored fingerprint with a non-invasive signal-hash suffix instead of heavyweight ThumbmarkJS — keeps the embed bundle small + privacy-light (tradeoff noted in the changeset). - Same-session resume (no engine change): get_session on load → if not ended, reuse sessionId + hydrate history via get_conversation_messages (newest-first reversed to chronological); ended/404 clears only the pointer (identity/consent survive) and starts fresh. - Pre-chat form: phone field shown by default (rides metadata.userPhone); explicit, default-unchecked email + SMS marketing-consent checkboxes that stamp consentAt and thread a consent record into session metadata. New config flags collectPhone/collectConsent/allowChatRestore (default true). - Cross-device "Restore my chats": explicit footer affordance (not a mid-turn pause) runs request_identity_otp → verify_identity_otp → resolve_identity over a shared transport (raw frames for verbs the engine client doesn't yet model), reusing the OTP UI; resolved list lets the user pick a conversation to replay; verifiedEmail persisted on success. All server-supplied strings rendered via textContent (0.6.0 XSS guarantees intact); Aurora-Glass styling. Zustand bundled into the IIFE global so the <script> embed has no undefined-global at load. Tests: 20 new unit tests (persistence/fingerprint/conversation) + 4 mock-WS Playwright specs (consent payload, persist→resume, ended-clears, cross-device OTP→resolve→replay). typecheck/test/build green. Changeset: minor → 0.7.0. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
🦋 Changeset detectedLatest commit: c1a1e15 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
… HTTP routes (engine rejects unknown WS verbs)
…pe verifiedEmail, await-connect on restore, fail-loud URL base, in-memory storage fallback (adversarial review) Adversarial-review hardening of the 0.7.0 identity/persistence client layer: - P1: postInternal now uses `credentials: 'omit'` (was 'include'). Auth is the Origin allowlist + authContext body, not cookies; 'include' would force Access-Control-Allow-Credentials + a reflected origin and break every /internal/* call against a plain origin-allowlisted CORS config. - P2: verifiedEmail is now session-scoped. It's cleared on clearSession() and is NEVER auto-stamped into a brand-new create_conversation_session. A new verifiedEmailSessionId binds the OTP proof to the session it was verified for, so it can't leak onto another visitor's session on a shared browser. The proof is rebound to a restored conversation (the visitor proved ownership in-flow). - P2: restore-link race fixed. The restore affordance awaits connect(), and requestIdentityOtp establishes a session first and always threads sessionId, so request-otp/verify-otp are session-consistent and verify can't hit "No active session" even if the email is submitted before the initial connect() resolves. - P2: httpBaseFromWsEndpoint now FAILS LOUD (returns null) on a non-absolute endpoint instead of a relative fallback that would POST identity/OTP data to the host page origin. The controller goes to error status and postInternal refuses. - P2: safeStorage returns an explicit in-memory (Map-backed) PersistStorage in privacy mode instead of undefined — undefined makes zustand v5 re-engage its own createJSONStorage(()=>localStorage), the exact thing the guard avoided. - P2: resume probe is now idempotent (resumeAttempted guard) — re-entering connect() after a transient error no longer re-fires resumeByFingerprint. - P3: corrected the mergeIdentity seed comment to match actual precedence. Adds/adjusts unit + mock-e2e coverage for all of the above. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The ADR-048 identity / persistence / consent client layer for
@smooai/chat-widget(vanilla TS + Shadow DOM). Built to the frozen contract; the engine (@smooai/smooth-operatorv1.8.0) already providescreateConversationSession(browserFingerprint),getSession, andgetMessages, so same-session resume is pure client work. The new cross-device verbs (request_identity_otp/verify_identity_otp/resolve_identity) are built to the contract and integrate when the server side (SMOODEV-2129d) lands.Highlights
zustand/vanilla+persist), keyed per agent (smoo-chat-widget:<agentId>). Persists only the session pointer + identity + consent +verifiedEmail+browserFingerprint— never the transcript (server is source of truth viagetMessages).versiondrivespersist.migrate; the storage adapter tolerates missing/locked-down localStorage.createConversationSession. Lightweight persisted-UUID-anchored token with a non-invasive signal-hash suffix (UA/lang/tz/screen) instead of heavyweight ThumbmarkJS — keeps the embed small + privacy-light. Tradeoff documented in the changeset.getSession→ if notended, reusesessionId+ hydrate history (getMessages, newest-first reversed to chronological), skip the pre-chat form. Ended/404 clears only the pointer (identity/consent survive) and starts fresh.metadata.userPhone); explicit, default-unchecked email + SMS marketing-consent checkboxes that stampconsentAtand thread aconsentrecord into session metadata. New flagscollectPhone/collectConsent/allowChatRestore(defaulttrue).request_identity_otp→verify_identity_otp→resolve_identityover a shared transport, reusing the OTP UI; resolved list lets the visitor pick a conversation to replay;verifiedEmailpersisted on success.All server-supplied strings rendered via
textContent(0.6.0 XSS guarantees intact); Aurora-Glass styling. Zustand is bundled into the IIFE global so the<script>embed has no undefined-global at load.Bundle-size delta
chat-widget.global.js)index.jsZustand (
vanilla+ tree-shakenpersist) is ~4 kB gzip of that; the rest is the new feature code (cross-device flow, restore UI, fingerprint, persistence). ThumbmarkJS was deliberately avoided to keep this delta small.Tests
persistence.test.ts,fingerprint.test.ts,conversation.test.ts) — store partialize/migrate/clearSession, fingerprint compute-once, and a mock-WebSocket controller covering: fingerprint + identity + consent + phone-on-metadata payload, resume reuses sessionId + hydrates history, ended/404 clears pointer + starts fresh, and the full cross-device OTP→resolve→replay flow.identity-persistence-mock.spec.ts) driving the real shadow-DOM UI on the built global bundle: exact consent payload, persist→resume replay, ended-clears, and cross-device restore.pnpm check(typecheck + test + build) andtypecheck:e2egreen; 78 unit + 6 e2e tests pass.Changeset
minor→ 0.7.0.🤖 Generated with Claude Code