Repository navigation
feat: setUserJwt, setUserJwtProvider and setAuthTokens (customer identity) - #203
Merged
Merged
Conversation
Customer identity (contract §2, §3, §5, §6): one HS256 JWT signed by the
app's backend is the whole identify.
- Gleap.setUserJwt(token) decodes the payload (no verification), takes
userId / user_id and sends the existing identify with userHash = token and
no contact fields. Bad shape, no user id or over 4096 chars: console
warning, no request. Memory only. The once-only signed-identify record adds
a fingerprint of a JWT, so a new token is sent once even when nothing else
changed; legacy hashes keep the gleapId:userId key.
- Gleap.setUserJwtProvider(fn) is asked on identify 401 token_expired (one
retry with the new token) and before a file-access renewal or identify
replay when the token expires within 5 minutes. One call in flight, 10 s
timeout, 30 s pause after a failure. token_expired / token_invalid never
clear the local session.
- Gleap.setAuthTokens(tokens) posts to /sessions/auth-tokens; names checked
against ^[a-zA-Z][a-zA-Z0-9_-]{0,39}$, null deletes, kept in memory and
resent after a signed identify on a session they were not sent to; 401
warns and keeps the session.
- clearIdentity drops the token and auth tokens; the provider stays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A signed identify now returns identity.grant. The SDK keeps it in memory and in the gleap-grant-<apiKey> cookie (same domain rules as the session cookie, expiring with the grant), never in the persisted session. It is restored on reload for the same session, sent as Gleap-Grant on every SDK request, pushed to the messenger and AI chatbar as a top-level grant field of session-update, and refreshed a minute before it expires. clearIdentity drops it and its cookie (the provider stays). A 400 identity_mismatch drops the local session and retries once as a fresh visitor. getIdentity returns the grant; initialize takes sessionDuration (ms), sent with identify. Gleap-Grant is masked in captured network logs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…cts, signed cookie - Grant refresh: delay from the grant's own lifetime (exp - iat) counted from receipt, at least 30 s, at most one refresh identify a minute, skipped while one is in flight. A short grant or a fast device clock no longer loops. - A token re-minted per page load (same kid and claims, new iat/exp/jti) is not identified again; a changed claim or key is. - setUserJwt never rejects: refusals and network/server errors are logged. - signature_required is logged. - Gleap.setSessionDuration(ms) next to the initialize option. - setUserJwtProvider accepts null (removes it) and a provider returning null. - gleap-grant-<apiKey> cookie: SameSite=Lax, Secure on https. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…fresh retries - Signed mode without a grant on an identified session: one identify per session load with the held token or hash, even when it was sent before (at most once a minute, never a loop). - An identify answer that lands after clearSession is dropped (logout generation), as is a partial update answer. - signature_required for another user than the session's clears the session. - A failed or skipped grant refresh is tried again a minute later; a failed identify is replayed when the browser comes back online. - A provider token that already expired counts as a provider failure; a refused token is not resent on every render (memory-only fingerprint). - setUserJwt before initialize decides once the cached session and its grant cookie are loaded, so a live grant costs no identify. - setUseCookies(false) erases the grant cookie. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…g, changelog Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…identify Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
# Conflicts: # CHANGELOG.md
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Customer identity and authentication, JS SDK part. Built against
.handover/customer-identity/CONTRACT.mdsections 2, 3, 5 and 6. Server side: branchfeat/customer-identityof the Server repo. Nothing changes on the wire: the token travels in the existinguserHashofPOST /sessions/identify.Methods
Gleap.setUserJwt(token): Promise<void>(§2, §6): decodes the JWT payload (base64url JSON, no verification), takesuserIdoruser_id(string or number) and sends the existing identify withuserHash = tokenand no contact fields. A token that is not three non-empty base64url parts, has no user id or is over 4096 chars gets a console warning and no request. Memory only. The once-only signed-identify record (gleapId:userId) gets a fingerprint of the JWT appended, so a new token is sent once even when nothing else changed; legacy hex hashes keep the old key, so existing caches are untouched.Gleap.setUserJwtProvider(fn)(§3, §6): called on identify401 token_expired(the identify is retried once with the new token) and before a file-access renewal or an identify replay when the stored token'sexpis within 5 minutes. One call in flight, 10 s timeout, 30 s pause after a failure.token_expired/token_invalidwarn and never clear the local session;signature_requiredand other 4xx behave as before.Gleap.setAuthTokens(tokens): Promise<void>(§5):POST /sessions/auth-tokenswith{ tokens }and the usual headers, after any identify in flight has settled. Names must match^[a-zA-Z][a-zA-Z0-9_-]{0,39}$(others skipped with a warning),nulldeletes. Kept in memory and resent after a signed identify on a session they were not sent to yet. A 401 warns and keeps the session.clearIdentity()also drops the token and the auth tokens; the provider stays set.identify(userId, data, userHash)is unchanged; a JWT passed asuserHashgets the same expiry handling.Types and JSDoc in
index.d.ts; README documentssetUserJwtfirst with a backend sample, then the provider, auth tokens, andidentifyas the legacy call.Tested
npm test: 36 suites, 630 tests pass.npm run buildcompiles (build output not committed, as with other feature PRs).New tests (
src/GleapUserJwt.test.js,src/GleapSession.test.js, XHR mocked like the existing session tests):userId/user_id, numbers, UTF-8, bad shapes (2 or 4 parts, empty part, non-base64url, non-JSON, missing / empty / object user id), oversizetoken_expired: provider called once, retry with the new token, a second expiry does not loop; parallel callers share one provider call; 30 s pause after failure; 10 s timeouttoken_expiredandtoken_invalidkeep the local sessionnulldelete, 401 keeps the session, resend after a signed identify on a new session,clearIdentitydrops tokens and keeps the providerNot run against a live server.
Final-review fixes (4987243)
exp - iat) counted from when it arrived, minus a minute, never under 30 s; at most one refresh identify per minute; skipped while one is in flight. A 60-70 s grant or a fast device clock used to re-fire at once on every new grant. A grant restored from the cookie (no receipt time) uses the device clock, still at least 30 s out.kidplus its claims withoutiat/exp/nbf/jti(was: the raw token string). A backend that mints a fresh token per page load no longer causes an identify on every load (Classic projects included); a changed claim or key is still sent once.setUserJwtnever rejects. Refusals are logged where they are read; a network error or other HTTP failure logssetUserJwt could not identify the user (...)and resolves.index.d.ts: "Never rejects; refusals are logged."signature_requiredis logged: "Gleap: this project enforces identity verification and the identify had no valid signature (signature_required). Pass a token from your backend to setUserJwt."setUserJwtProvider(provider: (() => string | null | Promise<string | null>) | null). A provider returningnullcounts as "no token now" (30 s pause, no warning).Gleap.setSessionDuration(ms)as a method (the initialize option stays), the same API as the mobile SDKs; README line.gleap-grant-<apiKey>:SameSite=Lax, andSecureon https (setGleapCookie(..., credential = true); the session cookie is unchanged).Checks:
npm test36 suites / 645 tests pass (new: refresh timing, short grant, fast clock, in flight, re-minted token, signature_required, never rejects, cookie attributes);npm run buildpasses (build/ not committed).Final re-check fixes (c3a0c03)
Contract addition (Server in parallel): in Signed mode the
/sessionsand identify answers for an identified session without a grant carryidentity: { signed: false, grant: null, mode: 'signed' }; Classic answers carry noidentity.identity.mode === 'signed', the session is identified, a token or hash is held and no live grant exists, one identify goes out per session load even though the "already sent" fingerprint matches. Once per load (reset only byclearSession), at most once a minute, never a loop; covers both orders (app identifies before or after the/sessionsanswer).clearSession) is recorded when an identify or partial update goes out; an answer that lands after a logout is dropped, so it cannot bring back user A afterclearIdentity().signature_requiredfor another user than the session's clears the session: B does not keep A's session.onlineevent.expis already past counts as a provider failure (30 s pause). A refused token (token_expired,token_invalid,signature_required) is remembered by a memory-only fingerprint and not resent on every render; a new token is sent.setUserJwtbeforeinitializewaits for the cached session and its grant cookie, so a live cached grant costs no identify.setUseCookies(false)erases the grant cookie (also when called beforeinitialize).Checks:
npm test36 suites / 656 tests pass (11 new: signed mode both orders + no loop + Classic / live grant send nothing, logout race, signature_required user switch, refused token not resent, expired provider token, online replay, failed refresh retried after a minute, setUserJwt before initialize, cookies off; 10 of them fail on the previous commit).npm run buildpasses (build/ not committed). Not run against a live server.🤖 Generated with Claude Code