Skip to content

SMOODEV-2129e: chat-widget 0.7.0 — identity / persistence / consent client layer#15

Merged
brentrager merged 3 commits into
mainfrom
SMOODEV-2129e
Jun 29, 2026
Merged

SMOODEV-2129e: chat-widget 0.7.0 — identity / persistence / consent client layer#15
brentrager merged 3 commits into
mainfrom
SMOODEV-2129e

Conversation

@brentrager

Copy link
Copy Markdown
Contributor

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-operator v1.8.0) already provides createConversationSession(browserFingerprint), getSession, and getMessages, 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

  • Persisted Zustand store (zustand/vanilla + persist), keyed per agent (smoo-chat-widget:<agentId>). Persists only the session pointer + identity + consent + verifiedEmail + browserFingerprintnever the transcript (server is source of truth via getMessages). version drives persist.migrate; the storage adapter tolerates missing/locked-down localStorage.
  • Browser fingerprint computed once + cached, sent on every 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.
  • Same-session resume: on load, getSession → if not ended, reuse sessionId + 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.
  • Pre-chat form: phone 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 flags collectPhone / collectConsent / allowChatRestore (default true).
  • Cross-device "Restore my chats": explicit footer affordance (not a mid-turn agent pause) → request_identity_otpverify_identity_otpresolve_identity over a shared transport, reusing the OTP UI; resolved list lets the visitor 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 is bundled into the IIFE global so the <script> embed has no undefined-global at load.

Bundle-size delta

Build 0.6.0 (gzip) 0.7.0 (gzip) Δ gzip
IIFE global (chat-widget.global.js) 28.62 kB 38.58 kB +9.96 kB
ESM index.js 25.43 kB 34.46 kB +9.03 kB

Zustand (vanilla + tree-shaken persist) 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

  • 20 new unit 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.
  • 4 new mock-WS Playwright specs (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) and typecheck:e2e green; 78 unit + 6 e2e tests pass.

Changeset

minor0.7.0.

🤖 Generated with Claude Code

…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-bot

changeset-bot Bot commented Jun 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c1a1e15

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@smooai/chat-widget Minor

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

brentrager and others added 2 commits June 27, 2026 17:47
… 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]>
@brentrager brentrager merged commit 9ea1dfc into main Jun 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant