tempest-react-sdk 0.80.0 → 0.82.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -10
- package/bin/create-tempest-app.e2e.test.mjs +31 -0
- package/bin/create-tempest-app.mjs +11 -1
- package/bin/lib/component/generate.test.mjs +13 -1
- package/bin/lib/doctor/doctor.e2e.test.mjs +29 -0
- package/bin/lib/doctor/vitest.mjs +120 -0
- package/bin/lib/doctor/vitest.test.mjs +139 -0
- package/bin/lib/fs/contain.mjs +7 -0
- package/bin/lib/openapi/escape.mjs +68 -4
- package/bin/lib/openapi/generate.hostile.test.mjs +150 -1
- package/bin/lib/openapi/generate.mjs +78 -13
- package/bin/lib/openapi/generate.test.mjs +13 -1
- package/bin/lib/openapi/pattern-risk.mjs +134 -0
- package/bin/lib/openapi/pattern-risk.test.mjs +45 -0
- package/bin/tempest-gen.test.mjs +100 -0
- package/bin/tempest.mjs +24 -8
- package/dist/access/use-can.cjs +1 -1
- package/dist/access/use-can.cjs.map +1 -1
- package/dist/access/use-can.js +25 -28
- package/dist/access/use-can.js.map +1 -1
- package/dist/auth/create-tempest-auth.cjs +1 -1
- package/dist/auth/create-tempest-auth.cjs.map +1 -1
- package/dist/auth/create-tempest-auth.js +234 -122
- package/dist/auth/create-tempest-auth.js.map +1 -1
- package/dist/auth/session-identity.cjs +2 -0
- package/dist/auth/session-identity.cjs.map +1 -0
- package/dist/auth/session-identity.js +12 -0
- package/dist/auth/session-identity.js.map +1 -0
- package/dist/br/boleto.cjs +1 -1
- package/dist/br/boleto.cjs.map +1 -1
- package/dist/br/boleto.js +2 -1
- package/dist/br/boleto.js.map +1 -1
- package/dist/br.d.ts +22 -3
- package/dist/components/AuthForm/AuthFieldControl.cjs +2 -0
- package/dist/components/AuthForm/AuthFieldControl.cjs.map +1 -0
- package/dist/components/AuthForm/AuthFieldControl.js +85 -0
- package/dist/components/AuthForm/AuthFieldControl.js.map +1 -0
- package/dist/components/AuthForm/AuthForm.cjs +2 -0
- package/dist/components/AuthForm/AuthForm.cjs.map +1 -0
- package/dist/components/AuthForm/AuthForm.js +83 -0
- package/dist/components/AuthForm/AuthForm.js.map +1 -0
- package/dist/components/AuthForm/AuthForm.module.cjs +3 -0
- package/dist/components/AuthForm/AuthForm.module.cjs.map +1 -0
- package/dist/components/AuthForm/AuthForm.module.js +14 -0
- package/dist/components/AuthForm/AuthForm.module.js.map +1 -0
- package/dist/components/AuthForm/AuthLinks.cjs +2 -0
- package/dist/components/AuthForm/AuthLinks.cjs.map +1 -0
- package/dist/components/AuthForm/AuthLinks.js +32 -0
- package/dist/components/AuthForm/AuthLinks.js.map +1 -0
- package/dist/components/AuthForm/ForgotPasswordForm.cjs +2 -0
- package/dist/components/AuthForm/ForgotPasswordForm.cjs.map +1 -0
- package/dist/components/AuthForm/ForgotPasswordForm.js +32 -0
- package/dist/components/AuthForm/ForgotPasswordForm.js.map +1 -0
- package/dist/components/AuthForm/LoginForm.cjs +2 -0
- package/dist/components/AuthForm/LoginForm.cjs.map +1 -0
- package/dist/components/AuthForm/LoginForm.js +39 -0
- package/dist/components/AuthForm/LoginForm.js.map +1 -0
- package/dist/components/AuthForm/ResetPasswordForm.cjs +2 -0
- package/dist/components/AuthForm/ResetPasswordForm.cjs.map +1 -0
- package/dist/components/AuthForm/ResetPasswordForm.js +39 -0
- package/dist/components/AuthForm/ResetPasswordForm.js.map +1 -0
- package/dist/components/AuthForm/SignUpForm.cjs +2 -0
- package/dist/components/AuthForm/SignUpForm.cjs.map +1 -0
- package/dist/components/AuthForm/SignUpForm.js +32 -0
- package/dist/components/AuthForm/SignUpForm.js.map +1 -0
- package/dist/components/AuthForm/auth-defaults.cjs +2 -0
- package/dist/components/AuthForm/auth-defaults.cjs.map +1 -0
- package/dist/components/AuthForm/auth-defaults.js +54 -0
- package/dist/components/AuthForm/auth-defaults.js.map +1 -0
- package/dist/components/AuthForm/auth-schemas.cjs +2 -0
- package/dist/components/AuthForm/auth-schemas.cjs.map +1 -0
- package/dist/components/AuthForm/auth-schemas.js +26 -0
- package/dist/components/AuthForm/auth-schemas.js.map +1 -0
- package/dist/components/AuthPage/AuthPage.cjs +2 -0
- package/dist/components/AuthPage/AuthPage.cjs.map +1 -0
- package/dist/components/AuthPage/AuthPage.js +97 -0
- package/dist/components/AuthPage/AuthPage.js.map +1 -0
- package/dist/components/AuthPage/AuthPage.module.cjs +3 -0
- package/dist/components/AuthPage/AuthPage.module.cjs.map +1 -0
- package/dist/components/AuthPage/AuthPage.module.js +24 -0
- package/dist/components/AuthPage/AuthPage.module.js.map +1 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.cjs +1 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.cjs.map +1 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.js +22 -21
- package/dist/components/Breadcrumbs/Breadcrumbs.js.map +1 -1
- package/dist/components/Button/Button.cjs +1 -1
- package/dist/components/Button/Button.cjs.map +1 -1
- package/dist/components/Button/Button.js +41 -40
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Chat/ChatAttachments.cjs +1 -1
- package/dist/components/Chat/ChatAttachments.cjs.map +1 -1
- package/dist/components/Chat/ChatAttachments.js +38 -38
- package/dist/components/FilterBar/filter-apply.cjs +1 -1
- package/dist/components/FilterBar/filter-apply.cjs.map +1 -1
- package/dist/components/FilterBar/filter-apply.js +7 -7
- package/dist/components/Form/Form.cjs +1 -1
- package/dist/components/Form/Form.cjs.map +1 -1
- package/dist/components/Form/Form.js +30 -32
- package/dist/components/Form/Form.js.map +1 -1
- package/dist/components/Layout/Layout.cjs +1 -1
- package/dist/components/Layout/Layout.cjs.map +1 -1
- package/dist/components/Layout/Layout.js +25 -24
- package/dist/components/Layout/Layout.js.map +1 -1
- package/dist/components/Layout/gap.cjs +2 -0
- package/dist/components/Layout/gap.cjs.map +1 -0
- package/dist/components/Layout/gap.js +15 -0
- package/dist/components/Layout/gap.js.map +1 -0
- package/dist/components/NavigationMenu/NavigationMenu.cjs +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.cjs.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.js +71 -70
- package/dist/components/NavigationMenu/NavigationMenu.js.map +1 -1
- package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
- package/dist/components/Sidebar/Sidebar.js.map +1 -1
- package/dist/components/Sidebar/SidebarEntry.cjs +1 -1
- package/dist/components/Sidebar/SidebarEntry.cjs.map +1 -1
- package/dist/components/Sidebar/SidebarEntry.js +30 -29
- package/dist/components/Sidebar/SidebarEntry.js.map +1 -1
- package/dist/data/create-data-provider.cjs +1 -1
- package/dist/data/create-data-provider.cjs.map +1 -1
- package/dist/data/create-data-provider.js +1 -2
- package/dist/data/create-data-provider.js.map +1 -1
- package/dist/forms/index.js +7 -7
- package/dist/forms/masked-inputs.cjs +1 -1
- package/dist/forms/masked-inputs.cjs.map +1 -1
- package/dist/forms/masked-inputs.js +15 -15
- package/dist/forms/validate-form.cjs +1 -1
- package/dist/forms/validate-form.cjs.map +1 -1
- package/dist/forms/validate-form.js +6 -1
- package/dist/forms/validate-form.js.map +1 -1
- package/dist/forms/zod-resolver.cjs +1 -1
- package/dist/forms/zod-resolver.cjs.map +1 -1
- package/dist/forms/zod-resolver.js +8 -3
- package/dist/forms/zod-resolver.js.map +1 -1
- package/dist/http/api-client.cjs +1 -1
- package/dist/http/api-client.cjs.map +1 -1
- package/dist/http/api-client.js +137 -116
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/credential-scope.cjs +1 -1
- package/dist/http/credential-scope.cjs.map +1 -1
- package/dist/http/credential-scope.js +33 -9
- package/dist/http/credential-scope.js.map +1 -1
- package/dist/http/presigned-post.cjs.map +1 -1
- package/dist/http/presigned-post.js.map +1 -1
- package/dist/http/resumable-upload.cjs +1 -1
- package/dist/http/resumable-upload.cjs.map +1 -1
- package/dist/http/resumable-upload.js +140 -134
- package/dist/http/resumable-upload.js.map +1 -1
- package/dist/http/upload-with-progress.cjs +1 -1
- package/dist/http/upload-with-progress.cjs.map +1 -1
- package/dist/http/upload-with-progress.js +17 -17
- package/dist/http/upload-with-progress.js.map +1 -1
- package/dist/http/use-describe-api-error.cjs +1 -1
- package/dist/http/use-describe-api-error.cjs.map +1 -1
- package/dist/http/use-describe-api-error.js +9 -9
- package/dist/oauth/GoogleSignIn.cjs +1 -1
- package/dist/oauth/GoogleSignIn.cjs.map +1 -1
- package/dist/oauth/GoogleSignIn.js +5 -4
- package/dist/oauth/GoogleSignIn.js.map +1 -1
- package/dist/offline/create-offline-sync.cjs +1 -1
- package/dist/offline/create-offline-sync.cjs.map +1 -1
- package/dist/offline/create-offline-sync.js +108 -68
- package/dist/offline/create-offline-sync.js.map +1 -1
- package/dist/query/offline-persistence.cjs +1 -1
- package/dist/query/offline-persistence.cjs.map +1 -1
- package/dist/query/offline-persistence.js +54 -44
- package/dist/query/offline-persistence.js.map +1 -1
- package/dist/sse/create-event-stream.cjs +1 -1
- package/dist/sse/create-event-stream.cjs.map +1 -1
- package/dist/sse/create-event-stream.js +48 -41
- package/dist/sse/create-event-stream.js.map +1 -1
- package/dist/sse/ticket-event-stream.cjs +1 -1
- package/dist/sse/ticket-event-stream.cjs.map +1 -1
- package/dist/sse/ticket-event-stream.js +48 -19
- package/dist/sse/ticket-event-stream.js.map +1 -1
- package/dist/sse/use-event-stream.cjs +1 -1
- package/dist/sse/use-event-stream.cjs.map +1 -1
- package/dist/sse/use-event-stream.js +19 -14
- package/dist/sse/use-event-stream.js.map +1 -1
- package/dist/styles/AuthForm.css +2 -0
- package/dist/styles/AuthPage.css +2 -0
- package/dist/styles/component/AuthForm.css +9 -0
- package/dist/styles/component/AuthPage.css +29 -0
- package/dist/styles/forms.css +36 -0
- package/dist/styles/manifest.json +63 -0
- package/dist/styles.css +1 -1
- package/dist/sw/background-sync.cjs +1 -1
- package/dist/sw/background-sync.cjs.map +1 -1
- package/dist/sw/background-sync.js +132 -112
- package/dist/sw/background-sync.js.map +1 -1
- package/dist/sw/cache.cjs +1 -1
- package/dist/sw/cache.cjs.map +1 -1
- package/dist/sw/cache.js +36 -30
- package/dist/sw/cache.js.map +1 -1
- package/dist/sw/create-push-handler.cjs +1 -1
- package/dist/sw/create-push-handler.cjs.map +1 -1
- package/dist/sw/create-push-handler.js +20 -10
- package/dist/sw/create-push-handler.js.map +1 -1
- package/dist/sw.d.ts +93 -28
- package/dist/tabular/compact.cjs +1 -1
- package/dist/tabular/compact.cjs.map +1 -1
- package/dist/tabular/compact.js +35 -16
- package/dist/tabular/compact.js.map +1 -1
- package/dist/tabular/manifest.cjs +1 -1
- package/dist/tabular/manifest.cjs.map +1 -1
- package/dist/tabular/manifest.js +33 -15
- package/dist/tabular/manifest.js.map +1 -1
- package/dist/tabular.d.ts +11 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +1057 -83
- package/dist/tempest-react-sdk.js +373 -365
- package/dist/theme/initial-theme.cjs +4 -4
- package/dist/theme/initial-theme.cjs.map +1 -1
- package/dist/theme/initial-theme.js +8 -5
- package/dist/theme/initial-theme.js.map +1 -1
- package/dist/utils/compressed-storage.cjs +1 -1
- package/dist/utils/compressed-storage.cjs.map +1 -1
- package/dist/utils/compressed-storage.js +13 -7
- package/dist/utils/compressed-storage.js.map +1 -1
- package/dist/utils/format.cjs +1 -1
- package/dist/utils/format.cjs.map +1 -1
- package/dist/utils/format.js +25 -25
- package/dist/utils/numbers.cjs +1 -1
- package/dist/utils/numbers.cjs.map +1 -1
- package/dist/utils/numbers.js +7 -7
- package/dist/utils/objects.cjs +1 -1
- package/dist/utils/objects.cjs.map +1 -1
- package/dist/utils/objects.js +13 -7
- package/dist/utils/objects.js.map +1 -1
- package/dist/utils/safe-url.cjs +1 -1
- package/dist/utils/safe-url.cjs.map +1 -1
- package/dist/utils/safe-url.js +4 -1
- package/dist/utils/safe-url.js.map +1 -1
- package/dist/webrtc/peer-mesh.cjs +1 -1
- package/dist/webrtc/peer-mesh.cjs.map +1 -1
- package/dist/webrtc/peer-mesh.js +7 -1
- package/dist/webrtc/peer-mesh.js.map +1 -1
- package/dist/ws/create-web-socket.cjs +1 -1
- package/dist/ws/create-web-socket.cjs.map +1 -1
- package/dist/ws/create-web-socket.js +77 -61
- package/dist/ws/create-web-socket.js.map +1 -1
- package/dist/ws/use-web-socket.cjs +1 -1
- package/dist/ws/use-web-socket.cjs.map +1 -1
- package/dist/ws/use-web-socket.js +4 -3
- package/dist/ws/use-web-socket.js.map +1 -1
- package/package.json +1 -1
- package/template-pwa/src/pwa.tsx +42 -0
- package/template-pwa/src/sw.ts +15 -10
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-tempest-auth.js","names":["LOGOUT_REVOCATION_WAIT_MS","settleWithin","pending","timer","resolve","ms","defaultParseTokens","d","data","createTempestAuth","rt","options","useAuthStore","createAuthStore","storeName","refreshKey","storageImpl","storage","readRefreshToken","writeRefreshToken","s","token","state","getToken","fetchUser","mePath","started","epoch","user","api","error","SessionSupersededError","ending","hooksRunning","loginsInFlight","revocations","credentialRequests","rotatingEpochs","trackCredentialRequest","settled","request","reportLogoutError","isDevBuild","trackRevocation","tracked","build","revokeDiscarded","logoutPath","refreshToken","endedSessionApi","refreshBody","createApiClient","nextEpoch","createRefreshQueue","rotate","refreshPath","parseTokens","queue","staleWithinSession","current","context","signedOut","awaitLogins","refreshFor","onUnauthorized","endSession","login","run","signIn","credentials","loginPath","finish","mine","done","embedded","parseUser","live","reason","onSessionEnd","sessionApi","revoke","path","withCredentials","inFlight","logout","revocation","redirectTo"],"sources":["../../src/auth/create-tempest-auth.ts"],"sourcesContent":["import { createApiClient, SessionSupersededError } from \"../http\";\nimport { isDevBuild } from \"../utils/dev-mode\";\nimport type { ApiClient, CsrfOptions, RequestAuthContext, RetryOptions } from \"../http\";\nimport { createAuthStore } from \"./create-auth-store\";\nimport type { AuthState } from \"./create-auth-store\";\nimport { createRefreshQueue } from \"./refresh-queue\";\n\n/** The token envelope returned by a Tempest FastAPI SDK login/refresh route. */\nexport interface TempestTokenResponse {\n /** The bearer access token. */\n access_token: string;\n /** Token type — always `\"bearer\"` for the SDK. */\n token_type?: string;\n /** Optional refresh token (when the API returns it in the body, not a cookie). */\n refresh_token?: string;\n}\n\n/**\n * What {@link CreateTempestAuthOptions.onSessionEnd} receives about the session\n * that just ended.\n */\nexport interface TempestSessionEnd<TUser> {\n /**\n * Why it ended: `\"logout\"` for an explicit `logout()`, `\"expired\"` when the\n * client ended it on its own — the refresh failed, or the request replayed\n * after a refresh still came back `401` — and `\"switch\"` when `login()`\n * succeeded for another set of credentials while this session was live.\n */\n reason: \"logout\" | \"expired\" | \"switch\";\n /** The user of the ended session, as the store held it. */\n user: TUser | null;\n /** The access token of the ended session, or `null` when it had none. */\n token: string | null;\n /**\n * A client that sends the ended session's access token — the store has\n * already been cleared, so `auth.api` no longer does.\n *\n * Use it for the server-side half of the teardown, such as deleting this\n * browser's push subscription. It never refreshes and never ends a session:\n * a `401` here just rejects. On `\"logout\"` the token is usually still\n * valid; on `\"expired\"` the server has already refused it, so expect the\n * call to fail and keep the local half of the teardown independent of it.\n */\n api: ApiClient;\n}\n\nexport interface CreateTempestAuthOptions<TUser> {\n /** Base URL of the API. Required. */\n baseURL: string;\n /**\n * Path segment the API is mounted under, such as `\"/api\"`, forwarded to\n * every client this preset builds — see the `prefix` option of\n * `createApiClient`.\n *\n * Safe to combine with the default paths below: the prefix is applied at\n * most once, so `\"/api/auth/login\"` under a `\"/api\"` prefix stays\n * `\"/api/auth/login\"`.\n */\n prefix?: string;\n /** Login route (`POST`). Default: `\"/api/auth/login\"`. */\n loginPath?: string;\n /** Refresh route (`POST`). Default: `\"/api/auth/refresh\"`. */\n refreshPath?: string;\n /**\n * Server-side logout route (`POST`), called by an explicit `logout()` to\n * revoke the ending session. Off by default.\n *\n * `logout()` alone only clears client state, and an HttpOnly refresh\n * cookie is out of its reach: with `withCredentials`, the cookie stays\n * redeemable until the backend revokes it. The tempest-fastapi-sdk mounts\n * `POST /auth/logout` (`204`, idempotent) when it stores refresh tokens in\n * the database — at `/auth/cookie/logout` when it delivers tokens both in\n * the body and in cookies — and stateless refresh tokens cannot be\n * revoked, so the route is absent there.\n *\n * The request goes out through the ended session's client (the same one\n * `onSessionEnd` receives, carrying its access token), with the body that\n * `refreshBody` builds from the refresh token stored before the clear:\n * `{ refresh_token }` in body mode, no body in cookie mode, which is what\n * that route reads. The local clear does not wait on it — `isAuthenticated`\n * is still `false` when `logout()` returns — but the promise `logout()`\n * returns does, so a navigation after `await auth.logout()` does not abort\n * it. A failure (network, `4xx`, `5xx`) is swallowed: revocation is\n * best-effort and never keeps the user signed in. A `login()` started\n * while it is in flight waits for it, because the logout response clears\n * the auth cookies and would otherwise clear the new account's.\n *\n * Only an explicit `logout()` revokes. On `\"switch\"` the cookie jar already\n * holds the new account's cookie, and on `\"expired\"` the server has already\n * refused the session.\n *\n * Overlapping sign-outs each send their own revocation, and `login()` waits\n * for all of them — at most 10 seconds, so one that\n * never answers delays the sign-in instead of blocking it. In cookie mode,\n * a revocation also waits (within the same bound) for the login and\n * refresh responses that were in flight when `logout()` ran: the server\n * sets the ended session's cookie when that response arrives, and a\n * revocation sent earlier would leave it behind. In body mode, the refresh\n * token such a superseded response brings back is revoked on its own.\n */\n logoutPath?: string;\n /**\n * Called with the error when a `logoutPath` revocation fails — network\n * failure, `4xx` or `5xx`. `logout()` still resolves, since the local\n * sign-out already happened; this is where the app logs or retries. A dev\n * build also warns in the console. Errors it throws are swallowed.\n */\n onLogoutError?: (error: unknown) => void;\n /** Optional current-user route (`GET`) called after login/refresh. */\n mePath?: string;\n /** Persist key for the store. Default: `\"tempest-auth\"`. */\n storeName?: string;\n /** Storage backend. Default: `\"local\"`. */\n storage?: \"local\" | \"session\";\n /** Send cookies (needed when the refresh token lives in an httpOnly cookie). */\n withCredentials?: boolean;\n /**\n * CSRF token for every write, forwarded to `createApiClient`'s `csrf`. Off\n * by default; `true` echoes the `csrf_token` cookie as `X-CSRF-Token`.\n *\n * The pair to `withCredentials`: once the refresh token lives in a cookie,\n * the browser attaches it to a `POST /auth/refresh` that another site\n * triggers. Login and refresh carry the header too — they go out with\n * `skipAuth`, and the CSRF header does not follow auth.\n */\n csrf?: boolean | CsrfOptions;\n /** Custom fetch implementation (testing / SSR). Defaults to `globalThis.fetch`. */\n fetcher?: typeof fetch;\n /**\n * Extract tokens from a login/refresh response. Default reads\n * `access_token` + `refresh_token`.\n */\n parseTokens?: (data: unknown) => { token: string; refreshToken?: string };\n /** Pull the user out of the login response, when the API embeds it. */\n parseUser?: (data: unknown) => TUser | null;\n /**\n * Build the refresh request body. Default sends `{ refresh_token }` when a\n * refresh token is stored, else `undefined` (cookie-based refresh).\n */\n refreshBody?: (refreshToken: string | null) => unknown;\n /**\n * Retry policy for `api`, forwarded to {@link createApiClient}. Off by\n * default; `true` enables the conservative built-in policy, which never\n * replays a write.\n */\n retry?: boolean | RetryOptions;\n /**\n * Where to send the browser when the session ends — a hard navigation via\n * `window.location.assign`, after the store is cleared.\n *\n * **Prefer leaving this unset.** `logout()` already clears the store, so a\n * `<RouteGuard when={isAuthenticated} redirectTo=\"/login\">` wrapped around\n * the protected area navigates on its own, which keeps the SPA alive and the\n * router history intact. Reach for this only when the expiry can happen\n * outside any guarded subtree and a full reload is acceptable.\n */\n redirectTo?: string;\n /**\n * Tear down per-user state when a session ends — on an explicit `logout()`,\n * when the client ends the session on its own after a failed refresh, and\n * when a successful `login()` replaces a live session. Without it, the automatic path had no place to hook into, and\n * state derived from the user (a push subscription, a persisted query\n * cache, a background-sync queue) survived into the next account.\n *\n * Order, on every path:\n *\n * 1. any refresh still in flight is superseded, so it cannot reinstall the\n * ending session's tokens;\n * 2. the store and the stored refresh token are cleared — synchronously, so\n * `isAuthenticated` is already `false` when `logout()` returns;\n * 3. this hook runs with a {@link TempestSessionEnd} whose `api` still sends\n * the ended session's access token;\n * 4. on `\"expired\"` with `redirectTo` set, the browser navigates — after the\n * hook settled, even if it threw; on `\"switch\"`, the new session is\n * installed — after the hook settled, even if it threw.\n *\n * It may be async. `logout()` resolves when it settles and rejects with what\n * it threw; on the automatic path the error is swallowed, because the\n * request that ended the session already rejects with its own `401`. It\n * runs once per session: a second `logout()`, or the other requests of a\n * burst of `401`s, find nothing left to end and skip it.\n *\n * On `\"switch\"` the hook runs only **after** the new credentials were\n * accepted, so a failed `login()` leaves the live session untouched. It runs\n * **before** the new session is installed: teardown of the previous\n * account's state (clearing a persisted cache, a background-sync queue) has\n * to finish before the new account's requests start filling them, or it\n * would wipe — or race with — the new account's data. The cost is that\n * `isAuthenticated` is `false` while the hook runs, which a route guard may\n * act on; keep the hook short. Its error is swallowed there, since the\n * login itself succeeded.\n *\n * Inside the hook, reach the backend only through `session.api`. `auth.api`\n * goes out with no token; `auth.login()` waits on the teardown in progress —\n * this hook — and never settles. Telling the hook apart from an outside\n * caller would need an async context the browser does not have, so this is\n * a documented rule, not a runtime check.\n */\n onSessionEnd?: (session: TempestSessionEnd<TUser>) => void | Promise<void>;\n}\n\nexport interface TempestAuth<TUser, TCredentials> {\n /** The persisted Zustand auth store hook (compatible with `<AuthGuard>`). */\n useAuthStore: ReturnType<typeof createAuthStore<TUser>>;\n /** A `createApiClient` wired with bearer auth + 401 → refresh → retry. */\n api: ApiClient;\n /** Authenticate, store the session, and resolve the user (or null). */\n login: (credentials: TCredentials) => Promise<TUser | null>;\n /**\n * End the session: supersede any refresh in flight, clear the store and the\n * stored refresh token, then run `onSessionEnd`.\n *\n * The store is cleared synchronously, before the first `await`, so callers\n * that ignore the promise see the signed-out state at once. The promise\n * resolves when `onSessionEnd` settled — and, with `logoutPath`, when the\n * server-side revocation settled too — and rejects with the hook's error;\n * a failed revocation never rejects it.\n */\n logout: () => Promise<void>;\n /**\n * Refresh the access token (deduplicated across concurrent callers).\n *\n * @throws {SessionSupersededError} When the session ended or changed while\n * the refresh was in flight; its tokens were discarded.\n */\n refresh: () => Promise<void>;\n /** The current access token, or null. */\n getToken: () => string | null;\n}\n\n/**\n * How long `login()` waits for pending `logoutPath` revocations, and how long a\n * cookie-mode revocation waits for the login or refresh responses still in\n * flight when `logout()` ran, before going ahead anyway.\n *\n * A bound, not a guess at latency: a fetcher that ignores abort, or a backend\n * that never answers, would otherwise leave the next `login()` waiting forever.\n * Ten seconds is past any healthy round trip and short enough that a stuck\n * revocation costs a slow sign-in, not a dead one.\n */\nconst LOGOUT_REVOCATION_WAIT_MS = 10_000;\n\n/**\n * Wait until every promise settled, or until `ms` passed, whichever is first.\n *\n * @param pending - What to wait for; rejections count as settled.\n * @param ms - The bound.\n * @returns A promise that always resolves.\n */\nfunction settleWithin(pending: readonly Promise<unknown>[], ms: number): Promise<void> {\n if (pending.length === 0) return Promise.resolve();\n return new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, ms);\n void Promise.allSettled(pending).then(() => {\n clearTimeout(timer);\n resolve();\n });\n });\n}\n\nfunction defaultParseTokens(data: unknown): { token: string; refreshToken?: string } {\n const d = (data ?? {}) as TempestTokenResponse;\n return { token: d.access_token, refreshToken: d.refresh_token };\n}\n\n/**\n * Turn-key auth preset wiring `createAuthStore` + `createRefreshQueue` +\n * `createApiClient` to the Tempest FastAPI SDK auth contract: login returns\n * `{ access_token, token_type }`, requests carry `Authorization: Bearer`, and a\n * `401` triggers a single deduplicated refresh + replay.\n *\n * The session is cleared whenever that path ends unauthorized anyway — the\n * refresh threw, or the replay came back `401` — so a refresh token that the\n * backend has revoked cannot leave the app holding a dead session. With\n * `redirectTo` set, the browser also leaves the page; without it, clearing the\n * store is enough for a `<RouteGuard>` to navigate on its own.\n *\n * Every refresh is bound to a **session epoch**, bumped by `logout()` and by a\n * `login()` whose credentials were accepted. A refresh that resolves after its epoch ended discards\n * what it brought back and rejects with {@link SessionSupersededError}, which\n * the client passes through without ending the new session. Before 0.77.0 a\n * refresh that was in flight during `logout()` reinstalled the old tokens —\n * and after `logout()` + `login(B)`, replaced B's token with A's while the UI\n * showed B (#435). A `401` from a request sent under an ended session is\n * refused the same way, instead of being replayed as the account signed in now.\n *\n * A `401` from a request that carried no token, while no session is installed,\n * surfaces without a refresh: in cookie mode that refresh would be answered by\n * the HttpOnly cookie `logout()` cannot clear, signing the previous account\n * back in (#474). With a `login()` in flight, that `401` waits for it and\n * replays with the token it installed, or surfaces when it failed — still\n * without a refresh (#496). Restore a session at boot with an explicit\n * `refresh()`, and pass `logoutPath` so the backend revokes the cookie:\n * without a working revocation, that boot `refresh()` restores the previous\n * account for whoever uses the browser next.\n *\n * Refreshes are collapsed per rotation, not just per overlap: a `401` whose\n * request left with a token that has since been replaced in the same session\n * replays with the new token and refreshes nothing. Three expired requests take\n * one refresh whether their `401`s arrive together or one after another.\n *\n * @example\n * const auth = createTempestAuth<User, { email: string; password: string }>({\n * baseURL: import.meta.env.VITE_API_URL,\n * mePath: \"/api/auth/me\",\n * });\n *\n * await auth.login({ email, password }); // stores session, returns the user\n * const orders = await auth.api.get(\"/api/orders\"); // sends the bearer token\n * await auth.logout(); // clears, then runs onSessionEnd\n *\n * @param options - The auth configuration.\n * @returns The store hook, a wired API client, and login/logout/refresh helpers.\n */\nexport function createTempestAuth<TUser, TCredentials = { email: string; password: string }>(\n options: CreateTempestAuthOptions<TUser>,\n): TempestAuth<TUser, TCredentials> {\n const {\n baseURL,\n prefix,\n loginPath = \"/api/auth/login\",\n refreshPath = \"/api/auth/refresh\",\n logoutPath,\n onLogoutError,\n mePath,\n storeName = \"tempest-auth\",\n storage = \"local\",\n withCredentials = false,\n csrf,\n fetcher,\n parseTokens = defaultParseTokens,\n parseUser,\n refreshBody = (rt) => (rt ? { refresh_token: rt } : undefined),\n retry,\n redirectTo,\n onSessionEnd,\n } = options;\n\n const useAuthStore = createAuthStore<TUser>({ name: storeName, storage });\n const refreshKey = `${storeName}-refresh`;\n\n function storageImpl(): Storage | null {\n if (typeof window === \"undefined\") return null;\n return storage === \"session\" ? window.sessionStorage : window.localStorage;\n }\n function readRefreshToken(): string | null {\n return storageImpl()?.getItem(refreshKey) ?? null;\n }\n function writeRefreshToken(token: string | null): void {\n const s = storageImpl();\n if (!s) return;\n if (token) s.setItem(refreshKey, token);\n else s.removeItem(refreshKey);\n }\n\n const state = (): AuthState<TUser> => useAuthStore.getState();\n const getToken = (): string | null => state().token;\n\n /**\n * Load the current user, without letting a `401` end the session.\n *\n * `skipAuthRetry` is what keeps this one client honest: the call is\n * authenticated, but a `401` here surfaces instead of triggering a refresh\n * and `onUnauthorized`. That was the behaviour of the throwaway client this\n * used to build per invocation, and it is the reason the preset needed\n * three clients before 0.66.0.\n *\n * Bound to the epoch current when the request leaves, like `rotate()`: a\n * profile that resolves after `logout()` — or after `logout()` +\n * `login(B)` — is discarded instead of written next to B's token, and the\n * `login()` that asked for it rejects as superseded (#473).\n *\n * @throws {SessionSupersededError} When the epoch changed while the\n * request was in flight, on success and on failure alike.\n */\n async function fetchUser(): Promise<TUser | null> {\n if (!mePath) return state().user;\n const started = epoch;\n let user: TUser;\n try {\n user = await api.get<TUser>(mePath, { skipAuthRetry: true });\n } catch (error) {\n if (epoch !== started) throw new SessionSupersededError();\n throw error;\n }\n if (epoch !== started) throw new SessionSupersededError();\n state().setUser(user);\n return user;\n }\n\n let epoch = 0;\n let ending: Promise<void> = Promise.resolve();\n let hooksRunning = 0;\n const loginsInFlight = new Set<Promise<unknown>>();\n const revocations = new Set<Promise<void>>();\n const credentialRequests = new Set<Promise<unknown>>();\n const rotatingEpochs = new Set<number>();\n\n /**\n * Track a login or refresh request until it settles, so a cookie-mode\n * revocation can wait for the response that sets the cookie it revokes.\n *\n * @param request - The request in flight.\n * @returns The same request.\n */\n function trackCredentialRequest<T>(request: Promise<T>): Promise<T> {\n const settled = request.then(\n () => undefined,\n () => undefined,\n );\n credentialRequests.add(settled);\n void settled.then(() => credentialRequests.delete(settled));\n return request;\n }\n\n /**\n * Surface a failed revocation: `onLogoutError`, plus a console warning in a\n * dev build.\n *\n * @param error - What the revocation rejected with.\n */\n function reportLogoutError(error: unknown): void {\n if (isDevBuild()) {\n console.warn(\n \"[tempest-react-sdk] createTempestAuth: logoutPath revocation failed\",\n error,\n );\n }\n try {\n onLogoutError?.(error);\n } catch {\n return;\n }\n }\n\n /**\n * Send one `logoutPath` revocation and track it until it settles. Never\n * rejects.\n *\n * @param build - Produces the request, after whatever wait it needs.\n * @returns A promise settling when the revocation settled.\n */\n function trackRevocation(build: () => Promise<unknown>): Promise<void> {\n const tracked = (async () => {\n try {\n await build();\n } catch (error) {\n reportLogoutError(error);\n }\n })();\n revocations.add(tracked);\n void tracked.then(() => revocations.delete(tracked));\n return tracked;\n }\n\n /**\n * Revoke, in body mode, the refresh token a superseded login or refresh\n * brought back. Nobody can use it — the client discarded it — but it would\n * stay valid on the server until it expires.\n *\n * Cookie mode is left to `revoke()`, which waits for these responses\n * instead: re-sending a cookie revocation here could carry whichever\n * account's cookie the browser holds by then.\n *\n * @param token - The superseded response's access token.\n * @param refreshToken - The superseded response's refresh token.\n */\n function revokeDiscarded(token: string | null, refreshToken: string | undefined): void {\n if (!logoutPath || withCredentials || !refreshToken) return;\n void trackRevocation(() =>\n endedSessionApi(token).post<unknown>(logoutPath, {\n body: refreshBody(refreshToken),\n }),\n );\n }\n\n /**\n * A client that sends an ended session's access token, and never refreshes\n * or ends a session: a `401` there just rejects.\n *\n * @param token - The ended session's access token.\n * @returns The client `onSessionEnd` and the `logoutPath` call go through.\n */\n function endedSessionApi(token: string | null): ApiClient {\n return createApiClient({\n baseURL,\n prefix,\n withCredentials,\n csrf,\n fetcher,\n getToken: () => token,\n });\n }\n\n /**\n * Start a new session epoch: supersede the refresh in flight and give the\n * new epoch a refresh queue of its own, so a `401` under the new session\n * never joins the old session's refresh.\n *\n * The client reads the epoch when each attempt leaves (`getSessionEpoch`),\n * which is what ties a `401` to the session that sent it — no set of\n * tokens to keep, and nothing that grows with the session's length.\n */\n function nextEpoch(): void {\n epoch += 1;\n queue = createRefreshQueue(rotate);\n }\n\n /**\n * Perform one refresh for the epoch that is current when it starts.\n *\n * Checked again after the round trip, on success and on failure alike: a\n * refresh that fails after `logout()` + `login(B)` must not reach\n * `onUnauthorized` either, or it would end B's session.\n *\n * @throws {SessionSupersededError} When the epoch changed during the call.\n */\n async function rotate(): Promise<void> {\n const started = epoch;\n rotatingEpochs.add(started);\n try {\n let data: unknown;\n try {\n data = await trackCredentialRequest(\n api.post<unknown>(refreshPath, {\n body: refreshBody(readRefreshToken()),\n skipAuth: true,\n }),\n );\n } catch (error) {\n if (epoch !== started) throw new SessionSupersededError();\n throw error;\n }\n const { token, refreshToken } = parseTokens(data);\n if (epoch !== started) {\n revokeDiscarded(token ?? null, refreshToken);\n throw new SessionSupersededError();\n }\n state().setToken(token);\n if (refreshToken) writeRefreshToken(refreshToken);\n } finally {\n rotatingEpochs.delete(started);\n }\n }\n\n let queue = createRefreshQueue(rotate);\n\n /**\n * Whether a `401` must be left alone: no refresh, no ending the session.\n *\n * Two cases, both of the current epoch: an attempt that carried no token\n * while a session is now installed (it left before the sign-in, and\n * refreshing or ending the new session for it would punish the wrong\n * session); and an attempt whose token the session has since rotated past\n * (the rotation it would trigger already happened).\n *\n * @param context - The attempt's credential.\n * @returns Whether the attempt is stale within the current session.\n */\n function staleWithinSession(context: RequestAuthContext): boolean {\n const current = getToken();\n if (current === null) return false;\n return context.token === null || context.token !== current;\n }\n\n /**\n * Whether nothing could make a tokenless `401` worth a refresh: no access\n * token and no stored refresh token.\n *\n * With `withCredentials`, the refresh would otherwise be answered by an\n * HttpOnly cookie the client cannot see or clear — after `logout()`, that\n * signed the previous account back in for whoever used the browser next\n * (#474). A `login()` in flight does not count as a session either: the\n * cookie a refresh would present during it is still the ended account's\n * (#496). Restoring a session from the cookie is an explicit `refresh()`,\n * typically at boot, which this does not touch.\n *\n * @returns Whether no session is installed.\n */\n function signedOut(): boolean {\n return getToken() === null && readRefreshToken() === null;\n }\n\n /**\n * Settle a tokenless `401` that arrived with no session installed while a\n * `login()` is in flight: wait for every login in flight, then let the\n * request replay when one of them installed a token.\n *\n * No refresh is sent. In cookie mode one would present whatever cookie\n * the browser holds — after `logout()` without a working `logoutPath`,\n * the previous account's: a failed login then left the client signed in\n * as that account, and a successful one could have its cookie overwritten\n * by the refresh's late `Set-Cookie` (#496).\n *\n * Skipped while an `onSessionEnd` hook runs: an account switch waits on\n * that hook, so a hook awaiting `auth.api` would wait on itself.\n *\n * @throws {Error} When no login is in flight, a hook is running, or every\n * login settled without installing a token — the `401` surfaces as is.\n */\n async function awaitLogins(): Promise<void> {\n if (loginsInFlight.size > 0 && hooksRunning === 0) {\n await Promise.allSettled([...loginsInFlight]);\n if (getToken() !== null) return;\n }\n throw new Error(\"anonymous 401 with no session\");\n }\n\n /**\n * The `refresh` the client calls on a `401`.\n *\n * - An attempt from an ended epoch is refused rather than replayed as\n * whoever is signed in now.\n * - A token this session already rotated past: the replay just picks up the\n * new one — no second refresh.\n * - No token, while a session is now installed: the `401` surfaces as is.\n * - No token, and no session installed: joins this epoch's refresh when\n * one is already in flight — the boot `refresh()` restoring a session\n * from the cookie, which the request then replays under; otherwise waits\n * for a `login()` in flight and replays with its token, see\n * `awaitLogins`; otherwise surfaces the `401` as is, see `signedOut`.\n * - Otherwise: join or start this epoch's refresh.\n *\n * @param context - The credential and epoch the rejected attempt left with.\n * @throws {SessionSupersededError} When the attempt belongs to an ended\n * session, or the session changed during the refresh.\n */\n async function refreshFor(context: RequestAuthContext): Promise<void> {\n if (context.epoch !== null && context.epoch !== epoch) throw new SessionSupersededError();\n if (staleWithinSession(context)) {\n if (context.token === null) throw new Error(\"anonymous 401 under a live session\");\n return;\n }\n if (context.token === null && signedOut()) {\n if (rotatingEpochs.has(epoch)) return queue();\n return awaitLogins();\n }\n return queue();\n }\n\n /**\n * The client's `onUnauthorized`: end the session only when the `401` is the\n * current session's own.\n *\n * The replay after a refresh is checked here too — it can come back `401`\n * after the user signed out and another account signed in, and ending the\n * session then would sign the new account out. A tokenless `401` with no\n * session installed ends nothing either: there is no session to end, and\n * ending \"nothing\" used to move the epoch and supersede the boot\n * `refresh()` in flight.\n *\n * @param context - The credential and epoch of the attempt that failed.\n * @throws {SessionSupersededError} When that attempt belongs to an ended\n * session.\n */\n async function onUnauthorized(_response: Response, context: RequestAuthContext): Promise<void> {\n if (context.epoch !== null && context.epoch !== epoch) throw new SessionSupersededError();\n if (staleWithinSession(context)) return;\n if (context.token === null && signedOut()) return;\n await endSession();\n }\n\n /**\n * Authenticate and install the new session.\n *\n * The previous session ends only once the new credentials are accepted: a\n * login that fails leaves it, and the refresh it may have in flight,\n * untouched. When one was live, `onSessionEnd` runs with `\"switch\"` before\n * the new tokens are installed — see the option for why that order. A\n * `logout()` still tearing down is awaited the same way, so the new\n * account's state never starts before the previous one's teardown ended.\n * Never call it from inside `onSessionEnd`: it would wait on that hook.\n * Every `logoutPath` revocation still in flight is awaited before the\n * request leaves — at most {@link LOGOUT_REVOCATION_WAIT_MS} — so a\n * cookie-clearing response cannot land after the new account's cookies.\n *\n * @param credentials - Sent as the login body.\n * @returns The user of the new session, or null.\n * @throws {SessionSupersededError} When a `logout()` or another `login()`\n * moved the session while this one was in flight — during its request,\n * during the teardown or while `mePath` was loading the user; the new\n * tokens, or the user, are discarded.\n */\n async function login(credentials: TCredentials): Promise<TUser | null> {\n if (revocations.size > 0) await settleWithin([...revocations], LOGOUT_REVOCATION_WAIT_MS);\n const run = signIn(credentials);\n loginsInFlight.add(run);\n try {\n return await run;\n } finally {\n loginsInFlight.delete(run);\n }\n }\n\n /**\n * The body of `login()`, tracked in `loginsInFlight` by its caller so a\n * tokenless `401` during the sign-in waits for it and replays with the\n * token it installs, see `awaitLogins`. Tracking starts only after a\n * pending revocation settled.\n *\n * @param credentials - Sent as the login body.\n * @returns The user of the new session, or null.\n * @throws {SessionSupersededError} See `login()`.\n */\n async function signIn(credentials: TCredentials): Promise<TUser | null> {\n const started = epoch;\n const data = await trackCredentialRequest(\n api.post<unknown>(loginPath, {\n body: credentials,\n skipAuth: true,\n }),\n );\n const { token, refreshToken } = parseTokens(data);\n if (epoch !== started) {\n revokeDiscarded(token ?? null, refreshToken);\n throw new SessionSupersededError();\n }\n const { done } = finish(\"switch\");\n const mine = epoch;\n await done.catch(() => undefined);\n if (epoch !== mine) throw new SessionSupersededError();\n state().setToken(token);\n writeRefreshToken(refreshToken ?? null);\n const embedded = parseUser?.(data);\n if (embedded != null) {\n state().setUser(embedded);\n return embedded;\n }\n return fetchUser();\n }\n\n /**\n * End the current session, synchronously up to the hook.\n *\n * The epoch moves first, so a refresh resolving between here and the hook\n * is already superseded. The hook only runs when there was a session to\n * end — a token, a stored refresh token or a user — which is what keeps a\n * burst of `401`s from running it once per request.\n *\n * With nothing left to end, the caller waits on the teardown already under\n * way (`ending`, which never rejects, so a later no-op does not inherit an\n * old error). Two callers skip that wait while a hook is running, because\n * they may be the hook itself and would wait on their own teardown forever:\n * `endSession` — a hook awaiting `auth.api` goes out tokenless, gets a\n * `401`, and lands here — and a `logout()` with no session left. `login()`\n * (`\"switch\"`) always waits: the new account's state must not start while\n * the previous one's teardown is still clearing it. A hook awaiting\n * `auth.login()` therefore never settles — misuse the option documents.\n *\n * `\"expired\"` with nothing live leaves the epoch alone: there is nothing to\n * supersede but a refresh with no session behind it — the boot `refresh()`\n * restoring one from the cookie — and moving the epoch would throw that\n * away. `logout()` and `login()` always move it, since they must supersede\n * such a refresh.\n *\n * The returned `done` is a fresh promise derived from the hook, not the one\n * `ending` holds: a `logout()` nobody awaits still surfaces the hook's error\n * as an unhandled rejection instead of losing it.\n *\n * @param reason - Whether the caller signed out, the client gave up, or a\n * login replaced the session.\n * @returns Whether a live session ended here, and a promise settling with\n * the hook.\n */\n function finish(reason: TempestSessionEnd<TUser>[\"reason\"]): {\n live: boolean;\n done: Promise<void>;\n } {\n const { user, token } = state();\n const live = token !== null || user !== null || readRefreshToken() !== null;\n if (live || reason !== \"expired\") nextEpoch();\n state().logout();\n writeRefreshToken(null);\n if (!live) {\n const wait = reason === \"switch\" || hooksRunning === 0;\n return { live, done: wait ? ending : Promise.resolve() };\n }\n if (!onSessionEnd) return { live, done: Promise.resolve() };\n const sessionApi = endedSessionApi(token);\n hooksRunning += 1;\n const run = (async () => {\n try {\n await onSessionEnd({ reason, user, token, api: sessionApi });\n } finally {\n hooksRunning -= 1;\n }\n })();\n ending = run.then(\n () => undefined,\n () => undefined,\n );\n return { live, done: run.then(() => undefined) };\n }\n\n /**\n * Send the `logoutPath` revocation for the session about to end, with the\n * credentials it holds now — read before `finish` clears them.\n *\n * Skipped without `logoutPath`, and in body mode when there is nothing to\n * revoke; in cookie mode it always goes out, since the cookie it revokes is\n * invisible to the client. Never rejects.\n *\n * In cookie mode it first waits — at most\n * {@link LOGOUT_REVOCATION_WAIT_MS} — for the login and refresh responses\n * in flight right now: each one sets a cookie for the session this\n * `logout()` ends, and a revocation that left before it would leave that\n * cookie behind.\n *\n * @returns A promise settling when the revocation settled, or null when\n * none was sent.\n */\n function revoke(): Promise<void> | null {\n if (!logoutPath) return null;\n const path = logoutPath;\n const { token, user } = state();\n const refreshToken = readRefreshToken();\n if (!withCredentials && token === null && user === null && refreshToken === null) {\n return null;\n }\n const inFlight = withCredentials ? [...credentialRequests] : [];\n return trackRevocation(async () => {\n await settleWithin(inFlight, LOGOUT_REVOCATION_WAIT_MS);\n return endedSessionApi(token).post<unknown>(path, { body: refreshBody(refreshToken) });\n });\n }\n\n function logout(): Promise<void> {\n const revocation = revoke();\n const { done } = finish(\"logout\");\n if (!revocation) return done;\n return Promise.all([revocation, done]).then(() => undefined);\n }\n\n /**\n * End the session after the client gave up on it and, when `redirectTo`\n * is set, leave the page once `onSessionEnd` settled.\n *\n * Separate from `logout` so an explicit sign-out stays a pure state change:\n * a caller that already navigates itself would otherwise get a second,\n * competing navigation. The hook is awaited before navigating because a\n * hard navigation aborts whatever request it still had in flight. Only the\n * call that ended a live session navigates; the rest of a burst of `401`s\n * waits on the same teardown, or returns at once inside a hook.\n */\n async function endSession(): Promise<void> {\n const { live, done } = finish(\"expired\");\n await done.catch(() => undefined);\n if (live && redirectTo && typeof window !== \"undefined\") {\n window.location.assign(redirectTo);\n }\n }\n\n const api = createApiClient({\n baseURL,\n prefix,\n withCredentials,\n csrf,\n fetcher,\n getToken,\n getSessionEpoch: () => epoch,\n refresh: refreshFor,\n retry,\n onUnauthorized,\n });\n\n const refresh = (): Promise<void> => queue();\n\n return { useAuthStore, api, login, logout, refresh, getToken };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"create-tempest-auth.js","names":["LOGOUT_REVOCATION_WAIT_MS","REFRESH_LOCK_WAIT_MS","settleWithin","pending","timer","resolve","ms","newLineage","host","UNLINEAGED_SESSION_KEY","warnedCookieWithoutLogout","warnCookieWithoutLogout","isDevBuild","storeName","defaultParseTokens","d","data","createTempestAuth","rt","options","withCredentials","logoutPath","useAuthStore","createAuthStore","refreshKey","storageImpl","storage","readRefreshToken","heldRefreshToken","writeRefreshToken","token","s","lineageKey","refreshLockName","heldLineage","writeLineage","lineage","state","getToken","fetchUser","mePath","started","epoch","user","api","error","SessionSupersededError","followSharedSession","ending","hooksRunning","loginsInFlight","revocations","credentialRequests","rotatingEpochs","trackCredentialRequest","settled","request","reportLogoutError","trackRevocation","tracked","build","revokeDiscarded","refreshToken","endedSessionApi","refreshBody","createApiClient","nextEpoch","createRefreshQueue","rotate","before","underRefreshLock","redeem","locks","run","controller","acquired","held","followed","refreshPath","parseTokens","queue","staleWithinSession","current","context","signedOut","awaitLogins","refreshFor","onUnauthorized","endSession","login","signIn","credentials","loginPath","finish","mine","done","embedded","parseUser","live","reason","runSessionEnd","onSessionEnd","sessionApi","ended","readSharedSession","raw","parsed","shared","sameUser","sameSession","continues","revoke","path","inFlight","logout","revocation","redirectTo","refresh","sessionKey","identity","listener","bindSessionIdentity","onStorage","event"],"sources":["../../src/auth/create-tempest-auth.ts"],"sourcesContent":["import { createApiClient, SessionSupersededError } from \"../http\";\nimport { bindSessionIdentity, type SessionIdentity } from \"./session-identity\";\nimport { isDevBuild } from \"../utils/dev-mode\";\nimport type { ApiClient, CsrfOptions, RequestAuthContext, RetryOptions } from \"../http\";\nimport { createAuthStore } from \"./create-auth-store\";\nimport type { AuthState } from \"./create-auth-store\";\nimport { createRefreshQueue } from \"./refresh-queue\";\n\n/** The token envelope returned by a Tempest FastAPI SDK login/refresh route. */\nexport interface TempestTokenResponse {\n /** The bearer access token. */\n access_token: string;\n /** Token type — always `\"bearer\"` for the SDK. */\n token_type?: string;\n /** Optional refresh token (when the API returns it in the body, not a cookie). */\n refresh_token?: string;\n}\n\n/**\n * What {@link CreateTempestAuthOptions.onSessionEnd} receives about the session\n * that just ended.\n */\nexport interface TempestSessionEnd<TUser> {\n /**\n * Why it ended: `\"logout\"` for an explicit `logout()`, `\"expired\"` when the\n * client ended it on its own — the refresh failed, or the request replayed\n * after a refresh still came back `401` — and `\"switch\"` when `login()`\n * succeeded for another set of credentials while this session was live.\n */\n reason: \"logout\" | \"expired\" | \"switch\";\n /** The user of the ended session, as the store held it. */\n user: TUser | null;\n /** The access token of the ended session, or `null` when it had none. */\n token: string | null;\n /**\n * A client that sends the ended session's access token — the store has\n * already been cleared, so `auth.api` no longer does.\n *\n * Use it for the server-side half of the teardown, such as deleting this\n * browser's push subscription. It never refreshes and never ends a session:\n * a `401` here just rejects. On `\"logout\"` the token is usually still\n * valid; on `\"expired\"` the server has already refused it, so expect the\n * call to fail and keep the local half of the teardown independent of it.\n */\n api: ApiClient;\n /**\n * Whether another tab of the origin ended or replaced the session, and this\n * tab is only catching up (#524). With `storage: \"local\"` the session's\n * keys are shared by every tab, so a `logout()` or a `login()` in one tab\n * ends the session in all of them: `reason` is `\"logout\"` when the other\n * tab left no session behind and `\"switch\"` when it installed another\n * account.\n *\n * The other tab already ran this hook for the same session. Teardown that\n * lives in this tab's memory (an in-memory cache, an open socket) still has\n * to run here; teardown of state the origin shares (IndexedDB, Cache\n * Storage, a push subscription) was done there, and on `\"switch\"` redoing\n * it here would wipe what the new account already wrote — skip it when\n * this is `true`.\n */\n elsewhere: boolean;\n}\n\nexport interface CreateTempestAuthOptions<TUser> {\n /** Base URL of the API. Required. */\n baseURL: string;\n /**\n * Path segment the API is mounted under, such as `\"/api\"`, forwarded to\n * every client this preset builds — see the `prefix` option of\n * `createApiClient`.\n *\n * Safe to combine with the default paths below: the prefix is applied at\n * most once, so `\"/api/auth/login\"` under a `\"/api\"` prefix stays\n * `\"/api/auth/login\"`.\n */\n prefix?: string;\n /** Login route (`POST`). Default: `\"/api/auth/login\"`. */\n loginPath?: string;\n /** Refresh route (`POST`). Default: `\"/api/auth/refresh\"`. */\n refreshPath?: string;\n /**\n * Server-side logout route (`POST`), called by an explicit `logout()` to\n * revoke the ending session. Off by default.\n *\n * `logout()` alone only clears client state, and an HttpOnly refresh\n * cookie is out of its reach: with `withCredentials`, the cookie stays\n * redeemable until the backend revokes it. The tempest-fastapi-sdk mounts\n * `POST /auth/logout` (`204`, idempotent) when it stores refresh tokens in\n * the database — at `/auth/cookie/logout` when it delivers tokens both in\n * the body and in cookies — and stateless refresh tokens cannot be\n * revoked, so the route is absent there.\n *\n * The request goes out through the ended session's client (the same one\n * `onSessionEnd` receives, carrying its access token), with the body that\n * `refreshBody` builds from the refresh token stored before the clear:\n * `{ refresh_token }` in body mode, no body in cookie mode, which is what\n * that route reads. The local clear does not wait on it — `isAuthenticated`\n * is still `false` when `logout()` returns — but the promise `logout()`\n * returns does, so a navigation after `await auth.logout()` does not abort\n * it. A failure (network, `4xx`, `5xx`) is swallowed: revocation is\n * best-effort and never keeps the user signed in. A `login()` started\n * while it is in flight waits for it, because the logout response clears\n * the auth cookies and would otherwise clear the new account's.\n *\n * An explicit `logout()` revokes, and so does a `\"switch\"` in body mode:\n * the replaced session's refresh token is still in the client's hands, and\n * the tempest-fastapi-sdk revokes by rotation family, so the session the\n * new login opened is untouched even for the same user. Before 0.82.0 a\n * switch revoked nothing and the replaced token stayed redeemable until it\n * expired. A `\"switch\"` in cookie mode does not revoke — the cookie jar\n * already holds the new account's cookie — and on `\"expired\"` the server\n * has already refused the session.\n *\n * Overlapping sign-outs each send their own revocation, and `login()` waits\n * for all of them — at most 10 seconds, so one that\n * never answers delays the sign-in instead of blocking it. In cookie mode,\n * a revocation also waits (within the same bound) for the login and\n * refresh responses that were in flight when `logout()` ran: the server\n * sets the ended session's cookie when that response arrives, and a\n * revocation sent earlier would leave it behind. In body mode, the refresh\n * token such a superseded response brings back is revoked on its own.\n */\n logoutPath?: string;\n /**\n * Called with the error when a `logoutPath` revocation fails — network\n * failure, `4xx` or `5xx`. `logout()` still resolves, since the local\n * sign-out already happened; this is where the app logs or retries. A dev\n * build also warns in the console. Errors it throws are swallowed.\n */\n onLogoutError?: (error: unknown) => void;\n /** Optional current-user route (`GET`) called after login/refresh. */\n mePath?: string;\n /** Persist key for the store. Default: `\"tempest-auth\"`. */\n storeName?: string;\n /** Storage backend. Default: `\"local\"`. */\n storage?: \"local\" | \"session\";\n /** Send cookies (needed when the refresh token lives in an httpOnly cookie). */\n withCredentials?: boolean;\n /**\n * CSRF token for every write, forwarded to `createApiClient`'s `csrf`. Off\n * by default; `true` echoes the `csrf_token` cookie as `X-CSRF-Token`.\n *\n * The pair to `withCredentials`: once the refresh token lives in a cookie,\n * the browser attaches it to a `POST /auth/refresh` that another site\n * triggers. Login and refresh carry the header too — they go out with\n * `skipAuth`, and the CSRF header does not follow auth.\n */\n csrf?: boolean | CsrfOptions;\n /** Custom fetch implementation (testing / SSR). Defaults to `globalThis.fetch`. */\n fetcher?: typeof fetch;\n /**\n * Extract tokens from a login/refresh response. Default reads\n * `access_token` + `refresh_token`.\n */\n parseTokens?: (data: unknown) => { token: string; refreshToken?: string };\n /** Pull the user out of the login response, when the API embeds it. */\n parseUser?: (data: unknown) => TUser | null;\n /**\n * Build the refresh request body. Default sends `{ refresh_token }` when a\n * refresh token is stored, else `undefined` (cookie-based refresh).\n */\n refreshBody?: (refreshToken: string | null) => unknown;\n /**\n * Retry policy for `api`, forwarded to {@link createApiClient}. Off by\n * default; `true` enables the conservative built-in policy, which never\n * replays a write.\n */\n retry?: boolean | RetryOptions;\n /**\n * Where to send the browser when the session ends — a hard navigation via\n * `window.location.assign`, after the store is cleared.\n *\n * **Prefer leaving this unset.** `logout()` already clears the store, so a\n * `<RouteGuard when={isAuthenticated} redirectTo=\"/login\">` wrapped around\n * the protected area navigates on its own, which keeps the SPA alive and the\n * router history intact. Reach for this only when the expiry can happen\n * outside any guarded subtree and a full reload is acceptable.\n */\n redirectTo?: string;\n /**\n * Tear down per-user state when a session ends — on an explicit `logout()`,\n * when the client ends the session on its own after a failed refresh, and\n * when a successful `login()` replaces a live session. Without it, the automatic path had no place to hook into, and\n * state derived from the user (a push subscription, a persisted query\n * cache, a background-sync queue) survived into the next account.\n *\n * Order, on every path:\n *\n * 1. any refresh still in flight is superseded, so it cannot reinstall the\n * ending session's tokens;\n * 2. the store and the stored refresh token are cleared — synchronously, so\n * `isAuthenticated` is already `false` when `logout()` returns;\n * 3. this hook runs with a {@link TempestSessionEnd} whose `api` still sends\n * the ended session's access token;\n * 4. on `\"expired\"` with `redirectTo` set, the browser navigates — after the\n * hook settled, even if it threw; on `\"switch\"`, the new session is\n * installed — after the hook settled, even if it threw.\n *\n * It may be async. `logout()` resolves when it settles and rejects with what\n * it threw; on the automatic path the error is swallowed, because the\n * request that ended the session already rejects with its own `401`. It\n * runs once per session: a second `logout()`, or the other requests of a\n * burst of `401`s, find nothing left to end and skip it.\n *\n * On `\"switch\"` the hook runs only **after** the new credentials were\n * accepted, so a failed `login()` leaves the live session untouched. It runs\n * **before** the new session is installed: teardown of the previous\n * account's state (clearing a persisted cache, a background-sync queue) has\n * to finish before the new account's requests start filling them, or it\n * would wipe — or race with — the new account's data. The cost is that\n * `isAuthenticated` is `false` while the hook runs, which a route guard may\n * act on; keep the hook short. Its error is swallowed there, since the\n * login itself succeeded.\n *\n * Inside the hook, reach the backend only through `session.api`. `auth.api`\n * goes out with no token; `auth.login()` waits on the teardown in progress —\n * this hook — and never settles. Telling the hook apart from an outside\n * caller would need an async context the browser does not have, so this is\n * a documented rule, not a runtime check.\n */\n onSessionEnd?: (session: TempestSessionEnd<TUser>) => void | Promise<void>;\n}\n\nexport interface TempestAuth<TUser, TCredentials> {\n /** The persisted Zustand auth store hook (compatible with `<AuthGuard>`). */\n useAuthStore: ReturnType<typeof createAuthStore<TUser>>;\n /** A `createApiClient` wired with bearer auth + 401 → refresh → retry. */\n api: ApiClient;\n /** Authenticate, store the session, and resolve the user (or null). */\n login: (credentials: TCredentials) => Promise<TUser | null>;\n /**\n * End the session: supersede any refresh in flight, clear the store and the\n * stored refresh token, then run `onSessionEnd`.\n *\n * The store is cleared synchronously, before the first `await`, so callers\n * that ignore the promise see the signed-out state at once. The promise\n * resolves when `onSessionEnd` settled — and, with `logoutPath`, when the\n * server-side revocation settled too — and rejects with the hook's error;\n * a failed revocation never rejects it.\n */\n logout: () => Promise<void>;\n /**\n * Refresh the access token (deduplicated across concurrent callers).\n *\n * @throws {SessionSupersededError} When the session ended or changed while\n * the refresh was in flight; its tokens were discarded.\n */\n refresh: () => Promise<void>;\n /** The current access token, or null. */\n getToken: () => string | null;\n /**\n * The key of the session signed in now, or `null` when nobody is.\n *\n * It changes when the session is replaced — `login()`, even into the same\n * account, a sign-in or sign-out in another tab, `logout()` — and stays\n * the same while that session's tokens rotate: it is the session lineage\n * `login()` mints, not the token. That is the identity a per-session\n * resource needs. `useTicketEventStream`/`createTicketEventStream` read it\n * on their own from `api`, and `createResumableUpload` from `getToken`;\n * pass it as `sessionKey` or `getPrincipal` anywhere else.\n *\n * A session that has no lineage — persisted before lineages existed, or\n * restored from a refresh cookie without a `login()` in this storage —\n * answers `\"unlineaged\"`. Replacing it still takes a\n * `login()`, which mints one, so the key still changes on a switch.\n */\n sessionKey: () => string | null;\n /**\n * Stop following the other tabs: removes the `storage` listener this\n * preset adds to `window`. Idempotent.\n *\n * An app keeps one preset for the page's lifetime and never needs it; it\n * exists for tests and hot-module reload, which build several presets in\n * one window. A disposed preset still compares the shared storage with\n * its session before every refresh, so it never redeems another tab's\n * refresh token.\n */\n dispose: () => void;\n}\n\n/**\n * How long `login()` waits for pending `logoutPath` revocations, and how long a\n * cookie-mode revocation waits for the login or refresh responses still in\n * flight when `logout()` ran, before going ahead anyway.\n *\n * A bound, not a guess at latency: a fetcher that ignores abort, or a backend\n * that never answers, would otherwise leave the next `login()` waiting forever.\n * Ten seconds is past any healthy round trip and short enough that a stuck\n * revocation costs a slow sign-in, not a dead one.\n */\nconst LOGOUT_REVOCATION_WAIT_MS = 10_000;\n\n/**\n * How long a refresh waits for another tab's refresh to release the\n * origin-wide refresh lock before redeeming without it.\n *\n * The same reasoning as {@link LOGOUT_REVOCATION_WAIT_MS}: past any healthy\n * round trip, and a bound so that a tab stuck holding the lock costs a\n * duplicate redemption, not a request that never settles.\n */\nconst REFRESH_LOCK_WAIT_MS = 10_000;\n\n/**\n * Wait until every promise settled, or until `ms` passed, whichever is first.\n *\n * @param pending - What to wait for; rejections count as settled.\n * @param ms - The bound.\n * @returns A promise that always resolves.\n */\nfunction settleWithin(pending: readonly Promise<unknown>[], ms: number): Promise<void> {\n if (pending.length === 0) return Promise.resolve();\n return new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, ms);\n void Promise.allSettled(pending).then(() => {\n clearTimeout(timer);\n resolve();\n });\n });\n}\n\n/**\n * Mint the identifier of a new session lineage: one per successful `login()`,\n * kept across every rotation of that session, see `createTempestAuth`.\n *\n * It only has to tell two sign-ins apart, never to resist guessing — it guards\n * nothing on the server and is readable by any script of the origin anyway. So\n * the fallback for a page without `crypto.randomUUID` (served over plain http,\n * where the browser withholds it) is a timestamp plus `Math.random`, not an\n * error.\n *\n * @returns A fresh lineage identifier.\n */\nfunction newLineage(): string {\n const host = globalThis.crypto as Partial<Crypto> | undefined;\n if (typeof host?.randomUUID === \"function\") return host.randomUUID();\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;\n}\n\n/**\n * The {@link TempestAuth.sessionKey} of a signed-in session that has no\n * lineage. One constant for all of them, since none can be told apart without\n * one — and no account switch can produce one.\n */\nconst UNLINEAGED_SESSION_KEY = \"unlineaged\";\n\n/**\n * The store names already warned about cookie mode without `logoutPath`, so a\n * hot-module reload that rebuilds the preset does not repeat the warning.\n */\nconst warnedCookieWithoutLogout = new Set<string>();\n\n/**\n * Warn, in a dev build and once per store, that cookie mode has no\n * `logoutPath` (#531).\n *\n * `logout()` cannot reach an HttpOnly refresh cookie, so without a revocation\n * the cookie stays redeemable and the boot `refresh()` of the next page load\n * signs the account that just signed out back in — for whoever uses the\n * browser next. Measured with the `cookieBackend` of\n * `create-tempest-auth.cookie.test.ts`: login, `logout()`, a new preset and\n * `refresh()` restore `A-tok-cookie-1`.\n *\n * A warning, not a requirement. Requiring `logoutPath` would break every app\n * whose backend cannot revoke — the tempest-fastapi-sdk mounts no logout route\n * with stateless refresh tokens — and would not close the hole for them\n * either: a route that answers `404` revokes nothing. Refusing the boot\n * `refresh()` after a logout would break the sign-ins that set the cookie\n * outside `login()`, such as an OAuth callback. Only the backend can end the\n * cookie; this makes the missing half visible while the app is being built.\n *\n * @param storeName - The preset's persist key.\n */\nfunction warnCookieWithoutLogout(storeName: string): void {\n if (!isDevBuild() || warnedCookieWithoutLogout.has(storeName)) return;\n warnedCookieWithoutLogout.add(storeName);\n console.warn(\n \"[tempest-react-sdk] createTempestAuth: withCredentials is set without logoutPath. \" +\n \"logout() cannot clear the HttpOnly refresh cookie, so the next refresh() — the \" +\n \"boot restore — signs the same account back in. Pass logoutPath (the backend's \" +\n \"cookie logout route) so the server revokes it.\",\n );\n}\n\nfunction defaultParseTokens(data: unknown): { token: string; refreshToken?: string } {\n const d = (data ?? {}) as TempestTokenResponse;\n return { token: d.access_token, refreshToken: d.refresh_token };\n}\n\n/**\n * Turn-key auth preset wiring `createAuthStore` + `createRefreshQueue` +\n * `createApiClient` to the Tempest FastAPI SDK auth contract: login returns\n * `{ access_token, token_type }`, requests carry `Authorization: Bearer`, and a\n * `401` triggers a single deduplicated refresh + replay.\n *\n * The session is cleared whenever that path ends unauthorized anyway — the\n * refresh threw, or the replay came back `401` — so a refresh token that the\n * backend has revoked cannot leave the app holding a dead session. With\n * `redirectTo` set, the browser also leaves the page; without it, clearing the\n * store is enough for a `<RouteGuard>` to navigate on its own.\n *\n * Every refresh is bound to a **session epoch**, bumped by `logout()` and by a\n * `login()` whose credentials were accepted. A refresh that resolves after its epoch ended discards\n * what it brought back and rejects with {@link SessionSupersededError}, which\n * the client passes through without ending the new session. Before 0.77.0 a\n * refresh that was in flight during `logout()` reinstalled the old tokens —\n * and after `logout()` + `login(B)`, replaced B's token with A's while the UI\n * showed B (#435). A `401` from a request sent under an ended session is\n * refused the same way, instead of being replayed as the account signed in now.\n *\n * A `401` from a request that carried no token, while no session is installed,\n * surfaces without a refresh: in cookie mode that refresh would be answered by\n * the HttpOnly cookie `logout()` cannot clear, signing the previous account\n * back in (#474). With a `login()` in flight, that `401` waits for it and\n * replays with the token it installed, or surfaces when it failed — still\n * without a refresh (#496). Restore a session at boot with an explicit\n * `refresh()`, and pass `logoutPath` so the backend revokes the cookie:\n * without a working revocation, that boot `refresh()` restores the previous\n * account for whoever uses the browser next.\n *\n * With `storage: \"local\"` the session belongs to the **origin**, not the tab:\n * every tab reads and writes the same keys and follows what another tab wrote\n * — on the `storage` event and, before any refresh, `logout()` or ending of\n * the session, by comparing the stored session with the one it holds. A\n * rotation elsewhere is adopted; a logout or another account's login\n * elsewhere ends the session here too, with `onSessionEnd` told it happened\n * `elsewhere`. Before 0.81.0 a tab left open after logout kept account A and,\n * on its next `401`, redeemed account B's refresh token from the shared\n * storage, persisting `{ user: A, token: B }` (#524). Whether the stored\n * session is the held one is decided by a **session lineage** that `login()`\n * mints and every rotation keeps — not by comparing users, which took two\n * sign-ins whose user had not loaded yet for the same account (#544). Two tabs\n * never redeem the same refresh token at once: the redemption runs under an\n * origin-wide Web Lock when the browser has one. `storage: \"session\"` is per\n * tab and behaves as before.\n *\n * Refreshes are collapsed per rotation, not just per overlap: a `401` whose\n * request left with a token that has since been replaced in the same session\n * replays with the new token and refreshes nothing. Three expired requests take\n * one refresh whether their `401`s arrive together or one after another.\n *\n * @example\n * const auth = createTempestAuth<User, { email: string; password: string }>({\n * baseURL: import.meta.env.VITE_API_URL,\n * mePath: \"/api/auth/me\",\n * });\n *\n * await auth.login({ email, password }); // stores session, returns the user\n * const orders = await auth.api.get(\"/api/orders\"); // sends the bearer token\n * await auth.logout(); // clears, then runs onSessionEnd\n *\n * @param options - The auth configuration.\n * @returns The store hook, a wired API client, and login/logout/refresh helpers.\n */\nexport function createTempestAuth<TUser, TCredentials = { email: string; password: string }>(\n options: CreateTempestAuthOptions<TUser>,\n): TempestAuth<TUser, TCredentials> {\n const {\n baseURL,\n prefix,\n loginPath = \"/api/auth/login\",\n refreshPath = \"/api/auth/refresh\",\n logoutPath,\n onLogoutError,\n mePath,\n storeName = \"tempest-auth\",\n storage = \"local\",\n withCredentials = false,\n csrf,\n fetcher,\n parseTokens = defaultParseTokens,\n parseUser,\n refreshBody = (rt) => (rt ? { refresh_token: rt } : undefined),\n retry,\n redirectTo,\n onSessionEnd,\n } = options;\n\n if (withCredentials && !logoutPath) warnCookieWithoutLogout(storeName);\n\n const useAuthStore = createAuthStore<TUser>({ name: storeName, storage });\n const refreshKey = `${storeName}-refresh`;\n\n function storageImpl(): Storage | null {\n if (typeof window === \"undefined\") return null;\n return storage === \"session\" ? window.sessionStorage : window.localStorage;\n }\n function readRefreshToken(): string | null {\n return storageImpl()?.getItem(refreshKey) ?? null;\n }\n let heldRefreshToken: string | null = readRefreshToken();\n function writeRefreshToken(token: string | null): void {\n heldRefreshToken = token;\n const s = storageImpl();\n if (!s) return;\n if (token) s.setItem(refreshKey, token);\n else s.removeItem(refreshKey);\n }\n\n const lineageKey = `${storeName}-lineage`;\n const refreshLockName = `tempest-react-sdk:${storeName}:refresh`;\n let heldLineage: string | null = storageImpl()?.getItem(lineageKey) ?? null;\n function writeLineage(lineage: string | null): void {\n heldLineage = lineage;\n const s = storageImpl();\n if (!s) return;\n if (lineage) s.setItem(lineageKey, lineage);\n else s.removeItem(lineageKey);\n }\n\n const state = (): AuthState<TUser> => useAuthStore.getState();\n const getToken = (): string | null => state().token;\n\n /**\n * Load the current user, without letting a `401` end the session.\n *\n * `skipAuthRetry` is what keeps this one client honest: the call is\n * authenticated, but a `401` here surfaces instead of triggering a refresh\n * and `onUnauthorized`. That was the behaviour of the throwaway client this\n * used to build per invocation, and it is the reason the preset needed\n * three clients before 0.66.0.\n *\n * Bound to the epoch current when the request leaves, like `rotate()`: a\n * profile that resolves after `logout()` — or after `logout()` +\n * `login(B)` — is discarded instead of written next to B's token, and the\n * `login()` that asked for it rejects as superseded (#473).\n *\n * Before the profile is written, the shared storage is followed (#544).\n * The write persists the whole `{ user, token }` this tab holds, so a tab\n * that had not yet seen another account's sign-in used to overwrite it\n * with its own token next to that account's refresh token — and the\n * other tab, following the write, paired that refresh token with this\n * account's user. Following first turns that into a session ended\n * elsewhere, and the profile is discarded.\n *\n * Bound to the session, not to the token the request carried: a token\n * that changed because another tab rotated the same session still\n * belongs to this user, and discarding the profile then would fail a\n * sign-in that succeeded.\n *\n * @throws {SessionSupersededError} When the epoch changed while the\n * request was in flight, on success and on failure alike, or another\n * tab ended or replaced the session.\n */\n async function fetchUser(): Promise<TUser | null> {\n if (!mePath) return state().user;\n const started = epoch;\n let user: TUser;\n try {\n user = await api.get<TUser>(mePath, { skipAuthRetry: true });\n } catch (error) {\n if (epoch !== started) throw new SessionSupersededError();\n throw error;\n }\n if (epoch !== started || !followSharedSession().continues) {\n throw new SessionSupersededError();\n }\n state().setUser(user);\n return user;\n }\n\n let epoch = 0;\n let ending: Promise<void> = Promise.resolve();\n let hooksRunning = 0;\n const loginsInFlight = new Set<Promise<unknown>>();\n const revocations = new Set<Promise<void>>();\n const credentialRequests = new Set<Promise<unknown>>();\n const rotatingEpochs = new Set<number>();\n\n /**\n * Track a login or refresh request until it settles, so a cookie-mode\n * revocation can wait for the response that sets the cookie it revokes.\n *\n * @param request - The request in flight.\n * @returns The same request.\n */\n function trackCredentialRequest<T>(request: Promise<T>): Promise<T> {\n const settled = request.then(\n () => undefined,\n () => undefined,\n );\n credentialRequests.add(settled);\n void settled.then(() => credentialRequests.delete(settled));\n return request;\n }\n\n /**\n * Surface a failed revocation: `onLogoutError`, plus a console warning in a\n * dev build.\n *\n * @param error - What the revocation rejected with.\n */\n function reportLogoutError(error: unknown): void {\n if (isDevBuild()) {\n console.warn(\n \"[tempest-react-sdk] createTempestAuth: logoutPath revocation failed\",\n error,\n );\n }\n try {\n onLogoutError?.(error);\n } catch {\n return;\n }\n }\n\n /**\n * Send one `logoutPath` revocation and track it until it settles. Never\n * rejects.\n *\n * @param build - Produces the request, after whatever wait it needs.\n * @returns A promise settling when the revocation settled.\n */\n function trackRevocation(build: () => Promise<unknown>): Promise<void> {\n const tracked = (async () => {\n try {\n await build();\n } catch (error) {\n reportLogoutError(error);\n }\n })();\n revocations.add(tracked);\n void tracked.then(() => revocations.delete(tracked));\n return tracked;\n }\n\n /**\n * Revoke, in body mode, the refresh token a superseded login or refresh\n * brought back. Nobody can use it — the client discarded it — but it would\n * stay valid on the server until it expires.\n *\n * Cookie mode is left to `revoke()`, which waits for these responses\n * instead: re-sending a cookie revocation here could carry whichever\n * account's cookie the browser holds by then.\n *\n * @param token - The superseded response's access token.\n * @param refreshToken - The superseded response's refresh token.\n */\n function revokeDiscarded(token: string | null, refreshToken: string | undefined): void {\n if (!logoutPath || withCredentials || !refreshToken) return;\n void trackRevocation(() =>\n endedSessionApi(token).post<unknown>(logoutPath, {\n body: refreshBody(refreshToken),\n }),\n );\n }\n\n /**\n * A client that sends an ended session's access token, and never refreshes\n * or ends a session: a `401` there just rejects.\n *\n * @param token - The ended session's access token.\n * @returns The client `onSessionEnd` and the `logoutPath` call go through.\n */\n function endedSessionApi(token: string | null): ApiClient {\n return createApiClient({\n baseURL,\n prefix,\n withCredentials,\n csrf,\n fetcher,\n getToken: () => token,\n });\n }\n\n /**\n * Start a new session epoch: supersede the refresh in flight and give the\n * new epoch a refresh queue of its own, so a `401` under the new session\n * never joins the old session's refresh.\n *\n * The client reads the epoch when each attempt leaves (`getSessionEpoch`),\n * which is what ties a `401` to the session that sent it — no set of\n * tokens to keep, and nothing that grows with the session's length.\n */\n function nextEpoch(): void {\n epoch += 1;\n queue = createRefreshQueue(rotate);\n }\n\n /**\n * Perform one refresh for the epoch that is current when it starts.\n *\n * Checked again after the round trip, on success and on failure alike: a\n * refresh that fails after `logout()` + `login(B)` must not reach\n * `onUnauthorized` either, or it would end B's session.\n *\n * Before the request, and again before installing its result, the stored\n * session is compared with the one this tab holds, see\n * `followSharedSession`. When another tab rotated the same account, its\n * tokens are adopted and nothing is redeemed: the replay goes out with\n * them. When another tab signed out, or signed in as someone else, nothing\n * is redeemed either — the refresh token in storage is not this session's\n * (#524) — and the call rejects as superseded.\n *\n * With `storage: \"local\"` the redemption runs under a Web Lock shared by\n * every tab of the origin, see `underRefreshLock`; the comparison with the\n * stored session happens once the lock is held, so a tab that waited\n * behind another tab's rotation adopts its tokens instead of presenting\n * the refresh token that rotation just spent.\n *\n * @throws {SessionSupersededError} When the epoch changed during the call,\n * or another tab ended or replaced the session.\n */\n async function rotate(): Promise<void> {\n const before = getToken();\n const started = epoch;\n rotatingEpochs.add(started);\n try {\n await underRefreshLock(() => redeem(before));\n } finally {\n rotatingEpochs.delete(started);\n }\n }\n\n /**\n * Run `run` while holding the origin-wide refresh lock, when there is one\n * to hold (run-7 hardening note).\n *\n * Two tabs whose requests expire together used to redeem the same refresh\n * token at once. A backend with reuse detection — the tempest-fastapi-sdk\n * has it — answers the second redemption by revoking the whole rotation\n * family, so the session ends in every tab. The common trigger is a\n * browser restart that restores several tabs, each calling the boot\n * `refresh()`: measured with two presets over a reuse-detecting backend,\n * both presented `ref-1`, the family was revoked, and no refresh token was\n * left alive.\n *\n * The lock is the Web Locks API (`navigator.locks`), held only for the\n * redemption round trip. Only `storage: \"local\"` shares a session between\n * tabs, so `\"session\"` takes no lock. Fail-open, by design: without the\n * API (SSR, an old browser, jsdom), when acquiring it throws (an opaque\n * origin), or after {@link REFRESH_LOCK_WAIT_MS} of waiting behind a tab\n * that never releases it, the redemption runs unlocked — the behaviour\n * before the lock existed — instead of leaving the request hanging.\n *\n * @param run - The redemption.\n * @returns A promise settling with `run`.\n */\n async function underRefreshLock(run: () => Promise<void>): Promise<void> {\n const locks =\n storage === \"local\" && typeof navigator !== \"undefined\"\n ? (navigator as Partial<Navigator>).locks\n : undefined;\n if (!locks) return run();\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), REFRESH_LOCK_WAIT_MS);\n let acquired = false;\n try {\n await locks.request(refreshLockName, { signal: controller.signal }, async () => {\n acquired = true;\n clearTimeout(timer);\n await run();\n });\n } catch (error) {\n if (acquired) throw error;\n clearTimeout(timer);\n await run();\n }\n }\n\n /**\n * The body of one rotation, run under the refresh lock.\n *\n * A tab that held no session when the rotation was asked for — the boot\n * `refresh()` — and finds one installed once the lock is held was restored\n * by another tab while it waited: that is the outcome it asked for, so it\n * resolves without redeeming.\n *\n * @param before - The access token held when the rotation was asked for.\n * @throws {SessionSupersededError} See `rotate()`.\n */\n async function redeem(before: string | null): Promise<void> {\n const held = before !== null || state().user !== null || heldRefreshToken !== null;\n const followed = followSharedSession();\n if (!held && getToken() !== null) return;\n if (!followed.continues) throw new SessionSupersededError();\n if (getToken() !== before) return;\n const started = epoch;\n let data: unknown;\n try {\n data = await trackCredentialRequest(\n api.post<unknown>(refreshPath, {\n body: refreshBody(readRefreshToken()),\n skipAuth: true,\n }),\n );\n } catch (error) {\n if (epoch !== started) throw new SessionSupersededError();\n throw error;\n }\n const { token, refreshToken } = parseTokens(data);\n if (epoch !== started || !followSharedSession().continues) {\n revokeDiscarded(token ?? null, refreshToken);\n throw new SessionSupersededError();\n }\n state().setToken(token);\n if (refreshToken) writeRefreshToken(refreshToken);\n }\n\n let queue = createRefreshQueue(rotate);\n\n /**\n * Whether a `401` must be left alone: no refresh, no ending the session.\n *\n * Two cases, both of the current epoch: an attempt that carried no token\n * while a session is now installed (it left before the sign-in, and\n * refreshing or ending the new session for it would punish the wrong\n * session); and an attempt whose token the session has since rotated past\n * (the rotation it would trigger already happened).\n *\n * @param context - The attempt's credential.\n * @returns Whether the attempt is stale within the current session.\n */\n function staleWithinSession(context: RequestAuthContext): boolean {\n const current = getToken();\n if (current === null) return false;\n return context.token === null || context.token !== current;\n }\n\n /**\n * Whether nothing could make a tokenless `401` worth a refresh: no access\n * token and no stored refresh token.\n *\n * With `withCredentials`, the refresh would otherwise be answered by an\n * HttpOnly cookie the client cannot see or clear — after `logout()`, that\n * signed the previous account back in for whoever used the browser next\n * (#474). A `login()` in flight does not count as a session either: the\n * cookie a refresh would present during it is still the ended account's\n * (#496). Restoring a session from the cookie is an explicit `refresh()`,\n * typically at boot, which this does not touch.\n *\n * @returns Whether no session is installed.\n */\n function signedOut(): boolean {\n return getToken() === null && readRefreshToken() === null;\n }\n\n /**\n * Settle a tokenless `401` that arrived with no session installed while a\n * `login()` is in flight: wait for every login in flight, then let the\n * request replay when one of them installed a token.\n *\n * No refresh is sent. In cookie mode one would present whatever cookie\n * the browser holds — after `logout()` without a working `logoutPath`,\n * the previous account's: a failed login then left the client signed in\n * as that account, and a successful one could have its cookie overwritten\n * by the refresh's late `Set-Cookie` (#496).\n *\n * Skipped while an `onSessionEnd` hook runs: an account switch waits on\n * that hook, so a hook awaiting `auth.api` would wait on itself.\n *\n * @throws {Error} When no login is in flight, a hook is running, or every\n * login settled without installing a token — the `401` surfaces as is.\n */\n async function awaitLogins(): Promise<void> {\n if (loginsInFlight.size > 0 && hooksRunning === 0) {\n await Promise.allSettled([...loginsInFlight]);\n if (getToken() !== null) return;\n }\n throw new Error(\"anonymous 401 with no session\");\n }\n\n /**\n * The `refresh` the client calls on a `401`.\n *\n * - An attempt from an ended epoch is refused rather than replayed as\n * whoever is signed in now.\n * - A token this session already rotated past: the replay just picks up the\n * new one — no second refresh.\n * - No token, while a session is now installed: the `401` surfaces as is.\n * - No token, and no session installed: joins this epoch's refresh when\n * one is already in flight — the boot `refresh()` restoring a session\n * from the cookie, which the request then replays under; otherwise waits\n * for a `login()` in flight and replays with its token, see\n * `awaitLogins`; otherwise surfaces the `401` as is, see `signedOut`.\n * - Otherwise: join or start this epoch's refresh.\n *\n * @param context - The credential and epoch the rejected attempt left with.\n * @throws {SessionSupersededError} When the attempt belongs to an ended\n * session, or the session changed during the refresh.\n */\n async function refreshFor(context: RequestAuthContext): Promise<void> {\n if (context.epoch !== null && context.epoch !== epoch) throw new SessionSupersededError();\n if (staleWithinSession(context)) {\n if (context.token === null) throw new Error(\"anonymous 401 under a live session\");\n return;\n }\n if (context.token === null && signedOut()) {\n if (rotatingEpochs.has(epoch)) return queue();\n return awaitLogins();\n }\n return queue();\n }\n\n /**\n * The client's `onUnauthorized`: end the session only when the `401` is the\n * current session's own.\n *\n * The replay after a refresh is checked here too — it can come back `401`\n * after the user signed out and another account signed in, and ending the\n * session then would sign the new account out. A tokenless `401` with no\n * session installed ends nothing either: there is no session to end, and\n * ending \"nothing\" used to move the epoch and supersede the boot\n * `refresh()` in flight. Neither does a `401` from a session another tab\n * already ended or replaced: ending it here would clear that tab's new\n * session from the shared storage.\n *\n * @param context - The credential and epoch of the attempt that failed.\n * @throws {SessionSupersededError} When that attempt belongs to an ended\n * session, here or in another tab.\n */\n async function onUnauthorized(_response: Response, context: RequestAuthContext): Promise<void> {\n if (context.epoch !== null && context.epoch !== epoch) throw new SessionSupersededError();\n if (!followSharedSession().continues) throw new SessionSupersededError();\n if (staleWithinSession(context)) return;\n if (context.token === null && signedOut()) return;\n await endSession();\n }\n\n /**\n * Authenticate and install the new session.\n *\n * The previous session ends only once the new credentials are accepted: a\n * login that fails leaves it, and the refresh it may have in flight,\n * untouched. When one was live, `onSessionEnd` runs with `\"switch\"` before\n * the new tokens are installed — see the option for why that order. A\n * `logout()` still tearing down is awaited the same way, so the new\n * account's state never starts before the previous one's teardown ended.\n * Never call it from inside `onSessionEnd`: it would wait on that hook.\n * Every `logoutPath` revocation still in flight is awaited before the\n * request leaves — at most {@link LOGOUT_REVOCATION_WAIT_MS} — so a\n * cookie-clearing response cannot land after the new account's cookies.\n *\n * @param credentials - Sent as the login body.\n * @returns The user of the new session, or null.\n * @throws {SessionSupersededError} When a `logout()` or another `login()`\n * moved the session while this one was in flight — during its request,\n * during the teardown or while `mePath` was loading the user; the new\n * tokens, or the user, are discarded.\n */\n async function login(credentials: TCredentials): Promise<TUser | null> {\n if (revocations.size > 0) await settleWithin([...revocations], LOGOUT_REVOCATION_WAIT_MS);\n const run = signIn(credentials);\n loginsInFlight.add(run);\n try {\n return await run;\n } finally {\n loginsInFlight.delete(run);\n }\n }\n\n /**\n * The body of `login()`, tracked in `loginsInFlight` by its caller so a\n * tokenless `401` during the sign-in waits for it and replays with the\n * token it installs, see `awaitLogins`. Tracking starts only after a\n * pending revocation settled.\n *\n * @param credentials - Sent as the login body.\n * @returns The user of the new session, or null.\n * @throws {SessionSupersededError} See `login()`.\n */\n async function signIn(credentials: TCredentials): Promise<TUser | null> {\n const started = epoch;\n const data = await trackCredentialRequest(\n api.post<unknown>(loginPath, {\n body: credentials,\n skipAuth: true,\n }),\n );\n const { token, refreshToken } = parseTokens(data);\n if (epoch !== started) {\n revokeDiscarded(token ?? null, refreshToken);\n throw new SessionSupersededError();\n }\n revokeDiscarded(getToken(), readRefreshToken() ?? undefined);\n const { done } = finish(\"switch\");\n const mine = epoch;\n await done.catch(() => undefined);\n if (epoch !== mine) throw new SessionSupersededError();\n writeLineage(newLineage());\n state().setToken(token);\n writeRefreshToken(refreshToken ?? null);\n const embedded = parseUser?.(data);\n if (embedded != null) {\n state().setUser(embedded);\n return embedded;\n }\n return fetchUser();\n }\n\n /**\n * End the current session, synchronously up to the hook.\n *\n * The epoch moves first, so a refresh resolving between here and the hook\n * is already superseded. The hook only runs when there was a session to\n * end — a token, a stored refresh token or a user — which is what keeps a\n * burst of `401`s from running it once per request.\n *\n * With nothing left to end, the caller waits on the teardown already under\n * way (`ending`, which never rejects, so a later no-op does not inherit an\n * old error). Two callers skip that wait while a hook is running, because\n * they may be the hook itself and would wait on their own teardown forever:\n * `endSession` — a hook awaiting `auth.api` goes out tokenless, gets a\n * `401`, and lands here — and a `logout()` with no session left. `login()`\n * (`\"switch\"`) always waits: the new account's state must not start while\n * the previous one's teardown is still clearing it. A hook awaiting\n * `auth.login()` therefore never settles — misuse the option documents.\n *\n * `\"expired\"` with nothing live leaves the epoch alone: there is nothing to\n * supersede but a refresh with no session behind it — the boot `refresh()`\n * restoring one from the cookie — and moving the epoch would throw that\n * away. `logout()` and `login()` always move it, since they must supersede\n * such a refresh.\n *\n * The returned `done` is a fresh promise derived from the hook, not the one\n * `ending` holds: a `logout()` nobody awaits still surfaces the hook's error\n * as an unhandled rejection instead of losing it.\n *\n * @param reason - Whether the caller signed out, the client gave up, or a\n * login replaced the session.\n * @returns Whether a live session ended here, and a promise settling with\n * the hook.\n */\n function finish(reason: TempestSessionEnd<TUser>[\"reason\"]): {\n live: boolean;\n done: Promise<void>;\n } {\n const { user, token } = state();\n const live = token !== null || user !== null || readRefreshToken() !== null;\n if (live || reason !== \"expired\") nextEpoch();\n state().logout();\n writeRefreshToken(null);\n writeLineage(null);\n if (!live) {\n const wait = reason === \"switch\" || hooksRunning === 0;\n return { live, done: wait ? ending : Promise.resolve() };\n }\n return { live, done: runSessionEnd({ reason, user, token, elsewhere: false }) };\n }\n\n /**\n * Run `onSessionEnd` for a session that just ended, tracked in `ending`\n * and `hooksRunning`.\n *\n * The returned promise is a fresh one derived from the hook, not the one\n * `ending` holds, so a caller that drops it still surfaces the hook's\n * error as an unhandled rejection instead of losing it.\n *\n * @param ended - What ended, minus the client, which is built here.\n * @returns A promise settling with the hook.\n */\n function runSessionEnd(ended: Omit<TempestSessionEnd<TUser>, \"api\">): Promise<void> {\n if (!onSessionEnd) return Promise.resolve();\n const sessionApi = endedSessionApi(ended.token);\n hooksRunning += 1;\n const run = (async () => {\n try {\n await onSessionEnd({ ...ended, api: sessionApi });\n } finally {\n hooksRunning -= 1;\n }\n })();\n ending = run.then(\n () => undefined,\n () => undefined,\n );\n return run.then(() => undefined);\n }\n\n /**\n * Read the session as the shared storage holds it: the persisted\n * `{ user, token }`, the stored refresh token and the session lineage.\n *\n * Read straight from storage rather than through\n * `useAuthStore.persist.rehydrate()`: a rehydration that finds no entry —\n * another tab ran `localStorage.clear()` — merges nothing and keeps the\n * session this tab holds, which is the stale session this exists to drop.\n * An entry that does not parse counts as no session.\n *\n * @returns The stored session, or null without storage (SSR).\n */\n function readSharedSession(): {\n user: TUser | null;\n token: string | null;\n refreshToken: string | null;\n lineage: string | null;\n } | null {\n const s = storageImpl();\n if (!s) return null;\n const refreshToken = s.getItem(refreshKey);\n const lineage = s.getItem(lineageKey);\n const raw = s.getItem(storeName);\n if (!raw) return { user: null, token: null, refreshToken, lineage };\n try {\n const parsed = JSON.parse(raw) as {\n state?: { user?: TUser | null; token?: string | null };\n };\n return {\n user: parsed.state?.user ?? null,\n token: parsed.state?.token ?? null,\n refreshToken,\n lineage,\n };\n } catch {\n return { user: null, token: null, refreshToken, lineage };\n }\n }\n\n /**\n * Catch up with what another tab wrote to the shared storage (#524).\n *\n * Every write this tab makes reaches the storage synchronously, so the\n * stored session differs from the held one only when another tab wrote\n * it. The stored one then wins, without redeeming or clearing anything:\n *\n * - a token of the same **lineage** — the identifier `login()` mints and\n * every rotation keeps — whether it is the same token (the other tab only\n * loaded the user) or a new one (it rotated): adopted, the session\n * continues;\n * - no token, or another lineage: the held session ended elsewhere. The\n * epoch moves, so nothing in flight from it can install tokens or end\n * the new session; the stored state is adopted; and `onSessionEnd` runs\n * with `elsewhere: true` — `\"logout\"` when no session is left,\n * `\"switch\"` when another sign-in is installed, even of the same user.\n *\n * Continuity used to be decided by comparing users, and two sessions whose\n * user had not loaded yet compared equal: a tab whose `login()` was still\n * waiting for `mePath` adopted another account's token as a rotation, and\n * its profile then persisted `{ user: A, token: B }` (#544). Requiring a\n * user would have closed that and turned every rotation of a session\n * without a user model into an account switch; the lineage answers the\n * actual question — is this the same sign-in — for both.\n *\n * A session persisted before lineages existed has none, on either side,\n * and keeps the old rule: the same token, or a token for the same user.\n * No `login()` of this version can produce that state, so a lineage-less\n * session can only have been rotated, never replaced.\n *\n * It never calls `finish()`: that clears the shared keys, which here would\n * erase the other tab's new session — and, with rotating refresh tokens,\n * sign every tab out whenever any tab rotates.\n *\n * The hook's error reaches only the callers that return `done`; the\n * automatic paths drop it.\n *\n * @returns Whether the held session continues, and a promise settling with\n * the hook when one ran.\n */\n function followSharedSession(): { continues: boolean; done: Promise<void> } {\n const shared = readSharedSession();\n const { user, token } = state();\n if (shared === null) return { continues: true, done: Promise.resolve() };\n const sameUser = JSON.stringify(shared.user) === JSON.stringify(user);\n if (\n shared.token === token &&\n shared.refreshToken === heldRefreshToken &&\n shared.lineage === heldLineage &&\n sameUser\n ) {\n return { continues: true, done: Promise.resolve() };\n }\n const sameSession =\n shared.lineage !== null || heldLineage !== null\n ? shared.lineage === heldLineage\n : shared.token === token || (token !== null && sameUser);\n const continues = shared.token !== null && sameSession;\n const live = token !== null || user !== null || heldRefreshToken !== null;\n if (!continues) nextEpoch();\n heldRefreshToken = shared.refreshToken;\n heldLineage = shared.lineage;\n useAuthStore.setState({\n user: shared.user,\n token: shared.token,\n isAuthenticated: shared.token !== null,\n });\n if (continues || !live) return { continues, done: Promise.resolve() };\n const reason = shared.token === null ? \"logout\" : \"switch\";\n const done = runSessionEnd({ reason, user, token, elsewhere: true });\n void done.catch(() => undefined);\n return { continues, done };\n }\n\n /**\n * Send the `logoutPath` revocation for the session about to end, with the\n * credentials it holds now — read before `finish` clears them.\n *\n * Skipped without `logoutPath`, and in body mode when there is nothing to\n * revoke; in cookie mode it always goes out, since the cookie it revokes is\n * invisible to the client. Never rejects.\n *\n * In cookie mode it first waits — at most\n * {@link LOGOUT_REVOCATION_WAIT_MS} — for the login and refresh responses\n * in flight right now: each one sets a cookie for the session this\n * `logout()` ends, and a revocation that left before it would leave that\n * cookie behind.\n *\n * @returns A promise settling when the revocation settled, or null when\n * none was sent.\n */\n function revoke(): Promise<void> | null {\n if (!logoutPath) return null;\n const path = logoutPath;\n const { token, user } = state();\n const refreshToken = readRefreshToken();\n if (!withCredentials && token === null && user === null && refreshToken === null) {\n return null;\n }\n const inFlight = withCredentials ? [...credentialRequests] : [];\n return trackRevocation(async () => {\n await settleWithin(inFlight, LOGOUT_REVOCATION_WAIT_MS);\n return endedSessionApi(token).post<unknown>(path, { body: refreshBody(refreshToken) });\n });\n }\n\n /**\n * End the session — unless another tab already did.\n *\n * A tab that has not caught up yet first follows the shared storage: when\n * the session it showed ended elsewhere, that is the whole sign-out. The\n * session live now belongs to another tab's sign-in, and revoking or\n * clearing it from here would sign that account out — before #524 it\n * also sent that account's refresh token to `logoutPath` next to the\n * ended account's bearer.\n *\n * @returns A promise settling with `onSessionEnd` and the revocation.\n */\n function logout(): Promise<void> {\n const followed = followSharedSession();\n if (!followed.continues) return followed.done;\n const revocation = revoke();\n const { done } = finish(\"logout\");\n if (!revocation) return done;\n return Promise.all([revocation, done]).then(() => undefined);\n }\n\n /**\n * End the session after the client gave up on it and, when `redirectTo`\n * is set, leave the page once `onSessionEnd` settled.\n *\n * Separate from `logout` so an explicit sign-out stays a pure state change:\n * a caller that already navigates itself would otherwise get a second,\n * competing navigation. The hook is awaited before navigating because a\n * hard navigation aborts whatever request it still had in flight. Only the\n * call that ended a live session navigates; the rest of a burst of `401`s\n * waits on the same teardown, or returns at once inside a hook.\n */\n async function endSession(): Promise<void> {\n const { live, done } = finish(\"expired\");\n await done.catch(() => undefined);\n if (live && redirectTo && typeof window !== \"undefined\") {\n window.location.assign(redirectTo);\n }\n }\n\n const api = createApiClient({\n baseURL,\n prefix,\n withCredentials,\n csrf,\n fetcher,\n getToken,\n getSessionEpoch: () => epoch,\n refresh: refreshFor,\n retry,\n onUnauthorized,\n });\n\n const refresh = (): Promise<void> => queue();\n\n /**\n * Read from the store's token and the held lineage. Both move before the\n * store notifies: `login()` writes the lineage before installing the\n * token, `finish()` clears the token in the notifying `logout()` call and\n * the lineage only after — which the token check already reads as `null` —\n * and following another tab updates the held lineage before `setState`.\n * So every store listener sees the key of the state it is told about.\n */\n const sessionKey = (): string | null =>\n getToken() === null ? null : (heldLineage ?? UNLINEAGED_SESSION_KEY);\n const identity: SessionIdentity = {\n current: sessionKey,\n subscribe: (listener) => useAuthStore.subscribe(listener),\n };\n bindSessionIdentity(api, identity);\n bindSessionIdentity(getToken, identity);\n\n /**\n * Follow a write another tab made to this session's keys. The browser\n * fires `storage` only in the tabs that did not write; `key` is null when\n * the other tab cleared the whole storage.\n *\n * @param event - The storage event.\n */\n function onStorage(event: StorageEvent): void {\n if (event.storageArea !== storageImpl()) return;\n if (\n event.key !== null &&\n event.key !== storeName &&\n event.key !== refreshKey &&\n event.key !== lineageKey\n ) {\n return;\n }\n followSharedSession();\n }\n\n const host = typeof window === \"undefined\" ? null : window;\n host?.addEventListener(\"storage\", onStorage);\n const dispose = (): void => host?.removeEventListener(\"storage\", onStorage);\n\n return { useAuthStore, api, login, logout, refresh, getToken, sessionKey, dispose };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-identity.cjs","names":["identities","bindSessionIdentity","target","identity","sessionIdentityOf"],"sources":["../../src/auth/session-identity.ts"],"sourcesContent":["/**\n * The session a credential belongs to, as `createTempestAuth` knows it.\n *\n * `current()` is a key that changes when the signed-in session is replaced —\n * a `login()`, another tab's sign-in, a `logout()` — and stays the same while\n * that session's tokens rotate. `null` means nobody is signed in.\n */\nexport interface SessionIdentity {\n /** The key of the session signed in now, or `null` for none. */\n current: () => string | null;\n /**\n * Call `listener` after every change of the auth state the key is read\n * from. Fires more often than the key changes: compare `current()`.\n *\n * @returns A function that removes the listener.\n */\n subscribe: (listener: () => void) => () => void;\n}\n\n/**\n * The objects `createTempestAuth` handed out — its `api` client and its\n * `getToken` getter — mapped to the session they act for.\n *\n * A `WeakMap` and not a property on the client: nothing is added to the\n * `ApiClient` shape, a preset that is dropped is collected with its client,\n * and an object the preset did not build — a hand-made client, a wrapper, a\n * spread copy — has no entry and keeps the behaviour it had before.\n */\nconst identities = new WeakMap<object, SessionIdentity>();\n\n/**\n * Record that `target` acts for the session `identity` describes.\n *\n * @param target - An object `createTempestAuth` returns.\n * @param identity - The session it belongs to.\n */\nexport function bindSessionIdentity(target: object, identity: SessionIdentity): void {\n identities.set(target, identity);\n}\n\n/**\n * The session `target` acts for, when `createTempestAuth` built it.\n *\n * @param target - A client or getter received from the app.\n * @returns The session identity, or `undefined` for anything else.\n */\nexport function sessionIdentityOf(target: unknown): SessionIdentity | undefined {\n if ((typeof target !== \"object\" && typeof target !== \"function\") || target === null) {\n return undefined;\n }\n return identities.get(target);\n}\n"],"mappings":"AA4BA,IAAMA,EAAa,IAAI,QAQvB,SAAgBC,EAAoB,EAAgB,EAAiC,CACjFD,EAAW,IAAIE,EAAQC,CAAQ,CACnC,CAQA,SAAgBC,EAAkB,EAA8C,CAC5E,GAAK,SAAOF,GAAW,UAAY,OAAOA,GAAW,YAAeA,IAAW,MAG/E,OAAOF,EAAW,IAAIE,CAAM,CAChC,C"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/auth/session-identity.ts
|
|
2
|
+
var e = /* @__PURE__ */ new WeakMap();
|
|
3
|
+
function t(t, n) {
|
|
4
|
+
e.set(t, n);
|
|
5
|
+
}
|
|
6
|
+
function n(t) {
|
|
7
|
+
if (!(typeof t != "object" && typeof t != "function" || t === null)) return e.get(t);
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
export { t as bindSessionIdentity, n as sessionIdentityOf };
|
|
11
|
+
|
|
12
|
+
//# sourceMappingURL=session-identity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-identity.js","names":["identities","bindSessionIdentity","target","identity","sessionIdentityOf"],"sources":["../../src/auth/session-identity.ts"],"sourcesContent":["/**\n * The session a credential belongs to, as `createTempestAuth` knows it.\n *\n * `current()` is a key that changes when the signed-in session is replaced —\n * a `login()`, another tab's sign-in, a `logout()` — and stays the same while\n * that session's tokens rotate. `null` means nobody is signed in.\n */\nexport interface SessionIdentity {\n /** The key of the session signed in now, or `null` for none. */\n current: () => string | null;\n /**\n * Call `listener` after every change of the auth state the key is read\n * from. Fires more often than the key changes: compare `current()`.\n *\n * @returns A function that removes the listener.\n */\n subscribe: (listener: () => void) => () => void;\n}\n\n/**\n * The objects `createTempestAuth` handed out — its `api` client and its\n * `getToken` getter — mapped to the session they act for.\n *\n * A `WeakMap` and not a property on the client: nothing is added to the\n * `ApiClient` shape, a preset that is dropped is collected with its client,\n * and an object the preset did not build — a hand-made client, a wrapper, a\n * spread copy — has no entry and keeps the behaviour it had before.\n */\nconst identities = new WeakMap<object, SessionIdentity>();\n\n/**\n * Record that `target` acts for the session `identity` describes.\n *\n * @param target - An object `createTempestAuth` returns.\n * @param identity - The session it belongs to.\n */\nexport function bindSessionIdentity(target: object, identity: SessionIdentity): void {\n identities.set(target, identity);\n}\n\n/**\n * The session `target` acts for, when `createTempestAuth` built it.\n *\n * @param target - A client or getter received from the app.\n * @returns The session identity, or `undefined` for anything else.\n */\nexport function sessionIdentityOf(target: unknown): SessionIdentity | undefined {\n if ((typeof target !== \"object\" && typeof target !== \"function\") || target === null) {\n return undefined;\n }\n return identities.get(target);\n}\n"],"mappings":";AA4BA,IAAMA,oBAAa,IAAI,QAAiC;AAQxD,SAAgBC,EAAoB,GAAgB,GAAiC;CACjF,EAAW,IAAIC,GAAQC,CAAQ;AACnC;AAQA,SAAgBC,EAAkB,GAA8C;CAC5E,IAAK,SAAOF,KAAW,YAAY,OAAOA,KAAW,cAAeA,MAAW,OAG/E,OAAOF,EAAW,IAAIE,CAAM;AAChC"}
|
package/dist/br/boleto.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var e=class extends Error{constructor(e){super(e),this.name=`BoletoError`}},t=Date.UTC(1997,9,7),n=Date.UTC(2022,4,29),r=864e5,i=1e3,a=9999,o=[2,1],s=2,c={1:`Prefeituras`,2:`Saneamento`,3:`Energia elétrica e gás`,4:`Telecomunicações`,5:`Órgãos governamentais`,6:`Carnes e assemelhados ou empresas identificadas por CNPJ`,7:`Multas de trânsito`,9:`Uso exclusivo do banco`},l=6;function u(e){return e.replace(/\D/g,``)}function d(t){if(!/^\d+$/.test(t))throw new e(`mod10Dac needs digits, got ${JSON.stringify(t)}.`);let n=0;for(let e=0;e<t.length;e+=1){let r=t.length-1-e,i=Number(t[e])*o[r%o.length];n+=i>9?i-9:i}let r=n%10;return r===0?0:10-r}function f(e){let t=0;for(let n=0;n<e.length;n+=1){let r=e.length-1-n;t+=Number(e[n])*(s+r%8)}return t}function p(t){if(!/^\d+$/.test(t))throw new e(`mod11DacCobranca needs digits, got ${JSON.stringify(t)}.`);let n=f(t)%11;return n===0||n===1||n===10?1:11-n}function m(t){if(!/^\d+$/.test(t))throw new e(`mod11DacArrecadacao needs digits, got ${JSON.stringify(t)}.`);let n=f(t)%11;return n===0||n===1?0:11-n}function h(e){let t=u(e);return t.length===47
|
|
1
|
+
var e=class extends Error{constructor(e){super(e),this.name=`BoletoError`}},t=Date.UTC(1997,9,7),n=Date.UTC(2022,4,29),r=864e5,i=1e3,a=9999,o=[2,1],s=2,c={1:`Prefeituras`,2:`Saneamento`,3:`Energia elétrica e gás`,4:`Telecomunicações`,5:`Órgãos governamentais`,6:`Carnes e assemelhados ou empresas identificadas por CNPJ`,7:`Multas de trânsito`,9:`Uso exclusivo do banco`},l=6;function u(e){return e.replace(/\D/g,``)}function d(t){if(!/^\d+$/.test(t))throw new e(`mod10Dac needs digits, got ${JSON.stringify(t)}.`);let n=0;for(let e=0;e<t.length;e+=1){let r=t.length-1-e,i=Number(t[e])*o[r%o.length];n+=i>9?i-9:i}let r=n%10;return r===0?0:10-r}function f(e){let t=0;for(let n=0;n<e.length;n+=1){let r=e.length-1-n;t+=Number(e[n])*(s+r%8)}return t}function p(t){if(!/^\d+$/.test(t))throw new e(`mod11DacCobranca needs digits, got ${JSON.stringify(t)}.`);let n=f(t)%11;return n===0||n===1||n===10?1:11-n}function m(t){if(!/^\d+$/.test(t))throw new e(`mod11DacArrecadacao needs digits, got ${JSON.stringify(t)}.`);let n=f(t)%11;return n===0||n===1?0:11-n}function h(e){let t=u(e);return t.length===47?t.startsWith(`8`)?null:`banco`:t.length===48?t.startsWith(`8`)?`arrecadacao`:null:t.length===44?t.startsWith(`8`)?`arrecadacao`:`banco`:null}function g(e,t){let n=new Date(e+t*r);return new Date(n.getUTCFullYear(),n.getUTCMonth(),n.getUTCDate())}function _(e,r={}){if(e===0)return null;let{epoch:a=`auto`,reference:o=new Date}=r;if(a===`legacy`)return{date:g(t,e),epoch:`legacy`};if(a===`current`)return{date:g(n,e),epoch:`current`};let s=g(t,e);if(e<i)return{date:s,epoch:`legacy`};let c=g(n,e),l=o.getTime();return Math.abs(c.getTime()-l)<=Math.abs(s.getTime()-l)?{date:c,epoch:`current`}:{date:s,epoch:`legacy`}}function v(o,s=`current`){let c=s===`legacy`?t:n,l=Date.UTC(o.getFullYear(),o.getMonth(),o.getDate()),u=Math.round((l-c)/r);if(u<i||u>a)throw new e(`${o.toISOString().slice(0,10)} is fator ${u} under the "${s}" epoch, outside the ${i}-${a} the field holds.`);return u}function y(e){let t=e.slice(0,8),n=Number(t.slice(0,4)),r=Number(t.slice(4,6)),i=Number(t.slice(6,8));if(n<1997||n>2099||r<1||r>12||i<1||i>31)return null;let a=new Date(n,r-1,i);return a.getMonth()===r-1&&a.getDate()===i?a:null}function b(e){let t=e.slice(19),n=e.slice(0,4)+t.slice(0,5),r=t.slice(5,15),i=t.slice(15,25);return n+d(n)+r+d(r)+i+d(i)+e.slice(4,5)+e.slice(5,19)}function x(e){let t=S(e)===11?m:d,n=``;for(let r=0;r<44;r+=11){let i=e.slice(r,r+11);n+=i+t(i)}return n}function S(e){return e[2]===`8`||e[2]===`9`?11:10}function C(e,t){let n=Number(e.slice(5,9)),r=_(n,t),i=e.slice(3,4);return{kind:`banco`,codigoBarras:e,linhaDigitavel:b(e),banco:e.slice(0,3),moeda:i,moedaLabel:i===`9`?`Real`:null,dv:e.slice(4,5),fatorVencimento:n,vencimento:r?.date??null,vencimentoEpoch:r?.epoch??null,valor:Number(e.slice(9,19))/100,campoLivre:e.slice(19)}}function w(e){let t=Number(e[1]),n=Number(e[2]),r=t===l,i=r?e.slice(23):e.slice(19),a=e.slice(4,15),o=n===6||n===8;return{kind:`arrecadacao`,codigoBarras:e,linhaDigitavel:x(e),segmento:t,segmentoLabel:c[t]??null,identificacaoValor:n,dvModulo:S(e),dv:e.slice(3,4),valor:o?Number(a)/100:null,valorRaw:a,empresa:r?e.slice(15,23):e.slice(15,19),empresaIsCnpj:r,campoLivre:i,vencimentoCampoLivre:y(i)}}function T(t){if(t.startsWith(`8`)){let n=t.slice(0,3)+t.slice(4),r=S(t)===11?m(n):d(n);if(String(r)!==t[3])throw new e(`Arrecadação check digit is ${t[3]}, recomputed ${r} (módulo ${S(t)}).`);return}let n=p(t.slice(0,4)+t.slice(5));if(String(n)!==t[4])throw new e(`Barcode check digit is ${t[4]}, recomputed ${n}.`)}function E(t){if(!`6789`.includes(t[2]))throw new e(`Position 3 of an arrecadação barcode must be 6, 7, 8 or 9 (identificação do valor), got ${t[2]}.`)}function D(t,n={}){let r=u(t);if(r.length!==44)throw new e(`A barcode has 44 digits, got ${r.length}. A 47- or 48-digit string is a typed line — use parseLinhaDigitavel.`);return r.startsWith(`8`)?(E(r),T(r),w(r)):(T(r),C(r,n))}function O(t,n={}){let r=u(t);if(r.length===47)return D(k(r),n);if(r.length===48)return D(A(r),n);throw new e(`A typed line has 47 digits (bank) or 48 (arrecadação), got ${r.length}. A 44-digit string is a barcode — use parseCodigoBarras.`)}function k(t){if(t.startsWith(`8`))throw new e(`A 47-digit typed line is a bank boleto and cannot start with 8: its barcode would read as an arrecadação slip.`);return[[t.slice(0,9),t.slice(9,10)],[t.slice(10,20),t.slice(20,21)],[t.slice(21,31),t.slice(31,32)]].forEach(([t,n],r)=>{let i=d(t);if(String(i)!==n)throw new e(`Field ${r+1} check digit is ${n}, recomputed ${i} (módulo 10).`)}),t.slice(0,4)+t.slice(32,33)+t.slice(33,47)+t.slice(4,9)+t.slice(10,20)+t.slice(21,31)}function A(t){let n=[0,12,24,36].map(e=>({body:t.slice(e,e+11),dv:t.slice(e+11,e+12)})),r=n.map(e=>e.body).join(``);if(!r.startsWith(`8`))throw new e(`A 48-digit typed line is an arrecadação slip and must start with 8, got ${r[0]}.`);E(r);let i=S(r)===11?m:d;return n.forEach((t,n)=>{let a=i(t.body);if(String(a)!==t.dv)throw new e(`Block ${n+1} check digit is ${t.dv}, recomputed ${a} (módulo ${S(r)}).`)}),r}function j(e){return O(e).codigoBarras}function M(e){return D(e).linhaDigitavel}function N(e){try{let t=u(e);return t.length===44?D(t):O(t),!0}catch{return!1}}function P(e){let t=u(e);return t.length===47?[`${t.slice(0,5)}.${t.slice(5,10)}`,`${t.slice(10,15)}.${t.slice(15,21)}`,`${t.slice(21,26)}.${t.slice(26,32)}`,t.slice(32,33),t.slice(33)].join(` `):t.length===48?[0,12,24,36].map(e=>t.slice(e,e+12)).join(` `):e}exports.BoletoError=e,exports.boletoDueDate=_,exports.boletoKind=h,exports.codigoBarrasToLinhaDigitavel=M,exports.fatorVencimento=v,exports.formatLinhaDigitavel=P,exports.linhaDigitavelToCodigoBarras=j,exports.mod10Dac=d,exports.mod11DacArrecadacao=m,exports.mod11DacCobranca=p,exports.parseCodigoBarras=D,exports.parseLinhaDigitavel=O,exports.validateBoleto=N;
|
|
2
2
|
//# sourceMappingURL=boleto.cjs.map
|
package/dist/br/boleto.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"boleto.cjs","names":["BoletoError","message","LEGACY_EPOCH_UTC","CURRENT_EPOCH_UTC","MS_PER_DAY","FATOR_MIN","FATOR_MAX","MOD10_WEIGHTS","MOD11_WEIGHT_FIRST","SEGMENTO_LABELS","SEGMENTO_CNPJ","digits","value","mod10Dac","sum","index","fromRight","product","remainder","mod11Sum","mod11DacCobranca","mod11DacArrecadacao","boletoKind","raw","dayAfter","utc","epochUtcMs","days","boletoDueDate","fator","options","epoch","legacy","current","at","reference","fatorVencimento","base","target","date","campoLivreDate","head","campoLivre","year","month","day","bancoLinha","barcode","field1","field2","field3","arrecadacaoLinha","dac","mod11Selector","line","start","block","readBanco","due","moeda","readArrecadacao","segmento","identificacaoValor","empresaIsCnpj","valorRaw","isMoney","assertBarcodeDv","body","assertIdentificacaoValor","parseCodigoBarras","parseLinhaDigitavel","bancoBarcode","arrecadacaoBarcode","dv","blocks","expected","linhaDigitavelToCodigoBarras","codigoBarrasToLinhaDigitavel","validateBoleto","formatLinhaDigitavel"],"sources":["../../src/br/boleto.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — the FEBRABAN spec in one file: the 47-digit linha\n * digitável, the 44-digit barcode, the two layouts (bank slips and arrecadação),\n * modulo-10 and modulo-11 check digits, the base date the due date counts from, and\n * the value scaling. Every piece cross-checks another — the conversion between the\n * two forms is what proves the check digits — so splitting it hides the one property\n * the file exists to guarantee.\n */\n/**\n * A boleto string could not be read, or failed a check digit.\n *\n * Its own class so a scanner screen can tell \"this is not a boleto\" apart from a\n * bug, and so the message can be shown to the operator as-is.\n */\nexport class BoletoError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"BoletoError\";\n }\n}\n\n/**\n * The two incompatible layouts that share the 44-digit barcode.\n *\n * `\"banco\"` is the cobrança boleto every bank issues; `\"arrecadacao\"` is the\n * concessionária/tributo slip, which starts with `8` and lays out its 44 digits\n * completely differently — same length, different meaning for every field.\n */\nexport type BoletoKind = \"banco\" | \"arrecadacao\";\n\n/** Which base date the fator de vencimento counts from. See {@link boletoDueDate}. */\nexport type BoletoEpoch = \"auto\" | \"legacy\" | \"current\";\n\n/** Options shared by every parser here. */\nexport interface BoletoOptions {\n /** Fator de vencimento epoch. Default `\"auto\"`. */\n epoch?: BoletoEpoch;\n /** Date `\"auto\"` measures proximity against. Default `new Date()`. */\n reference?: Date;\n}\n\n/** A cobrança boleto — the kind a bank issues against an invoice. */\nexport interface BoletoBanco {\n kind: \"banco\";\n /** 44 digits. */\n codigoBarras: string;\n /** 47 digits. */\n linhaDigitavel: string;\n /** 3-digit bank code in the clearing house, e.g. `\"341\"`. */\n banco: string;\n /** 1 digit. `\"9\"` is BRL; nothing else is in use. */\n moeda: string;\n /** `\"Real\"` for `\"9\"`, `null` for anything else. */\n moedaLabel: string | null;\n /** The general check digit, position 5 of the barcode. */\n dv: string;\n /** Raw 4-digit field. `0` means the boleto carries no due date. */\n fatorVencimento: number;\n /** Due date, or `null` when the fator is `0`. */\n vencimento: Date | null;\n /** Which epoch {@link vencimento} was resolved under. `null` when there is none. */\n vencimentoEpoch: Exclude<BoletoEpoch, \"auto\"> | null;\n /** Reais. `0` when the issuer left the amount for the payer to fill in. */\n valor: number;\n /** 25 digits the issuing bank defines. Not interpretable without its manual. */\n campoLivre: string;\n}\n\n/** An arrecadação/convênio slip — utilities, taxes, traffic fines. */\nexport interface BoletoArrecadacao {\n kind: \"arrecadacao\";\n /** 44 digits, always starting with `8`. */\n codigoBarras: string;\n /** 48 digits, in four blocks of twelve. */\n linhaDigitavel: string;\n /** Position 2. See {@link segmentoLabel}. */\n segmento: number;\n /** Human label, or `null` for a value the layout does not define. */\n segmentoLabel: string | null;\n /** Position 3: `6`/`8` mean real money, `7`/`9` mean a reference quantity. */\n identificacaoValor: number;\n /** Which modulo position 3 selects for the general check digit. */\n dvModulo: 10 | 11;\n /** The general check digit, position 4. */\n dv: string;\n /** Reais, or `null` when position 3 says the field is a reference, not money. */\n valor: number | null;\n /** The raw 11-digit value field, useful when {@link valor} is `null`. */\n valorRaw: string;\n /**\n * Positions 16-19 — the 4-digit code FEBRABAN assigns the company — or, on\n * segmento 6, positions 16-23, which are the first eight CNPJ digits.\n */\n empresa: string;\n /** `true` when {@link empresa} is a CNPJ prefix rather than a FEBRABAN code. */\n empresaIsCnpj: boolean;\n /** 25 digits, or 21 when the CNPJ took four of them. Issuer-defined. */\n campoLivre: string;\n /**\n * Due date read from the first eight digits of the campo livre.\n *\n * The layout says a due date, **if present**, must sit there as `AAAAMMDD` —\n * but the field is optional and nothing marks its presence, so a campo livre\n * that merely looks like a date lands here too. Treat it as a hint for a UI,\n * never as the date a payment settles against.\n */\n vencimentoCampoLivre: Date | null;\n}\n\n/** What the two parsers return. Narrow on `kind`. */\nexport type Boleto = BoletoBanco | BoletoArrecadacao;\n\n/** Base date of the fator de vencimento as FEBRABAN defined it in 1997. */\nconst LEGACY_EPOCH_UTC = Date.UTC(1997, 9, 7);\n\n/**\n * Base date the fator de vencimento restarted from.\n *\n * FEBRABAN communication FB-009/2023: the 4-digit field hit its ceiling of 9999\n * on 2025-02-21 (1997-10-07 + 9999 days), so from 2025-02-22 the counter restarts\n * at 1000 against a new base of 2022-05-29 (2022-05-29 + 1000 days = 2025-02-22).\n */\nconst CURRENT_EPOCH_UTC = Date.UTC(2022, 4, 29);\n\nconst MS_PER_DAY = 86_400_000;\n\n/** First fator FEBRABAN ever put in circulation, in either epoch. */\nconst FATOR_MIN = 1000;\n\n/** Last fator the 4-digit field can hold. */\nconst FATOR_MAX = 9999;\n\nconst MOD10_WEIGHTS = [2, 1] as const;\nconst MOD11_WEIGHT_FIRST = 2;\nconst MOD11_WEIGHT_LAST = 9;\n\nconst SEGMENTO_LABELS: Record<number, string> = {\n 1: \"Prefeituras\",\n 2: \"Saneamento\",\n 3: \"Energia elétrica e gás\",\n 4: \"Telecomunicações\",\n 5: \"Órgãos governamentais\",\n 6: \"Carnes e assemelhados ou empresas identificadas por CNPJ\",\n 7: \"Multas de trânsito\",\n 9: \"Uso exclusivo do banco\",\n};\n\n/** Segmento whose company identifier is a CNPJ prefix instead of a FEBRABAN code. */\nconst SEGMENTO_CNPJ = 6;\n\n/** Digits of a value that may carry the usual dots, spaces and dashes. */\nfunction digits(value: string): string {\n return value.replace(/\\D/g, \"\");\n}\n\n/**\n * DAC módulo 10, FEBRABAN flavour.\n *\n * Multipliers cycle `2, 1, 2, 1, …` from right to left; the **digits** of each\n * product are summed individually (so `6 × 2 = 12` contributes `1 + 2`); the DAC\n * is `10 - (sum mod 10)`, and `0` when that remainder is `0`.\n *\n * Ported from \"Layout Padrão de Arrecadação/Recebimento com Utilização do Código\n * de Barras\", FEBRABAN version 07 (effective 2023-03-01), section 07 — whose own\n * worked example gives `01230067896 → 3`.\n *\n * @param value - Digits only.\n * @returns The check digit, 0-9.\n * @throws {BoletoError} When `value` is empty or holds a non-digit.\n */\nexport function mod10Dac(value: string): number {\n if (!/^\\d+$/.test(value))\n throw new BoletoError(`mod10Dac needs digits, got ${JSON.stringify(value)}.`);\n let sum = 0;\n for (let index = 0; index < value.length; index += 1) {\n const fromRight = value.length - 1 - index;\n const product = Number(value[index]) * MOD10_WEIGHTS[fromRight % MOD10_WEIGHTS.length]!;\n sum += product > 9 ? product - 9 : product;\n }\n const remainder = sum % 10;\n return remainder === 0 ? 0 : 10 - remainder;\n}\n\n/** Sum of `digit × weight` with weights cycling 2…9 from the right. */\nfunction mod11Sum(value: string): number {\n const span = MOD11_WEIGHT_LAST - MOD11_WEIGHT_FIRST + 1;\n let sum = 0;\n for (let index = 0; index < value.length; index += 1) {\n const fromRight = value.length - 1 - index;\n sum += Number(value[index]) * (MOD11_WEIGHT_FIRST + (fromRight % span));\n }\n return sum;\n}\n\n/**\n * DAC módulo 11 for a **cobrança** barcode — position 5 of a bank boleto.\n *\n * Weights cycle `2…9` from right to left, the products are summed whole, and the\n * DAC is `11 - (sum mod 11)`. A remainder of `0`, `1` or `10` would put `11`, `10`\n * or `1` in a one-digit field, and FEBRABAN resolves all three to **`1`**.\n *\n * That last rule is where cobrança and arrecadação disagree — see\n * {@link mod11DacArrecadacao}, which resolves the same remainders to `0`. Using\n * one flavour on the other layout produces a check digit that is wrong exactly\n * 3 times in 11, which is why they are separate functions here.\n *\n * @param value - The 43 digits of the barcode with position 5 removed.\n * @returns The check digit, 1-9.\n * @throws {BoletoError} When `value` is empty or holds a non-digit.\n */\nexport function mod11DacCobranca(value: string): number {\n if (!/^\\d+$/.test(value)) {\n throw new BoletoError(`mod11DacCobranca needs digits, got ${JSON.stringify(value)}.`);\n }\n const remainder = mod11Sum(value) % 11;\n return remainder === 0 || remainder === 1 || remainder === 10 ? 1 : 11 - remainder;\n}\n\n/**\n * DAC módulo 11 for an **arrecadação** barcode — position 4 of a `8…` slip.\n *\n * Same weights and same subtraction as {@link mod11DacCobranca}, but a remainder\n * of `0` or `1` resolves to **`0`**.\n *\n * Ported from the FEBRABAN version 07 layout, section 10, and pinned by that\n * document's own worked example: the 43-digit sequence\n * `8220000215048200974123220154098290108605940` sums to 705, `705 mod 11 = 1`,\n * and the barcode the spec prints carries `0` in position 4.\n *\n * @param value - The 43 digits of the barcode with position 4 removed.\n * @returns The check digit, 0 or 2-9.\n * @throws {BoletoError} When `value` is empty or holds a non-digit.\n */\nexport function mod11DacArrecadacao(value: string): number {\n if (!/^\\d+$/.test(value)) {\n throw new BoletoError(`mod11DacArrecadacao needs digits, got ${JSON.stringify(value)}.`);\n }\n const remainder = mod11Sum(value) % 11;\n return remainder === 0 || remainder === 1 ? 0 : 11 - remainder;\n}\n\n/**\n * Which layout a string is in, without throwing.\n *\n * @param value - A barcode or typed line, masked or not.\n * @returns The layout, or `null` when the length is not 44, 47 or 48.\n */\nexport function boletoKind(value: string): BoletoKind | null {\n const raw = digits(value);\n if (raw.length === 47) return \"banco\";\n if (raw.length === 48) return \"arrecadacao\";\n if (raw.length === 44) return raw.startsWith(\"8\") ? \"arrecadacao\" : \"banco\";\n return null;\n}\n\n/** Local midnight of the calendar day `days` after a UTC epoch. */\nfunction dayAfter(epochUtcMs: number, days: number): Date {\n const utc = new Date(epochUtcMs + days * MS_PER_DAY);\n return new Date(utc.getUTCFullYear(), utc.getUTCMonth(), utc.getUTCDate());\n}\n\n/**\n * Resolve a fator de vencimento to a calendar date.\n *\n * The field is four digits of days since a base date, and it has had **two** base\n * dates: 1997-10-07 until the counter saturated at 9999 on 2025-02-21, then\n * 2022-05-29 from 2025-02-22, when FEBRABAN restarted it at 1000.\n *\n * !!! danger \"The two epochs are genuinely ambiguous\"\n * Every fator from 1000 to 9999 has a reading under each base — 1997-10-07\n * gives a date in `2000-07-03 … 2025-02-21`, 2022-05-29 gives one in\n * `2025-02-22 … 2049-10-14`. Nothing in the barcode says which. `\"auto\"`\n * picks whichever lands nearer `reference`, which is right for the case that\n * matters (a slip being paid now) and wrong for an archive sweep. Pass\n * `\"legacy\"` or `\"current\"` when you know.\n *\n * @param fator - The raw 4-digit field as a number. `0` means \"no due date\".\n * @param options - Epoch selection. Default `\"auto\"` against `new Date()`.\n * @returns Local midnight of the due date, or `null` when `fator` is `0`.\n *\n * @example\n * boletoDueDate(1000, { epoch: \"legacy\" }); // 2000-07-03\n * boletoDueDate(1000, { epoch: \"current\" }); // 2025-02-22\n */\nexport function boletoDueDate(\n fator: number,\n options: BoletoOptions = {},\n): { date: Date; epoch: Exclude<BoletoEpoch, \"auto\"> } | null {\n if (fator === 0) return null;\n const { epoch = \"auto\", reference = new Date() } = options;\n\n if (epoch === \"legacy\") return { date: dayAfter(LEGACY_EPOCH_UTC, fator), epoch: \"legacy\" };\n if (epoch === \"current\") return { date: dayAfter(CURRENT_EPOCH_UTC, fator), epoch: \"current\" };\n\n const legacy = dayAfter(LEGACY_EPOCH_UTC, fator);\n if (fator < FATOR_MIN) return { date: legacy, epoch: \"legacy\" };\n const current = dayAfter(CURRENT_EPOCH_UTC, fator);\n const at = reference.getTime();\n return Math.abs(current.getTime() - at) <= Math.abs(legacy.getTime() - at)\n ? { date: current, epoch: \"current\" }\n : { date: legacy, epoch: \"legacy\" };\n}\n\n/**\n * Invert {@link boletoDueDate}: the fator that encodes a due date.\n *\n * @param date - The due date. Only its local calendar day is used.\n * @param epoch - Base date to count from. Default `\"current\"`, the epoch in force\n * since 2025-02-22 and therefore the one a boleto issued today must use.\n * @returns The 4-digit fator.\n * @throws {BoletoError} When the date falls outside the 1000-9999 window of that\n * epoch, because no fator can represent it.\n *\n * @example\n * fatorVencimento(new Date(2025, 1, 22)); // 1000\n */\nexport function fatorVencimento(\n date: Date,\n epoch: Exclude<BoletoEpoch, \"auto\"> = \"current\",\n): number {\n const base = epoch === \"legacy\" ? LEGACY_EPOCH_UTC : CURRENT_EPOCH_UTC;\n const target = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate());\n const fator = Math.round((target - base) / MS_PER_DAY);\n if (fator < FATOR_MIN || fator > FATOR_MAX) {\n throw new BoletoError(\n `${date.toISOString().slice(0, 10)} is fator ${fator} under the \"${epoch}\" epoch, ` +\n `outside the ${FATOR_MIN}-${FATOR_MAX} the field holds.`,\n );\n }\n return fator;\n}\n\n/**\n * `AAAAMMDD` at the head of an arrecadação campo livre, when it reads as a date.\n *\n * The caller only ever passes a slice of a validated barcode, so the head is\n * always eight digits; what is not guaranteed is that those digits are a date, and\n * `20260231` has to come back `null` rather than roll into March.\n */\nfunction campoLivreDate(campoLivre: string): Date | null {\n const head = campoLivre.slice(0, 8);\n const year = Number(head.slice(0, 4));\n const month = Number(head.slice(4, 6));\n const day = Number(head.slice(6, 8));\n if (year < 1997 || year > 2099 || month < 1 || month > 12 || day < 1 || day > 31) return null;\n const date = new Date(year, month - 1, day);\n return date.getMonth() === month - 1 && date.getDate() === day ? date : null;\n}\n\n/** Assemble the 47-digit typed line from a validated 44-digit bank barcode. */\nfunction bancoLinha(barcode: string): string {\n const campoLivre = barcode.slice(19);\n const field1 = barcode.slice(0, 4) + campoLivre.slice(0, 5);\n const field2 = campoLivre.slice(5, 15);\n const field3 = campoLivre.slice(15, 25);\n return (\n field1 +\n mod10Dac(field1) +\n field2 +\n mod10Dac(field2) +\n field3 +\n mod10Dac(field3) +\n barcode.slice(4, 5) +\n barcode.slice(5, 19)\n );\n}\n\n/** Assemble the 48-digit typed line from a validated 44-digit arrecadação barcode. */\nfunction arrecadacaoLinha(barcode: string): string {\n const dac = mod11Selector(barcode) === 11 ? mod11DacArrecadacao : mod10Dac;\n let line = \"\";\n for (let start = 0; start < 44; start += 11) {\n const block = barcode.slice(start, start + 11);\n line += block + dac(block);\n }\n return line;\n}\n\n/** Which modulo position 3 of an arrecadação barcode selects. */\nfunction mod11Selector(barcode: string): 10 | 11 {\n return barcode[2] === \"8\" || barcode[2] === \"9\" ? 11 : 10;\n}\n\n/** Read a 44-digit bank barcode, whose general DV has already been checked. */\nfunction readBanco(barcode: string, options: BoletoOptions): BoletoBanco {\n const fator = Number(barcode.slice(5, 9));\n const due = boletoDueDate(fator, options);\n const moeda = barcode.slice(3, 4);\n return {\n kind: \"banco\",\n codigoBarras: barcode,\n linhaDigitavel: bancoLinha(barcode),\n banco: barcode.slice(0, 3),\n moeda,\n moedaLabel: moeda === \"9\" ? \"Real\" : null,\n dv: barcode.slice(4, 5),\n fatorVencimento: fator,\n vencimento: due?.date ?? null,\n vencimentoEpoch: due?.epoch ?? null,\n valor: Number(barcode.slice(9, 19)) / 100,\n campoLivre: barcode.slice(19),\n };\n}\n\n/** Read a 44-digit arrecadação barcode, whose general DV has already been checked. */\nfunction readArrecadacao(barcode: string): BoletoArrecadacao {\n const segmento = Number(barcode[1]);\n const identificacaoValor = Number(barcode[2]);\n const empresaIsCnpj = segmento === SEGMENTO_CNPJ;\n const campoLivre = empresaIsCnpj ? barcode.slice(23) : barcode.slice(19);\n const valorRaw = barcode.slice(4, 15);\n const isMoney = identificacaoValor === 6 || identificacaoValor === 8;\n return {\n kind: \"arrecadacao\",\n codigoBarras: barcode,\n linhaDigitavel: arrecadacaoLinha(barcode),\n segmento,\n segmentoLabel: SEGMENTO_LABELS[segmento] ?? null,\n identificacaoValor,\n dvModulo: mod11Selector(barcode),\n dv: barcode.slice(3, 4),\n valor: isMoney ? Number(valorRaw) / 100 : null,\n valorRaw,\n empresa: empresaIsCnpj ? barcode.slice(15, 23) : barcode.slice(15, 19),\n empresaIsCnpj,\n campoLivre,\n vencimentoCampoLivre: campoLivreDate(campoLivre),\n };\n}\n\n/** Recompute and compare the general check digit of a 44-digit barcode. */\nfunction assertBarcodeDv(barcode: string): void {\n if (barcode.startsWith(\"8\")) {\n const body = barcode.slice(0, 3) + barcode.slice(4);\n const dac = mod11Selector(barcode) === 11 ? mod11DacArrecadacao(body) : mod10Dac(body);\n if (String(dac) !== barcode[3]) {\n throw new BoletoError(\n `Arrecadação check digit is ${barcode[3]}, recomputed ${dac} (módulo ${mod11Selector(barcode)}).`,\n );\n }\n return;\n }\n const dac = mod11DacCobranca(barcode.slice(0, 4) + barcode.slice(5));\n if (String(dac) !== barcode[4]) {\n throw new BoletoError(`Barcode check digit is ${barcode[4]}, recomputed ${dac}.`);\n }\n}\n\n/** Reject an arrecadação barcode whose position 3 is not one of 6, 7, 8, 9. */\nfunction assertIdentificacaoValor(barcode: string): void {\n if (!\"6789\".includes(barcode[2]!)) {\n throw new BoletoError(\n `Position 3 of an arrecadação barcode must be 6, 7, 8 or 9 (identificação do valor), got ${barcode[2]}.`,\n );\n }\n}\n\n/**\n * Read a 44-digit barcode — either layout — into its fields.\n *\n * The first digit picks the layout: `8` is an arrecadação/convênio slip, anything\n * else is a cobrança boleto. They are **not** variants of one format; every field\n * moves. Narrow the result on `kind` before touching it.\n *\n * @param value - 44 digits. Spaces and punctuation are ignored.\n * @param options - Fator de vencimento epoch. See {@link boletoDueDate}.\n * @returns The parsed boleto, with the matching typed line filled in.\n * @throws {BoletoError} On a length other than 44, on a general check digit that\n * does not recompute, or on an arrecadação slip whose position 3 is out of spec.\n *\n * @example\n * const boleto = parseCodigoBarras(scanned);\n * if (boleto.kind === \"banco\") console.log(boleto.valor, boleto.vencimento);\n */\nexport function parseCodigoBarras(value: string, options: BoletoOptions = {}): Boleto {\n const barcode = digits(value);\n if (barcode.length !== 44) {\n throw new BoletoError(\n `A barcode has 44 digits, got ${barcode.length}. ` +\n \"A 47- or 48-digit string is a typed line — use parseLinhaDigitavel.\",\n );\n }\n if (barcode.startsWith(\"8\")) {\n assertIdentificacaoValor(barcode);\n assertBarcodeDv(barcode);\n return readArrecadacao(barcode);\n }\n assertBarcodeDv(barcode);\n return readBanco(barcode, options);\n}\n\n/**\n * Read a typed line — 47 digits for a bank boleto, 48 for an arrecadação slip.\n *\n * Both layouts interleave check digits with the data, so the function rebuilds the\n * 44-digit barcode as it goes and every DV is verified: the three (bank) or four\n * (arrecadação) block digits, plus the general one.\n *\n * @param value - 47 or 48 digits. The usual `.`, ` ` and `-` are ignored.\n * @param options - Fator de vencimento epoch. See {@link boletoDueDate}.\n * @returns The parsed boleto, with the barcode filled in.\n * @throws {BoletoError} On a length other than 47 or 48, or on any check digit\n * that does not recompute.\n *\n * @example\n * const boleto = parseLinhaDigitavel(\"34191.09008 64592.181109 00000.463074 1 84410000002000\");\n */\nexport function parseLinhaDigitavel(value: string, options: BoletoOptions = {}): Boleto {\n const line = digits(value);\n if (line.length === 47) return parseCodigoBarras(bancoBarcode(line), options);\n if (line.length === 48) return parseCodigoBarras(arrecadacaoBarcode(line), options);\n throw new BoletoError(\n `A typed line has 47 digits (bank) or 48 (arrecadação), got ${line.length}. ` +\n \"A 44-digit string is a barcode — use parseCodigoBarras.\",\n );\n}\n\n/** Verify the three field DVs of a 47-digit line and rebuild the barcode. */\nfunction bancoBarcode(line: string): string {\n const fields: [string, string][] = [\n [line.slice(0, 9), line.slice(9, 10)],\n [line.slice(10, 20), line.slice(20, 21)],\n [line.slice(21, 31), line.slice(31, 32)],\n ];\n fields.forEach(([body, dv], index) => {\n const dac = mod10Dac(body);\n if (String(dac) !== dv) {\n throw new BoletoError(\n `Field ${index + 1} check digit is ${dv}, recomputed ${dac} (módulo 10).`,\n );\n }\n });\n return (\n line.slice(0, 4) +\n line.slice(32, 33) +\n line.slice(33, 47) +\n line.slice(4, 9) +\n line.slice(10, 20) +\n line.slice(21, 31)\n );\n}\n\n/** Verify the four block DVs of a 48-digit line and rebuild the barcode. */\nfunction arrecadacaoBarcode(line: string): string {\n const blocks = [0, 12, 24, 36].map((start) => ({\n body: line.slice(start, start + 11),\n dv: line.slice(start + 11, start + 12),\n }));\n const barcode = blocks.map((block) => block.body).join(\"\");\n if (!barcode.startsWith(\"8\")) {\n throw new BoletoError(\n `A 48-digit typed line is an arrecadação slip and must start with 8, got ${barcode[0]}.`,\n );\n }\n assertIdentificacaoValor(barcode);\n const dac = mod11Selector(barcode) === 11 ? mod11DacArrecadacao : mod10Dac;\n blocks.forEach((block, index) => {\n const expected = dac(block.body);\n if (String(expected) !== block.dv) {\n throw new BoletoError(\n `Block ${index + 1} check digit is ${block.dv}, recomputed ${expected} ` +\n `(módulo ${mod11Selector(barcode)}).`,\n );\n }\n });\n return barcode;\n}\n\n/**\n * Convert a typed line to its 44-digit barcode.\n *\n * @param value - 47 or 48 digits.\n * @returns The 44-digit barcode.\n * @throws {BoletoError} On a bad length or a check digit that does not recompute.\n */\nexport function linhaDigitavelToCodigoBarras(value: string): string {\n return parseLinhaDigitavel(value).codigoBarras;\n}\n\n/**\n * Convert a 44-digit barcode to its typed line — 47 digits, or 48 for `8…`.\n *\n * @param value - 44 digits.\n * @returns The typed line, check digits included.\n * @throws {BoletoError} On a bad length or a general check digit that does not\n * recompute.\n */\nexport function codigoBarrasToLinhaDigitavel(value: string): string {\n return parseCodigoBarras(value).linhaDigitavel;\n}\n\n/**\n * Whether a boleto string is internally consistent.\n *\n * Checks every digit the layout can check — the block DVs of a typed line and the\n * general DV in both representations. It says nothing about whether the boleto\n * exists, is registered, or is still payable: only a bank can answer that. What it\n * *does* catch is the common failure, a mistyped or truncated line.\n *\n * @param value - A barcode (44) or typed line (47/48), masked or not.\n * @returns `true` when everything recomputes.\n *\n * @example\n * if (!validateBoleto(input)) setError(\"Confira a linha digitável.\");\n */\nexport function validateBoleto(value: string): boolean {\n try {\n const raw = digits(value);\n if (raw.length === 44) parseCodigoBarras(raw);\n else parseLinhaDigitavel(raw);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Group a typed line the way it is printed, so a human can read it back.\n *\n * 47 digits become `AAABC.CCCCD EEEEE.EEEEEF GGGGG.GGGGGH I JJJJKKKKKKKKKK`;\n * 48 digits become four blocks of twelve. Anything else is returned untouched —\n * this is a display helper, not a validator.\n *\n * @param value - A typed line, masked or not.\n * @returns The grouped string.\n *\n * @example\n * formatLinhaDigitavel(\"34191090086459218110900000463074184410000002000\");\n * // \"34191.09008 64592.181109 00000.463074 1 84410000002000\"\n */\nexport function formatLinhaDigitavel(value: string): string {\n const line = digits(value);\n if (line.length === 47) {\n return [\n `${line.slice(0, 5)}.${line.slice(5, 10)}`,\n `${line.slice(10, 15)}.${line.slice(15, 21)}`,\n `${line.slice(21, 26)}.${line.slice(26, 32)}`,\n line.slice(32, 33),\n line.slice(33),\n ].join(\" \");\n }\n if (line.length === 48) {\n return [0, 12, 24, 36].map((start) => line.slice(start, start + 12)).join(\" \");\n }\n return value;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"boleto.cjs","names":["BoletoError","message","LEGACY_EPOCH_UTC","CURRENT_EPOCH_UTC","MS_PER_DAY","FATOR_MIN","FATOR_MAX","MOD10_WEIGHTS","MOD11_WEIGHT_FIRST","SEGMENTO_LABELS","SEGMENTO_CNPJ","digits","value","mod10Dac","sum","index","fromRight","product","remainder","mod11Sum","mod11DacCobranca","mod11DacArrecadacao","boletoKind","raw","dayAfter","utc","epochUtcMs","days","boletoDueDate","fator","options","epoch","legacy","current","at","reference","fatorVencimento","base","target","date","campoLivreDate","head","campoLivre","year","month","day","bancoLinha","barcode","field1","field2","field3","arrecadacaoLinha","dac","mod11Selector","line","start","block","readBanco","due","moeda","readArrecadacao","segmento","identificacaoValor","empresaIsCnpj","valorRaw","isMoney","assertBarcodeDv","body","assertIdentificacaoValor","parseCodigoBarras","parseLinhaDigitavel","bancoBarcode","arrecadacaoBarcode","dv","blocks","expected","linhaDigitavelToCodigoBarras","codigoBarrasToLinhaDigitavel","validateBoleto","formatLinhaDigitavel"],"sources":["../../src/br/boleto.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — the FEBRABAN spec in one file: the 47-digit linha\n * digitável, the 44-digit barcode, the two layouts (bank slips and arrecadação),\n * modulo-10 and modulo-11 check digits, the base date the due date counts from, and\n * the value scaling. Every piece cross-checks another — the conversion between the\n * two forms is what proves the check digits — so splitting it hides the one property\n * the file exists to guarantee.\n */\n/**\n * A boleto string could not be read, or failed a check digit.\n *\n * Its own class so a scanner screen can tell \"this is not a boleto\" apart from a\n * bug, and so the message can be shown to the operator as-is.\n */\nexport class BoletoError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"BoletoError\";\n }\n}\n\n/**\n * The two incompatible layouts that share the 44-digit barcode.\n *\n * `\"banco\"` is the cobrança boleto every bank issues; `\"arrecadacao\"` is the\n * concessionária/tributo slip, which starts with `8` and lays out its 44 digits\n * completely differently — same length, different meaning for every field.\n */\nexport type BoletoKind = \"banco\" | \"arrecadacao\";\n\n/** Which base date the fator de vencimento counts from. See {@link boletoDueDate}. */\nexport type BoletoEpoch = \"auto\" | \"legacy\" | \"current\";\n\n/** Options shared by every parser here. */\nexport interface BoletoOptions {\n /** Fator de vencimento epoch. Default `\"auto\"`. */\n epoch?: BoletoEpoch;\n /** Date `\"auto\"` measures proximity against. Default `new Date()`. */\n reference?: Date;\n}\n\n/** A cobrança boleto — the kind a bank issues against an invoice. */\nexport interface BoletoBanco {\n kind: \"banco\";\n /** 44 digits. */\n codigoBarras: string;\n /** 47 digits. */\n linhaDigitavel: string;\n /** 3-digit bank code in the clearing house, e.g. `\"341\"`. */\n banco: string;\n /** 1 digit. `\"9\"` is BRL; nothing else is in use. */\n moeda: string;\n /** `\"Real\"` for `\"9\"`, `null` for anything else. */\n moedaLabel: string | null;\n /** The general check digit, position 5 of the barcode. */\n dv: string;\n /** Raw 4-digit field. `0` means the boleto carries no due date. */\n fatorVencimento: number;\n /** Due date, or `null` when the fator is `0`. */\n vencimento: Date | null;\n /** Which epoch {@link vencimento} was resolved under. `null` when there is none. */\n vencimentoEpoch: Exclude<BoletoEpoch, \"auto\"> | null;\n /** Reais. `0` when the issuer left the amount for the payer to fill in. */\n valor: number;\n /** 25 digits the issuing bank defines. Not interpretable without its manual. */\n campoLivre: string;\n}\n\n/** An arrecadação/convênio slip — utilities, taxes, traffic fines. */\nexport interface BoletoArrecadacao {\n kind: \"arrecadacao\";\n /** 44 digits, always starting with `8`. */\n codigoBarras: string;\n /** 48 digits, in four blocks of twelve. */\n linhaDigitavel: string;\n /** Position 2. See {@link segmentoLabel}. */\n segmento: number;\n /** Human label, or `null` for a value the layout does not define. */\n segmentoLabel: string | null;\n /** Position 3: `6`/`8` mean real money, `7`/`9` mean a reference quantity. */\n identificacaoValor: number;\n /** Which modulo position 3 selects for the general check digit. */\n dvModulo: 10 | 11;\n /** The general check digit, position 4. */\n dv: string;\n /** Reais, or `null` when position 3 says the field is a reference, not money. */\n valor: number | null;\n /** The raw 11-digit value field, useful when {@link valor} is `null`. */\n valorRaw: string;\n /**\n * Positions 16-19 — the 4-digit code FEBRABAN assigns the company — or, on\n * segmento 6, positions 16-23, which are the first eight CNPJ digits.\n */\n empresa: string;\n /** `true` when {@link empresa} is a CNPJ prefix rather than a FEBRABAN code. */\n empresaIsCnpj: boolean;\n /** 25 digits, or 21 when the CNPJ took four of them. Issuer-defined. */\n campoLivre: string;\n /**\n * Due date read from the first eight digits of the campo livre.\n *\n * The layout says a due date, **if present**, must sit there as `AAAAMMDD` —\n * but the field is optional and nothing marks its presence, so a campo livre\n * that merely looks like a date lands here too. Treat it as a hint for a UI,\n * never as the date a payment settles against.\n */\n vencimentoCampoLivre: Date | null;\n}\n\n/** What the two parsers return. Narrow on `kind`. */\nexport type Boleto = BoletoBanco | BoletoArrecadacao;\n\n/** Base date of the fator de vencimento as FEBRABAN defined it in 1997. */\nconst LEGACY_EPOCH_UTC = Date.UTC(1997, 9, 7);\n\n/**\n * Base date the fator de vencimento restarted from.\n *\n * FEBRABAN communication FB-009/2023: the 4-digit field hit its ceiling of 9999\n * on 2025-02-21 (1997-10-07 + 9999 days), so from 2025-02-22 the counter restarts\n * at 1000 against a new base of 2022-05-29 (2022-05-29 + 1000 days = 2025-02-22).\n */\nconst CURRENT_EPOCH_UTC = Date.UTC(2022, 4, 29);\n\nconst MS_PER_DAY = 86_400_000;\n\n/** First fator FEBRABAN ever put in circulation, in either epoch. */\nconst FATOR_MIN = 1000;\n\n/** Last fator the 4-digit field can hold. */\nconst FATOR_MAX = 9999;\n\nconst MOD10_WEIGHTS = [2, 1] as const;\nconst MOD11_WEIGHT_FIRST = 2;\nconst MOD11_WEIGHT_LAST = 9;\n\nconst SEGMENTO_LABELS: Record<number, string> = {\n 1: \"Prefeituras\",\n 2: \"Saneamento\",\n 3: \"Energia elétrica e gás\",\n 4: \"Telecomunicações\",\n 5: \"Órgãos governamentais\",\n 6: \"Carnes e assemelhados ou empresas identificadas por CNPJ\",\n 7: \"Multas de trânsito\",\n 9: \"Uso exclusivo do banco\",\n};\n\n/** Segmento whose company identifier is a CNPJ prefix instead of a FEBRABAN code. */\nconst SEGMENTO_CNPJ = 6;\n\n/**\n * Digits of a value that may carry the usual dots, spaces and dashes.\n *\n * Every non-digit is dropped, not only `.`, ` ` and `-`, on purpose (#534):\n * a line copied from a PDF arrives with non-breaking spaces, line breaks, tabs\n * or a leading \"Linha digitável:\", and all four parse today — an allowlist of\n * the three usual separators would reject each of them. Surrounding text that\n * carries digits (a value, a due date) changes the length and fails, and a line\n * assembled from scattered digits still has to pass every field DV and the\n * general DV, so the lenient read cannot produce a boleto the text did not hold.\n */\nfunction digits(value: string): string {\n return value.replace(/\\D/g, \"\");\n}\n\n/**\n * DAC módulo 10, FEBRABAN flavour.\n *\n * Multipliers cycle `2, 1, 2, 1, …` from right to left; the **digits** of each\n * product are summed individually (so `6 × 2 = 12` contributes `1 + 2`); the DAC\n * is `10 - (sum mod 10)`, and `0` when that remainder is `0`.\n *\n * Ported from \"Layout Padrão de Arrecadação/Recebimento com Utilização do Código\n * de Barras\", FEBRABAN version 07 (effective 2023-03-01), section 07 — whose own\n * worked example gives `01230067896 → 3`.\n *\n * @param value - Digits only.\n * @returns The check digit, 0-9.\n * @throws {BoletoError} When `value` is empty or holds a non-digit.\n */\nexport function mod10Dac(value: string): number {\n if (!/^\\d+$/.test(value))\n throw new BoletoError(`mod10Dac needs digits, got ${JSON.stringify(value)}.`);\n let sum = 0;\n for (let index = 0; index < value.length; index += 1) {\n const fromRight = value.length - 1 - index;\n const product = Number(value[index]) * MOD10_WEIGHTS[fromRight % MOD10_WEIGHTS.length]!;\n sum += product > 9 ? product - 9 : product;\n }\n const remainder = sum % 10;\n return remainder === 0 ? 0 : 10 - remainder;\n}\n\n/** Sum of `digit × weight` with weights cycling 2…9 from the right. */\nfunction mod11Sum(value: string): number {\n const span = MOD11_WEIGHT_LAST - MOD11_WEIGHT_FIRST + 1;\n let sum = 0;\n for (let index = 0; index < value.length; index += 1) {\n const fromRight = value.length - 1 - index;\n sum += Number(value[index]) * (MOD11_WEIGHT_FIRST + (fromRight % span));\n }\n return sum;\n}\n\n/**\n * DAC módulo 11 for a **cobrança** barcode — position 5 of a bank boleto.\n *\n * Weights cycle `2…9` from right to left, the products are summed whole, and the\n * DAC is `11 - (sum mod 11)`. A remainder of `0`, `1` or `10` would put `11`, `10`\n * or `1` in a one-digit field, and FEBRABAN resolves all three to **`1`**.\n *\n * That last rule is where cobrança and arrecadação disagree — see\n * {@link mod11DacArrecadacao}, which resolves the same remainders to `0`. Using\n * one flavour on the other layout produces a check digit that is wrong exactly\n * 3 times in 11, which is why they are separate functions here.\n *\n * @param value - The 43 digits of the barcode with position 5 removed.\n * @returns The check digit, 1-9.\n * @throws {BoletoError} When `value` is empty or holds a non-digit.\n */\nexport function mod11DacCobranca(value: string): number {\n if (!/^\\d+$/.test(value)) {\n throw new BoletoError(`mod11DacCobranca needs digits, got ${JSON.stringify(value)}.`);\n }\n const remainder = mod11Sum(value) % 11;\n return remainder === 0 || remainder === 1 || remainder === 10 ? 1 : 11 - remainder;\n}\n\n/**\n * DAC módulo 11 for an **arrecadação** barcode — position 4 of a `8…` slip.\n *\n * Same weights and same subtraction as {@link mod11DacCobranca}, but a remainder\n * of `0` or `1` resolves to **`0`**.\n *\n * Ported from the FEBRABAN version 07 layout, section 10, and pinned by that\n * document's own worked example: the 43-digit sequence\n * `8220000215048200974123220154098290108605940` sums to 705, `705 mod 11 = 1`,\n * and the barcode the spec prints carries `0` in position 4.\n *\n * @param value - The 43 digits of the barcode with position 4 removed.\n * @returns The check digit, 0 or 2-9.\n * @throws {BoletoError} When `value` is empty or holds a non-digit.\n */\nexport function mod11DacArrecadacao(value: string): number {\n if (!/^\\d+$/.test(value)) {\n throw new BoletoError(`mod11DacArrecadacao needs digits, got ${JSON.stringify(value)}.`);\n }\n const remainder = mod11Sum(value) % 11;\n return remainder === 0 || remainder === 1 ? 0 : 11 - remainder;\n}\n\n/**\n * Which layout a string is in, without throwing.\n *\n * Length and first digit have to agree: a 47-digit line is cobrança and never\n * starts with `8`, a 48-digit one is arrecadação and always does. A string where\n * they disagree is in neither layout and comes back `null` — the same input\n * {@link parseLinhaDigitavel} rejects, so the two never name different layouts\n * for one string.\n *\n * @param value - A barcode or typed line, masked or not.\n * @returns The layout, or `null` when the length is not 44, 47 or 48, or when a\n * 47-digit line starts with `8` or a 48-digit one does not.\n */\nexport function boletoKind(value: string): BoletoKind | null {\n const raw = digits(value);\n if (raw.length === 47) return raw.startsWith(\"8\") ? null : \"banco\";\n if (raw.length === 48) return raw.startsWith(\"8\") ? \"arrecadacao\" : null;\n if (raw.length === 44) return raw.startsWith(\"8\") ? \"arrecadacao\" : \"banco\";\n return null;\n}\n\n/** Local midnight of the calendar day `days` after a UTC epoch. */\nfunction dayAfter(epochUtcMs: number, days: number): Date {\n const utc = new Date(epochUtcMs + days * MS_PER_DAY);\n return new Date(utc.getUTCFullYear(), utc.getUTCMonth(), utc.getUTCDate());\n}\n\n/**\n * Resolve a fator de vencimento to a calendar date.\n *\n * The field is four digits of days since a base date, and it has had **two** base\n * dates: 1997-10-07 until the counter saturated at 9999 on 2025-02-21, then\n * 2022-05-29 from 2025-02-22, when FEBRABAN restarted it at 1000.\n *\n * !!! danger \"The two epochs are genuinely ambiguous\"\n * Every fator from 1000 to 9999 has a reading under each base — 1997-10-07\n * gives a date in `2000-07-03 … 2025-02-21`, 2022-05-29 gives one in\n * `2025-02-22 … 2049-10-14`. Nothing in the barcode says which. `\"auto\"`\n * picks whichever lands nearer `reference`, which is right for the case that\n * matters (a slip being paid now) and wrong for an archive sweep. Pass\n * `\"legacy\"` or `\"current\"` when you know.\n *\n * @param fator - The raw 4-digit field as a number. `0` means \"no due date\".\n * @param options - Epoch selection. Default `\"auto\"` against `new Date()`.\n * @returns Local midnight of the due date, or `null` when `fator` is `0`.\n *\n * @example\n * boletoDueDate(1000, { epoch: \"legacy\" }); // 2000-07-03\n * boletoDueDate(1000, { epoch: \"current\" }); // 2025-02-22\n */\nexport function boletoDueDate(\n fator: number,\n options: BoletoOptions = {},\n): { date: Date; epoch: Exclude<BoletoEpoch, \"auto\"> } | null {\n if (fator === 0) return null;\n const { epoch = \"auto\", reference = new Date() } = options;\n\n if (epoch === \"legacy\") return { date: dayAfter(LEGACY_EPOCH_UTC, fator), epoch: \"legacy\" };\n if (epoch === \"current\") return { date: dayAfter(CURRENT_EPOCH_UTC, fator), epoch: \"current\" };\n\n const legacy = dayAfter(LEGACY_EPOCH_UTC, fator);\n if (fator < FATOR_MIN) return { date: legacy, epoch: \"legacy\" };\n const current = dayAfter(CURRENT_EPOCH_UTC, fator);\n const at = reference.getTime();\n return Math.abs(current.getTime() - at) <= Math.abs(legacy.getTime() - at)\n ? { date: current, epoch: \"current\" }\n : { date: legacy, epoch: \"legacy\" };\n}\n\n/**\n * Invert {@link boletoDueDate}: the fator that encodes a due date.\n *\n * @param date - The due date. Only its local calendar day is used.\n * @param epoch - Base date to count from. Default `\"current\"`, the epoch in force\n * since 2025-02-22 and therefore the one a boleto issued today must use.\n * @returns The 4-digit fator.\n * @throws {BoletoError} When the date falls outside the 1000-9999 window of that\n * epoch, because no fator can represent it.\n *\n * @example\n * fatorVencimento(new Date(2025, 1, 22)); // 1000\n */\nexport function fatorVencimento(\n date: Date,\n epoch: Exclude<BoletoEpoch, \"auto\"> = \"current\",\n): number {\n const base = epoch === \"legacy\" ? LEGACY_EPOCH_UTC : CURRENT_EPOCH_UTC;\n const target = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate());\n const fator = Math.round((target - base) / MS_PER_DAY);\n if (fator < FATOR_MIN || fator > FATOR_MAX) {\n throw new BoletoError(\n `${date.toISOString().slice(0, 10)} is fator ${fator} under the \"${epoch}\" epoch, ` +\n `outside the ${FATOR_MIN}-${FATOR_MAX} the field holds.`,\n );\n }\n return fator;\n}\n\n/**\n * `AAAAMMDD` at the head of an arrecadação campo livre, when it reads as a date.\n *\n * The caller only ever passes a slice of a validated barcode, so the head is\n * always eight digits; what is not guaranteed is that those digits are a date, and\n * `20260231` has to come back `null` rather than roll into March.\n */\nfunction campoLivreDate(campoLivre: string): Date | null {\n const head = campoLivre.slice(0, 8);\n const year = Number(head.slice(0, 4));\n const month = Number(head.slice(4, 6));\n const day = Number(head.slice(6, 8));\n if (year < 1997 || year > 2099 || month < 1 || month > 12 || day < 1 || day > 31) return null;\n const date = new Date(year, month - 1, day);\n return date.getMonth() === month - 1 && date.getDate() === day ? date : null;\n}\n\n/** Assemble the 47-digit typed line from a validated 44-digit bank barcode. */\nfunction bancoLinha(barcode: string): string {\n const campoLivre = barcode.slice(19);\n const field1 = barcode.slice(0, 4) + campoLivre.slice(0, 5);\n const field2 = campoLivre.slice(5, 15);\n const field3 = campoLivre.slice(15, 25);\n return (\n field1 +\n mod10Dac(field1) +\n field2 +\n mod10Dac(field2) +\n field3 +\n mod10Dac(field3) +\n barcode.slice(4, 5) +\n barcode.slice(5, 19)\n );\n}\n\n/** Assemble the 48-digit typed line from a validated 44-digit arrecadação barcode. */\nfunction arrecadacaoLinha(barcode: string): string {\n const dac = mod11Selector(barcode) === 11 ? mod11DacArrecadacao : mod10Dac;\n let line = \"\";\n for (let start = 0; start < 44; start += 11) {\n const block = barcode.slice(start, start + 11);\n line += block + dac(block);\n }\n return line;\n}\n\n/** Which modulo position 3 of an arrecadação barcode selects. */\nfunction mod11Selector(barcode: string): 10 | 11 {\n return barcode[2] === \"8\" || barcode[2] === \"9\" ? 11 : 10;\n}\n\n/** Read a 44-digit bank barcode, whose general DV has already been checked. */\nfunction readBanco(barcode: string, options: BoletoOptions): BoletoBanco {\n const fator = Number(barcode.slice(5, 9));\n const due = boletoDueDate(fator, options);\n const moeda = barcode.slice(3, 4);\n return {\n kind: \"banco\",\n codigoBarras: barcode,\n linhaDigitavel: bancoLinha(barcode),\n banco: barcode.slice(0, 3),\n moeda,\n moedaLabel: moeda === \"9\" ? \"Real\" : null,\n dv: barcode.slice(4, 5),\n fatorVencimento: fator,\n vencimento: due?.date ?? null,\n vencimentoEpoch: due?.epoch ?? null,\n valor: Number(barcode.slice(9, 19)) / 100,\n campoLivre: barcode.slice(19),\n };\n}\n\n/** Read a 44-digit arrecadação barcode, whose general DV has already been checked. */\nfunction readArrecadacao(barcode: string): BoletoArrecadacao {\n const segmento = Number(barcode[1]);\n const identificacaoValor = Number(barcode[2]);\n const empresaIsCnpj = segmento === SEGMENTO_CNPJ;\n const campoLivre = empresaIsCnpj ? barcode.slice(23) : barcode.slice(19);\n const valorRaw = barcode.slice(4, 15);\n const isMoney = identificacaoValor === 6 || identificacaoValor === 8;\n return {\n kind: \"arrecadacao\",\n codigoBarras: barcode,\n linhaDigitavel: arrecadacaoLinha(barcode),\n segmento,\n segmentoLabel: SEGMENTO_LABELS[segmento] ?? null,\n identificacaoValor,\n dvModulo: mod11Selector(barcode),\n dv: barcode.slice(3, 4),\n valor: isMoney ? Number(valorRaw) / 100 : null,\n valorRaw,\n empresa: empresaIsCnpj ? barcode.slice(15, 23) : barcode.slice(15, 19),\n empresaIsCnpj,\n campoLivre,\n vencimentoCampoLivre: campoLivreDate(campoLivre),\n };\n}\n\n/** Recompute and compare the general check digit of a 44-digit barcode. */\nfunction assertBarcodeDv(barcode: string): void {\n if (barcode.startsWith(\"8\")) {\n const body = barcode.slice(0, 3) + barcode.slice(4);\n const dac = mod11Selector(barcode) === 11 ? mod11DacArrecadacao(body) : mod10Dac(body);\n if (String(dac) !== barcode[3]) {\n throw new BoletoError(\n `Arrecadação check digit is ${barcode[3]}, recomputed ${dac} (módulo ${mod11Selector(barcode)}).`,\n );\n }\n return;\n }\n const dac = mod11DacCobranca(barcode.slice(0, 4) + barcode.slice(5));\n if (String(dac) !== barcode[4]) {\n throw new BoletoError(`Barcode check digit is ${barcode[4]}, recomputed ${dac}.`);\n }\n}\n\n/** Reject an arrecadação barcode whose position 3 is not one of 6, 7, 8, 9. */\nfunction assertIdentificacaoValor(barcode: string): void {\n if (!\"6789\".includes(barcode[2]!)) {\n throw new BoletoError(\n `Position 3 of an arrecadação barcode must be 6, 7, 8 or 9 (identificação do valor), got ${barcode[2]}.`,\n );\n }\n}\n\n/**\n * Read a 44-digit barcode — either layout — into its fields.\n *\n * The first digit picks the layout: `8` is an arrecadação/convênio slip, anything\n * else is a cobrança boleto. They are **not** variants of one format; every field\n * moves. Narrow the result on `kind` before touching it.\n *\n * @param value - 44 digits. Spaces and punctuation are ignored.\n * @param options - Fator de vencimento epoch. See {@link boletoDueDate}.\n * @returns The parsed boleto, with the matching typed line filled in.\n * @throws {BoletoError} On a length other than 44, on a general check digit that\n * does not recompute, or on an arrecadação slip whose position 3 is out of spec.\n *\n * @example\n * const boleto = parseCodigoBarras(scanned);\n * if (boleto.kind === \"banco\") console.log(boleto.valor, boleto.vencimento);\n */\nexport function parseCodigoBarras(value: string, options: BoletoOptions = {}): Boleto {\n const barcode = digits(value);\n if (barcode.length !== 44) {\n throw new BoletoError(\n `A barcode has 44 digits, got ${barcode.length}. ` +\n \"A 47- or 48-digit string is a typed line — use parseLinhaDigitavel.\",\n );\n }\n if (barcode.startsWith(\"8\")) {\n assertIdentificacaoValor(barcode);\n assertBarcodeDv(barcode);\n return readArrecadacao(barcode);\n }\n assertBarcodeDv(barcode);\n return readBanco(barcode, options);\n}\n\n/**\n * Read a typed line — 47 digits for a bank boleto, 48 for an arrecadação slip.\n *\n * Both layouts interleave check digits with the data, so the function rebuilds the\n * 44-digit barcode as it goes and every DV is verified: the three (bank) or four\n * (arrecadação) block digits, plus the general one.\n *\n * The length fixes the layout, and the first digit has to agree with it. A\n * 47-digit line starting with `8` is rejected rather than read: its first three\n * digits would be a bank code `8xx`, but the 44-digit barcode it rebuilds starts\n * with `8` too, and in the barcode that digit is what marks an arrecadação slip —\n * {@link parseCodigoBarras}, and any reader that only sees the barcode, would read\n * it as arrecadação. Before this check\n * `81600000090000000000000000000190900000000000000` passed every field DV, came\n * back as `kind: \"arrecadacao\"` with `valor` 900000000 and a 48-digit line, while\n * the cobrança reading of the same digits is bank 816 with `valor` 0 (#529). No\n * single reading is right for both forms, so neither is returned.\n *\n * @param value - 47 or 48 digits. The usual `.`, ` ` and `-` are ignored.\n * @param options - Fator de vencimento epoch. See {@link boletoDueDate}.\n * @returns The parsed boleto, with the barcode filled in.\n * @throws {BoletoError} On a length other than 47 or 48, on a 47-digit line that\n * starts with `8` or a 48-digit one that does not, or on any check digit that does\n * not recompute.\n *\n * @example\n * const boleto = parseLinhaDigitavel(\"34191.09008 64592.181109 00000.463074 1 84410000002000\");\n */\nexport function parseLinhaDigitavel(value: string, options: BoletoOptions = {}): Boleto {\n const line = digits(value);\n if (line.length === 47) return parseCodigoBarras(bancoBarcode(line), options);\n if (line.length === 48) return parseCodigoBarras(arrecadacaoBarcode(line), options);\n throw new BoletoError(\n `A typed line has 47 digits (bank) or 48 (arrecadação), got ${line.length}. ` +\n \"A 44-digit string is a barcode — use parseCodigoBarras.\",\n );\n}\n\n/** Verify the three field DVs of a 47-digit line and rebuild the barcode. */\nfunction bancoBarcode(line: string): string {\n if (line.startsWith(\"8\")) {\n throw new BoletoError(\n \"A 47-digit typed line is a bank boleto and cannot start with 8: \" +\n \"its barcode would read as an arrecadação slip.\",\n );\n }\n const fields: [string, string][] = [\n [line.slice(0, 9), line.slice(9, 10)],\n [line.slice(10, 20), line.slice(20, 21)],\n [line.slice(21, 31), line.slice(31, 32)],\n ];\n fields.forEach(([body, dv], index) => {\n const dac = mod10Dac(body);\n if (String(dac) !== dv) {\n throw new BoletoError(\n `Field ${index + 1} check digit is ${dv}, recomputed ${dac} (módulo 10).`,\n );\n }\n });\n return (\n line.slice(0, 4) +\n line.slice(32, 33) +\n line.slice(33, 47) +\n line.slice(4, 9) +\n line.slice(10, 20) +\n line.slice(21, 31)\n );\n}\n\n/** Verify the four block DVs of a 48-digit line and rebuild the barcode. */\nfunction arrecadacaoBarcode(line: string): string {\n const blocks = [0, 12, 24, 36].map((start) => ({\n body: line.slice(start, start + 11),\n dv: line.slice(start + 11, start + 12),\n }));\n const barcode = blocks.map((block) => block.body).join(\"\");\n if (!barcode.startsWith(\"8\")) {\n throw new BoletoError(\n `A 48-digit typed line is an arrecadação slip and must start with 8, got ${barcode[0]}.`,\n );\n }\n assertIdentificacaoValor(barcode);\n const dac = mod11Selector(barcode) === 11 ? mod11DacArrecadacao : mod10Dac;\n blocks.forEach((block, index) => {\n const expected = dac(block.body);\n if (String(expected) !== block.dv) {\n throw new BoletoError(\n `Block ${index + 1} check digit is ${block.dv}, recomputed ${expected} ` +\n `(módulo ${mod11Selector(barcode)}).`,\n );\n }\n });\n return barcode;\n}\n\n/**\n * Convert a typed line to its 44-digit barcode.\n *\n * @param value - 47 or 48 digits.\n * @returns The 44-digit barcode.\n * @throws {BoletoError} On a bad length or a check digit that does not recompute.\n */\nexport function linhaDigitavelToCodigoBarras(value: string): string {\n return parseLinhaDigitavel(value).codigoBarras;\n}\n\n/**\n * Convert a 44-digit barcode to its typed line — 47 digits, or 48 for `8…`.\n *\n * @param value - 44 digits.\n * @returns The typed line, check digits included.\n * @throws {BoletoError} On a bad length or a general check digit that does not\n * recompute.\n */\nexport function codigoBarrasToLinhaDigitavel(value: string): string {\n return parseCodigoBarras(value).linhaDigitavel;\n}\n\n/**\n * Whether a boleto string is internally consistent.\n *\n * Checks every digit the layout can check — the block DVs of a typed line and the\n * general DV in both representations. It says nothing about whether the boleto\n * exists, is registered, or is still payable: only a bank can answer that. What it\n * *does* catch is the common failure, a mistyped or truncated line.\n *\n * @param value - A barcode (44) or typed line (47/48), masked or not.\n * @returns `true` when everything recomputes.\n *\n * @example\n * if (!validateBoleto(input)) setError(\"Confira a linha digitável.\");\n */\nexport function validateBoleto(value: string): boolean {\n try {\n const raw = digits(value);\n if (raw.length === 44) parseCodigoBarras(raw);\n else parseLinhaDigitavel(raw);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Group a typed line the way it is printed, so a human can read it back.\n *\n * 47 digits become `AAABC.CCCCD EEEEE.EEEEEF GGGGG.GGGGGH I JJJJKKKKKKKKKK`;\n * 48 digits become four blocks of twelve. Anything else is returned untouched —\n * this is a display helper, not a validator.\n *\n * @param value - A typed line, masked or not.\n * @returns The grouped string.\n *\n * @example\n * formatLinhaDigitavel(\"34191090086459218110900000463074184410000002000\");\n * // \"34191.09008 64592.181109 00000.463074 1 84410000002000\"\n */\nexport function formatLinhaDigitavel(value: string): string {\n const line = digits(value);\n if (line.length === 47) {\n return [\n `${line.slice(0, 5)}.${line.slice(5, 10)}`,\n `${line.slice(10, 15)}.${line.slice(15, 21)}`,\n `${line.slice(21, 26)}.${line.slice(26, 32)}`,\n line.slice(32, 33),\n line.slice(33),\n ].join(\" \");\n }\n if (line.length === 48) {\n return [0, 12, 24, 36].map((start) => line.slice(start, start + 12)).join(\" \");\n }\n return value;\n}\n"],"mappings":"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"}
|
package/dist/br/boleto.js
CHANGED
|
@@ -46,7 +46,7 @@ function m(t) {
|
|
|
46
46
|
}
|
|
47
47
|
function h(e) {
|
|
48
48
|
let t = u(e);
|
|
49
|
-
return t.length === 47 ? "banco" : t.length === 48 ? "arrecadacao" : t.length === 44 ? t.startsWith("8") ? "arrecadacao" : "banco" : null;
|
|
49
|
+
return t.length === 47 ? t.startsWith("8") ? null : "banco" : t.length === 48 ? t.startsWith("8") ? "arrecadacao" : null : t.length === 44 ? t.startsWith("8") ? "arrecadacao" : "banco" : null;
|
|
50
50
|
}
|
|
51
51
|
function g(e, t) {
|
|
52
52
|
let n = new Date(e + t * r);
|
|
@@ -163,6 +163,7 @@ function O(t, n = {}) {
|
|
|
163
163
|
throw new e(`A typed line has 47 digits (bank) or 48 (arrecadação), got ${r.length}. A 44-digit string is a barcode — use parseCodigoBarras.`);
|
|
164
164
|
}
|
|
165
165
|
function k(t) {
|
|
166
|
+
if (t.startsWith("8")) throw new e("A 47-digit typed line is a bank boleto and cannot start with 8: its barcode would read as an arrecadação slip.");
|
|
166
167
|
return [
|
|
167
168
|
[t.slice(0, 9), t.slice(9, 10)],
|
|
168
169
|
[t.slice(10, 20), t.slice(20, 21)],
|