{"_id":"@atol-sh/js","_rev":"4-fe724c00afb101eae2142ed605ef5877","name":"@atol-sh/js","dist-tags":{"latest":"0.3.0"},"versions":{"0.1.0":{"name":"@atol-sh/js","version":"0.1.0","keywords":["atol","auth","authorization","oidc","pkce","dpop","webcrypto","permissions"],"author":{"name":"Atol"},"license":"Apache-2.0","_id":"@atol-sh/js@0.1.0","maintainers":[{"name":"remiphilippe","email":"remi@aiku.fr"}],"homepage":"https://atol.sh","bugs":{"url":"https://github.com/atol-sh/atol-sdk-js/issues"},"dist":{"shasum":"6add9e34e018b2fb2d3f5a80d2bff58943cec411","tarball":"https://registry.npmjs.org/@atol-sh/js/-/js-0.1.0.tgz","fileCount":17,"integrity":"sha512-+n+4yKXaTGvVtF2ccnbqymgjkrPYSPWB6Ss+FTsPcy5wC9ecdKJpSa7bLxMf+aAaA4W9dBWGL3yHoXrjKc6Iuw==","signatures":[{"sig":"MEUCIAad8DlDNFA4+WElD1nbUYp9vcZU4T1rlpFOqInZXC/TAiEAtusoyHOKGQmYv+hnwRtgK9E/YkgcmGfzaNRqe108yao=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":521709},"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"}},"./device":{"import":{"types":"./dist/device.d.ts","default":"./dist/device.js"},"default":"./dist/device.js","require":{"types":"./dist/device.d.cts","default":"./dist/device.cjs"}},"./package.json":"./package.json"},"gitHead":"42db4d84528288675c942459f63b4257a4a90487","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","test:coverage":"vitest run --coverage","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-js.git","type":"git"},"_npmVersion":"11.12.1","description":"Atol core JS SDK - framework-agnostic browser security core: OIDC (PKCE), DPoP, silent renew, permissions, WebCrypto","directories":{},"sideEffects":false,"_nodeVersion":"26.0.0","dependencies":{"jose":"^5.10.0","oidc-client-ts":"^3.3.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.4.0","jsdom":"^26.1.0","vitest":"^3.1.0","publint":"^0.3.21","typescript":"^5.7.0","@types/node":"^22.20.0","fake-indexeddb":"^6.2.5","@vitest/coverage-v8":"^3.2.6","@arethetypeswrong/cli":"^0.18.4"},"peerDependencies":{"@atol-sh/fingerprint":">=0.1.0"},"peerDependenciesMeta":{"@atol-sh/fingerprint":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/js_0.1.0_1783397690848_0.8953172330608221","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@atol-sh/js","version":"0.1.1","keywords":["atol","auth","authorization","oidc","pkce","dpop","webcrypto","permissions"],"author":{"name":"Atol"},"license":"Apache-2.0","_id":"@atol-sh/js@0.1.1","maintainers":[{"name":"remiphilippe","email":"remi@aiku.fr"}],"homepage":"https://atol.sh","bugs":{"url":"https://github.com/atol-sh/atol-sdk-js/issues"},"dist":{"shasum":"cb4d4caf3639d336c1f8749d3727f988dd629a88","tarball":"https://registry.npmjs.org/@atol-sh/js/-/js-0.1.1.tgz","fileCount":17,"integrity":"sha512-2mOzmSTm6nKNJ5eC8+TiZ1UOF4+DP2D2xKs3wNMBKH3udUhDuxWLZL9KbfhZBUzMGN3Z9bsyfizN7SPEi04Miw==","signatures":[{"sig":"MEQCIGDBSxbL8OTWAzTCFT2lgs3/U9IV9AiGjnaCVBqbJ7EpAiA+I3AR5ar+kremc4eMgYu6TdMv6vMBJ/nLWD9BQurBuQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@atol-sh%2fjs@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":539611},"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"}},"./device":{"import":{"types":"./dist/device.d.ts","default":"./dist/device.js"},"default":"./dist/device.js","require":{"types":"./dist/device.d.cts","default":"./dist/device.cjs"}},"./package.json":"./package.json"},"gitHead":"692ddf43187ac64453344d2bc4c59caa0b98640c","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","test:coverage":"vitest run --coverage","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:b5fc52c9-e66a-4e8e-9393-b77c19dc7327"}},"repository":{"url":"git+https://github.com/atol-sh/atol-sdk-js.git","type":"git"},"_npmVersion":"11.18.0","description":"Atol core JS SDK - framework-agnostic browser security core: OIDC (PKCE), DPoP, silent renew, permissions, WebCrypto","directories":{},"sideEffects":false,"_nodeVersion":"22.23.1","dependencies":{"jose":"^5.10.0","oidc-client-ts":"^3.3.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.4.0","jsdom":"^26.1.0","vitest":"^3.1.0","publint":"^0.3.21","typescript":"^5.7.0","@types/node":"^22.20.0","fake-indexeddb":"^6.2.5","@vitest/coverage-v8":"^3.2.6","@arethetypeswrong/cli":"^0.18.4"},"peerDependencies":{"@atol-sh/fingerprint":">=0.1.0"},"peerDependenciesMeta":{"@atol-sh/fingerprint":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/js_0.1.1_1783524907136_0.6293243715566716","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@atol-sh/js","version":"0.1.2","keywords":["atol","auth","authorization","oidc","pkce","dpop","webcrypto","permissions"],"author":{"name":"Atol"},"license":"Apache-2.0","_id":"@atol-sh/js@0.1.2","maintainers":[{"name":"remiphilippe","email":"remi@aiku.fr"}],"homepage":"https://atol.sh","bugs":{"url":"https://github.com/atol-sh/atol-sdk-js/issues"},"dist":{"shasum":"9d854dde72c2d63871a1055677995ab9e24e87e2","tarball":"https://registry.npmjs.org/@atol-sh/js/-/js-0.1.2.tgz","fileCount":17,"integrity":"sha512-k2ZHUcmM6edy6hconoSMsl/7XYSEo/9iBpOD7+Z6MHmDE1ln/wUevtIZLRqJwZpAxyx4081yjdFeKSXmjSWA4Q==","signatures":[{"sig":"MEUCIDF2Z8k8UGMl7C3P3t0jl1Xanuc5Rk3yEar5U28nNCo2AiEAzMKjsvkC+PNL182CyVfY8apP7Le9ltSK2i3FhnJErp4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@atol-sh%2fjs@0.1.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":547490},"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"}},"./device":{"import":{"types":"./dist/device.d.ts","default":"./dist/device.js"},"default":"./dist/device.js","require":{"types":"./dist/device.d.cts","default":"./dist/device.cjs"}},"./package.json":"./package.json"},"gitHead":"60a11d2abf91b61c56fd246fb824b831273aaf7a","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","test:coverage":"vitest run --coverage","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:b5fc52c9-e66a-4e8e-9393-b77c19dc7327"}},"repository":{"url":"git+https://github.com/atol-sh/atol-sdk-js.git","type":"git"},"_npmVersion":"11.18.0","description":"Atol core JS SDK - framework-agnostic browser security core: OIDC (PKCE), DPoP, silent renew, permissions, WebCrypto","directories":{},"sideEffects":false,"_nodeVersion":"22.23.1","dependencies":{"jose":"^5.10.0","oidc-client-ts":"^3.3.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.4.0","jsdom":"^26.1.0","vitest":"^3.1.0","publint":"^0.3.21","typescript":"^5.7.0","@types/node":"^22.20.0","fake-indexeddb":"^6.2.5","@vitest/coverage-v8":"^3.2.6","@arethetypeswrong/cli":"^0.18.4"},"peerDependencies":{"@atol-sh/fingerprint":">=0.1.0"},"peerDependenciesMeta":{"@atol-sh/fingerprint":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/js_0.1.2_1783569074602_0.21481285587231258","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@atol-sh/js","version":"0.3.0","description":"Atol core JS SDK - framework-agnostic browser security core: OIDC (PKCE), DPoP, silent renew, permissions, WebCrypto","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"},"./device":{"import":{"types":"./dist/device.d.ts","default":"./dist/device.js"},"require":{"types":"./dist/device.d.cts","default":"./dist/device.cjs"},"default":"./dist/device.js"},"./package.json":"./package.json"},"sideEffects":false,"publishConfig":{"access":"public"},"scripts":{"build":"tsup","dev":"tsup --watch","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","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":{"jose":"^5.10.0","oidc-client-ts":"^3.3.0"},"peerDependencies":{"@atol-sh/fingerprint":"^0.2.0"},"peerDependenciesMeta":{"@atol-sh/fingerprint":{"optional":true}},"devDependencies":{"@arethetypeswrong/cli":"^0.18.4","@types/node":"^22.20.0","@vitest/coverage-v8":"^3.2.6","fake-indexeddb":"^6.2.5","jsdom":"^26.1.0","publint":"^0.3.21","tsup":"^8.4.0","typescript":"^5.7.0","vitest":"^3.1.0"},"repository":{"type":"git","url":"git+https://github.com/atol-sh/atol-sdk-js.git"},"homepage":"https://atol.sh","bugs":{"url":"https://github.com/atol-sh/atol-sdk-js/issues"},"keywords":["atol","auth","authorization","oidc","pkce","dpop","webcrypto","permissions"],"_id":"@atol-sh/js@0.3.0","_integrity":"sha512-0Lm1SYAovjs+/RfrYy8/mfv7rwtBiydVYjJDOrclkrrrCNh7kvD85roMT14Bnd83mdm1lcvU1acXXAYmvLS7Ww==","_resolved":"/home/runner/work/atol-sdk-js/atol-sdk-js/release-artifact/atol-sh-js-0.3.0.tgz","_from":"file:/home/runner/work/atol-sdk-js/atol-sdk-js/release-artifact/atol-sh-js-0.3.0.tgz","_nodeVersion":"22.23.1","_npmVersion":"11.18.0","dist":{"integrity":"sha512-0Lm1SYAovjs+/RfrYy8/mfv7rwtBiydVYjJDOrclkrrrCNh7kvD85roMT14Bnd83mdm1lcvU1acXXAYmvLS7Ww==","shasum":"abbfc2d98cafabe9cad9561521c59785ec0da767","tarball":"https://registry.npmjs.org/@atol-sh/js/-/js-0.3.0.tgz","fileCount":19,"unpackedSize":3201008,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@atol-sh%2fjs@0.3.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIDA7DPhX/r5gCevAiMVuZoIV1cGrereRs+jifehyKoipAiAB7DsUK3X4T4rxV9x/DqxrpfjiOWkJPcUXqpsuPp9DzQ=="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:b5fc52c9-e66a-4e8e-9393-b77c19dc7327"}},"directories":{},"maintainers":[{"name":"remiphilippe","email":"remi@aiku.fr"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/js_0.3.0_1786380732683_0.7544474779428998"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-07T04:14:50.525Z","modified":"2026-08-10T16:52:13.249Z","0.1.0":"2026-07-07T04:14:51.000Z","0.1.1":"2026-07-08T15:35:07.376Z","0.1.2":"2026-07-09T03:51:14.843Z","0.3.0":"2026-08-10T16:52:12.916Z"},"bugs":{"url":"https://github.com/atol-sh/atol-sdk-js/issues"},"author":{"name":"Atol"},"license":"Apache-2.0","homepage":"https://atol.sh","keywords":["atol","auth","authorization","oidc","pkce","dpop","webcrypto","permissions"],"repository":{"type":"git","url":"git+https://github.com/atol-sh/atol-sdk-js.git"},"description":"Atol core JS SDK - framework-agnostic browser security core: OIDC (PKCE), DPoP, silent renew, permissions, WebCrypto","maintainers":[{"name":"remiphilippe","email":"remi@aiku.fr"}],"readme":"# @atol-sh/js\n\n[![npm version](https://img.shields.io/npm/v/@atol-sh/js.svg)](https://www.npmjs.com/package/@atol-sh/js)\n[![CI](https://github.com/atol-sh/atol-sdk-js/actions/workflows/ci.yml/badge.svg)](https://github.com/atol-sh/atol-sdk-js/actions/workflows/ci.yml)\n[![license](https://img.shields.io/npm/l/@atol-sh/js.svg)](./LICENSE)\n\nFramework-agnostic browser security core for [Atol](https://atol.sh): a\nsingle config-driven client for OIDC authentication (Authorization Code +\nPKCE), DPoP sender-constrained tokens (RFC 9449), silent renewal, cross-tab\ncoordination, and permission loading. `@atol-sh/react` is the React\nbinding, built as a thin wrapper over this same core.\n\nBuilding a React app? Use [`@atol-sh/react`](https://github.com/atol-sh/atol-sdk-react)\ninstead, the React binding over this core. Either way you'll need a\npublishable key id (`atol_kid_...`) -- get one from [console.atol.sh](https://console.atol.sh).\n\n## Install\n\n```bash\nnpm install @atol-sh/js\n```\n\n## Quickstart\n\nThe keystone export is `createAtolBrowserAuth`. Everything else in the\npackage is a lower-level primitive it is built from.\n\n```ts\nimport { createAtolBrowserAuth } from '@atol-sh/js'\n\nconst auth = createAtolBrowserAuth({\n  issuer: 'https://id.atol.sh',        // OIDC issuer URL\n  clientId: 'atol_kid_abc123',         // publishable key id, safe for the browser\n  audience: 'https://api.example.com', // optional API audience\n  scopes: 'openid profile email',      // optional; used verbatim (no offline_access by default)\n  redirectUri: 'https://app.example.com/callback',\n  postLogoutRedirectUri: 'https://app.example.com/logout',\n  dpop: true,                          // optional RFC 9449 sender-constrained tokens\n  useRefreshTokens: false,             // default (same-site); REQUIRED true for cross-site apps\n  storage: 'memory',                   // tokens are always memory-only\n})\n\nconst unsubscribe = auth.onSessionChange(({ isAuthenticated, user, organizationId }) => {\n  render(isAuthenticated, user, organizationId)\n})\n\n// On startup: handles a redirect callback, or silently restores a session\n// from the IdP session cookie.\nawait auth.init()\n\n// Start a login. returnTo is transaction-bound application state; it never\n// changes the registered redirect URI. After verified credentials commit,\n// the SDK restores it with same-origin history navigation.\nawait auth.beginLogin({ returnTo: '/dashboard' })\n\n// On the redirect URI page: exchange the code for tokens. init() calls\n// this automatically, so most apps never call it directly.\nawait auth.completeLogin()\n\n// Switch the credential context after the user chooses an organization.\n// This mints a new access/ID-token pair; it is not a generic token refresh.\nawait auth.selectOrganization('01KV4BNVVNZ4TNY0PP0CVXPZWP')\n\n// Get a token for an API call (DPoP-aware: pass the request you're about\n// to make so the proof's `htm`/`htu` bind to it):\nconst result = await auth.getAccessToken({\n  dpop: { method: 'GET', url: 'https://api.example.com/me' },\n})\nif (result) {\n  const headers = result.proof\n    ? { Authorization: `DPoP ${result.token}`, DPoP: result.proof }\n    : { Authorization: `Bearer ${result.token}` }\n}\n\n// Sign out (revokes the refresh token, then RP-initiated end_session):\nawait auth.logout()\n```\n\n### Response-vault v1\n\nRegistered clients can opt into the public response-vault protocol when their\nauthorization server and same-origin relay support it:\n\n```ts\nconst auth = createAtolBrowserAuth({\n  issuer: 'https://id.example.com',\n  clientId: 'example-spa',\n  audience: 'https://api.example.com',\n  scopes: 'openid profile email',\n  redirectUri: 'https://app.example.com/callback',\n  postLogoutRedirectUri: 'https://app.example.com/logout',\n  dpop: { required: true },\n  responseVault: true,\n})\n```\n\n`responseVault: true` pushes authorization parameters in JSON request bodies,\nnavigates to a parameter-free `/auth/authorize`, and redeems the server-held\ncode through the same-origin client relay. Authorization codes, state, nonce,\nPKCE material, request URIs, transaction IDs, and tokens never appear in a\nnavigation URL or `postMessage`. Every grant gets a fresh non-extractable DPoP\nkey, and tokens are validated before the one-use result is acknowledged.\nInvalid or superseded grants are durably rejected, revoking any exact token\nfamily relay already issued instead of leaving a server-side orphan.\n\nThe server publishes its v1 endpoints and registered binding in OIDC\ndiscovery. A random PAR idempotency key makes an ambiguous push exactly\nretryable, while a separate cancellation capability authorizes settlement\nafter mutable cookie and CSRF authority is cleared. Acknowledgement deletes\nthe delivery and enters a bounded `publishing` state; the SDK publishes\nvalidated credentials locally, finalizes server adoption, then atomically\nmarks local custody `published` and releases its slot. Failure before that\nlast local transition rejects and revokes the exact family within the\ncompensation window.\n\nResponse-vault v1 requires a non-empty audience, mandatory DPoP, a clean\ncallback URL, and a distinct same-origin post-logout URL. It does not support\n`deriveEncryptionKey`, whose URL-fragment delivery is incompatible with a\nparameter-free callback. See [RESPONSE_VAULT_PROTOCOL.md](./RESPONSE_VAULT_PROTOCOL.md)\nfor the supported v1 contract.\n\nOther methods on the returned client: `forceRenew()` bypasses the\nfreshness check and renews immediately; `stepUp()` forces re-authentication\n(`prompt=login`, `max_age=0`, `acr_values=mfa` by default) for a sensitive\noperation that requires a fresh auth; `selectOrganization(id)` starts a\ndistinct silent organization grant and exposes the selected ID on\n`AtolSession.organizationId`; `signDPoPProof(method, url)` signs a\nstandalone RFC 9449 proof for a request to your own protected resource;\n`isDPoPActive()` reports whether DPoP is actually in effect (it can be\n`false` even when `dpop: true` was requested, if WebCrypto is unavailable);\n`destroy()` tears down timers and subscriptions. If a browser resource close\nthrows, the client stays destroyed, retains only that cleanup ownership, and\nthrows `AtolBrowserAuthCleanupError`; calling `destroy()` again retries the\nincomplete step.\n\n## Lower-level primitives\n\nThe core also exports the building blocks for consumers who assemble their\nown flows:\n\n- **DPoP** - `createDPoPKeyManager` (per-tab non-exportable ES256 key,\n  RFC 9449 proofs).\n- **Verification** - `verifyIdToken` (JWKS signature + issuer + audience +\n  required OIDC claims and callback nonce).\n- **OIDC / PKCE** - `createUserManager`, `exchangeCode`,\n  `refreshTokenGrant`, `revokeToken`. Browser authorization, including\n  `prompt=none` iframe renewal, is admitted through durable ordinary PAR\n  before navigation. Once `exchangeCode` dispatches the single-use code,\n  transport loss or an incomplete response rejects with\n  `AuthorizationCodeExchangeUncertainError` and code\n  `authorization_code_exchange_uncertain`.\n- **Cross-tab** - `createTokenBroadcast`, `hasBroadcastSupport`,\n  `withTabLock`, `hasTabLockSupport`.\n- **Permissions** - `loadPermissions`, `permissionKey`.\n- **Encryption** - `KEKVault`, `unwrapDEK`, `extractKEKFromFragment`,\n  `kekBase64ToBytes`, `base64ToBytes` / `bytesToBase64Url` /\n  `stringToBase64Url`.\n- **Claims** - `parseAtolUser`, `decodeJWTPayload`, `getTokenExpiry`.\n\nSee the published type declarations (`dist/index.d.ts`) for the full\nsignatures; every export carries a doc comment.\n\n## Security\n\nThis is a security library first. Key properties:\n\n- **Memory-only tokens.** Access, ID, and (when opted in) refresh tokens\n  live in an in-memory store and are never written to `localStorage` or\n  `sessionStorage`. `sessionStorage` holds transient OIDC state/PKCE plus\n  non-secret tab ownership and, in response-vault mode, one opaque active\n  reference. `localStorage` holds only the non-secret credential generation\n  and signed-out status. A page reload starts with an empty token store; the\n  client recovers by silently renewing against the IdP session cookie unless\n  durable local authority records that the application is signed out.\n- **Ordinary redirect custody.** A DPoP authorization key staged before an\n  ordinary full-page redirect remains in IndexedDB for the SDK's explicit\n  15-minute browser-ceremony window plus the full 10-minute exchange lifetime\n  of a code issued at that boundary. OIDC state cleanup and callback\n  validation use the same checked 25-minute custody window; equality is\n  expired. This is a fail-closed client custody policy, not authorization\n  server authority. Response-vault mode instead has one server-enforced\n  10-minute whole-transaction deadline and stores its key in that separate\n  transaction record.\n- **Ordinary authorization protocol v2.** Discovery must publish the nested v2\n  PAR, pre-navigation cancellation, and settlement endpoints. The SDK sends\n  one canonical credentialless form request, persists its cancellation\n  capability before transport, and navigates with only `client_id` and the\n  admitted `request_uri`. If it cannot durably adopt the PAR response, it\n  resubmits the byte-identical form to the pre-navigation endpoint before\n  removing custody. PAR responses require exact `application/json`, and\n  settlement requests require exact `application/json`, both without\n  media-type parameters. The SDK authenticates a cross-origin response only\n  through the browser-visible `Atol-Browser-Authorization: v2` header. It does\n  not use hidden CORS response headers as authority. Missing, unexposed,\n  repeated, or unequal signals retain or settle custody and never authorize\n  navigation or cleanup.\n- **Response vault.** A response-vault client stores a bounded authorization\n  transaction and non-extractable key handle in IndexedDB, with\n  only an opaque same-tab reference in `sessionStorage`. One durable\n  client-origin slot admits exactly one transaction across tabs and registered\n  bindings. PAR and issuer-init use POST bodies; authorize and callback\n  navigations are parameter-free. The same-origin relay supports exact\n  redelivery after an ambiguous network loss. Capability acknowledgement\n  deletes the delivery and opens bounded compensation, local publication\n  installs validated credentials, and capability finalization confirms\n  adoption. A crash that loses an acknowledged success rejects the exact\n  unavailable family before obtaining one fresh silent grant; terminal errors\n  retain and re-surface their exact classification. Bearer material is never\n  persisted for recovery. Validation and authority failures enter an\n  idempotently retryable rejection lineage that revokes and consumes the exact\n  transaction, including a finalized transaction during the compensation\n  window. Finalization reaches local `adopted`; `completePublication()`\n  atomically changes it to `published` and releases the live origin slot.\n  Ordinary crash scans compensate abandoned `adopted` custody and exclude\n  clean `published` custody, while explicit logout includes both.\n  Delivery-recovery routes remain private one-shot custody, and cleanup\n  diagnostics retain no caught browser or transport errors. Active-custody\n  recovery exposes only named binding, invalidation, busy, rejection, and\n  cleanup diagnostics; unknown storage or WebCrypto values are discarded.\n- **Iframe-default renewal, refresh required for cross-site.** By default\n  the client renews in a hidden iframe against the IdP session cookie\n  (`prompt=none`) and requests no refresh token, so there is no long-lived\n  bearer credential to leak. The iframe path depends on the IdP session\n  cookie being sent from a third-party context, which Safari ITP blocks\n  outright and Chrome's third-party-cookie phase-out removes, so it fails\n  **unconditionally** for a cross-site app -- one served on a different\n  registrable domain than the IdP. Such apps therefore **must** set\n  `useRefreshTokens: true`; that appends `offline_access` and renews via the\n  refresh-token grant (memory-only, rotated, and DPoP-bound when `dpop` is\n  on). Same-site apps (a console on a subdomain of the IdP) reach the\n  first-party session cookie in the iframe and leave it `false`.\n- **DPoP sender-constraint (RFC 9449).** When `dpop: true`, a per-tab,\n  non-exportable ES256 keypair is generated on `init()` and a proof JWT is\n  attached to every token-endpoint call and to `getAccessToken({ dpop })`\n  results. A stolen access token cannot be replayed from another client\n  without the private key, which never leaves the browser's WebCrypto\n  keystore. Every token response is rejected unless `token_type=DPoP` and\n  the access token's `cnf.jkt` matches that tab key. Use\n  `dpop: { required: true }` to fail initialization when key creation is\n  unavailable instead of permitting the boolean option's Bearer fallback.\n- **PKCE + nonce + ID token verification.** Login uses OIDC Authorization\n  Code with PKCE (no implicit flow, no client secret in the browser).\n  Callback state and PKCE are consumed through oidc-client-ts, while the SDK\n  performs the DPoP-aware code exchange and verifies the ID token against\n  the issuer's JWKS, including the stored nonce when present.\n- **Signal-only cross-tab coordination.** `BroadcastChannel` messages carry\n  only credential-invalidation and session-ended signals. Access, ID, and\n  refresh tokens never cross tabs; each receiving tab renews with its own\n  DPoP key and grant.\n- **Durable RP-initiated logout.** `logout()` atomically rotates the\n  origin-wide credential generation into a non-secret signed-out tombstone\n  before asynchronous teardown. It attempts refresh-token revocation\n  (RFC 7009) and the issuer's `end_session` endpoint with a one-use callback\n  state, and surfaces either failure. Reloads and tabs without\n  `BroadcastChannel` remain signed out even if the IdP session cookie still\n  exists. Failed attempts retain exact in-memory revocation and ID-token-hint\n  custody for a same-runtime retry. Only an explicit `beginLogin()` clears\n  the tombstone under a fresh generation.\n- **Exact abandoned-login cancellation.** In response-vault mode,\n  `cancelLogin()` is available only before client initialization on a\n  signed-out browser. It claims and rejects the exact pending interactive\n  generation, releases local custody, and never navigates to the issuer or\n  ends an established session. Ambiguous rejection remains durable for an\n  exact retry under an `interactive_cancel` provenance. Hidden-iframe renewal\n  custody and generic lifecycle rejection can never satisfy that provenance.\n  Damaged local key custody remains cancellable only when the durable source\n  mode still proves that the transaction was interactive. Cleanup ownership is\n  latched only after that durable cancellation claim succeeds. If rejection\n  releases custody before tab-owner rotation is persisted, a same-runtime\n  retry must replace and reread the retained local fence before it resolves.\n  `beginLogin()` and `cancelLogin()` execute in call order, and every\n  concurrent cancellation caller receives the same in-flight promise even if\n  logout or another precondition changes while it runs. A same-tick logout\n  therefore awaits an earlier queued cancellation; later cancellation\n  requests are unavailable until logout finishes. Destroy cleanup is re-armed\n  only after a later explicit login successfully establishes new interactive\n  custody. Unsupported modes and initialized clients reject.\n\nFound a vulnerability? See [SECURITY.md](./SECURITY.md) for how to report\nit privately.\n\n## Device intelligence (optional subpath)\n\n```ts\nimport { DeviceCollector } from '@atol-sh/js/device'\n\nconst collector = new DeviceCollector('https://api.atol.sh', publishState)\nawait collector.collect({\n  credentialGeneration: 1,\n  authorize: async ({ method, url }) => {\n    const credential = await auth.getAccessToken({ dpop: { method, url } })\n    if (!credential) throw new Error('No authenticated session')\n    return credential.proof\n      ? {\n          scheme: 'DPoP',\n          accessToken: credential.token,\n          dpopProof: credential.proof,\n        }\n      : { scheme: 'Bearer', accessToken: credential.token }\n  },\n})\n```\n\nThe `@atol-sh/js/device` subpath is split out of the core barrel and\ndynamic-imports `@atol-sh/fingerprint` (an optional peer dependency) so\nconsumers who don't use device intelligence never pull it into their\nbundle. Install `@atol-sh/fingerprint` alongside `@atol-sh/js` to use it.\nThe collector validates the complete identify response before publishing\ndevice state; malformed or drifted success payloads produce a closed error\nand never receive invented defaults. Its operation-scoped credential owner\nacquires the access token and DPoP proof atomically for the exact identify\nURL; neither value is stored by the collector. Increment the non-secret\n`credentialGeneration` whenever the session credential rotates. A newer\ngeneration immediately fences late responses from every older request. Call\n`collector.invalidate()` on logout before releasing the collector; future\ncredentials must continue with a higher generation.\n\n## Browser support\n\nRequires a browser with WebCrypto (`crypto.subtle`): all evergreen\nbrowsers (Chrome, Firefox, Safari, Edge) on both desktop and mobile.\nWhen WebCrypto is unavailable (for example, a non-browser or non-secure\ncontext), DPoP key generation returns `null` and the client falls back to\nplain Bearer tokens automatically -- `isDPoPActive()` reports `false` and\nno proof is attached for ordinary clients. Response-vault mode never falls\nback: missing WebCrypto is a fatal authorization error.\n\n## Troubleshooting\n\n- **Silent renew fails under Safari ITP, or any cross-site third-party-cookie\n  blocking.** The default renewal model uses a hidden iframe against the\n  IdP session cookie, which requires that cookie to be readable in a\n  third-party context. Safari ITP blocks this outright, and Chrome's\n  third-party-cookie phase-out removes it too. Fix: set\n  `useRefreshTokens: true` so the client renews via the refresh-token grant\n  instead.\n- **\"invalid redirect_uri\".** The `redirectUri` you pass must be allow-listed\n  for your publishable key in [console.atol.sh](https://console.atol.sh).\n  Add the exact URI (scheme, host, port, path) there.\n- **DPoP silently falls back to Bearer.** `dpop: true` only takes effect\n  when WebCrypto is available; call `isDPoPActive()` to check whether it\n  actually did. A server-issued `DPoP-Nonce` challenge is retried\n  automatically with the nonce attached -- no consumer action needed.\n- **Permission checks return `false` unexpectedly.** `loadPermissions`\n  denies by default: a `false` means either the server-side permission\n  genuinely isn't granted, or the bulk load itself failed (network error,\n  shape mismatch) and the caller is expected to treat \"unknown\" the same as\n  \"denied\".\n\n## Contributing\n\nSee [CONTRIBUTING.md](./CONTRIBUTING.md) for dev setup, the test/coverage\ngate, and the PR process. Please read [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md)\nbefore participating.\n\n## Changelog\n\nSee [CHANGELOG.md](./CHANGELOG.md) for release notes.\n\n## License\n\nApache-2.0. See [LICENSE](./LICENSE) and [NOTICE](./NOTICE).\n","readmeFilename":"README.md"}