AUTH4D-65 browser SDK in-memory refresh and silent renewal

@auth4/sdk-browser can use the rotating browser refresh tokens added by AUTH4D-63. This is an SDK change only; it adds no endpoints and uses the existing POST /oauth/token and POST /oauth/revoke routes.

Opt-in

new Auth4BrowserClient({ issuer, clientId, redirectUri, refresh: true }) enables refresh. The browser application must also have allowBrowserRefresh enabled, or the authorization request fails with invalid_scope.

  • With refresh: true, login() and authorizationUrl() add offline_access to the requested scopes (the default becomes openid profile email offline_access).
  • Without it, behavior is unchanged: offline_access in scope throws configuration_error, and any refresh_token in a token response is discarded.

Renewal

  • The refresh token is held in an ES private field in memory only. It is never written to sessionStorage, localStorage, cookies or the DOM, and is not part of any event or JSON.stringify(client) output. A page reload or new tab therefore still needs a new sign-in.
  • Renewal starts before the access token expires: 60 seconds early plus up to 30 seconds of random jitter (each bounded to a quarter of the token lifetime), so a five-minute token renews 210 to 240 seconds after it was issued. The request is grant_type=refresh_token&refresh_token=…&client_id=… to the discovered token endpoint, and the rotated refresh token replaces the old one.
  • A renewed id_token, when returned, is verified like the sign-in ID token. Its nonce may be absent, and its sub must match the signed-in user.
  • Renewal is single-flight. The scheduled timer and every concurrent getAccessTokenSilently() call share one token request.
  • Network errors, HTTP 5xx and HTTP 429 are transient. The current token stays usable until it expires, and renewal is retried after at most 15 seconds.
  • Any other outcome ends the session: invalid_grant (reuse, revocation or idle/absolute expiry), unauthorized_client (the application opted out), or a token response that cannot be validated. The SDK clears memory, emits signed_out with reason refresh_rejected, and throws refresh_failed.

API additions

Member Behavior
refresh?: boolean option Opt in to offline_access and in-memory renewal.
getAccessTokenSilently() Async. Returns a live access token, renewing first if the session is in its renewal window. Without refresh it behaves like getAccessToken().
onSessionEvent(listener) Subscribes to { type: "renewed", session } and { type: "signed_out", reason: "logout" | "refresh_rejected" }. Returns an unsubscribe function. Events never carry token values.
refresh_failed error code A renewal failed. If the session ended, sign in again.

getAccessToken() stays synchronous. With refresh enabled and an already-expired token (for example after a throttled background tab), it throws token_expired, keeps the session, and starts a renewal. getSession() keeps returning the session while that renewal is pending.

Sign-out

logout() clears memory, emits signed_out with reason logout, and then revokes the refresh token (token_type_hint=refresh_token), which ends its family and the server session. It then revokes the access token as before.

Source: docs/api-spec/auth4d-65.md