{"_id":"@atol-sh/react","_rev":"5-bde2b9499cc691390dd0a55942a8ad90","name":"@atol-sh/react","dist-tags":{"latest":"0.5.1"},"versions":{"0.3.0":{"name":"@atol-sh/react","version":"0.3.0","keywords":["atol","auth","authorization","oidc","react","permissions"],"author":{"name":"Atol"},"license":"Apache-2.0","_id":"@atol-sh/react@0.3.0","maintainers":[{"name":"remiphilippe","email":"remi@aiku.fr"}],"homepage":"https://atol.sh","bugs":{"url":"https://github.com/atol-sh/atol-sdk-react/issues"},"dist":{"shasum":"7ef96b06f75ae4b51b3e62f098b0fbbf92c1a676","tarball":"https://registry.npmjs.org/@atol-sh/react/-/react-0.3.0.tgz","fileCount":11,"integrity":"sha512-p47DcHAE42LEOP+7smHgaC8LdLctxE/YllcCdrc7g3PfrTGRu44L8g8x09bMCqdG9ndlz006DpJSAjRx0IRKSQ==","signatures":[{"sig":"MEYCIQCnz7AugvTVtBmmI8TcUrCPnCbNCdAuUjaeY6sfV/B1JAIhAObQUoTHVHIlD9EHG2vwhoGwAQQGfDo78cQANCkPKh3l","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":162660},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"default":"./dist/index.js","require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"gitHead":"e7340e91b959f86d04591036bb08fa55ad4f8875","scripts":{"dev":"tsup --watch","lint":"tsc --noEmit","test":"vitest run","build":"tsup","clean":"rm -rf dist","typecheck":"tsc --noEmit","test:watch":"vitest","verify:pack":"publint && attw --pack . --profile node16","prepublishOnly":"npm run typecheck && npm test && npm run build"},"_npmUser":{"name":"remiphilippe","email":"remi@aiku.fr"},"repository":{"url":"git+https://github.com/atol-sh/atol-sdk-react.git","type":"git"},"_npmVersion":"11.12.1","description":"Atol React SDK — authentication and frontend authorization","directories":{},"sideEffects":false,"_nodeVersion":"26.0.0","dependencies":{"jose":"^5.10.0","@atol-sh/js":"^0.1.0","oidc-client-ts":"^3.3.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.4.0","jsdom":"^26.1.0","react":"^18.3.0","vitest":"^3.1.0","publint":"^0.3.21","react-dom":"^18.3.0","typescript":"^5.7.0","@types/node":"^22.20.0","@types/react":"^18.3.0","fake-indexeddb":"^6.2.5","@types/react-dom":"^18.3.0","@vitest/coverage-v8":"^3.2.6","@arethetypeswrong/cli":"^0.18.4","@testing-library/react":"^16.3.0","@testing-library/jest-dom":"^6.6.0"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0","react-dom":"^18.0.0 || ^19.0.0","@atol-sh/fingerprint":">=0.1.0"},"peerDependenciesMeta":{"react":{"optional":false},"react-dom":{"optional":false},"@atol-sh/fingerprint":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/react_0.3.0_1783435885399_0.5104895955799966","host":"s3://npm-registry-packages-npm-production"}},"0.3.1":{"name":"@atol-sh/react","version":"0.3.1","keywords":["atol","auth","authorization","oidc","react","permissions"],"author":{"name":"Atol"},"license":"Apache-2.0","_id":"@atol-sh/react@0.3.1","maintainers":[{"name":"remiphilippe","email":"remi@aiku.fr"}],"homepage":"https://atol.sh","bugs":{"url":"https://github.com/atol-sh/atol-sdk-react/issues"},"dist":{"shasum":"546853e1a87b762b77e4134a0ba1f167795f3191","tarball":"https://registry.npmjs.org/@atol-sh/react/-/react-0.3.1.tgz","fileCount":17,"integrity":"sha512-ZWkAj6qxH7mRunMFfBFRtHlA9LxerXRZntNTT5o4n2aX4GjM/LKWlyGteqdZskogcJBVwK8UjzQcwQ7p9IneqQ==","signatures":[{"sig":"MEYCIQCwkaxzMjDSd9QamiZRSXgpiWU5L0p5TBF/PH4r3qbF+gIhAM8ujHu7/fMz33yGWF9eEk4VmKp1HkJrjYDA31t+/vhc","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@atol-sh%2freact@0.3.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":199028},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"default":"./dist/index.js","require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./testing":{"import":{"types":"./dist/testing.d.ts","default":"./dist/testing.js"},"default":"./dist/testing.js","require":{"types":"./dist/testing.d.cts","default":"./dist/testing.cjs"}},"./package.json":"./package.json"},"gitHead":"3b1ed25f754de2e050beba123f7ac0479960ad34","scripts":{"dev":"tsup --watch","lint":"tsc --noEmit","test":"vitest run","build":"tsup","clean":"rm -rf dist","typecheck":"tsc --noEmit","test:watch":"vitest","verify:pack":"publint && attw --pack . --profile node16","prepublishOnly":"npm run typecheck && npm test && npm run build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:4c39c648-081f-4319-af3d-ec0a1bb56bf0"}},"repository":{"url":"git+https://github.com/atol-sh/atol-sdk-react.git","type":"git"},"_npmVersion":"11.18.0","description":"Atol React SDK — authentication and frontend authorization","directories":{},"sideEffects":false,"_nodeVersion":"22.23.1","dependencies":{"jose":"^5.10.0","@atol-sh/js":"^0.1.1","oidc-client-ts":"^3.3.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.4.0","jsdom":"^26.1.0","react":"^18.3.0","vitest":"^3.1.0","publint":"^0.3.21","react-dom":"^18.3.0","typescript":"^5.7.0","@types/node":"^22.20.0","@types/react":"^18.3.0","fake-indexeddb":"^6.2.5","@types/react-dom":"^18.3.0","@vitest/coverage-v8":"^3.2.6","@arethetypeswrong/cli":"^0.18.4","@testing-library/react":"^16.3.0","@testing-library/jest-dom":"^6.6.0"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0","react-dom":"^18.0.0 || ^19.0.0","@atol-sh/fingerprint":">=0.1.0"},"peerDependenciesMeta":{"react":{"optional":false},"react-dom":{"optional":false},"@atol-sh/fingerprint":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/react_0.3.1_1783525462848_0.0855918212309914","host":"s3://npm-registry-packages-npm-production"}},"0.3.2":{"name":"@atol-sh/react","version":"0.3.2","keywords":["atol","auth","authorization","oidc","react","permissions"],"author":{"name":"Atol"},"license":"Apache-2.0","_id":"@atol-sh/react@0.3.2","maintainers":[{"name":"remiphilippe","email":"remi@aiku.fr"}],"homepage":"https://atol.sh","bugs":{"url":"https://github.com/atol-sh/atol-sdk-react/issues"},"dist":{"shasum":"d558c5c2ee9b7cadeadf00fe0698634c09accea6","tarball":"https://registry.npmjs.org/@atol-sh/react/-/react-0.3.2.tgz","fileCount":17,"integrity":"sha512-hT1yIs8QYcI42CFki2UZ5PnIX9KCHMwIHUMqkUXI7/AiYfjdof6PT1DIY2aFoxDh4RcJkorCwUm8FijX2zFJeA==","signatures":[{"sig":"MEUCIADRjdmf5cLhP0kdrUa4LNUffC4mj625Rt7IUb6B1nuhAiEA5D6IgVqu8r0zc0+FkO0SQaBCrHXS0RjRdTlMyNVS7qM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@atol-sh%2freact@0.3.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":202010},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"default":"./dist/index.js","require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./testing":{"import":{"types":"./dist/testing.d.ts","default":"./dist/testing.js"},"default":"./dist/testing.js","require":{"types":"./dist/testing.d.cts","default":"./dist/testing.cjs"}},"./package.json":"./package.json"},"gitHead":"65d6a5562d461471f36a1fcec1520ee5d5a784e5","scripts":{"dev":"tsup --watch","lint":"tsc --noEmit","test":"vitest run","build":"tsup","clean":"rm -rf dist","typecheck":"tsc --noEmit","test:watch":"vitest","verify:pack":"publint && attw --pack . --profile node16","prepublishOnly":"npm run typecheck && npm test && npm run build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:4c39c648-081f-4319-af3d-ec0a1bb56bf0"}},"repository":{"url":"git+https://github.com/atol-sh/atol-sdk-react.git","type":"git"},"_npmVersion":"11.18.0","description":"Atol React SDK — authentication and frontend authorization","directories":{},"sideEffects":false,"_nodeVersion":"22.23.1","dependencies":{"jose":"^5.10.0","@atol-sh/js":"^0.1.2","oidc-client-ts":"^3.3.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.4.0","jsdom":"^26.1.0","react":"^18.3.0","vitest":"^3.1.0","publint":"^0.3.21","react-dom":"^18.3.0","typescript":"^5.7.0","@types/node":"^22.20.0","@types/react":"^18.3.0","fake-indexeddb":"^6.2.5","@types/react-dom":"^18.3.0","@vitest/coverage-v8":"^3.2.6","@arethetypeswrong/cli":"^0.18.4","@testing-library/react":"^16.3.0","@testing-library/jest-dom":"^6.6.0"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0","react-dom":"^18.0.0 || ^19.0.0","@atol-sh/fingerprint":">=0.1.0"},"peerDependenciesMeta":{"react":{"optional":false},"react-dom":{"optional":false},"@atol-sh/fingerprint":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/react_0.3.2_1783569528689_0.20882511801285708","host":"s3://npm-registry-packages-npm-production"}},"0.3.3":{"name":"@atol-sh/react","version":"0.3.3","keywords":["atol","auth","authorization","oidc","react","permissions"],"author":{"name":"Atol"},"license":"Apache-2.0","_id":"@atol-sh/react@0.3.3","maintainers":[{"name":"remiphilippe","email":"remi@aiku.fr"}],"homepage":"https://atol.sh","bugs":{"url":"https://github.com/atol-sh/atol-sdk-react/issues"},"dist":{"shasum":"cbf851417e326e32cd31f635e2e98355bc49d3aa","tarball":"https://registry.npmjs.org/@atol-sh/react/-/react-0.3.3.tgz","fileCount":17,"integrity":"sha512-E5tzVjW1P6QEwgxo5oS9XZRe1+uoAhMJd2oru1wJWVwelpEbkSOWh7ny3I6pOSmHv+zNhvwTsoU/zCTbeJ6yPQ==","signatures":[{"sig":"MEYCIQCb1ivDlZIS5XBMnrrm40P/9E/TvjqR6LDM64qf0jJrCwIhAMNQYDtf6aHxwzJeBO394k7r0lWRHYWcO4m96XJs+Gme","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@atol-sh%2freact@0.3.3","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":224497},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"default":"./dist/index.js","require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./testing":{"import":{"types":"./dist/testing.d.ts","default":"./dist/testing.js"},"default":"./dist/testing.js","require":{"types":"./dist/testing.d.cts","default":"./dist/testing.cjs"}},"./package.json":"./package.json"},"gitHead":"6f08c1445c82347e609fbc05807c87c3d87e0c75","scripts":{"dev":"tsup --watch","lint":"tsc --noEmit","test":"vitest run","build":"tsup","clean":"rm -rf dist","typecheck":"tsc --noEmit","test:watch":"vitest","verify:pack":"publint && attw --pack . --profile node16","prepublishOnly":"npm run typecheck && npm test && npm run build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:4c39c648-081f-4319-af3d-ec0a1bb56bf0"}},"overrides":{"esbuild":"0.28.1"},"repository":{"url":"git+https://github.com/atol-sh/atol-sdk-react.git","type":"git"},"_npmVersion":"11.18.0","description":"Atol React SDK — authentication and frontend authorization","directories":{},"sideEffects":false,"_nodeVersion":"22.23.1","allowScripts":{"esbuild@0.28.1":true,"fsevents@2.3.3":false},"dependencies":{"jose":"^5.10.0","@atol-sh/js":"^0.1.2","oidc-client-ts":"^3.3.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.4.0","jsdom":"^29.1.1","react":"^18.3.0","vitest":"^4.1.10","publint":"^0.3.21","react-dom":"^18.3.0","typescript":"^5.7.0","@types/node":"^22.20.0","@types/react":"^18.3.0","fake-indexeddb":"^6.2.5","@types/react-dom":"^18.3.0","@vitest/coverage-v8":"^4.1.10","@arethetypeswrong/cli":"^0.18.5","@testing-library/react":"^16.3.0","@testing-library/jest-dom":"^6.6.0"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0","react-dom":"^18.0.0 || ^19.0.0","@atol-sh/fingerprint":">=0.1.0"},"peerDependenciesMeta":{"react":{"optional":false},"react-dom":{"optional":false},"@atol-sh/fingerprint":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/react_0.3.3_1784562722323_0.4365870657599471","host":"s3://npm-registry-packages-npm-production"}},"0.5.1":{"name":"@atol-sh/react","version":"0.5.1","description":"Atol React SDK — authentication and frontend authorization","license":"Apache-2.0","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"},"default":"./dist/index.js"},"./testing":{"import":{"types":"./dist/testing.d.ts","default":"./dist/testing.js"},"require":{"types":"./dist/testing.d.cts","default":"./dist/testing.cjs"},"default":"./dist/testing.js"},"./package.json":"./package.json"},"sideEffects":false,"publishConfig":{"access":"public"},"scripts":{"build":"tsup","dev":"tsup --watch","test":"vitest run","test:watch":"vitest","typecheck":"tsc --noEmit && tsc --project tsconfig.tests.json","lint":"tsc --noEmit","clean":"rm -rf dist","prepublishOnly":"npm run typecheck && npm test && npm run build","verify:pack":"publint && attw --pack . --profile node16"},"engines":{"node":">=18"},"author":{"name":"Atol"},"dependencies":{"@atol-sh/js":"^0.3.0","jose":"^5.10.0","oidc-client-ts":"^3.3.0"},"peerDependencies":{"@atol-sh/fingerprint":"^0.2.0","react":"^18.0.0 || ^19.0.0","react-dom":"^18.0.0 || ^19.0.0"},"devDependencies":{"@arethetypeswrong/cli":"^0.18.5","@testing-library/jest-dom":"^6.6.0","@testing-library/react":"^16.3.0","@types/node":"^22.20.0","@types/react":"^18.3.0","@types/react-dom":"^18.3.0","@vitest/coverage-v8":"^4.1.10","fake-indexeddb":"^6.2.5","jsdom":"^29.1.1","publint":"^0.3.21","react":"^18.3.0","react-dom":"^18.3.0","tsup":"^8.4.0","typescript":"^5.7.0","vitest":"^4.1.10"},"peerDependenciesMeta":{"@atol-sh/fingerprint":{"optional":true},"react":{"optional":false},"react-dom":{"optional":false}},"overrides":{"esbuild":"0.28.1"},"repository":{"type":"git","url":"git+https://github.com/atol-sh/atol-sdk-react.git"},"homepage":"https://atol.sh","bugs":{"url":"https://github.com/atol-sh/atol-sdk-react/issues"},"keywords":["atol","auth","authorization","oidc","react","permissions"],"allowScripts":{"esbuild@0.28.1":true,"fsevents@2.3.3":false},"_id":"@atol-sh/react@0.5.1","_integrity":"sha512-OSazZ1Tzdppg3WyTRZ1DgOB+iGo1ocno5ztRwDVaO1Ge2HswKXA/5QZ4obXJPqI4LdHOQVosgSK8ReeTLdE3Rg==","_resolved":"/home/runner/work/atol-sdk-react/atol-sdk-react/release-artifact/atol-sh-react-0.5.1.tgz","_from":"file:/home/runner/work/atol-sdk-react/atol-sdk-react/release-artifact/atol-sh-react-0.5.1.tgz","_nodeVersion":"22.23.1","_npmVersion":"11.18.0","dist":{"integrity":"sha512-OSazZ1Tzdppg3WyTRZ1DgOB+iGo1ocno5ztRwDVaO1Ge2HswKXA/5QZ4obXJPqI4LdHOQVosgSK8ReeTLdE3Rg==","shasum":"814d6567a9c127f825d4a6eccd424c196a14ee51","tarball":"https://registry.npmjs.org/@atol-sh/react/-/react-0.5.1.tgz","fileCount":17,"unpackedSize":303941,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@atol-sh%2freact@0.5.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCmql99044bJKAbhZy5hqieLrjjTwt2DLWHexYE4QClPQIhAJnGL3uWKiQZyHx9KH0B2m7vCCybvN/mCHlj1jM6/Y0T"}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:4c39c648-081f-4319-af3d-ec0a1bb56bf0"}},"directories":{},"maintainers":[{"name":"remiphilippe","email":"remi@aiku.fr"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/react_0.5.1_1786384655374_0.26271759584721033"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-07T14:51:25.160Z","modified":"2026-08-10T17:57:36.163Z","0.3.0":"2026-07-07T14:51:25.545Z","0.3.1":"2026-07-08T15:44:23.020Z","0.3.2":"2026-07-09T03:58:48.846Z","0.3.3":"2026-07-20T15:52:02.468Z","0.5.1":"2026-08-10T17:57:35.830Z"},"bugs":{"url":"https://github.com/atol-sh/atol-sdk-react/issues"},"author":{"name":"Atol"},"license":"Apache-2.0","homepage":"https://atol.sh","keywords":["atol","auth","authorization","oidc","react","permissions"],"repository":{"type":"git","url":"git+https://github.com/atol-sh/atol-sdk-react.git"},"description":"Atol React SDK — authentication and frontend authorization","maintainers":[{"name":"remiphilippe","email":"remi@aiku.fr"}],"readme":"# @atol-sh/react\n\n[![npm version](https://img.shields.io/npm/v/@atol-sh/react.svg)](https://www.npmjs.com/package/@atol-sh/react)\n[![license](https://img.shields.io/npm/l/@atol-sh/react.svg)](./LICENSE)\n[![CI](https://github.com/atol-sh/atol-sdk-react/actions/workflows/ci.yml/badge.svg)](https://github.com/atol-sh/atol-sdk-react/actions/workflows/ci.yml)\n\nReact SDK for [Atol](https://atol.sh) — authentication and frontend authorization in one package. One provider, one hook, one identity.\n\nHandles OIDC login/logout, JWT token management, and permission-based UI gating. Permissions are loaded in bulk at login time for instant `can()` / `cannot()` checks with no API calls on the render path.\n\n## Install\n\n```bash\nnpm install @atol-sh/react\n```\n\n## Quick Start\n\n### 1. Get your publishable key\n\nGo to [console.atol.sh](https://console.atol.sh) > **Settings > API Keys**. Copy the **Key ID** (`atol_kid_...`). This is safe for frontend code.\n\n### 2. Wrap your app\n\n```tsx\nimport { AtolProvider } from '@atol-sh/react'\n\nfunction App() {\n  return (\n    <AtolProvider\n      keyId=\"atol_kid_abc123\"\n      authority=\"https://auth.atol.sh\"\n      redirectUri={window.location.origin + '/callback'}\n      postLogoutRedirectUri={window.location.origin + '/signed-out'}\n      useRefreshTokens\n      permissions={[\n        { action: 'edit', resource: 'publication' },\n        { action: 'manage', resource: 'team' },\n        { action: 'view', resource: 'analytics' },\n      ]}\n    >\n      <YourApp />\n    </AtolProvider>\n  )\n}\n```\n\n`redirectUri` and, when set, `postLogoutRedirectUri` must be allow-listed for\nthis app in [console.atol.sh](https://console.atol.sh). `<AtolProvider>`'s\n`init()` handles the OIDC callback in place on `redirectUri`; no dedicated\ncallback component is needed. `postLogoutRedirectUri` defaults to\n`redirectUri`, but a distinct signed-out landing avoids sending a logout\nresponse through the login-callback route.\n\n### Response-vault authorization\n\nEnable `responseVault` only for a client whose issuer discovery document\nregisters the exact response-vault binding. The discovered client ID, issuer,\naudience, scope, redirect URI, client origin, and every protocol endpoint must\nmatch the provider configuration. DPoP is mandatory and cannot fall back to\nBearer delivery.\n\n```tsx\n<AtolProvider\n  keyId=\"registered-client-id\"\n  authority=\"https://id.example.com\"\n  audience=\"https://api.example.com\"\n  scope=\"openid profile email\"\n  redirectUri=\"https://app.example.com/callback\"\n  postLogoutRedirectUri=\"https://app.example.com/signed-out\"\n  responseVault\n>\n  <YourApp />\n</AtolProvider>\n```\n\nThe issuer and application must use matching secure schemes and satisfy the\nprofile's same-site browser boundary. The redirect and post-logout URLs must\nbe clean absolute URLs on the same application origin, without credentials,\nquery, or fragment, and must be distinct. `deriveEncryptionKey` is\nincompatible because that flow uses fragment delivery.\n\n### 3. Use the hooks\n\n```tsx\nimport { useAtol, usePermissions, Can } from '@atol-sh/react'\n\nfunction Dashboard() {\n  const { isAuthenticated, user, organizationId, login, logout } = useAtol()\n  const { can } = usePermissions()\n\n  if (!isAuthenticated) {\n    return <button onClick={() => login()}>Sign in</button>\n  }\n\n  return (\n    <div>\n      <p>Welcome, {user.name}</p>\n      <p>Active organization: {organizationId ?? 'none selected'}</p>\n      <button onClick={logout}>Sign out</button>\n\n      {can('edit', 'publication') && <EditButton />}\n\n      <Can action=\"manage\" resource=\"team\">\n        <TeamSettings />\n      </Can>\n    </div>\n  )\n}\n```\n\n## Next.js / React Server Components\n\nThe SDK is a client component (it uses React context, browser crypto, and\nIndexedDB). The published bundle already carries a `\"use client\"` directive,\nso under the Next.js App Router you just import and mount it from your own\nclient boundary:\n\n```tsx\n'use client'\nimport { AtolProvider } from '@atol-sh/react'\n\nexport function Providers({ children }: { children: React.ReactNode }) {\n  return (\n    <AtolProvider keyId=\"atol_kid_abc123\" authority=\"https://auth.atol.sh\" redirectUri=\"...\">\n      {children}\n    </AtolProvider>\n  )\n}\n```\n\nThen render `<Providers>` from your root layout. Do not import the SDK into a\nServer Component.\n\n## Token Storage\n\nTokens live **in memory only**. The OIDC user object — access token, ID\ntoken, and (when `useRefreshTokens` is on) the refresh token — is never\nwritten to localStorage or sessionStorage, so persisted credentials are\nnot readable by a later XSS payload.\n\nOrdinary authorization uses sessionStorage for OIDC state/PKCE plus a random\nnon-secret tab generation. One non-secret localStorage authority record holds\nthe origin-wide generation and durable signed-out status in the same write,\nfencing missed cross-tab signals and session resurrection. Response-vault\nmode also stores one opaque active reference in sessionStorage and bounded\ntransaction custody in IndexedDB. None of these records contains bearer\ntokens.\n\nBecause the in-memory store is empty after a page reload, the provider\ntransparently performs one silent renew against the Atol session cookie\non mount. Users with a live IdP session stay signed in across reloads unless\nthe durable authority is signed out. A signed-out client does not consult the\nIdP session again until the user explicitly calls `login()`.\n\n### Everything the browser keeps\n\nNone of it is authoritative — the Atol control plane is the source of truth.\nThe browser holds only what a session needs:\n\n| What | Where | Survives reload? |\n|------|-------|------------------|\n| Access / ID / refresh tokens | memory only | No — re-obtained by silent renew |\n| OIDC state + PKCE verifier and non-secret tab generation | `sessionStorage` | Only across the owning tab's authorization lifecycle |\n| Non-secret credential generation and signed-out tombstone | `localStorage` | Yes - fences stale callbacks and suppresses silent session resurrection until explicit login |\n| Response-vault active reference | `sessionStorage` | Only while one host-wide authorization owns the tab |\n| Response-vault intent, exact cancellation custody, and non-extractable key handles | IndexedDB | Bounded by transaction expiry; never contains bearer tokens |\n| KEK vault (zero-knowledge encryption, when `deriveEncryptionKey`) | IndexedDB, under a device-bound non-extractable key | Yes — rehydrated on load |\n| DPoP key (ordinary `useDPoP`) | memory, per-tab non-exportable keypair | No |\n| Permissions (`usePermissions`) | memory (React state) | No — re-fetched at login |\n\n## API Reference\n\n### `<AtolProvider>`\n\nMount once at the root of your app.\n\n| Prop | Type | Required | Description |\n|------|------|----------|-------------|\n| `keyId` | `string` | Yes | Publishable key ID (`atol_kid_...`) |\n| `authority` | `string` | Yes | OIDC issuer URL (`https://auth.atol.sh`) |\n| `apiUrl` | `string` | No | Control-plane API base URL that serves permission checks and device identify. This is the API host, NOT the OIDC issuer. Device intelligence inherits it. Default: `https://api.atol.sh` |\n| `redirectUri` | `string` | Yes | Exact registered login callback URL. Response-vault mode requires a clean secure URL with no credentials, query, or fragment |\n| `postLogoutRedirectUri` | `string` | No | URL to redirect to after RP-initiated logout. Ordinary mode defaults to `redirectUri`; response-vault mode requires an explicit, distinct clean URL on the same origin |\n| `scope` | `string` | No | OIDC scopes. Default: `\"openid profile email\"` |\n| `audience` | `string` | No | API audience identifier |\n| `responseVault` | `boolean` | No | Enable the exact discovered response-vault v1 browser authorization profile. Requires a registered `audience`, exact registered scope and binding values, a distinct `postLogoutRedirectUri`, and DPoP; incompatible with `deriveEncryptionKey`. Default: `false` |\n| `permissions` | `PermissionCheck[]` | No | Permissions to bulk-load at login |\n| `onLogin` | `(user: AtolUser) => void` | No | Called after successful login |\n| `onLogout` | `() => void` | No | Called once whenever an authenticated session ends, including a passive expiry/logout signal |\n| `onPermissionsError` | `(error: Error) => void` | No | Called when the bulk permission load fails (checks stay deny-by-default) |\n| `useRefreshTokens` | `boolean` | No | Renewal strategy. Default (`false`) renews via a hidden-iframe silent authorize against the IdP session cookie (`prompt=none`) and requests/stores no refresh token. The iframe path needs the IdP session cookie in a third-party context, which Safari ITP blocks and Chrome's third-party-cookie phase-out removes, so it fails **unconditionally** for a cross-site app (UI on a different registrable domain from the IdP). Such apps **must** set `true`: this appends `offline_access` and renews via the refresh-token grant. Same-site apps (UI on a subdomain of the IdP) reach the first-party cookie in the iframe and leave it `false`. Default: `false` |\n| `useRefreshTokensFallback` | `boolean` | No | On a transient refresh-grant failure, fall back to the iframe path once before surfacing the error. Default: `false` |\n| `useDPoP` | `boolean` | No | Sender-constrained tokens (RFC 9449). Generates a per-tab non-exportable ES256 keypair and attaches a DPoP proof to every token-endpoint call, so a stolen refresh token cannot be redeemed elsewhere. Default: `false` |\n| `deriveEncryptionKey` | `boolean` | No | Zero-knowledge encryption: ask the server to derive a KEK at login and deliver a wrapped DEK. The unwrapped DEK is exposed as `encryptionKey`. Default: `false` |\n| `deviceIntelligence` | `DeviceConfig` | No | Device intelligence config (see below) |\n| `allowTestSessions` | `boolean` | No | **Test only.** Enable the test-session seam so e2e tests can seed an authenticated session without a live OIDC round-trip. Gate it behind a build/env flag — it bypasses OIDC validation and must never ship enabled. Default: `false`. See [EXAMPLES.md](./EXAMPLES.md#testing-seed-a-session-in-cypress--playwright) |\n| `initialTestSession` | `TokenResponse` | No | **Test only** (requires `allowTestSessions`). A token response the provider adopts on mount instead of the cold-load restore — authenticated at first paint, no OIDC round-trip and no silent-renew iframe. Preferred for route-guarded apps on a stubbed backend. See [EXAMPLES.md](./EXAMPLES.md#preferred-initialtestsession-deterministic-no-restore-iframe) |\n\n### `useAtol()`\n\nPrimary hook. Returns auth state and methods.\n\n```typescript\nconst {\n  isLoading,        // true while OIDC is initializing\n  isAuthenticated,  // true when user is logged in\n  user,             // AtolUser | null\n  organizationId,   // string | null - organization bound to the credential\n  error,            // AtolDiagnosticError | null\n  encryptionKey,    // Uint8Array | null — unwrapped DEK (deriveEncryptionKey)\n  encryptionError,  // AtolDiagnosticError | null - key unavailable\n  device,           // DeviceState — device intelligence (see below)\n  login,            // (options?: { returnTo?: string }) => Promise<void>\n  selectOrganization, // (organizationId: string) => Promise<AtolSession>\n  logout,           // () => Promise<void>\n  getAccessToken,   // () => Promise<string | undefined> — renews when stale\n  forceRenew,       // () => Promise<string | undefined> — rotate now\n} = useAtol()\n```\n\n`getAccessToken()` returns the current access token and silently renews\nit when it is within 60s of expiry. Renewal is single-flight per tab.\nCross-tab messages carry invalidation or session-ended signals only; each\ntab renews locally so proof-bound access and refresh tokens never cross tabs.\n\n`forceRenew()` bypasses the freshness check and rotates immediately —\nuseful when the server signals an out-of-band claim change.\n\n`selectOrganization(organizationId)` asks the core client for a distinct,\norganization-scoped credential transaction and resolves with the verified\nsession. If selection fails, the previous credential and `organizationId`\nremain active. Competing selections preserve the core client's latest intent.\n\nEvery rejected `useAtol()` action exposes only an `AtolDiagnosticError` with a\nclosed code and message. Login, organization selection, logout, token access,\nrenewal, and DPoP proof failures never retain the core exception, a callback\nor resource URL, or another caught value. Mount one `<AtolProvider>` at the\napplication root; a concurrent second provider is rejected instead of\ndisplacing the client that owns browser credentials. If teardown fails, that\nclient keeps a poisoned ownership slot. A later mount retries the incomplete\nsteps and cannot create a replacement unless cleanup finishes completely.\nSynchronous setup failures roll back their acquired client, and subscription\nteardown failures cannot bypass credential-client release.\n\n> **Known gap:** step-up re-authentication (`stepUp`) exists on the\n> `@atol-sh/js` core client but is not yet exposed through `useAtol()`.\n> Apps that need to force a step-up (e.g. on a `step_up_required`\n> challenge from the management API) should reach for the core client\n> directly for now.\n\n### `usePermissions()`\n\nPermission checker for UI gating. `can()` is a synchronous Map lookup, no API call.\n\n```typescript\nconst { can, cannot, isLoading, error } = usePermissions()\n\ncan('edit', 'publication')     // true if allowed\ncannot('manage', 'billing')   // true if denied\n```\n\nOnly permissions declared in `<AtolProvider permissions={[...]}>` are\navailable. Unlisted permissions return `false` (deny by default).\n\n`error` is non-null when the bulk permission load failed. Checks still\ndeny, but your app can tell \"user has no permissions\" apart from\n\"permissions failed to load\" and show a retry instead of a permanent\naccess-denied. Pair with the `onPermissionsError` provider callback.\n\n### `useUser()`\n\nConvenience hook — returns just the user.\n\n```typescript\nconst user = useUser()\n// user?.id, user?.email, user?.name, user?.authMethod\n```\n\nOrganization scope is credential state, not user profile data. Read\n`organizationId` from `useAtol()`; it is `null` when the current authenticated\ncredential is not scoped to an organization.\n\n### `<Can>`\n\nConditionally renders children based on a permission.\n\n```tsx\n<Can action=\"edit\" resource=\"publication\">\n  <EditButton />\n</Can>\n\n<Can action=\"manage\" resource=\"billing\" fallback={<UpgradePrompt />}>\n  <BillingSettings />\n</Can>\n```\n\n### `<ProtectedRoute>`\n\nRoute guard. Redirects to login if not authenticated, checks permission if specified.\n\n```tsx\n<ProtectedRoute>\n  <Dashboard />\n</ProtectedRoute>\n\n<ProtectedRoute action=\"manage\" resource=\"team\" fallback={<AccessDenied />}>\n  <TeamSettings />\n</ProtectedRoute>\n```\n\n### `<LoginButton>` / `<LogoutButton>`\n\nPre-wired buttons. Pass any `<button>` props.\n\n```tsx\n<LoginButton className=\"btn\">Sign in</LoginButton>\n<LogoutButton>Sign out</LogoutButton>\n```\n\n## Zero-Knowledge Encryption\n\nWith `deriveEncryptionKey: true`, login derives a Key Encryption Key\n(KEK) server-side from the user's credentials and delivers it in the\nredirect URL fragment (never stored server-side in usable form). A\nwrapped Data Encryption Key (DEK) rides on the ID token. The SDK unwraps\nthe DEK locally and exposes it:\n\n```tsx\nconst { encryptionKey } = useAtol()  // Uint8Array | null (32 bytes)\n```\n\nThe KEK is persisted under a device-bound, non-extractable WebCrypto key\nin IndexedDB (`KEKVault`), so silent renews and page reloads rehydrate\nthe DEK without a password prompt. Logout wipes the vault. Lower-level\nprimitives (`KEKVault`, `unwrapDEK`, `extractKEKFromFragment`) are\nexported for apps that want custom DEK lifetime control.\n\n## Device Intelligence\n\nOptional device fingerprinting and risk signals via\n[`@atol-sh/fingerprint`](https://www.npmjs.com/package/@atol-sh/fingerprint)\n(install it as a peer; it is lazy-loaded only when enabled — zero bundle\ncost otherwise).\n\n```tsx\n<AtolProvider\n  /* ... */\n  deviceIntelligence={{\n    enabled: true,\n    apiUrl: 'https://api.atol.sh', // Atol API host (default). NOT the OIDC authority.\n    collectOnMount: true,          // default\n    recheckInterval: 0,            // ms; 0 disables periodic re-identify\n  }}\n>\n```\n\n```tsx\nimport { useDevice } from '@atol-sh/react'\n\nconst { deviceId, known, newDevice, confidence, platform, browser, osVersion,\n        signals, loading, error } = useDevice()\n\nif (signals?.bot || signals?.tampered) {\n  // step up or block\n}\n```\n\n`signals` mirrors the server's smart-signal analysis exactly:\n`bot`, `vpn`, `proxy`, `tor`, `incognito`, `tampered`, `emulator`,\n`rooted`, `geo_mismatch`, `device_mismatch`, `device_shared`,\n`shared_user_count`, and `anomaly_score` (0-1 composite). The underlying core\ncollector validates the exact response and fails closed on missing, unknown,\nor mistyped fields. For each credential generation, the provider asks the core\nclient for the exact access-token/DPoP-proof pair bound to the identify POST.\nThe device layer receives only an operation-scoped authorization callback and\ndoes not retain either credential. Device state is visible only while its\ncredential generation and API host still match the current provider render.\n\n## AtolUser\n\nThe `user` object parsed from Atol JWT claims:\n\n```typescript\ninterface AtolUser {\n  id: string            // User ID (JWT sub)\n  email: string         // Email address\n  emailVerified: boolean // email_verified claim\n  name: string          // Display name\n  authMethod: string    // How they authenticated (e.g. \"oidc\")\n  mfaVerified: boolean  // MFA completed\n}\n```\n\n## How It Works\n\n1. `<AtolProvider>` initializes an OIDC client (Authorization Code + PKCE flow)\n2. On login, the user is redirected to `auth.atol.sh` for authentication\n3. After redirect back, the SDK exchanges the code for tokens\n4. User info is parsed from JWT claims — no `/userinfo` API call needed\n5. Permissions are bulk-loaded for the organization bound to the current\n   credential in one batch call using the publishable key\n6. `can()` / `cannot()` check permissions from the pre-loaded Map (synchronous, no network)\n7. Tokens are held in memory only (never localStorage/sessionStorage; sessionStorage carries just the OIDC state/PKCE verifier across the login redirect) and renewed silently — refresh-token grant when `useRefreshTokens` is on, hidden iframe otherwise. After a reload, one silent renew restores the session from the IdP cookie.\n\nThe publishable key (`atol_kid_...`) is the only credential in your frontend code — no secrets.\n\n## Security model\n\nClient-side permission checks (`can` / `cannot` / `<Can>` / `<ProtectedRoute>`)\nare **UI gating only**. They run against claims parsed in the browser and a\npermission map that browser code can read. They keep the UI coherent; they are\nnot an authorization boundary. Always re-authorize every privileged action on\nthe server, which verifies the access token from `getAccessToken()`.\n\nTokens are held in memory (never localStorage/sessionStorage). This defends\nagainst persisted-credential theft and cross-origin reads. It does **not**\ndefend against an active same-origin attacker (XSS or untrusted embedded\nscript) that can read memory while present on the page — mitigate that with a\nstrict CSP, Trusted Types, and SRI. Cross-tab messages never transport access,\nID, or refresh tokens. A credential-invalidation signal triggers a local\nserver-verified renewal with that tab's own DPoP key, while a session-ended\nsignal clears local state.\n\n`mfaVerified` / `emailVerified` on `AtolUser` are convenience copies of token\nclaims for UI use; treat the server's view as authoritative for security\ndecisions.\n\n## Troubleshooting\n\n**`redirect_uri` mismatch.** The `redirectUri` passed to `<AtolProvider>` must\nexactly match an allow-listed redirect URI for this app in\n[console.atol.sh](https://console.atol.sh) > **Settings > API Keys**.\n\n**Silent renew fails on Safari or in a cross-site embed.** The default\niframe-based renewal needs the IdP's first-party session cookie inside a\nthird-party iframe, which Safari ITP and Chrome's third-party-cookie\nphase-out block. If your app's UI is on a different registrable domain from\nthe OIDC issuer, set `useRefreshTokens` so renewal uses the refresh-token\ngrant instead.\n\n**DPoP silently falls back to Bearer.** Optional `useDPoP` requires WebCrypto\n(`crypto.subtle`), which is unavailable in some embedded/insecure contexts.\nCheck whether DPoP is actually active with the core client's\n`isDPoPActive()`, or inspect whether `signDPoPProof()` returns a `proof` —\n`undefined` means the ordinary flow fell back to a plain Bearer token.\nResponse-vault mode never falls back: missing WebCrypto or an invalid DPoP\nbinding is a terminal initialization or grant error.\n\n**Seeing a `DPoP-Nonce` challenge in the network tab.** This is expected —\nthe token endpoint issued a nonce challenge and the SDK automatically\nretries once with the server-provided nonce. No action needed.\n\n**Permission checks always return `false`.** Either the `action`/`resource`\npair was never declared in `<AtolProvider permissions={[...]}>` (unlisted\nchecks deny by default), or the bulk permission load failed. Check\n`usePermissions().error` to distinguish \"no permission granted\" from\n\"permissions failed to load\", and pair it with the `onPermissionsError`\nprovider callback.\n\n## Requirements\n\n- React 18+\n- [Atol account](https://console.atol.sh) (15-day free trial)\n\n## License\n\nApache 2.0\n","readmeFilename":"README.md"}