From 8b3884bb4c4725e0a17637f1aa298573c4642591 Mon Sep 17 00:00:00 2001 From: rishu685 Date: Tue, 25 Aug 2026 13:16:37 +0530 Subject: [PATCH 1/7] fix(widget): merge anonymous visitor history in cookie authentication mode (#104) - Trigger visitor pass claim in tokenSource when stored pass exists even if fromHost() returns null - Send credentials: include and check conversations_moved before clearing pass in cookie mode - Add unit test in test_api.py for cookie mode linking - Fixes #104 --- src/agent_manager/api/static/widget.js | 18 ++++++++++--- .../api/static/widget/auth/tokenSource.ts | 22 ++++++++++----- tests/agent_manager/test_api.py | 27 +++++++++++++++++++ tests/e2e/widget.spec.ts | 8 ++++++ 4 files changed, 66 insertions(+), 9 deletions(-) diff --git a/src/agent_manager/api/static/widget.js b/src/agent_manager/api/static/widget.js index e9614007..12c43a4c 100644 --- a/src/agent_manager/api/static/widget.js +++ b/src/agent_manager/api/static/widget.js @@ -52348,19 +52348,31 @@ var TokenSource = class { * about before signing in. */ async hostToken() { const token = await this.fromHost(); - if (token) await this.claimVisitorHistory(token); + if (token || this.storedPass()) { + void 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: { "Content-Type": "application/json", Authorization: `Bearer ${hostToken}` }, + headers, + credentials: "include", body: JSON.stringify({ anonymous_token: pass }) }); - if (response.status < 500) this.clearPass(); + 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(); + } } catch { } } diff --git a/src/agent_manager/api/static/widget/auth/tokenSource.ts b/src/agent_manager/api/static/widget/auth/tokenSource.ts index 573fab22..3352e227 100644 --- a/src/agent_manager/api/static/widget/auth/tokenSource.ts +++ b/src/agent_manager/api/static/widget/auth/tokenSource.ts @@ -118,22 +118,32 @@ export class TokenSource { * about before signing in. */ private async hostToken(): Promise { const token = await this.fromHost(); - if (token) await this.claimVisitorHistory(token); + if (token || this.storedPass()) { + void this.claimVisitorHistory(token); + } return token; } - private async claimVisitorHistory(hostToken: string): Promise { + private async claimVisitorHistory(hostToken: string | null): 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: { "Content-Type": "application/json", Authorization: `Bearer ${hostToken}` }, + headers, + credentials: "include", body: JSON.stringify({ anonymous_token: pass }), }); - // 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(); + 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(); + } } 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 89ff9bee..189b553c 100644 --- a/tests/agent_manager/test_api.py +++ b/tests/agent_manager/test_api.py @@ -393,6 +393,33 @@ 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_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 55e2cf37..a3658f8c 100644 --- a/tests/e2e/widget.spec.ts +++ b/tests/e2e/widget.spec.ts @@ -26,6 +26,14 @@ 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({ From 542997689c775db29ea2e08f3e60afd1b431c482 Mon Sep 17 00:00:00 2001 From: rishu685 Date: Tue, 25 Aug 2026 13:24:15 +0530 Subject: [PATCH 2/7] fix(widget): always clear visitor pass after successful cookie-mode link - Remove conversations_moved guard in cookie mode: the pass is spent whether or not conversations moved, keeping it would fire a wasted POST /auth/link on every subsequent page load - Add test: /auth/link returns 401 in cookie mode with no session cookie 876+1 tests passing, ruff/mypy clean --- src/agent_manager/api/static/widget.js | 5 +---- .../api/static/widget/auth/tokenSource.ts | 8 ++++---- tests/agent_manager/test_api.py | 16 ++++++++++++++++ 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/agent_manager/api/static/widget.js b/src/agent_manager/api/static/widget.js index 12c43a4c..d86920cb 100644 --- a/src/agent_manager/api/static/widget.js +++ b/src/agent_manager/api/static/widget.js @@ -52366,10 +52366,7 @@ var TokenSource = class { 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(); - } + this.clearPass(); } else if (hostToken !== null && response.status >= 400 && response.status < 500 && response.status !== 401) { this.clearPass(); } diff --git a/src/agent_manager/api/static/widget/auth/tokenSource.ts b/src/agent_manager/api/static/widget/auth/tokenSource.ts index 3352e227..a0f8cf5e 100644 --- a/src/agent_manager/api/static/widget/auth/tokenSource.ts +++ b/src/agent_manager/api/static/widget/auth/tokenSource.ts @@ -137,10 +137,10 @@ export class TokenSource { 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(); - } + // In Bearer mode the pass is always cleared on success (conversation may be 0 if already merged). + // In cookie mode we also always clear: the pass is spent whether or not conversations moved — + // keeping it would fire a wasted POST /auth/link on every subsequent page load. + this.clearPass(); } else if (hostToken !== null && response.status >= 400 && response.status < 500 && response.status !== 401) { this.clearPass(); } diff --git a/tests/agent_manager/test_api.py b/tests/agent_manager/test_api.py index 189b553c..408abb58 100644 --- a/tests/agent_manager/test_api.py +++ b/tests/agent_manager/test_api.py @@ -420,6 +420,22 @@ def test_linking_via_cookie_authentication() -> None: 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) From e29329d5dd87e1021b71e3187384a4e09cd998c7 Mon Sep 17 00:00:00 2001 From: rishu685 Date: Tue, 25 Aug 2026 13:29:12 +0530 Subject: [PATCH 3/7] fix(widget): restrict automatic visitor pass claim in tokenSource to cookie mode - Avoid attempting claimVisitorHistory when fromHost() returns null in Bearer mode (tokenUrl/provider configured but returned 401) - Keeps pre-login visitor pass intact for anonymous chatting prior to login in tokenUrl/provider mode - Passes node widget.test.mjs unit test and all CI checks --- src/agent_manager/api/static/widget.js | 5 ++++- src/agent_manager/api/static/widget/auth/tokenSource.ts | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/agent_manager/api/static/widget.js b/src/agent_manager/api/static/widget.js index d86920cb..e644f882 100644 --- a/src/agent_manager/api/static/widget.js +++ b/src/agent_manager/api/static/widget.js @@ -52344,11 +52344,14 @@ 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.storedPass()) { + if (token || this.isCookieMode() && this.storedPass()) { void this.claimVisitorHistory(token); } return token; diff --git a/src/agent_manager/api/static/widget/auth/tokenSource.ts b/src/agent_manager/api/static/widget/auth/tokenSource.ts index a0f8cf5e..3f5354d8 100644 --- a/src/agent_manager/api/static/widget/auth/tokenSource.ts +++ b/src/agent_manager/api/static/widget/auth/tokenSource.ts @@ -114,11 +114,15 @@ 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.storedPass()) { + if (token || (this.isCookieMode() && this.storedPass())) { void this.claimVisitorHistory(token); } return token; From 41ce7aed1b3578d4ec9069a0aaee6abaa32ef67b Mon Sep 17 00:00:00 2001 From: rishu685 Date: Wed, 26 Aug 2026 17:40:33 +0530 Subject: [PATCH 4/7] fix(widget): ensure same-SPA cookie login hand-off and await visitor pass claim - Re-evaluate identity resolution in current() when storedPass() exists in cookie mode, supporting same-SPA cookie login hand-off without page reload - Await claimVisitorHistory in hostToken() to eliminate background races before history/conversation requests - Add unit test in widget.test.mjs for same-SPA cookie login hand-off --- src/agent_manager/api/static/widget.js | 8 +++- src/agent_manager/api/static/widget.test.mjs | 38 +++++++++++++++++++ .../api/static/widget/auth/tokenSource.ts | 8 +++- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/agent_manager/api/static/widget.js b/src/agent_manager/api/static/widget.js index e644f882..ea5c444c 100644 --- a/src/agent_manager/api/static/widget.js +++ b/src/agent_manager/api/static/widget.js @@ -52299,6 +52299,8 @@ 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; @@ -52307,7 +52309,9 @@ var TokenSource = class { }); } async current() { - if (!this.cached) await this.resolve(() => this.storedPass()); + if (!this.cached || this.isCookieMode() && this.storedPass() !== null) { + await this.resolve(() => this.storedPass()); + } return this.cached; } /** After a 401: whatever we sent is no good, so get another. */ @@ -52352,7 +52356,7 @@ var TokenSource = class { async hostToken() { const token = await this.fromHost(); if (token || this.isCookieMode() && this.storedPass()) { - void this.claimVisitorHistory(token); + await this.claimVisitorHistory(token); } return token; } diff --git a/src/agent_manager/api/static/widget.test.mjs b/src/agent_manager/api/static/widget.test.mjs index 2feb6279..7e8b4934 100644 --- a/src/agent_manager/api/static/widget.test.mjs +++ b/src/agent_manager/api/static/widget.test.mjs @@ -747,6 +747,44 @@ 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 + 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 3f5354d8..e14c25ee 100644 --- a/src/agent_manager/api/static/widget/auth/tokenSource.ts +++ b/src/agent_manager/api/static/widget/auth/tokenSource.ts @@ -46,6 +46,8 @@ 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; @@ -64,7 +66,9 @@ export class TokenSource { } async current(): Promise { - if (!this.cached) await this.resolve(() => this.storedPass()); + if (!this.cached || (this.isCookieMode() && this.storedPass() !== null)) { + await this.resolve(() => this.storedPass()); + } return this.cached; } @@ -123,7 +127,7 @@ export class TokenSource { private async hostToken(): Promise { const token = await this.fromHost(); if (token || (this.isCookieMode() && this.storedPass())) { - void this.claimVisitorHistory(token); + await this.claimVisitorHistory(token); } return token; } From 711716c478f7386b8734f29a2a5fa92a185f3449 Mon Sep 17 00:00:00 2001 From: rishu685 Date: Wed, 26 Aug 2026 17:41:53 +0530 Subject: [PATCH 5/7] test(e2e): add Playwright coverage for same-SPA cookie login visitor merge - Verifies visitor pass cached -> cookie login occurs -> hand-off merges history -> first thread list request observes merged threads --- tests/e2e/widget.spec.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/e2e/widget.spec.ts b/tests/e2e/widget.spec.ts index a3658f8c..9c9eee4c 100644 --- a/tests/e2e/widget.spec.ts +++ b/tests/e2e/widget.spec.ts @@ -1469,3 +1469,40 @@ 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"); +}); From 408cb4398f54d22108c304029a3d4294ba1951e3 Mon Sep 17 00:00:00 2001 From: rishu685 Date: Wed, 26 Aug 2026 17:46:01 +0530 Subject: [PATCH 6/7] fix(widget): prevent per-request token resolution race in cookie mode while keeping same-SPA cookie login hand-off --- src/agent_manager/api/static/widget.js | 9 +++++---- src/agent_manager/api/static/widget.test.mjs | 1 + .../api/static/widget/auth/tokenSource.ts | 12 +++++------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/agent_manager/api/static/widget.js b/src/agent_manager/api/static/widget.js index ea5c444c..86bda0b1 100644 --- a/src/agent_manager/api/static/widget.js +++ b/src/agent_manager/api/static/widget.js @@ -52309,9 +52309,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. */ @@ -52373,7 +52371,10 @@ var TokenSource = class { body: JSON.stringify({ anonymous_token: pass }) }); if (response.ok) { - this.clearPass(); + 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(); } diff --git a/src/agent_manager/api/static/widget.test.mjs b/src/agent_manager/api/static/widget.test.mjs index 7e8b4934..797a1bbc 100644 --- a/src/agent_manager/api/static/widget.test.mjs +++ b/src/agent_manager/api/static/widget.test.mjs @@ -771,6 +771,7 @@ assert.equal(mintedPass, false, "never asks for a visitor pass"); // User logs in via cookie on the host site loggedInViaCookie = true; + tokens.reset(); // what refreshIdentity() does await client.createConversation(); const sentAfterCookieLogin = calls.filter((c) => c.url.endsWith("/conversations")).pop().auth; assert.equal(sentAfterCookieLogin, undefined, "no bearer sent after cookie login"); diff --git a/src/agent_manager/api/static/widget/auth/tokenSource.ts b/src/agent_manager/api/static/widget/auth/tokenSource.ts index e14c25ee..5b2a34ef 100644 --- a/src/agent_manager/api/static/widget/auth/tokenSource.ts +++ b/src/agent_manager/api/static/widget/auth/tokenSource.ts @@ -66,9 +66,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; } @@ -145,10 +143,10 @@ export class TokenSource { body: JSON.stringify({ anonymous_token: pass }), }); if (response.ok) { - // In Bearer mode the pass is always cleared on success (conversation may be 0 if already merged). - // In cookie mode we also always clear: the pass is spent whether or not conversations moved — - // keeping it would fire a wasted POST /auth/link on every subsequent page load. - this.clearPass(); + 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(); } From 68c776f0ffa0f0256042f90b97eee3af2e76de51 Mon Sep 17 00:00:00 2001 From: rishu685 Date: Thu, 27 Aug 2026 22:19:46 +0530 Subject: [PATCH 7/7] fix(widget): enable zero-code cookie mode visitor history hand-off - Update TokenSource.current() to re-evaluate identity when storedPass() exists in Cookie mode - Allows same-SPA cookie login to automatically adopt visitor history without requiring hosts to call refreshIdentity() - Passes all 877 pytest tests, 39 Playwright E2E tests, and widget unit self-checks --- src/agent_manager/api/static/widget.js | 4 +++- src/agent_manager/api/static/widget.test.mjs | 3 +-- src/agent_manager/api/static/widget/auth/tokenSource.ts | 4 +++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/agent_manager/api/static/widget.js b/src/agent_manager/api/static/widget.js index 86bda0b1..9ad731af 100644 --- a/src/agent_manager/api/static/widget.js +++ b/src/agent_manager/api/static/widget.js @@ -52309,7 +52309,9 @@ var TokenSource = class { }); } async current() { - if (!this.cached) await this.resolve(() => this.storedPass()); + if (!this.cached || this.isCookieMode() && this.storedPass() !== null) { + await this.resolve(() => this.storedPass()); + } return this.cached; } /** After a 401: whatever we sent is no good, so get another. */ diff --git a/src/agent_manager/api/static/widget.test.mjs b/src/agent_manager/api/static/widget.test.mjs index 797a1bbc..d8b87794 100644 --- a/src/agent_manager/api/static/widget.test.mjs +++ b/src/agent_manager/api/static/widget.test.mjs @@ -769,9 +769,8 @@ assert.equal(mintedPass, false, "never asks for a visitor pass"); 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 + // User logs in via cookie on the host site (zero-code: no refreshIdentity/reset called) loggedInViaCookie = true; - tokens.reset(); // what refreshIdentity() does await client.createConversation(); const sentAfterCookieLogin = calls.filter((c) => c.url.endsWith("/conversations")).pop().auth; assert.equal(sentAfterCookieLogin, undefined, "no bearer sent after cookie login"); diff --git a/src/agent_manager/api/static/widget/auth/tokenSource.ts b/src/agent_manager/api/static/widget/auth/tokenSource.ts index 5b2a34ef..642d0ad6 100644 --- a/src/agent_manager/api/static/widget/auth/tokenSource.ts +++ b/src/agent_manager/api/static/widget/auth/tokenSource.ts @@ -66,7 +66,9 @@ export class TokenSource { } async current(): Promise { - if (!this.cached) await this.resolve(() => this.storedPass()); + if (!this.cached || (this.isCookieMode() && this.storedPass() !== null)) { + await this.resolve(() => this.storedPass()); + } return this.cached; }