Skip to content

feat: an expired signed session continues as a visitor (Intercom parity); version 19.7.0 - #212

Merged
boehlerlukas merged 1 commit into
masterfrom
feat/signed-session-expiry-visitor
Nov 7, 2026
Merged

boehlerlukas merged 1 commit into
masterfrom
feat/signed-session-expiry-visitor

Conversation

@boehlerlukas

Copy link
Copy Markdown
Contributor

What

On a Signed project, a session that was signed and whose grant expires without a fresh identity token now ends. The SDK drops the identified session and continues as a fresh visitor with no conversation history, like Gleap.clearIdentity() (the JWT provider stays set). Until now the user stayed identified but unsigned (14 days of history, protected fields read-only).

A later setUserJwt (or a provider token) signs the user in again, and the server's existing guest → user merge (mergeCurrentSessionWithExistingSession) moves the visitor's activity on this device into their contact.

Intercom behaviour this matches

Authenticating users in the Messenger with JWTs, "Customer experience":

"If the session expires, and no fresh JWT is sent, the user's session will end. The user will see a fresh Messenger as a logged-out website visitor. It will not contain their conversation history."
"Once the Messenger is rebooted with Intercom('boot') and a valid JWT, the Messenger will identify the user and show the user their historical conversations and a new session. We will additionally merge any logged-out activity that occurred on the same device into that authenticated user's account."

FAQ, "What if the session expires in the middle of the user doing something?":

"If there is activity within the Messenger from a user after the cookie expires, Intercom will issue a fresh short lived cookie of 1 hour to avoid negative impact to the user experience."

Users without a JWT keep the 14-day rule, as in User conversation history visibility.

The visitor fallback is web-only in Intercom. On iOS and Android, Intercom asks setUserJwtProvider and otherwise lets requests "fail until a new JWT is set" (intercom-react-native README, iOS 19.9.0, Android 18.10.0). So the native SDKs are left unchanged.

How

  • Signed-session record. When a grant arrives, the SDK stores { sid, uid, exp } (never the grant) in the signed-session-<projectId> cache entry and cookie. This outlives the grant cookie, so a reload after the expiry knows the session was signed. A grant restored from an older SDK's cookie is recorded too.
  • Reload after the expiry. The server answers /sessions in Signed mode with identity: { mode: 'signed', signed: false }. If this session's signed lifetime has passed, the SDK makes one renewal attempt: an identify already in flight, else the held token or hash (through withFreshIdentify, so an expiring token goes to the provider first), else the JWT provider. An expired token with no provider is not sent. If no live grant comes back, the session ends. A network failure keeps the session. The decision waits for in-flight identifies.
  • Page left open past the expiry. A timer fires at exp.
    • If another tab renewed the grant, the SDK takes it from the cookie.
    • If the messenger is open, it waits until the messenger closes, at most one hour after the expiry. This is our version of Intercom's one-hour cookie.
    • Otherwise it calls /sessions, and the decision above applies.
    • Before exp, the existing refresh still renews a minute ahead through the provider.
  • No loops. At most one renewal attempt per answer (signedSessionExpiryRunning). After the drop, no record is left. No timer is armed for an expiry that already passed. The grace check only reads local state once a minute. The provider backoff and single-flight rules apply unchanged.
  • Unchanged.
    • identify(userId, data) without a token or hash (never signed, so no record).
    • A grant that is only missing before its exp (cookie deleted, cookies off, IP host). It is restored as in 19.5.2.
    • Classic projects. The answer carries no identity, so nothing is decided.
    • Server-rejected grants that still look live (rotation). The existing re-identify still applies.

Server

No change needed. Today a /sessions answer for a session with an expired grant already returns only the session pair, userId, name and lang. Protected values and token-written fields are stripped (publicSessionData.logic). The answer also says identity.signed === false, which is the signal the SDK acts on. Hiding more on the server would also hit the native SDKs and never-signed identified users, which keep today's behaviour.

Wrappers

No changes. React Native and Flutter (mobile) use the native SDKs. Capacitor's web build depends on gleap ^19.5.0 and picks up 19.7.0. Flutter web loads sdk.gleap.io/latest.

Tests

npm test: 38 suites, 719 tests pass. 19 new tests are in GleapSession.test.js › "signed session expiry". They cover:

  • a reload after the expiry turning into a fresh visitor, with no further requests
  • setUserJwt signing in again on the visitor session
  • a provider token keeping the user signed
  • a provider returning null
  • a setUserJwt in flight before the answer
  • a refused token
  • an expired held token with no provider (not sent)
  • a network failure keeping the session
  • a never-signed identified user staying identified
  • a grant that is only missing before its expiry
  • a Classic answer
  • a merged visitor getting the expiry timer
  • clearSession
  • a page left open past the expiry: drop, renewal, another tab's cookie grant, waiting while the messenger is open, and the one-hour cap

npm run build compiles (build output not committed).

Version

19.7.0 (behaviour change). CHANGELOG entry added.

🤖 Generated with Claude Code

…ty); version 19.7.0

When a session that was signed loses its grant by expiry and no fresh
identity token arrives, the SDK now ends the identified session and
continues as a fresh visitor with no history, like clearIdentity (the JWT
provider stays set). This matches Intercom's web Messenger: "If the session
expires, and no fresh JWT is sent, the user's session will end. The user will
see a fresh Messenger as a logged-out website visitor."

- Records { sid, uid, exp } of the last grant (no credential) in the
  signed-session-<projectId> cache entry and cookie, so a reload after the
  expiry knows the session was signed. Grants restored from an older SDK's
  cookie are recorded too.
- A Signed-mode /sessions answer saying "not signed" for a session whose
  signed lifetime passed: one renewal attempt (an identify in flight, the
  held token or hash through withFreshIdentify, else the JWT provider), then
  the session ends unless a live grant arrived. Network failures keep it.
- A page left open past the expiry: checks at exp, takes a grant another tab
  renewed from the cookie, waits while the messenger is open (at most an
  hour, mirroring Intercom's one-hour cookie for activity after expiry),
  then asks /sessions and decides as above.
- Unchanged: identify(userId, data) without a token or hash (never signed),
  a grant only missing before its exp, Classic projects.
- README, typings and CHANGELOG describe the new behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@boehlerlukas

Copy link
Copy Markdown
Contributor Author

Production probe on the smoke project, confirming the server contract this PR relies on:

  • A signed identify with sessionDuration: 300000 returned a 300 s grant.
  • After expiry, /sessions (with the expired grant, or without any grant) answers identity: { signed: false, mode: "signed" } with only gleapId, userId and name. Email and plan are already stripped.
  • A fresh visitor session identified with a new token is merged into the original contact (same gleapId) and gets a new grant.

🤖 Generated with Claude Code

@boehlerlukas
boehlerlukas merged commit f203bf6 into master Nov 7, 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