Skip to content

Refactor Cookie-Mode Anonymous History Hand-off to Avoid Repeated /auth/link Probes #131

Description

@Asaf-prog

Refactor Cookie-Mode Anonymous History Hand-off to Avoid Repeated /auth/link Probes

Context

Follow-up to #129.

#129 fixes the main anonymous → authenticated history hand-off flow in host_token / cookie authentication mode.

The important behavior is now covered:

Visitor starts anonymously
        ↓
Visitor signs in through the host application
        ↓
Opens conversation history
        ↓
Anonymous history is linked
        ↓
Authenticated history contains the previous conversations

This works without requiring the host application to explicitly call:

refreshIdentity()
reset()

However, the current implementation uses a forced identity check when conversation history is loaded.

Conceptually:

listConversations(...)
    
current({ forceCheck: true })
    
attempt /auth/link

This guarantees correct ordering after login, but introduces an unnecessary network probe while the user is still anonymous.


Problem

While an anonymous visitor still has a stored visitor pass, every forced history check may attempt:

POST /auth/link
    ↓
401 Unauthorized
    ↓
GET /conversations

Repeated history operations may therefore look like:

open history
→ POST /auth/link
→ 401
→ GET /conversations

load another page
→ POST /auth/link
→ 401
→ GET /conversations

refresh history
→ POST /auth/link
→ 401
→ GET /conversations

The main functionality is correct, but we are using failed authentication requests as a mechanism for detecting whether the host application's authentication state has changed.

This has several disadvantages:

  • unnecessary network traffic;
  • avoidable 401 responses;
  • extra latency before conversation-history requests;
  • authentication-state detection becomes coupled to a specific UI operation;
  • the frontend is effectively trying to infer whether a session cookie became valid;
  • the lifecycle becomes more complicated through forceCheck, cookie snapshots, focus/visibility events, and retry state.

Core Design Problem

In cookie authentication mode, the widget does not own the authenticated credential.

The browser may have an authenticated session cookie, potentially an HttpOnly cookie, but the widget cannot reliably determine from JavaScript whether that cookie represents a valid authenticated session.

This means the frontend does not have a reliable event equivalent to:

authentication changed from anonymous → authenticated

Trying to reconstruct that state through:

timeouts
polling
document.cookie
focus events
visibility events
forced /auth/link attempts

is inherently indirect.

The server, however, already has the authoritative information.

For every request it can determine:

Who authenticated this request?

Goal

Make anonymous-history adoption in cookie mode deterministic without requiring the widget to repeatedly probe /auth/link.

We should preserve all of the following:

  1. Anonymous usage should not generate repeated /auth/link requests.
  2. No timeout, sleep, or arbitrary retry interval should be required.
  3. No host-side refreshIdentity() call should be required for the normal zero-code cookie flow.
  4. Once the browser becomes authenticated, the first request that depends on account history must observe the merged history.
  5. The merge must remain idempotent.
  6. Existing bearer/token-based hand-off flows must continue to work.

Suggested Direction: Server-Side Opportunistic Hand-off

Instead of asking the frontend to determine whether authentication changed, consider letting the server perform the hand-off when both pieces of information are available on the same request:

authenticated principal
+
anonymous visitor pass

Conceptually:

Browser request
      │
      ├── authenticated session cookie
      │
      └── anonymous visitor-pass metadata
                ↓
             Server
                ↓
      Resolve authenticated principal
                ↓
      Visitor pass also present?
                ↓
             Yes
                ↓
      Adopt anonymous conversations
                ↓
      Continue original request

For example, the widget could carry the visitor pass separately from the actual authentication credential:

GET /conversations

Cookie: session=<authenticated-session>
X-Extra-Visitor-Pass: <anonymous-pass>

The exact transport/header name should be decided as part of the implementation.

The important semantic distinction is:

session cookie
    ↓
Who is making the request?

visitor pass
    ↓
Which anonymous history may need to be adopted?

The anonymous visitor pass should not become the primary authentication credential once an authenticated cookie is available.


Why Separate the Credentials

We should avoid relying on ambiguous credential precedence such as:

Authorization: Bearer <anonymous-pass>
Cookie: session=<authenticated-user>

and then depending on authentication middleware to choose the desired identity.

These represent two different concepts:

Authentication credential
→ authenticated account identity

History hand-off credential
→ anonymous identity being adopted

The architecture should make that distinction explicit.

The LLM, UI caller, or request payload must also not be able to arbitrarily choose the authenticated identity.


Expected Flow

Still Anonymous

visitor pass exists
        ↓
GET /conversations
        ↓
server sees no authenticated account session
        ↓
no adoption
        ↓
normal anonymous response

There should be no additional:

POST /auth/link → 401

required just to determine that the visitor is still anonymous.


User Signs In

The host application establishes its normal session cookie.

The widget does not need to know that this happened.

host login
    ↓
browser now has authenticated cookie

Then:

next GET /conversations
        ↓
browser sends authenticated session cookie
        +
visitor hand-off pass
        ↓
server resolves authenticated principal
        ↓
server adopts anonymous history
        ↓
GET /conversations continues
        ↓
response already includes merged conversations

The important ordering is:

authenticate
    ↓
adopt history
    ↓
execute history request

not:

GET history
    ↓
discover later that history should have been merged

Keep /auth/link

This issue does not necessarily require removing the existing /auth/link endpoint.

It may still be useful for:

  • explicit identity-refresh flows;
  • bearer/token authentication;
  • external integrations;
  • backwards compatibility.

The goal is specifically to avoid making repeated /auth/link probing the identity-change detection mechanism for zero-code cookie mode.


Application Boundary

The hand-off should happen at an appropriate authenticated application boundary.

Conceptually:

HTTP Request
      ↓
Authentication
      ↓
Principal
      ↓
AnonymousHistoryAdopter
      ↓
Route / ConversationService

The implementation should avoid duplicating adoption logic independently across multiple endpoints.

If history adoption becomes request middleware or another shared application-level component, it must remain explicit enough that:

  • its side effects are understandable;
  • errors are handled correctly;
  • it does not unexpectedly run for unrelated authentication modes;
  • the same anonymous pass is not repeatedly processed after adoption.

Failure Semantics

History hand-off is important, but failures should have clearly defined behavior.

Cases to consider:

Invalid / expired visitor pass

An authenticated user's normal request should not become permanently unusable because an old visitor-pass reference exists.

The system should determine whether to:

ignore + discard invalid hand-off state

or return a specific recoverable error.

The behavior must be explicit and tested.

Temporary infrastructure failure

If adoption fails because of a temporary server/database failure, we should not silently mark the pass as successfully consumed.

A future request may need to retry.

Already adopted pass

The operation must remain idempotent.

same pass
→ adoption already completed
→ no duplicate conversations
→ normal request continues

Security Considerations

The visitor pass must never allow one authenticated user to adopt another user's data without proper validation.

The server must continue validating:

visitor-pass authenticity
ownership/state
whether it was already consumed
target authenticated principal

The new hand-off mechanism must not weaken the authorization guarantees already enforced by /auth/link.

The visitor-pass transport must also not accidentally expose it through:

  • logs;
  • URLs/query strings;
  • analytics;
  • browser history.

Prefer a request header or another transport appropriate to the existing authentication architecture.


Remove Frontend Authentication Guessing Where Possible

Once a deterministic server-side mechanism exists, review whether the following cookie-mode complexity can be removed or simplified:

forceCheck
cookie snapshot tracking
identityCheckDirty
focus-based identity detection
visibilitychange-based identity detection
storage-event identity detection

Some of these mechanisms may still have value for other authentication modes.

Do not remove them mechanically.

The goal is to ensure that cookie-mode correctness does not depend on the frontend guessing authentication state.


Tests

1. Repeated anonymous history requests

visitor remains anonymous

listConversations()
listConversations()
listConversations()

Expected:
- normal anonymous history works
- no repeated POST /auth/link probing
- no unnecessary 401 identity probes

2. Zero-code login transition

anonymous visitor
    ↓
creates conversation
    ↓
host application signs user in
    ↓
NO reset()
NO refreshIdentity()
NO timeout/sleep
    ↓
next listConversations()

Expected ordering:

authenticated principal resolved
→ anonymous history adopted
→ conversations loaded

The response must include the previously anonymous conversation.

3. Immediate login transition

Login should work even if it occurs immediately after the previous anonymous request.

Correctness must not depend on an arbitrary elapsed time.

4. Pagination

list page 1
list page 2
list page 3

must not trigger repeated hand-off probes once identity state is already known/adoption is complete.

5. Already adopted pass

Repeated authenticated requests do not duplicate or reassign conversations.

6. Invalid visitor pass

An invalid/expired pass does not leak data and follows the explicitly defined failure policy.

7. Concurrent authenticated requests

If multiple requests arrive immediately after login, history must only be adopted once and all requests must observe a consistent final state.

The adoption path should be safe under concurrency.

8. Existing bearer mode

Existing token/bearer-based anonymous → account linking behavior must remain unchanged.


Acceptance Criteria

  • Cookie-mode history hand-off does not rely on unconditional forceCheck calls.
  • Repeated anonymous history requests do not repeatedly call /auth/link.
  • No fixed timeout or sleep is required to detect login.
  • Zero-code cookie authentication continues to work without refreshIdentity().
  • The first authenticated history request observes merged anonymous history.
  • Authentication state is determined by the server rather than inferred through frontend timing.
  • Anonymous hand-off identity is kept semantically separate from authenticated account credentials.
  • The hand-off remains idempotent.
  • Concurrent first requests after login cannot cause duplicate adoption.
  • Existing bearer/token linking behavior is preserved.
  • Invalid or expired visitor passes have explicitly defined behavior.
  • Tests cover repeated anonymous requests, immediate login, pagination, concurrency, invalid passes, and the zero-code transition.
  • Frontend cookie-mode identity-tracking complexity is simplified where it is no longer necessary.

Non-Goals

  • Replacing the complete authentication architecture.
  • Removing /auth/link if it is still useful for other authentication modes.
  • Adding polling or periodic session checks.
  • Introducing a fixed retry delay.
  • Requiring host applications to implement a new login callback for the standard cookie-mode flow.

Why This Matters

The widget should not need to guess whether the browser became authenticated.

In cookie mode, the server is already the authority that can answer that question reliably.

Moving the hand-off decision to the point where authenticated identity and anonymous history are both known gives us a simpler invariant:

If an authenticated request arrives with valid anonymous history,
adopt it before serving account-dependent data.

That removes repeated failed authentication probes and makes the anonymous → account transition deterministic rather than timing-dependent.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions