{"_id":"@antzsoft/wso2-auth-thingsboard-js","_rev":"6-fb2ae16b568f1859c3938ecd6d4d8845","name":"@antzsoft/wso2-auth-thingsboard-js","dist-tags":{"latest":"1.3.0"},"versions":{"1.0.0":{"name":"@antzsoft/wso2-auth-thingsboard-js","version":"1.0.0","keywords":["wso2","oauth2","auth","identity","antz","thingsboard"],"license":"MIT","_id":"@antzsoft/wso2-auth-thingsboard-js@1.0.0","maintainers":[{"name":"antz.karthik","email":"karthikmitta@lifesciencetrust.com"},{"name":"antzsoft_trellisys","email":"antzsoft@lifesciencetrust.com"}],"dist":{"shasum":"3966ce2704a76ffe8ea3ba6cf61e4f24c61f4a80","tarball":"https://registry.npmjs.org/@antzsoft/wso2-auth-thingsboard-js/-/wso2-auth-thingsboard-js-1.0.0.tgz","fileCount":18,"integrity":"sha512-u4x0IlL/eHvAW4fbMwiq6SWABJMcCFmF8y6DPwDh3JZ9+6wvCFKd5gKLh2iRfUd3dD2HzzfLMmaCQx4z4VfiVQ==","signatures":[{"sig":"MEQCID5TtEMgOoDommqwCLUCl73bGIuimorML1O63tQCHLcpAiBkSsht6UoyiRE/GXBDIo+Ek/++gFFvhNxr/qGww2rYmA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":76976},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"6c911fb5faa23d82fb6e6285fd9dcdaa3313fbce","scripts":{"dev":"tsc --watch","build":"tsc"},"_npmUser":{"name":"antz.karthik","email":"karthikmitta@lifesciencetrust.com"},"_npmVersion":"10.9.2","description":"Minimal WSO2 IS login-redirect + token-storage connector for ThingsBoard CE's Angular frontend — a standard (non-SPA) confidential-client web app whose backend does its own OAuth2 code exchange.","directories":{},"_nodeVersion":"22.14.0","_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.4.5"},"_npmOperationalInternal":{"tmp":"tmp/wso2-auth-thingsboard-js_1.0.0_1784876561747_0.32865322134086794","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@antzsoft/wso2-auth-thingsboard-js","version":"1.0.1","keywords":["wso2","oauth2","auth","identity","antz","thingsboard"],"license":"MIT","_id":"@antzsoft/wso2-auth-thingsboard-js@1.0.1","maintainers":[{"name":"antz.karthik","email":"karthikmitta@lifesciencetrust.com"},{"name":"antzsoft_trellisys","email":"antzsoft@lifesciencetrust.com"}],"dist":{"shasum":"7dd03cd194a59081e6c79b4d574fb973b3f6c4db","tarball":"https://registry.npmjs.org/@antzsoft/wso2-auth-thingsboard-js/-/wso2-auth-thingsboard-js-1.0.1.tgz","fileCount":18,"integrity":"sha512-N1iwlnv6f5DMOwVrgej+VOrk1uL0c0aAZ8e+yumjLlAAxPkCm6SPnIPy89469Jg0dBfT1Ev0letEXNPL1+JHGw==","signatures":[{"sig":"MEUCIQCcxUNKjV1MVFRoPj9kvi95fxtykbHaNmKsimVatcoX6wIgYdvFZFiIdJO9ABqrOqeuJ3Ea/sg7H8YiXUcfZ8ZrJZI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":79831},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"57136cf7177bb384cde3ebeaf25767171919c30a","scripts":{"dev":"tsc --watch","build":"tsc"},"_npmUser":{"name":"antz.karthik","email":"karthikmitta@lifesciencetrust.com"},"_npmVersion":"10.9.2","description":"Minimal WSO2 IS login-redirect + token-storage connector for ThingsBoard CE's Angular frontend — a standard (non-SPA) confidential-client web app whose backend does its own OAuth2 code exchange.","directories":{},"_nodeVersion":"22.14.0","_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.4.5"},"_npmOperationalInternal":{"tmp":"tmp/wso2-auth-thingsboard-js_1.0.1_1785817741081_0.5066873468510085","host":"s3://npm-registry-packages-npm-production"}},"1.0.2":{"name":"@antzsoft/wso2-auth-thingsboard-js","version":"1.0.2","keywords":["wso2","oauth2","auth","identity","antz","thingsboard"],"license":"MIT","_id":"@antzsoft/wso2-auth-thingsboard-js@1.0.2","maintainers":[{"name":"antz.karthik","email":"karthikmitta@lifesciencetrust.com"},{"name":"antzsoft_trellisys","email":"antzsoft@lifesciencetrust.com"}],"dist":{"shasum":"ac9bc0e2f22b75917706c73334a285e56165b9dd","tarball":"https://registry.npmjs.org/@antzsoft/wso2-auth-thingsboard-js/-/wso2-auth-thingsboard-js-1.0.2.tgz","fileCount":18,"integrity":"sha512-j4s/3FH4IMMySDBOy1lKyEkA2fp/RBcDuF5TfRBW8l1YCLXiocMEBkRR5tyHbmV4B04DZw3CXfXWVhi04P0oeA==","signatures":[{"sig":"MEYCIQCg3stkBDCKbjAdtxHmGQd9wVoWam3ZEZD7m4HFfzOTuAIhAP1W8CyRnU7Or3Y6LM3JogzpCGYAQdzZGguFASZSov8P","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":110335},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"9cfc1d12bec029727f1e5dc6f444cbe026094332","scripts":{"dev":"tsc --watch","build":"tsc"},"_npmUser":{"name":"antz.karthik","email":"karthikmitta@lifesciencetrust.com"},"_npmVersion":"10.9.2","description":"Minimal WSO2 IS login-redirect + token-storage connector for ThingsBoard CE's Angular frontend — a standard (non-SPA) confidential-client web app whose backend does its own OAuth2 code exchange.","directories":{},"_nodeVersion":"22.14.0","_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.4.5"},"_npmOperationalInternal":{"tmp":"tmp/wso2-auth-thingsboard-js_1.0.2_1786686927449_0.34328504444660957","host":"s3://npm-registry-packages-npm-production"}},"1.2.0":{"name":"@antzsoft/wso2-auth-thingsboard-js","version":"1.2.0","keywords":["wso2","oauth2","auth","identity","antz","thingsboard"],"license":"MIT","_id":"@antzsoft/wso2-auth-thingsboard-js@1.2.0","maintainers":[{"name":"antz.karthik","email":"karthikmitta@lifesciencetrust.com"},{"name":"antzsoft_trellisys","email":"antzsoft@lifesciencetrust.com"}],"dist":{"shasum":"dc8af89f6f981b430bbbc9202f3e7db64854cf27","tarball":"https://registry.npmjs.org/@antzsoft/wso2-auth-thingsboard-js/-/wso2-auth-thingsboard-js-1.2.0.tgz","fileCount":18,"integrity":"sha512-n1us4+c/NGb/VRothni87MOWffyJnqEpeTESBcARejV4HQsSKy2HrtVdbZpWERceWOasrnoDPf6FpE0ztK2wVA==","signatures":[{"sig":"MEUCIGAJGhKWyMT8JE8KimtBs8ZMwO1X9hu+uVBEORkOdkHsAiEAuaWyg7QfBEPgEeRogLQRqbQuauyAyiPHpMwsj6C+pvk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":124866},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"ae335ba5241dc12919de279349ed92294da8e6e8","scripts":{"dev":"tsc --watch","build":"tsc"},"_npmUser":{"name":"antz.karthik","email":"karthikmitta@lifesciencetrust.com"},"_npmVersion":"11.17.0","description":"Minimal WSO2 IS login-redirect + token-storage connector for ThingsBoard CE's Angular frontend — a standard (non-SPA) confidential-client web app whose backend does its own OAuth2 code exchange.","directories":{},"_nodeVersion":"24.19.0","_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.4.5"},"_npmOperationalInternal":{"tmp":"tmp/wso2-auth-thingsboard-js_1.2.0_1787548771060_0.8226952899692361","host":"s3://npm-registry-packages-npm-production"}},"1.2.1":{"name":"@antzsoft/wso2-auth-thingsboard-js","version":"1.2.1","keywords":["wso2","oauth2","auth","identity","antz","thingsboard"],"license":"MIT","_id":"@antzsoft/wso2-auth-thingsboard-js@1.2.1","maintainers":[{"name":"antz.karthik","email":"karthikmitta@lifesciencetrust.com"},{"name":"antzsoft_trellisys","email":"antzsoft@lifesciencetrust.com"}],"dist":{"shasum":"ece341ba5378a296338d07784aac518f77fe997f","tarball":"https://registry.npmjs.org/@antzsoft/wso2-auth-thingsboard-js/-/wso2-auth-thingsboard-js-1.2.1.tgz","fileCount":18,"integrity":"sha512-tK8rPvLYuMnKv1cmymbeikt5pE17nOOZn531xV6kxJZkTQ0L7Wo9dP9dolgTxo4AyYM5KKaTM3CzKOEbtIzsAg==","signatures":[{"sig":"MEYCIQCPIqcMctNQC9UMSLnn6KlzSWx5vpRBaM+WYEyo6ewG7QIhAKB5eQK6U/V+Nuy1cmhxcQtZ8GLFxu8XfG1EyuYLkmwd","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":130038},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"30cf0c4448cd2e8fda820fb005871ecdb56c3fa4","scripts":{"dev":"tsc --watch","build":"tsc"},"_npmUser":{"name":"antz.karthik","email":"karthikmitta@lifesciencetrust.com"},"_npmVersion":"11.17.0","description":"Minimal WSO2 IS login-redirect + token-storage connector for ThingsBoard CE's Angular frontend — a standard (non-SPA) confidential-client web app whose backend does its own OAuth2 code exchange.","directories":{},"_nodeVersion":"24.19.0","_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.4.5"},"_npmOperationalInternal":{"tmp":"tmp/wso2-auth-thingsboard-js_1.2.1_1787748412747_0.5849176327318435","host":"s3://npm-registry-packages-npm-production"}},"1.3.0":{"_id":"@antzsoft/wso2-auth-thingsboard-js@1.3.0","dist":{"shasum":"97242f65a922b1ef77972c0df616d63d151597c4","tarball":"https://registry.npmjs.org/@antzsoft/wso2-auth-thingsboard-js/-/wso2-auth-thingsboard-js-1.3.0.tgz","fileCount":78,"integrity":"sha512-cDwqyFNQLtHYF42P1tanCJf6jHYMrEXhY+kEPD0btmgkPcB82UHF4k+A+97E7IvoRMyVgfVzts7DIekK4vbicA==","signatures":[{"sig":"MEUCICEJuF+eqgz85igkIxjXnwtUKUnpET+8B1tJyRJTuHWEAiEAzbh4NYefrr5+nMWdSoWRdu9/huC1E8uxNXG8TLrGlnU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCkjL+ThOmauspQnnFyYyDjQLJG0uZE/dZYsJKuY5/kSQIgOmrxOosB3nIvZG9NMB7ooPxOJnLR51ju13WzQyEKzvw="}],"unpackedSize":476498},"main":"./dist/index.js","name":"@antzsoft/wso2-auth-thingsboard-js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"0d03a5b2494b8a553ec0d5be9acddf0490808472","license":"MIT","scripts":{"dev":"tsc --watch","test":"npm run build && node --no-warnings --loader ./test/ext-resolver.mjs test/storage.test.mjs && node --no-warnings --loader ./test/ext-resolver.mjs test/capture.test.mjs && node --no-warnings --loader ./test/ext-resolver.mjs test/refresh.test.mjs && node --no-warnings --loader ./test/ext-resolver.mjs test/auto-refresh.test.mjs && node --no-warnings --loader ./test/ext-resolver.mjs test/visibility-catchup.test.mjs && node --no-warnings --loader ./test/ext-resolver.mjs test/session-poll.test.mjs && node --no-warnings --loader ./test/ext-resolver.mjs test/focus-revalidation.test.mjs && node --no-warnings --loader ./test/ext-resolver.mjs test/silent-check.test.mjs && node --no-warnings --loader ./test/ext-resolver.mjs test/cooldown.test.mjs && node --no-warnings --loader ./test/ext-resolver.mjs test/login-url.test.mjs && node --no-warnings --loader ./test/ext-resolver.mjs test/logout.test.mjs && node --no-warnings --loader ./test/ext-resolver.mjs test/browser-binding.test.mjs && node --no-warnings --loader ./test/ext-resolver.mjs test/native-transport.test.mjs && node --no-warnings --loader ./test/ext-resolver.mjs test/sso-transport.test.mjs && node --no-warnings --loader ./test/ext-resolver.mjs test/capabilities.test.mjs && node --no-warnings --loader ./test/ext-resolver.mjs test/engine-transports.test.mjs && node --no-warnings --loader ./test/ext-resolver.mjs test/refresh-scheduling.test.mjs && node --no-warnings --loader ./test/ext-resolver.mjs test/coordinator.test.mjs","build":"tsc && node scripts/fix-dist-esm.mjs"},"version":"1.3.0","_npmUser":{"name":"antz.karthik","email":"karthikmitta@lifesciencetrust.com"},"keywords":["wso2","oauth2","auth","identity","antz","thingsboard"],"_npmVersion":"11.17.0","description":"Minimal WSO2 IS login-redirect + token-storage connector for ThingsBoard CE's Angular frontend — a standard (non-SPA) confidential-client web app whose backend does its own OAuth2 code exchange.","directories":{},"maintainers":[{"name":"antz.karthik","email":"karthikmitta@lifesciencetrust.com"},{"name":"antzsoft_trellisys","email":"antzsoft@lifesciencetrust.com"}],"_nodeVersion":"24.19.0","_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.4.5"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/wso2-auth-thingsboard-js_1.3.0_1789557798946_0.8797726468615112"}}},"time":{"created":"2026-07-24T07:02:41.660Z","modified":"2026-09-16T11:23:19.251Z","1.0.0":"2026-07-24T07:02:41.902Z","1.0.1":"2026-08-04T04:29:01.231Z","1.0.2":"2026-08-14T05:55:27.593Z","1.2.0":"2026-08-24T05:19:31.216Z","1.2.1":"2026-08-26T12:46:52.944Z","1.3.0":"2026-09-16T11:23:19.034Z"},"license":"MIT","keywords":["wso2","oauth2","auth","identity","antz","thingsboard"],"description":"Minimal WSO2 IS login-redirect + token-storage connector for ThingsBoard CE's Angular frontend — a standard (non-SPA) confidential-client web app whose backend does its own OAuth2 code exchange.","maintainers":[{"name":"antz.karthik","email":"karthikmitta@lifesciencetrust.com"},{"name":"antzsoft_trellisys","email":"antzsoft@lifesciencetrust.com"}],"readme":"# @antzsoft/wso2-auth-thingsboard-js\n\nMinimal login-redirect + token-capture connector for WSO2 Identity Server 7.x, built for\nThingsBoard CE's Angular frontend — a standard **confidential-client web app** (separate Java\nbackend + separate frontend), not a PKCE/SPA client.\n\nUnlike `@antzsoft/wso2-auth-web` (which owns the whole OAuth2 code exchange in the browser), this\nSDK never talks to WSO2 directly and never holds a client secret. The backend does its own\nauthorization-code exchange server-side (via Spring Security's stock OAuth2 login) and attaches\nthe WSO2 access/refresh token to the post-login redirect as query params — this package's whole\njob is to build the login-redirect URL, capture those query params, store them, and (optionally)\nkeep them fresh.\n\n---\n\n## Contents\n\n- [What's in the package](#whats-in-the-package)\n- [Installation](#installation)\n- [Configuration](#configuration)\n- [Core API](#core-api)\n  - [login() / getLoginUrl()](#login--getloginurl)\n  - [withLoginHint()](#withloginhint)\n  - [captureTokenFromRedirect()](#capturetokenfromredirect)\n  - [getStoredAccessToken() / getStoredTokens()](#getstoredaccesstoken--getstoredtokens)\n  - [getStoredIdToken() / getStoredOAuth2ClientId()](#getstoredidtoken--getstoredoauth2clientid)\n  - [logout()](#logout)\n  - [ssoLogout()](#ssologout)\n  - [refresh() / startAutoRefresh() / stopAutoRefresh()](#refresh--startautorefresh--stopautorefresh)\n  - [getSessionInfo() / startSessionPoll() / stopSessionPoll()](#getsessioninfo--startsessionpoll--stopsessionpoll)\n  - [clearStoredTokens()](#clearstoredtokens)\n- [Silent cross-app SSO](#silent-cross-app-sso)\n- [Backend Contract](#backend-contract)\n- [Integration Guide (ThingsBoard)](#integration-guide-thingsboard)\n\n---\n\n## What's in the package\n\n| Export | Description |\n|--------|-------------|\n| `AntzAuthClient` | Core client class — login/logout navigation, token capture/storage, proactive refresh |\n| `withLoginHint` | Stateless helper for apps that build their own per-provider login URLs |\n| `AuthTransportError` | Thrown by `refresh()`/`getSessionInfo()`; carries an HTTP `httpStatus` when available |\n| `isRetryable` / `isNetworkError` | The keep-vs-end-session classification — `isRetryable(err)` is `true` for a transient failure (network / 5xx / 429), `false` for a confirmed-dead token (4xx) |\n| `AntzAuthConfig`, `AntzTokenSet`, `AntzLoginOptions`, `AntzSessionInfo`, `AntzUserClaims` | TypeScript types |\n\n> **Breaking export change (unreleased):** `AntzWso2RefreshError` is retired. `refresh()` and\n> `getSessionInfo()` now throw the shared `AuthTransportError` (`.httpStatus` instead of\n> `.status`); classify failures with `isRetryable(err)`. The connector's engine layers\n> (`src/core/`, `src/transport/`) are now vendored copies of `@antzsoft/auth-web`'s tested\n> `SessionManager`, kept byte-identical by `scripts/check-shared.sh` — see\n> `docs/ENGINE-PORT-PLAN.md`. Version bump and publish happen at Phase 6.\n\n### Engine mode (advanced)\n\nAlongside `AntzAuthClient` — still used for WSO2 token capture, silent cross-app SSO and\nRP-initiated logout — the package also exports the tested lifecycle engine and its two\ntransports. As of ENGINE-PORT-PLAN.md Phase 4 the ThingsBoard fork's `AuthService` drives\n**TB's own JWT pair** through a `SessionManager` (`tbManager` + `ThingsboardJwtAdapter`),\nretiring its hand-rolled proactive-refresh timer / visibility catch-up / cross-tab listener:\n\n| Export | Purpose |\n|---|---|\n| `SessionManager` | The framework-free lifecycle engine — refresh timer, single-flight lock, retry-vs-logout, persistence. One instance per session lifecycle. |\n| `createSsoTransport(config)` | `AuthTransport` for the WSO2 session — `refresh` / `validateSession` / `changePassword` / `logout` against the backend's `/api/auth/wso2/*` routes. |\n| `createNativeTransport(config)` | `AuthTransport` for the app's own credential API — `/api/auth/login`, `/api/auth/token`, `/api/auth/changePassword`, `/api/auth/logout`, `/api/auth/user`. `verifyCredentials` is opt-in via `verifyCredentialsPath` (ThingsBoard CE has no verify-only endpoint, and reusing the login route would mint a session that then has to be torn down). |\n| `createRestTransport(options)` | For a transport this package does not ship — JSON-over-HTTP against another API, with per-request hooks. |\n| `attachBrowserLifecycle(manager, config)` | Wires `visibilitychange` / `online` / `storage` events to a `SessionManager`, plus refresh-token expiry warnings; returns a detach function. |\n| `deriveModeCapabilities(flags, transport?, overrides?)` | Collapses the `checkUser` response into `{ mode, capabilities }` for the login screen. `mode` is `'sso' \\| 'direct' \\| null` (`null` = not provisioned). |\n| `decodeJwt` / `expiryFromJwt` / `jwtToUser` | For a transport whose server issues JWTs. Use `expiryFromJwt()` rather than multiplying `exp` by hand — `TokenSet` expiries are epoch **milliseconds** and `exp` is in **seconds**. |\n| `SplitStorageAdapter` · `localStorageAdapter` · `createMemoryStorageAdapter()` | Storage adapters. ThingsBoard supplies its own (`ThingsboardJwtAdapter`) because it persists `jwt_token` under the app's existing keys. |\n| `AuthTransportError` · `AuthNotSupportedError` · `isRetryable()` · `isNetworkError()` | `isRetryable()` is the keep-vs-end-session decision: 4xx → the credential is dead, 5xx / network → transient. |\n\n**Capabilities are read from the transport, not declared.** The three feature flags\n(`credentialLogin`, `changePassword`, `verifyCredentials`) are `typeof transport.x === 'function'`;\nonly `passwordRecovery` and `usesRedirectCallback` come from the mode. A hand-written boolean can\nlie — it keeps saying `true` after someone deletes the method, and the screen then renders a control\nthat throws when pressed. `overrides` covers the one honest exception: an app that implements an\noperation *outside* the transport, as the fork does with login.\n\nThere is **no `switchMode`**. ThingsBoard picks the mode per user on the server (`bypassSso`), so\nit is not a user-facing choice; `capabilities` is the only part of the dual-mode API surface\nadopted.\n\nThere is no framework adapter (no React/Vue/Angular hook) — the class is plain TypeScript with\nonly browser globals (`localStorage`, `window`, `fetch`, `URL`). ThingsBoard's `AuthService`\n(Angular) wraps it directly; any other backend-mediated-redirect app can do the same.\n\n---\n\n## Installation\n\n```bash\nnpm install @antzsoft/wso2-auth-thingsboard-js\n```\n\nFor local SDK development, build/publish it to a local Verdaccio registry the same way as the\n`web`/`reactnative` SDKs — see `../PUBLISH.md` (the outer `sdks/` publishing tooling covers all\nfour packages: `web`, `rn`, `thingsboard-js`, and `spring`).\n\n---\n\n## Configuration\n\n```ts\nimport { AntzAuthClient } from \"@antzsoft/wso2-auth-thingsboard-js\";\n\nconst auth = new AntzAuthClient({\n  loginUrl: \"/oauth2/authorization/wso2\",       // optional — see below\n  logoutUrl: \"/logout\",                          // optional — see below\n  refreshUrl: \"/api/auth/wso2/refresh\",          // optional — enables refresh()/startAutoRefresh()\n  headersProvider: () => ({ Authorization: `Bearer ${myAppToken}` }), // optional\n});\n```\n\n### Config options\n\n| Option | Type | Required | Default | Description |\n|--------|------|----------|---------|-------------|\n| `loginUrl` | `string` | only for `login()`/`getLoginUrl()` | — | The app's own OAuth2 login-initiation URL (e.g. Spring Security's stock `/oauth2/authorization/{registrationId}`). Omit if the app builds its own per-provider URLs and only uses this SDK for token capture — `withLoginHint()` still works standalone. |\n| `logoutUrl` | `string` | only for `logout()` | — | The app's own logout URL. |\n| `accessTokenParam` | `string` | no | `wso2AccessToken` | Query param the backend attaches the WSO2 access token as. |\n| `refreshTokenParam` | `string` | no | `wso2RefreshToken` | Query param the backend attaches the WSO2 refresh token as. |\n| `idTokenParam` | `string` | no | `wso2IdToken` | Query param for the captured OIDC id_token (requires `openid` scope on the WSO2 app). |\n| `logoutUrlParam` | `string` | no | `wso2LogoutUrl` | Query param for WSO2's end-session URL — used by `ssoLogout()`. |\n| `oauth2ClientIdParam` | `string` | no | `wso2OAuth2ClientId` | Query param identifying which registered WSO2 app this session came from. |\n| `expiresInParam` | `string` | no | `wso2ExpiresIn` | Query param for the access token's remaining lifetime in seconds, as of the redirect. |\n| `storageKeyPrefix` | `string` | no | `antz_wso2_` | `localStorage` key prefix. |\n| `refreshUrl` | `string` | only for `refresh()`/`startAutoRefresh()` | — | The app's own backend endpoint that refreshes the WSO2 token pair server-side (this SDK never holds the confidential client's secret). |\n| `refreshBufferSeconds` | `number` | no | `60` | How many seconds before actual expiry to proactively refresh. |\n| `revalidateOnFocus` | `boolean` | no | `true` | Gates `startFocusRevalidation()`. When `false` that call is a no-op — no listener attached, throttle never consulted — so focus revalidation can be disabled from config without removing the call site. |\n| `revalidateOnFocusMinIntervalSeconds` | `number` | no | `300` | Minimum gap between two `startFocusRevalidation()` firings, in seconds (300 = 5 minutes). The window starts when `startFocusRevalidation()` is called, so the page load's own check counts as the first one — a refocus shortly after load is throttled, not fired. `0` disables the throttle; `startFocusRevalidation`'s own second argument overrides this per call. |\n| `headersProvider` | `() => Record<string, string>` | no | — | Called once per `refresh()`/`getSessionInfo()` request to supply extra headers (e.g. the host app's own bearer token, if the backend endpoint requires authentication). |\n| `onSessionExpired` | `() => void` | no | — | Fires when the proactive refresh loop confirms the WSO2 refresh token itself is dead (a 4xx response from `refreshUrl`, e.g. `invalid_grant`) — never for a transient network/5xx failure, which is retried silently instead. |\n| `sessionInfoUrl` | `string` | only for `getSessionInfo()`/`startSessionPoll()` | — | The app's own backend endpoint that returns the current WSO2 token's expiry, read server-side from WSO2's token DB (the `wso2-session-info` bundle). |\n| `sessionPollIntervalSeconds` | `number` | no | `0` (off) | How often `startSessionPoll()` calls `getSessionInfo()` to detect server-side session revocation ahead of the access token's own natural expiry. |\n\n---\n\n## Core API\n\n### `login()` / `getLoginUrl()`\n\nNavigates the browser to the app's own OAuth2 login-initiation URL (`loginUrl`), with an optional\n`login_hint` to prefill WSO2's hosted login form.\n\n```ts\nauth.login({ loginHint: \"jane@example.com\" }); // navigates immediately\nconst href = auth.getLoginUrl();                // for <a href> bindings, doesn't navigate\n```\n\n`login_hint` is a pure UX convenience — it is **never** a user-existence check. WSO2 still owns\nall credential/existence validation.\n\n**Multi-account / session-mismatch enforcement** — `expectedUser`/`autoSwitchAccount` guard\nagainst WSO2's single-session-per-browser behavior (see `docs/multi-account-login-behavior.md`):\n\n```ts\nauth.login({ loginHint: \"jane@example.com\", expectedUser: \"jane@example.com\" });\n// default (autoSwitchAccount unset/false): if the browser's live WSO2 SSO session belongs to a\n// DIFFERENT user, the backend redirects to /login?loginError=... instead of completing the\n// login as the wrong user.\n\nauth.login({ loginHint: \"jane@example.com\", expectedUser: \"jane@example.com\", autoSwitchAccount: true });\n// opt-in: same user → silent reuse (no login page shown); different user → backend forces a\n// fresh WSO2 credential prompt (prompt=login) for jane@example.com instead of erroring.\n```\n\nUnlike `login_hint`, this SDK never checks the result itself — it never sees the OAuth2 callback,\nthe backend does (see `../docs/INTEGRATION_GUIDE.md`'s `Oauth2AuthenticationSuccessHandler` section). A\nmismatch surfaces as the app's existing `loginError` query param, the same mechanism any other\nOAuth2 login failure already uses — there is no thrown JS exception to catch here.\n\n---\n\n### `withLoginHint()`\n\nStateless helper for apps that build their own per-provider login URLs (e.g. ThingsBoard, which\ncan have several configured OAuth2 clients) rather than using a single fixed `loginUrl`. Carries\nthe same multi-account enforcement as `login()`/`getLoginUrl()` above: since there's only ever the\none username the user typed into the form, it sets **both** `login_hint` and `expected_user` to\n`loginHint` (there's no separate `expectedUser` to pass).\n\n```ts\nimport { withLoginHint } from \"@antzsoft/wso2-auth-thingsboard-js\";\n\nconst url = withLoginHint(oauth2Client.url, username); // appends login_hint AND expected_user\n\nconst urlAutoSwitch = withLoginHint(oauth2Client.url, username, /* autoSwitchAccount */ true);\n// also appends auto_switch_account=true — see the `login()` example above for the semantics\n```\n\n---\n\n### `captureTokenFromRedirect()`\n\nReads the WSO2 access/refresh/id token, end-session URL, OAuth2 client id, and expiry (whichever\nare present) off the current URL's query params — attached by the backend's OAuth2 success\nhandler after login — stores them, and strips them from the URL. Call this once on app bootstrap.\n\n```ts\nconst tokens = auth.captureTokenFromRedirect();\n// { accessToken, refreshToken?, idToken?, logoutUrl?, expiresAt? } | null\n```\n\nReturns `null` if no access token query param was present (e.g. a non-SSO page load).\n\n---\n\n### `getStoredAccessToken()` / `getStoredTokens()`\n\n```ts\nconst token = auth.getStoredAccessToken(); // string | null — for later API calls (e.g. change-password)\nconst tokens = auth.getStoredTokens();     // AntzTokenSet | null\n```\n\n### `getStoredIdToken()` / `getStoredOAuth2ClientId()`\n\n```ts\nconst idToken = auth.getStoredIdToken();               // string | null — present only if `openid` scope was granted\nconst oauth2ClientId = auth.getStoredOAuth2ClientId();  // string | null — which registered WSO2 app this session came from\n```\n\n---\n\n### `logout()`\n\nNavigates the browser to the app's own `logoutUrl` and clears stored WSO2 tokens. This is a\n**local, app-only** logout — it does not touch WSO2's shared SSO session. Use `ssoLogout()`\ninstead when you need to actually kill the `commonAuthId` cookie.\n\n```ts\nauth.logout();\n```\n\n---\n\n### `ssoLogout()`\n\nReal WSO2 SSO logout: a **top-level browser navigation** (not `fetch`, not an iframe) to WSO2's\nend-session endpoint with the id_token captured at login, killing the shared `commonAuthId`\ncookie so a later SSO redirect — in this app or any other — can't silently re-authenticate the\nuser.\n\n```ts\nconst navigated = auth.ssoLogout(\"https://your-app.example.com/login\"); // post_logout_redirect_uri\nif (!navigated) {\n  auth.clearStoredTokens(); // local-only fallback\n}\n```\n\nA real navigation is required here, not an iframe/fetch:\n- WSO2's `/oidc/logout` does not send `Access-Control-Allow-Credentials`, so a cross-origin\n  `fetch` would never carry the `commonAuthId` cookie even with `credentials: 'include'`.\n- WSO2 commonly sends `X-Frame-Options`/a restrictive `frame-ancestors` CSP on its login/logout\n  pages (clickjacking protection), which silently blocks an iframe from loading them at all.\n\n**Returns `false`** (and does nothing) if no id_token/logout URL was captured at login — e.g. the\nWSO2 app wasn't registered with `openid` scope, or this session didn't go through a real SSO\nredirect at all (a local-login-only session). Callers should fall back to `logout()` in that case.\n\n---\n\n### `refresh()` / `startAutoRefresh()` / `stopAutoRefresh()`\n\nRenews the stored WSO2 access/refresh token pair via the app's own `refreshUrl` — the backend\ncalls WSO2's `refresh_token` grant server-side, since this SDK never holds the confidential\nclient's secret.\n\n```ts\nconst tokens = await auth.refresh(); // throws if refreshUrl unset, no refresh token stored, or the request fails\n\nauth.startAutoRefresh(); // self-re-arming proactive refresh, fires `refreshBufferSeconds` before expiry\nauth.stopAutoRefresh();  // cancel a pending scheduled refresh\n```\n\n`startAutoRefresh()` is a no-op if no WSO2 token is currently stored (e.g. a local-login-only\nsession that never went through WSO2 SSO) or `expiresAt` isn't known yet. A failed refresh either:\n\n- **retries** after a short delay, for a transient network/5xx failure — never treated as\n  \"session ended,\" or\n- **stops and calls `onSessionExpired`**, for a confirmed-dead refresh token (a 4xx response —\n  WSO2 itself rejected it, e.g. `invalid_grant`).\n\n`startAutoRefresh()` also installs a `visibilitychange` listener: if the tab was backgrounded (or\nsuspended) long enough that the scheduled timer didn't fire on time, regaining focus triggers an\nimmediate catch-up refresh instead of waiting for the next scheduled attempt or the next API call.\n`stopAutoRefresh()` removes both the timer and this listener.\n\nConcurrent `refresh()` calls (e.g. the scheduled timer and a `visibilitychange` catch-up landing\naround the same time) share a single in-flight request rather than each independently POSTing —\nWSO2 rotates the refresh token on use, so two parallel requests would otherwise race and one would\nget rejected with an already-stale refresh token. The underlying `fetch()` is also guarded by a\n15s timeout, so a stuck request against an unreachable backend can't stall the refresh loop\nforever — a timeout is treated the same as any other network failure (retried, never treated as\n\"session ended\").\n\n---\n\n### `getSessionInfo()` / `startSessionPoll()` / `stopSessionPoll()`\n\nFetches the current WSO2 access/refresh token's expiry via the app's own `sessionInfoUrl` — server-side\ntruth read from WSO2's token DB by the `wso2-session-info` bundle, rather than trusting the\nlocally-stored `expiresAt`'s clock or assuming the token is still valid. This is how a session that\nwas revoked server-side (e.g. an admin-forced logout, or a password changed on another device) gets\ndetected — something the proactive refresh timer alone can't catch, since it only reacts to the\nlocally-known `expiresAt`.\n\n```ts\nconst info = await auth.getSessionInfo(); // throws if sessionInfoUrl unset, no access token stored, or the request fails\n\nauth.startSessionPoll(() => {\n  // Called once, and the poll stops itself, when the session is confirmed dead server-side (4xx).\n  console.log(\"Session expired — redirect to login\");\n});\nauth.stopSessionPoll(); // cancel a running poll\n```\n\n`startSessionPoll()` is a no-op unless both `sessionInfoUrl` and `sessionPollIntervalSeconds` are\nconfigured (the latter defaults to `0`/off), or if a poll is already running. Paused while the tab\nis hidden (`document.visibilityState === 'hidden'`) — a network/5xx failure on a tick is swallowed\nand retried on the next tick, never treated as \"session ended.\" `clearStoredTokens()` and\n`ssoLogout()` both stop a running poll automatically, same as they do for auto-refresh.\n\n---\n\n### `clearStoredTokens()`\n\nClears any stored WSO2 tokens without navigating anywhere. Also stops auto-refresh and the session poll, if running.\n\n```ts\nauth.clearStoredTokens(); // e.g. after a successful password change (WSO2 revokes the old token anyway)\n```\n\n---\n\n## Silent cross-app SSO\n\nWhen a WSO2 SSO session already exists in the browser (login elsewhere), the app can pick it up\nautomatically — auto-login on the login page, auto-logout if the shared session vanished elsewhere,\nauto-switch if it now belongs to a different (already TB-provisioned) user — without ever showing a\ncredential form. Full spec: `docs/thingsboard-silent-sso-design.md` (repo root) and\n`../docs/silent-cross-app-sso.md`.\n\n> **These three methods perform a real top-level browser navigation and do not resolve\n> synchronously** — same warning as `login()`/`getLoginUrl()` above, but easier to miss here since\n> nothing in the method names says \"navigate.\" Don't `await` `checkSilentSession`/\n> `checkSilentSessionAuthenticated` expecting a return value; call `handleSilentCheckReturn()` once\n> at bootstrap instead.\n\n```ts\n// Login page: call unconditionally — the SDK's own cooldown/grace-period guards prevent a\n// redirect loop, not the call site.\nauth.checkSilentSession({ loginUrl: oauth2Client.url }); // loginUrl only needed if config.loginUrl isn't set\n\n// Authenticated page: call once per page load, with the current user's own email.\nauth.checkSilentSessionAuthenticated(currentUser.email, { loginUrl: oauth2Client.url });\n\n// App bootstrap, alongside (before) captureTokenFromRedirect():\nconst { handled } = await auth.handleSilentCheckReturn(async ({ reason, previousUser, newUser }) => {\n  // reason: 'no_session' | 'different_user' | 'app_access_denied' | 'app_rejected_by_own_policy'\n  // Only called when something actually changed — never for a same-user no-op or a fresh\n  // case-1 auto-login. Clear this app's own session state here; never touch WSO2's commonAuthId.\n});\nif (!handled) {\n  auth.captureTokenFromRedirect(); // normal login-return path, untouched\n}\n```\n\n- **`buildSilentCheckUrl(options?)`** — builds the `silent=true&prompt=none[&expected_user=...]`\n  probe URL. Pass `options.loginUrl` for apps that build per-provider URLs rather than a single\n  `config.loginUrl` (same reasoning as `withLoginHint`'s standalone `url` parameter).\n- **`checkSilentSession(options?)`** — login-page auto-login. No-ops within a short cooldown after\n  the last silent-check return, or within a short grace period after any successful token capture\n  (real login or silent-check success) — both guard against redirect loops/races, see the design\n  doc for the exact regressions each one prevents.\n- **`checkSilentSessionAuthenticated(expectedUserEmail, options?)`** — authenticated-page\n  auto-logout/auto-switch. Same guards, plus lets the backend distinguish same-user (no-op) from\n  different-user (auto-switch) from no-session (auto-logout).\n- **`handleSilentCheckReturn(onSharedSessionChanged?)`** — interprets a silent-check return.\n  Returns `{ handled: false }` immediately for a normal (non-silent) URL. `onSharedSessionChanged`\n  is awaited once, only on an actual change; `previousUser`/`newUser` are `AntzUserClaims` (decoded\n  id_token claims — `sub`, `email`, etc.).\n\nA background silent probe never auto-provisions a brand-new TB account — an authenticated-but-not-\nyet-TB-provisioned WSO2 identity comes back as `reason: 'app_access_denied'` instead of silently\ncreating one. That check lives server-side (`Oauth2AuthenticationSuccessHandler`, ThingsBoard\nsample), not in this SDK.\n\n### `startFocusRevalidation()` / `stopFocusRevalidation()`\n\n`revalidateOnFocus` for this SDK — the same feature `@antzsoft/wso2-auth-web`'s\n`UseAntzAuthOptions.revalidateOnFocus` and `Antzsoft.Wso2Auth.BlazorServer`'s\n`AntzAuthConfig.RevalidateOnFocus` ship, and gated by the matching\n`AntzAuthConfig.revalidateOnFocus` flag (default `true`). Re-runs the authenticated-page silent check on tab refocus,\nnot just on the app's own login/logout transitions — otherwise a tab left open and refocused after a\ncross-app global logout or account switch elsewhere wouldn't notice until the next real navigation.\n\n```ts\nauth.startFocusRevalidation(() => {\n  auth.checkSilentSessionAuthenticated(currentUser.email, { loginUrl: oauth2Client.url });\n});\nauth.stopFocusRevalidation(); // remove the listener\n```\n\nUnlike Web/Blazor's variant — a background `fetch`, invisible to the user — `checkSilentSessionAuthenticated`\nhere is a real top-level browser navigation, since the browser never holds a token to check locally.\n`startFocusRevalidation` accounts for that: on top of `checkSilentSessionAuthenticated`'s own\ncooldown/grace-period guards (which only cover the round trip itself), it throttles how often `onFocus`\nfires — default **300s (5 minutes)** — so a user alt-tabbing back and forth a few times a minute\ndoesn't reload the whole app on every single refocus.\n\nDisabling it: focus revalidation here is opt-in by call — nothing happens unless the app calls\n`startFocusRevalidation()`. Set `AntzAuthConfig.revalidateOnFocus = false` (default `true`) to turn it\noff from configuration without removing the call site — the call becomes a no-op, no listener is\nattached, and the throttle below is never consulted. `stopFocusRevalidation()` remains the way to\ndetach a listener already installed.\n\nThat throttle is configurable, resolved in this order:\n\n1. `startFocusRevalidation(onFocus, minIntervalSeconds)` — the optional second argument, per call site.\n2. `AntzAuthConfig.revalidateOnFocusMinIntervalSeconds` — set once where the client is constructed.\n3. `DEFAULT_REVALIDATE_ON_FOCUS_MIN_INTERVAL_SECONDS` (300 = 5 minutes).\n\n`0` is honoured as \"no throttle\"; negative and non-finite values fall through to the next source.\n\n```ts\n// every refocus, at most once every 15s\nauth.startFocusRevalidation(() => auth.checkSilentSessionAuthenticated(email, opts), 15);\n```\n\n### Customizing error messages\n\nAn app can reject a WSO2-authenticated, TB-provisioned user via its own rule (role, group, feature\nflag, a check against another backend) that neither WSO2 nor TB's OAuth2 mapper config can express —\nby registering an `Oauth2AppAccessPolicy` bean server-side (ThingsBoard sample,\n`Oauth2AuthenticationSuccessHandler`). On rejection, the just-issued WSO2 refresh token is revoked\n(this app's own `client_id` only) and:\n\n- **Manual login** — the browser lands back on `/login?loginError=<message>`, same convention as\n  the existing account-mismatch error; read the message off the query string to show it directly, or\n  match on it to show your own copy instead.\n- **Silent probe** — surfaces through `handleSilentCheckReturn`'s `onSharedSessionChanged` as\n  `reason: 'app_rejected_by_own_policy'` — the same single place you already handle `no_session`/\n  `different_user`/`app_access_denied`.\n\nThis SDK has no config surface for the policy itself — see\n`Oauth2AppAccessPolicy`/`Oauth2AppAccessDeniedException` in the ThingsBoard sample fork\n(`docs/silent-cross-app-sso.md` §3.2a).\n\n---\n\n## Backend Contract\n\nThis SDK is one half of a pair — the other half is the backend's own OAuth2 success handler,\nwhich must attach these query params to the post-login redirect (all optional except the access\ntoken; names configurable via the `*Param` config options above):\n\n| Query param | Purpose |\n|---|---|\n| `wso2AccessToken` | The WSO2 access token (required — its absence means `captureTokenFromRedirect()` returns `null`) |\n| `wso2RefreshToken` | The WSO2 refresh token |\n| `wso2IdToken` | The OIDC id_token, if the WSO2 app was logged in via an `openid`-scoped flow |\n| `wso2LogoutUrl` | WSO2's end-session (`/oidc/logout`) URL for this tenant, if an id_token was captured |\n| `wso2OAuth2ClientId` | Identifier for which registered WSO2 app this session came from |\n| `wso2ExpiresIn` | Seconds until the access token expires, as of the redirect |\n| `wso2Silent` | `true` on any return from a [silent cross-app SSO](#silent-cross-app-sso) probe — read by `handleSilentCheckReturn()`, never a normal login return |\n| `wso2SilentSwitch` | `true` alongside a successful `wso2Silent` return whose identity differs from what the app previously knew |\n| `wso2SilentError` | The OAuth2/backend error code on a failed `wso2Silent` return (e.g. `login_required`, `access_denied`, `app_access_denied_by_policy`) — mutually exclusive with `wso2AccessToken` being present |\n\nAnd, for `refresh()`/`startAutoRefresh()` to work, an endpoint at the configured `refreshUrl`\naccepting `POST { refreshToken, oauth2ClientId }` and returning\n`{ accessToken, refreshToken?, idToken?, expiresIn? }`.\n\nAnd, for `getSessionInfo()`/`startSessionPoll()` to work, an endpoint at the configured\n`sessionInfoUrl` accepting `GET` with an `X-Wso2-Access-Token` header (the caller's own stored WSO2\naccess token) and returning\n`{ accessTokenExpiresAt, accessTokenExpiresInSeconds, refreshTokenExpiresAt, refreshTokenExpiresInSeconds }`\n(the first/third as Unix epoch seconds — this SDK converts to epoch milliseconds itself).\n\nSee `com.antzsoft.wso2auth:wso2-auth-spring` (the companion Spring Boot connector) for a\nready-made server-side implementation of this contract.\n\n---\n\n## Integration Guide (ThingsBoard)\n\nSee `../docs/INTEGRATION_GUIDE.md` in this repo for the full end-to-end wiring into ThingsBoard's\nAngular `AuthService`/`LoginComponent`/`AuthController` (Spring), including the `global` logout\nflow, JWKS validation, change-password, and M2M SCIM2 user administration on the backend side.\n","readmeFilename":"README.md"}