From a14b09bd402d0e28494bf27f13d64414108853fe Mon Sep 17 00:00:00 2001 From: Asaf <128684083+Asaf-prog@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:29:16 +0300 Subject: [PATCH] Revert "fix(widget): merge anonymous visitor history in cookie authentication mode (#104)" --- src/agent_manager/api/static/widget.js | 27 ++--------- src/agent_manager/api/static/widget.test.mjs | 38 ---------------- .../api/static/widget/auth/tokenSource.ts | 32 +++---------- tests/agent_manager/test_api.py | 43 ------------------ tests/e2e/widget.spec.ts | 45 ------------------- 5 files changed, 11 insertions(+), 174 deletions(-) diff --git a/src/agent_manager/api/static/widget.js b/src/agent_manager/api/static/widget.js index 9ad731af..e9614007 100644 --- a/src/agent_manager/api/static/widget.js +++ b/src/agent_manager/api/static/widget.js @@ -52299,8 +52299,6 @@ var TokenSource = class { /** Bumped by `reset()` so a resolution already in flight, once it lands, can * tell it is answering a question nobody is asking anymore. */ this.generation = 0; - /** Avoid repeating unauthenticated claim attempts for the same pass in cookie mode. */ - this.lastClaimAttemptPass = null; this.tokenUrl = options.tokenUrl ?? ""; this.provider = options.provider ?? null; this.storage = options.storage ?? localStorage; @@ -52309,9 +52307,7 @@ var TokenSource = class { }); } async current() { - if (!this.cached || this.isCookieMode() && this.storedPass() !== null) { - await this.resolve(() => this.storedPass()); - } + if (!this.cached) await this.resolve(() => this.storedPass()); return this.cached; } /** After a 401: whatever we sent is no good, so get another. */ @@ -52348,38 +52344,23 @@ var TokenSource = class { })()); return this.pending; } - isCookieMode() { - return !this.tokenUrl && !this.provider; - } /** A host token, plus the one-time hand-off of whatever this browser chatted * about before signing in. */ async hostToken() { const token = await this.fromHost(); - if (token || this.isCookieMode() && this.storedPass()) { - await this.claimVisitorHistory(token); - } + if (token) await this.claimVisitorHistory(token); return token; } async claimVisitorHistory(hostToken) { const pass = this.storedPass(); if (!pass) return; try { - const headers = { "Content-Type": "application/json" }; - if (hostToken) headers.Authorization = `Bearer ${hostToken}`; const response = await fetch(`${this.endpoint}${LINK_ENDPOINT}`, { method: "POST", - headers, - credentials: "include", + headers: { "Content-Type": "application/json", Authorization: `Bearer ${hostToken}` }, body: JSON.stringify({ anonymous_token: pass }) }); - if (response.ok) { - const data = await response.json().catch(() => null); - if (hostToken !== null || (data?.conversations_moved ?? 0) > 0) { - this.clearPass(); - } - } else if (hostToken !== null && response.status >= 400 && response.status < 500 && response.status !== 401) { - this.clearPass(); - } + if (response.status < 500) this.clearPass(); } catch { } } diff --git a/src/agent_manager/api/static/widget.test.mjs b/src/agent_manager/api/static/widget.test.mjs index d8b87794..2feb6279 100644 --- a/src/agent_manager/api/static/widget.test.mjs +++ b/src/agent_manager/api/static/widget.test.mjs @@ -747,44 +747,6 @@ assert.equal(mintedPass, false, "never asks for a visitor pass"); ); } -// In cookie mode (host_token), a visitor chats anonymously, then signs in via cookie in the same SPA. -// Calling client methods re-evaluates identity, claims history via /auth/link, and sends requests with cookie. -{ - resetPage(); - localStorage.setItem(visitorPassKey("https://api.example"), "old-cookie-pass"); - let loggedInViaCookie = false; - const calls = []; - globalThis.fetch = async (url, options = {}) => { - calls.push({ url, auth: options.headers?.Authorization }); - if (url.endsWith("/auth/link")) { - return loggedInViaCookie ? jsonResponse({ conversations_moved: 1 }) : jsonResponse({}, false, 401); - } - return jsonResponse({ conversation_id: "c1" }); - }; - const tokens = new TokenSource("https://api.example"); - const client = new AgentChatClient("https://api.example", tokens); - - // Before login: claimVisitorHistory gets 401, pass is kept, bearer old-cookie-pass sent - await client.createConversation(); - const sentWhileSignedOut = calls.filter((c) => c.url.endsWith("/conversations")).pop().auth; - assert.equal(sentWhileSignedOut, "Bearer old-cookie-pass", "visitor pass used while signed out"); - - // User logs in via cookie on the host site (zero-code: no refreshIdentity/reset called) - loggedInViaCookie = true; - await client.createConversation(); - const sentAfterCookieLogin = calls.filter((c) => c.url.endsWith("/conversations")).pop().auth; - assert.equal(sentAfterCookieLogin, undefined, "no bearer sent after cookie login"); - assert.ok( - calls.some((c) => c.url.endsWith("/auth/link")), - "cookie mode pre-login conversations are merged", - ); - assert.equal( - localStorage.getItem(visitorPassKey("https://api.example")), - null, - "visitor pass is cleared after successful cookie merge", - ); -} - // reset() keeps the visitor pass; only forget() discards it. Getting this // backwards silently throws away the conversations the merge exists to rescue. { diff --git a/src/agent_manager/api/static/widget/auth/tokenSource.ts b/src/agent_manager/api/static/widget/auth/tokenSource.ts index 642d0ad6..573fab22 100644 --- a/src/agent_manager/api/static/widget/auth/tokenSource.ts +++ b/src/agent_manager/api/static/widget/auth/tokenSource.ts @@ -46,8 +46,6 @@ export class TokenSource { /** Bumped by `reset()` so a resolution already in flight, once it lands, can * tell it is answering a question nobody is asking anymore. */ private generation = 0; - /** Avoid repeating unauthenticated claim attempts for the same pass in cookie mode. */ - private lastClaimAttemptPass: string | null = null; private readonly tokenUrl: string; private readonly provider: TokenProvider | null; private readonly storage: Storage; @@ -66,9 +64,7 @@ export class TokenSource { } async current(): Promise { - if (!this.cached || (this.isCookieMode() && this.storedPass() !== null)) { - await this.resolve(() => this.storedPass()); - } + if (!this.cached) await this.resolve(() => this.storedPass()); return this.cached; } @@ -118,40 +114,26 @@ export class TokenSource { return this.pending; } - private isCookieMode(): boolean { - return !this.tokenUrl && !this.provider; - } - /** A host token, plus the one-time hand-off of whatever this browser chatted * about before signing in. */ private async hostToken(): Promise { const token = await this.fromHost(); - if (token || (this.isCookieMode() && this.storedPass())) { - await this.claimVisitorHistory(token); - } + if (token) await this.claimVisitorHistory(token); return token; } - private async claimVisitorHistory(hostToken: string | null): Promise { + private async claimVisitorHistory(hostToken: string): Promise { const pass = this.storedPass(); if (!pass) return; try { - const headers: Record = { "Content-Type": "application/json" }; - if (hostToken) headers.Authorization = `Bearer ${hostToken}`; const response = await fetch(`${this.endpoint}${LINK_ENDPOINT}`, { method: "POST", - headers, - credentials: "include", + headers: { "Content-Type": "application/json", Authorization: `Bearer ${hostToken}` }, body: JSON.stringify({ anonymous_token: pass }), }); - if (response.ok) { - const data = (await response.json().catch(() => null)) as { conversations_moved?: number } | null; - if (hostToken !== null || (data?.conversations_moved ?? 0) > 0) { - this.clearPass(); - } - } else if (hostToken !== null && response.status >= 400 && response.status < 500 && response.status !== 401) { - this.clearPass(); - } + // Drop the pass on any verdict, including a refusal — only a server that + // never answered is worth asking again. + if (response.status < 500) this.clearPass(); } catch { // Offline: keep the pass so the next page load retries the hand-off. } diff --git a/tests/agent_manager/test_api.py b/tests/agent_manager/test_api.py index 408abb58..89ff9bee 100644 --- a/tests/agent_manager/test_api.py +++ b/tests/agent_manager/test_api.py @@ -393,49 +393,6 @@ def test_signing_in_adopts_the_conversations_a_visitor_already_started() -> None assert client.get(f"/conversations/{cid}/messages", headers=visitor).status_code == 403 -def test_linking_via_cookie_authentication() -> None: - """In host_token (cookie) mode, /auth/link is called with session cookies.""" - app = build_test_app( - ConversationService(RecordingEngine(), MemoryRepository()), - extra_auth_mode=AuthMode.HOST_TOKEN, - extra_auth_cookie=HOST_COOKIE, - extra_auth_claim_user_id="id", - ) - anon_client = TestClient(app) - pass_token = anon_client.post("/auth/anonymous").json()["token"] - visitor = {"Authorization": f"Bearer {pass_token}"} - cid = anon_client.post("/conversations", headers=visitor).json()["conversation_id"] - anon_client.post( - f"/conversations/{cid}/messages", json={"message": "hi from visitor"}, headers=visitor - ) - - alice_client = TestClient(app, cookies=session_cookie(id="alice")) - linked = alice_client.post("/auth/link", json={"anonymous_token": pass_token}) - - assert linked.status_code == 200 - assert linked.json() == {"conversations_moved": 1} - assert [t["conversation_id"] for t in alice_client.get("/conversations").json()["items"]] == [ - cid - ] - assert alice_client.get(f"/conversations/{cid}/messages").status_code == 200 - - -def test_linking_via_cookie_returns_401_when_not_logged_in() -> None: - """Without a session cookie the server must reject the link request, not silently succeed.""" - app = build_test_app( - ConversationService(RecordingEngine(), MemoryRepository()), - extra_auth_mode=AuthMode.HOST_TOKEN, - extra_auth_cookie=HOST_COOKIE, - extra_auth_claim_user_id="id", - ) - client = TestClient(app) - pass_token = client.post("/auth/anonymous").json()["token"] - - # No cookie, no bearer — the server has no way to identify the adopting caller. - result = client.post("/auth/link", json={"anonymous_token": pass_token}) - assert result.status_code == 401 - - def test_linking_refuses_a_pass_that_is_not_ours_or_already_spent() -> None: app = build_test_app(ConversationService(RecordingEngine(), MemoryRepository())) client = TestClient(app) diff --git a/tests/e2e/widget.spec.ts b/tests/e2e/widget.spec.ts index 9c9eee4c..55e2cf37 100644 --- a/tests/e2e/widget.spec.ts +++ b/tests/e2e/widget.spec.ts @@ -26,14 +26,6 @@ async function mockConversationApi( const calls: string[] = []; await pinVisitorPass(page); - await page.route("**/auth/link", async (route) => { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ conversations_moved: 0 }), - }); - }); - await page.route(/\/conversations\?/, async (route) => { calls.push(`GET ${new URL(route.request().url()).pathname}`); await route.fulfill({ @@ -1469,40 +1461,3 @@ test("thread drawer paginates and appends next pages on scroll", async ({ page } await expect.poll(() => shadowText(page, ".thread-drawer")).toContain("Thread Three"); }); - -test("visitor pass cached, cookie login hand-off merges history and first thread list request observes merged threads", async ({ page }) => { - const calls: string[] = []; - await pinVisitorPass(page); - - let linkCalled = false; - await page.route("**/auth/link", async (route) => { - linkCalled = true; - calls.push(`POST ${new URL(route.request().url()).pathname}`); - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ conversations_moved: 1 }), - }); - }); - - await page.route(/\/conversations\?/, async (route) => { - calls.push(`GET ${new URL(route.request().url()).pathname}`); - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ - items: linkCalled - ? [{ conversation_id: "merged-thread", title: "Merged Thread", last_message_at: "2026-06-28T00:00:00Z" }] - : [], - next_cursor: null, - }), - }); - }); - - await page.goto("/widget-demo.html"); - await shadowClick(page, ".launcher"); - await shadowClick(page, '[aria-label="Conversations"]'); - - await expect.poll(() => linkCalled).toBe(true); - await expect.poll(() => shadowText(page, ".thread-drawer")).toContain("Merged Thread"); -});