@mosano-product-framework/sdk 0.2.0 → 0.2.2

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/auth/index.ts","../src/auth/oauth-state.ts","../src/auth/tenant-directory.ts"],"names":[],"mappings":";;;;AAAA,IAAA,YAAA,GAAA;AAAA,QAAA,CAAA,YAAA,EAAA;AAAA,EAAA,gBAAA,EAAA,MAAA,gBAAA;AAAA,EAAA,cAAA,EAAA,MAAA,cAAA;AAAA,EAAA,gCAAA,EAAA,MAAA,gCAAA;AAAA,EAAA,wBAAA,EAAA,MAAA,wBAAA;AAAA,EAAA,cAAA,EAAA,MAAA,cAAA;AAAA,EAAA,sBAAA,EAAA,MAAA,sBAAA;AAAA,EAAA,WAAA,EAAA,MAAA,WAAA;AAAA,EAAA,SAAA,EAAA,MAAA,SAAA;AAAA,EAAA,gBAAA,EAAA,MAAA,gBAAA;AAAA,EAAA,yBAAA,EAAA,MAAA,yBAAA;AAAA,EAAA,oBAAA,EAAA,MAAA,oBAAA;AAAA,EAAA,mBAAA,EAAA,MAAA,mBAAA;AAAA,EAAA,qBAAA,EAAA,MAAA,qBAAA;AAAA,EAAA,oBAAA,EAAA,MAAA,oBAAA;AAAA,EAAA,oBAAA,EAAA,MAAA,oBAAA;AAAA,EAAA,qBAAA,EAAA,MAAA,qBAAA;AAAA,EAAA,iBAAA,EAAA,MAAA,iBAAA;AAAA,EAAA,oBAAA,EAAA,MAAA,oBAAA;AAAA,EAAA,SAAA,EAAA,MAAA,SAAA;AAAA,EAAA,aAAA,EAAA,MAAA,aAAA;AAAA,EAAA,SAAA,EAAA,MAAA,SAAA;AAAA,EAAA,iBAAA,EAAA,MAAA,iBAAA;AAAA,EAAA,WAAA,EAAA,MAAA,WAAA;AAAA,EAAA,oBAAA,EAAA,MAAA,oBAAA;AAAA,EAAA,cAAA,EAAA,MAAA,cAAA;AAAA,EAAA,uBAAA,EAAA,MAAA,uBAAA;AAAA,EAAA,oBAAA,EAAA,MAAA,oBAAA;AAAA,EAAA,oBAAA,EAAA,MAAA;AAAA,CAAA,CAAA;;;ACsCA,IAAM,uBAAA,GAA0B,iBAAA;AA6DzB,SAAS,qBAAA,CACd,OAAA,GAAkC,EAAC,EAClB;AACjB,EAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,OAAA,IAAW,oBAAA,EAAqB;AACxD,EAAA,MAAM,GAAA,GAAM,QAAQ,UAAA,IAAc,uBAAA;AAElC,EAAA,OAAO;AAAA,IACL,KAAK,KAAA,EAAO;AACV,MAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,KAAA,KAAU,EAAA,EAAI;AAG7C,QAAA,MAAM,IAAI,MAAM,kDAAkD,CAAA;AAAA,MACpE;AACA,MAAA,IAAI;AACF,QAAA,OAAA,CAAQ,OAAA,CAAQ,KAAK,KAAK,CAAA;AAAA,MAC5B,CAAA,CAAA,MAAQ;AAAA,MAGR;AAAA,IACF,CAAA;AAAA,IAEA,IAAA,GAAO;AACL,MAAA,MAAM,KAAA,GAAQ,KAAK,IAAA,EAAK;AACxB,MAAA,IAAA,CAAK,KAAA,EAAM;AACX,MAAA,OAAO,KAAA;AAAA,IACT,CAAA;AAAA,IAEA,IAAA,GAAO;AACL,MAAA,IAAI;AACF,QAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,OAAA,CAAQ,GAAG,CAAA;AACjC,QAAA,OAAO,KAAA,KAAU,KAAK,IAAA,GAAO,KAAA;AAAA,MAC/B,CAAA,CAAA,MAAQ;AACN,QAAA,OAAO,IAAA;AAAA,MACT;AAAA,IACF,CAAA;AAAA,IAEA,KAAA,GAAQ;AACN,MAAA,IAAI;AACF,QAAA,OAAA,CAAQ,WAAW,GAAG,CAAA;AAAA,MACxB,CAAA,CAAA,MAAQ;AAAA,MAER;AAAA,IACF;AAAA,GACF;AACF;;;AChFO,IAAM,sBAAA,GAAyB,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA","file":"chunk-GPWGOYCA.js","sourcesContent":["/**\n * Auth layer: token claims, tenant selection, session lifecycle.\n *\n * ⚠️ Claim decoding in this module DECODES BUT DOES NOT VERIFY. Its output is\n * UX-only and is forgeable by a hostile client. See the security block at the\n * top of `claims.ts`.\n *\n * @packageDocumentation\n */\n\nexport type {\n MPFRole,\n MPFClaims,\n MPFClaimsV1,\n MPFTenantClaim,\n ClaimsParser,\n} from './claims-types.js';\n\nexport {\n decodeAccessToken,\n tryDecodeAccessToken,\n listTenants,\n rolesForTenant,\n defaultRoleForTenant,\n canSelect,\n expiresAt,\n isExpired,\n tokenLifetimeSeconds,\n clearClaimsCache,\n registerClaimsParser,\n supportedClaimsVersions,\n} from './claims.js';\n\nexport {\n MPFClaimsError,\n MPFUnsupportedClaimsVersionError,\n DeadSessionError,\n isDeadSession,\n} from './errors.js';\n\nexport {\n createTenantSelection,\n type TenantSelection,\n type TenantSelectionStore,\n type TenantSelectionPersistence,\n type CreateTenantSelectionOptions,\n} from './tenant-selection.js';\n\nexport {\n createDefaultStorage,\n createSessionStorage,\n createMemoryStorage,\n type MPFAuthStorage,\n} from './storage.js';\n\nexport {\n createOAuthStateStore,\n type OAuthStateStore,\n type OAuthStateStoreOptions,\n} from './oauth-state.js';\n\nexport {\n createCrossTabCoordinator,\n applyJitter,\n type CrossTabCoordinator,\n type CrossTabMode,\n type CrossTabOptions,\n type TokenBroadcast,\n type LockManagerLike,\n type BroadcastChannelLike,\n} from './cross-tab.js';\n\nexport {\n TENANT_DIRECTORY_QUERY,\n type TenantDirectoryEntry,\n type TenantDirectoryLoader,\n} from './tenant-directory.js';\n\nexport {\n SessionManager,\n createSessionManager,\n isSupersededError,\n REFRESH_TOKEN_SUPERSEDED,\n type SessionManagerOptions,\n type SignOutResult,\n type TokenPair,\n} from './session-manager.js';\n","/**\n * OAuth `state` persistence — CSRF protection for the Google OAuth flow.\n *\n * ## What the state is for\n *\n * Without it, an OAuth callback route accepts *any* `code` presented to it. An\n * attacker who obtains an authorization code for their own account can send the\n * victim to `.../callback?code=<attacker's code>`; the victim's browser posts it,\n * the server exchanges it, and the victim is now signed in **as the attacker** —\n * whose account the attacker still controls. Anything the victim then does\n * (entering payment details, uploading documents, accepting an invitation)\n * happens inside the attacker's account.\n *\n * That is not theoretical: it was confirmed exploitable end to end in the\n * reference boilerplate, whose callback route discarded `state` and auto-posted\n * `code` on page load with no user interaction.\n *\n * The state closes it: the server mints an unguessable value, the client stores\n * it before redirecting, and the server accepts the callback only if the value\n * comes back. An attacker cannot produce a state that matches the victim's\n * browser storage.\n *\n * ## Why this store exists\n *\n * The state must survive a **full-page redirect to Google and back**, so it\n * cannot live in memory or in React state. That leaves web storage, and\n * \"remember to persist a value across a redirect\" is precisely the step the\n * boilerplate omitted. This makes it one call each way.\n *\n * Sending the state stays explicit: `GoogleOAuthCallbackRequest.state` is\n * required, so TypeScript rejects a callback that forgets it. This store solves\n * *where to keep it*, not *whether to send it* — a security parameter should not\n * be invisibly injected.\n */\n\nimport { createSessionStorage, type MPFAuthStorage } from './storage.js';\n\n/** Default storage key for the pending OAuth state. */\nconst DEFAULT_OAUTH_STATE_KEY = 'mpf.oauth_state';\n\nexport interface OAuthStateStore {\n /** Persist the state returned by `getGoogleOAuthURL`, before redirecting. */\n save(state: string): void;\n /**\n * Read and **clear** the stored state, for sending on the callback.\n *\n * Single-use by design: a state that could be replayed is not much better than\n * no state at all, and the server also rejects reuse.\n */\n take(): string | null;\n /** Read without clearing. For diagnostics; prefer `take()` in the flow. */\n peek(): string | null;\n /** Discard any pending state (e.g. the user abandoned the flow). */\n clear(): void;\n}\n\nexport interface OAuthStateStoreOptions {\n /**\n * Where to keep the pending state.\n *\n * Defaults to `sessionStorage`, which is the right default for a redirect\n * flow: it is scoped to the tab that started it, so a state minted in one tab\n * cannot be consumed by another, and it is cleared when the tab closes.\n *\n * **Limitation worth knowing:** `sessionStorage` is per-tab, so a callback\n * that lands in a *different* tab (a provider opening a new tab, or the user\n * moving the URL) will not find the state and the callback will be rejected —\n * failing closed, which is the correct direction, but it will look like a\n * spurious error. Pass `createDefaultStorage()` (localStorage) if your flow\n * can land in another tab, accepting that the state is then visible to every\n * tab on the origin.\n *\n * A cookie is not offered: one set from JavaScript cannot be `httpOnly`, so it\n * is no less readable than web storage while adding `SameSite` pitfalls on the\n * cross-site return leg.\n */\n storage?: MPFAuthStorage;\n /** Storage key. @default 'mpf.oauth_state' */\n storageKey?: string;\n}\n\n/**\n * Create a store for the pending OAuth state.\n *\n * @example\n * ```ts\n * const oauthState = createOAuthStateStore();\n * const auth = createAuthClient({ baseUrl, oauthStateStore: oauthState });\n *\n * // Starting the flow — the client saves the state for you.\n * const { url } = await auth.getGoogleOAuthURL({ redirect_uri });\n * window.location.href = url;\n *\n * // On the callback route, send it back explicitly.\n * const state = oauthState.take();\n * if (!state) throw new Error('No pending OAuth flow in this tab');\n * const tokens = await auth.handleGoogleOAuthCallback({ code, redirect_uri, state });\n * ```\n */\nexport function createOAuthStateStore(\n options: OAuthStateStoreOptions = {},\n): OAuthStateStore {\n const storage = options.storage ?? createSessionStorage();\n const key = options.storageKey ?? DEFAULT_OAUTH_STATE_KEY;\n\n return {\n save(state) {\n if (typeof state !== 'string' || state === '') {\n // Storing an empty state would guarantee a failed callback later, with\n // the cause long out of view. Fail where the mistake is.\n throw new Error('[MPF SDK] Refusing to store an empty OAuth state');\n }\n try {\n storage.setItem(key, state);\n } catch {\n // A blocked or full quota must not break starting the flow; the callback\n // will fail closed instead.\n }\n },\n\n take() {\n const state = this.peek();\n this.clear();\n return state;\n },\n\n peek() {\n try {\n const value = storage.getItem(key);\n return value === '' ? null : value;\n } catch {\n return null;\n }\n },\n\n clear() {\n try {\n storage.removeItem(key);\n } catch {\n // ignore\n }\n },\n };\n}\n","/**\n * Tenant display metadata — names and slugs.\n *\n * ## Why this is separate from the claim set\n *\n * The access token's `tnts` array carries tenant **ids and roles only**. That is\n * the authoritative, freshest answer to \"which tenants may this user select\",\n * and it needs no network call — but it renders a switcher full of UUIDs.\n *\n * Names live in the database (`identity.tenants`), so fetching them is a real\n * network operation that can really fail. Keeping it in its own module, behind\n * its own state, means the two questions stay distinguishable:\n *\n * - \"which tenants can I select?\" — claims, synchronous, cannot fail\n * - \"what are they called?\" — network, asynchronous, can fail\n *\n * Merging them would make a failed name lookup indistinguishable from having no\n * tenants, which is the exact bug class the tri-state exists to prevent.\n *\n * ## Reachability\n *\n * The `tenantless` Hasura role has select permissions on `identity_tenants`\n * scoped to `X-Hasura-User-Id`, so a signed-in user with NO tenant selected can\n * read the names of the tenants they belong to. That is precisely the switcher's\n * situation.\n */\n\n/** Display metadata for one tenant. */\nexport interface TenantDirectoryEntry {\n id: string;\n name?: string;\n slug?: string;\n}\n\n/**\n * Loads display metadata for the given tenant ids.\n *\n * Injected rather than implemented here so that neither this module nor the\n * React layer has to depend on the GraphQL client — which keeps `src/auth/` a\n * leaf and the React layer free of `src/client/graphql/`. Use\n * {@link TENANT_DIRECTORY_QUERY} with whichever GraphQL client the app already\n * has.\n */\nexport type TenantDirectoryLoader = (\n tenantIds: string[],\n) => Promise<TenantDirectoryEntry[]>;\n\n/**\n * A ready-made query for the loader.\n *\n * Runs under the `tenantless` role — i.e. with no `X-MPF-Tenant` header — which\n * is why a switcher can populate itself before any tenant is selected.\n *\n * @example\n * ```ts\n * import { TENANT_DIRECTORY_QUERY } from '@mosano-product-framework/sdk/auth';\n *\n * const loadTenantDirectory = async () => {\n * const data = await gql.query(TENANT_DIRECTORY_QUERY, undefined, { tenant: null });\n * return data.identity_tenants;\n * };\n * ```\n */\nexport const TENANT_DIRECTORY_QUERY = `query MPFTenantDirectory {\n identity_tenants {\n id\n name\n slug\n }\n}`;\n"]}
1
+ {"version":3,"sources":["../src/auth/index.ts","../src/auth/oauth-state.ts","../src/auth/tenant-directory.ts"],"names":[],"mappings":";;;;AAAA,IAAA,YAAA,GAAA;AAAA,QAAA,CAAA,YAAA,EAAA;AAAA,EAAA,gBAAA,EAAA,MAAA,gBAAA;AAAA,EAAA,cAAA,EAAA,MAAA,cAAA;AAAA,EAAA,gCAAA,EAAA,MAAA,gCAAA;AAAA,EAAA,wBAAA,EAAA,MAAA,wBAAA;AAAA,EAAA,cAAA,EAAA,MAAA,cAAA;AAAA,EAAA,sBAAA,EAAA,MAAA,sBAAA;AAAA,EAAA,WAAA,EAAA,MAAA,WAAA;AAAA,EAAA,SAAA,EAAA,MAAA,SAAA;AAAA,EAAA,gBAAA,EAAA,MAAA,gBAAA;AAAA,EAAA,yBAAA,EAAA,MAAA,yBAAA;AAAA,EAAA,oBAAA,EAAA,MAAA,oBAAA;AAAA,EAAA,mBAAA,EAAA,MAAA,mBAAA;AAAA,EAAA,qBAAA,EAAA,MAAA,qBAAA;AAAA,EAAA,oBAAA,EAAA,MAAA,oBAAA;AAAA,EAAA,oBAAA,EAAA,MAAA,oBAAA;AAAA,EAAA,qBAAA,EAAA,MAAA,qBAAA;AAAA,EAAA,iBAAA,EAAA,MAAA,iBAAA;AAAA,EAAA,oBAAA,EAAA,MAAA,oBAAA;AAAA,EAAA,SAAA,EAAA,MAAA,SAAA;AAAA,EAAA,aAAA,EAAA,MAAA,aAAA;AAAA,EAAA,SAAA,EAAA,MAAA,SAAA;AAAA,EAAA,iBAAA,EAAA,MAAA,iBAAA;AAAA,EAAA,WAAA,EAAA,MAAA,WAAA;AAAA,EAAA,oBAAA,EAAA,MAAA,oBAAA;AAAA,EAAA,cAAA,EAAA,MAAA,cAAA;AAAA,EAAA,uBAAA,EAAA,MAAA,uBAAA;AAAA,EAAA,oBAAA,EAAA,MAAA,oBAAA;AAAA,EAAA,oBAAA,EAAA,MAAA;AAAA,CAAA,CAAA;;;ACsCA,IAAM,uBAAA,GAA0B,iBAAA;AA6DzB,SAAS,qBAAA,CACd,OAAA,GAAkC,EAAC,EAClB;AACjB,EAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,OAAA,IAAW,oBAAA,EAAqB;AACxD,EAAA,MAAM,GAAA,GAAM,QAAQ,UAAA,IAAc,uBAAA;AAElC,EAAA,OAAO;AAAA,IACL,KAAK,KAAA,EAAO;AACV,MAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,KAAA,KAAU,EAAA,EAAI;AAG7C,QAAA,MAAM,IAAI,MAAM,kDAAkD,CAAA;AAAA,MACpE;AACA,MAAA,IAAI;AACF,QAAA,OAAA,CAAQ,OAAA,CAAQ,KAAK,KAAK,CAAA;AAAA,MAC5B,CAAA,CAAA,MAAQ;AAAA,MAGR;AAAA,IACF,CAAA;AAAA,IAEA,IAAA,GAAO;AACL,MAAA,MAAM,KAAA,GAAQ,KAAK,IAAA,EAAK;AACxB,MAAA,IAAA,CAAK,KAAA,EAAM;AACX,MAAA,OAAO,KAAA;AAAA,IACT,CAAA;AAAA,IAEA,IAAA,GAAO;AACL,MAAA,IAAI;AACF,QAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,OAAA,CAAQ,GAAG,CAAA;AACjC,QAAA,OAAO,KAAA,KAAU,KAAK,IAAA,GAAO,KAAA;AAAA,MAC/B,CAAA,CAAA,MAAQ;AACN,QAAA,OAAO,IAAA;AAAA,MACT;AAAA,IACF,CAAA;AAAA,IAEA,KAAA,GAAQ;AACN,MAAA,IAAI;AACF,QAAA,OAAA,CAAQ,WAAW,GAAG,CAAA;AAAA,MACxB,CAAA,CAAA,MAAQ;AAAA,MAER;AAAA,IACF;AAAA,GACF;AACF;;;AChFO,IAAM,sBAAA,GAAyB,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA","file":"chunk-FYUVJL4T.js","sourcesContent":["/**\n * Auth layer: token claims, tenant selection, session lifecycle.\n *\n * ⚠️ Claim decoding in this module DECODES BUT DOES NOT VERIFY. Its output is\n * UX-only and is forgeable by a hostile client. See the security block at the\n * top of `claims.ts`.\n *\n * @packageDocumentation\n */\n\nexport type {\n MPFRole,\n MPFClaims,\n MPFClaimsV1,\n MPFTenantClaim,\n ClaimsParser,\n} from './claims-types.js';\n\nexport {\n decodeAccessToken,\n tryDecodeAccessToken,\n listTenants,\n rolesForTenant,\n defaultRoleForTenant,\n canSelect,\n expiresAt,\n isExpired,\n tokenLifetimeSeconds,\n clearClaimsCache,\n registerClaimsParser,\n supportedClaimsVersions,\n} from './claims.js';\n\nexport {\n MPFClaimsError,\n MPFUnsupportedClaimsVersionError,\n DeadSessionError,\n isDeadSession,\n} from './errors.js';\n\nexport {\n createTenantSelection,\n type TenantSelection,\n type TenantSelectionStore,\n type TenantSelectionPersistence,\n type CreateTenantSelectionOptions,\n} from './tenant-selection.js';\n\nexport {\n createDefaultStorage,\n createSessionStorage,\n createMemoryStorage,\n type MPFAuthStorage,\n} from './storage.js';\n\nexport {\n createOAuthStateStore,\n type OAuthStateStore,\n type OAuthStateStoreOptions,\n} from './oauth-state.js';\n\nexport {\n createCrossTabCoordinator,\n applyJitter,\n type CrossTabCoordinator,\n type CrossTabMode,\n type CrossTabOptions,\n type TokenBroadcast,\n type LockManagerLike,\n type BroadcastChannelLike,\n} from './cross-tab.js';\n\nexport {\n TENANT_DIRECTORY_QUERY,\n type TenantDirectoryEntry,\n type TenantDirectoryLoader,\n} from './tenant-directory.js';\n\nexport {\n SessionManager,\n createSessionManager,\n isSupersededError,\n REFRESH_TOKEN_SUPERSEDED,\n type SessionManagerOptions,\n type SignOutResult,\n type TokenPair,\n} from './session-manager.js';\n","/**\n * OAuth `state` persistence — CSRF protection for the Google OAuth flow.\n *\n * ## What the state is for\n *\n * Without it, an OAuth callback route accepts *any* `code` presented to it. An\n * attacker who obtains an authorization code for their own account can send the\n * victim to `.../callback?code=<attacker's code>`; the victim's browser posts it,\n * the server exchanges it, and the victim is now signed in **as the attacker** —\n * whose account the attacker still controls. Anything the victim then does\n * (entering payment details, uploading documents, accepting an invitation)\n * happens inside the attacker's account.\n *\n * That is not theoretical: it was confirmed exploitable end to end in the\n * reference boilerplate, whose callback route discarded `state` and auto-posted\n * `code` on page load with no user interaction.\n *\n * The state closes it: the server mints an unguessable value, the client stores\n * it before redirecting, and the server accepts the callback only if the value\n * comes back. An attacker cannot produce a state that matches the victim's\n * browser storage.\n *\n * ## Why this store exists\n *\n * The state must survive a **full-page redirect to Google and back**, so it\n * cannot live in memory or in React state. That leaves web storage, and\n * \"remember to persist a value across a redirect\" is precisely the step the\n * boilerplate omitted. This makes it one call each way.\n *\n * Sending the state stays explicit: `GoogleOAuthCallbackRequest.state` is\n * required, so TypeScript rejects a callback that forgets it. This store solves\n * *where to keep it*, not *whether to send it* — a security parameter should not\n * be invisibly injected.\n */\n\nimport { createSessionStorage, type MPFAuthStorage } from './storage.js';\n\n/** Default storage key for the pending OAuth state. */\nconst DEFAULT_OAUTH_STATE_KEY = 'mpf.oauth_state';\n\nexport interface OAuthStateStore {\n /** Persist the state returned by `getGoogleOAuthURL`, before redirecting. */\n save(state: string): void;\n /**\n * Read and **clear** the stored state, for sending on the callback.\n *\n * Single-use by design: a state that could be replayed is not much better than\n * no state at all, and the server also rejects reuse.\n */\n take(): string | null;\n /** Read without clearing. For diagnostics; prefer `take()` in the flow. */\n peek(): string | null;\n /** Discard any pending state (e.g. the user abandoned the flow). */\n clear(): void;\n}\n\nexport interface OAuthStateStoreOptions {\n /**\n * Where to keep the pending state.\n *\n * Defaults to `sessionStorage`, which is the right default for a redirect\n * flow: it is scoped to the tab that started it, so a state minted in one tab\n * cannot be consumed by another, and it is cleared when the tab closes.\n *\n * **Limitation worth knowing:** `sessionStorage` is per-tab, so a callback\n * that lands in a *different* tab (a provider opening a new tab, or the user\n * moving the URL) will not find the state and the callback will be rejected —\n * failing closed, which is the correct direction, but it will look like a\n * spurious error. Pass `createDefaultStorage()` (localStorage) if your flow\n * can land in another tab, accepting that the state is then visible to every\n * tab on the origin.\n *\n * A cookie is not offered: one set from JavaScript cannot be `httpOnly`, so it\n * is no less readable than web storage while adding `SameSite` pitfalls on the\n * cross-site return leg.\n */\n storage?: MPFAuthStorage;\n /** Storage key. @default 'mpf.oauth_state' */\n storageKey?: string;\n}\n\n/**\n * Create a store for the pending OAuth state.\n *\n * @example\n * ```ts\n * const oauthState = createOAuthStateStore();\n * const auth = createAuthClient({ baseUrl, oauthStateStore: oauthState });\n *\n * // Starting the flow — the client saves the state for you.\n * const { url } = await auth.getGoogleOAuthURL({ redirect_uri });\n * window.location.href = url;\n *\n * // On the callback route, send it back explicitly.\n * const state = oauthState.take();\n * if (!state) throw new Error('No pending OAuth flow in this tab');\n * const tokens = await auth.handleGoogleOAuthCallback({ code, redirect_uri, state });\n * ```\n */\nexport function createOAuthStateStore(\n options: OAuthStateStoreOptions = {},\n): OAuthStateStore {\n const storage = options.storage ?? createSessionStorage();\n const key = options.storageKey ?? DEFAULT_OAUTH_STATE_KEY;\n\n return {\n save(state) {\n if (typeof state !== 'string' || state === '') {\n // Storing an empty state would guarantee a failed callback later, with\n // the cause long out of view. Fail where the mistake is.\n throw new Error('[MPF SDK] Refusing to store an empty OAuth state');\n }\n try {\n storage.setItem(key, state);\n } catch {\n // A blocked or full quota must not break starting the flow; the callback\n // will fail closed instead.\n }\n },\n\n take() {\n const state = this.peek();\n this.clear();\n return state;\n },\n\n peek() {\n try {\n const value = storage.getItem(key);\n return value === '' ? null : value;\n } catch {\n return null;\n }\n },\n\n clear() {\n try {\n storage.removeItem(key);\n } catch {\n // ignore\n }\n },\n };\n}\n","/**\n * Tenant display metadata — names and slugs.\n *\n * ## Why this is separate from the claim set\n *\n * The access token's `tnts` array carries tenant **ids and roles only**. That is\n * the authoritative, freshest answer to \"which tenants may this user select\",\n * and it needs no network call — but it renders a switcher full of UUIDs.\n *\n * Names live in the database (`identity.tenants`), so fetching them is a real\n * network operation that can really fail. Keeping it in its own module, behind\n * its own state, means the two questions stay distinguishable:\n *\n * - \"which tenants can I select?\" — claims, synchronous, cannot fail\n * - \"what are they called?\" — network, asynchronous, can fail\n *\n * Merging them would make a failed name lookup indistinguishable from having no\n * tenants, which is the exact bug class the tri-state exists to prevent.\n *\n * ## Reachability\n *\n * The `tenantless` Hasura role has select permissions on `identity_tenants`\n * scoped to `X-Hasura-User-Id`, so a signed-in user with NO tenant selected can\n * read the names of the tenants they belong to. That is precisely the switcher's\n * situation.\n */\n\n/** Display metadata for one tenant. */\nexport interface TenantDirectoryEntry {\n id: string;\n name?: string;\n slug?: string;\n}\n\n/**\n * Loads display metadata for the given tenant ids.\n *\n * Injected rather than implemented here so that neither this module nor the\n * React layer has to depend on the GraphQL client — which keeps `src/auth/` a\n * leaf and the React layer free of `src/client/graphql/`. Use\n * {@link TENANT_DIRECTORY_QUERY} with whichever GraphQL client the app already\n * has.\n */\nexport type TenantDirectoryLoader = (\n tenantIds: string[],\n) => Promise<TenantDirectoryEntry[]>;\n\n/**\n * A ready-made query for the loader.\n *\n * Runs under the `tenantless` role — i.e. with no `X-MPF-Tenant` header — which\n * is why a switcher can populate itself before any tenant is selected.\n *\n * @example\n * ```ts\n * import { TENANT_DIRECTORY_QUERY } from '@mosano-product-framework/sdk/auth';\n *\n * const loadTenantDirectory = async () => {\n * const data = await gql.query(TENANT_DIRECTORY_QUERY, undefined, { tenant: null });\n * return data.identity_tenants;\n * };\n * ```\n */\nexport const TENANT_DIRECTORY_QUERY = `query MPFTenantDirectory {\n identity_tenants {\n id\n name\n slug\n }\n}`;\n"]}
@@ -1,5 +1,5 @@
1
- import { MPFClient, attachStandardMiddlewares } from './chunk-LRM6JJ63.js';
2
- import { __export } from './chunk-AJWM5MDZ.js';
1
+ import { MPFClient, attachStandardMiddlewares } from './chunk-3OKR4MSV.js';
2
+ import { __export } from './chunk-S5DKGPUY.js';
3
3
 
4
4
  // src/storage/index.ts
5
5
  var storage_exports = {};
@@ -208,5 +208,5 @@ function createStorageClient(options) {
208
208
  }
209
209
 
210
210
  export { StorageClientV0, createStorageClient, storage_exports };
211
- //# sourceMappingURL=chunk-EXPYHNPV.js.map
212
- //# sourceMappingURL=chunk-EXPYHNPV.js.map
211
+ //# sourceMappingURL=chunk-HZWDNBJS.js.map
212
+ //# sourceMappingURL=chunk-HZWDNBJS.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/storage/index.ts","../src/storage/v0.ts"],"names":[],"mappings":"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","file":"chunk-EXPYHNPV.js","sourcesContent":["import { attachStandardMiddlewares } from '../client/core/client-factory.js';\nimport type { StorageClientOptions } from './types.js';\nimport { StorageClientV0 } from './v0.js';\n\n/**\n * Creates a Storage client for API v0\n *\n * @param options - Client configuration options\n * @returns StorageClientV0 instance\n *\n * @example\n * ```typescript\n * import { createStorageClient } from '@mosano-product-framework/sdk/storage';\n *\n * // Create authenticated storage client\n * const storage = createStorageClient({\n * baseUrl: 'https://api.example.com/storage',\n * tokenProvider: {\n * getAccessToken: () => localStorage.getItem('accessToken'),\n * onTokenExpired: () => { window.location.href = '/login'; },\n * },\n * });\n *\n * // Upload a file\n * const metadata = await storage.uploadFile(myFile);\n *\n * // Get a download URL\n * const { url } = await storage.getDownloadSignedUrl(metadata.id);\n * ```\n */\nexport function createStorageClient(options: StorageClientOptions): StorageClientV0 {\n const client = new StorageClientV0(options);\n\n attachStandardMiddlewares(client, {\n tokenProvider: options.tokenProvider,\n tenantProvider: options.tenantProvider,\n });\n\n return client;\n}\n\nexport { StorageClientV0 } from './v0.js';\nexport * from './types.js';\n","import { MPFClient } from '../client/core/client.js';\nimport type { RequestOptions } from '../client/core/types.js';\nimport type {\n StorageClientOptions,\n FileMetadataResponse,\n UploadSignedUrlResponse,\n DownloadSignedUrlResponse,\n GetUploadSignedUrlRequest,\n GetBatchUploadSignedUrlsRequest,\n GetBatchDownloadSignedUrlsRequest,\n BatchDeleteFilesRequest,\n BatchConfirmUploadsRequest,\n BatchReplaceFilesEntry,\n} from './types.js';\n\n/**\n * Storage API v0 client\n *\n * Provides methods for file upload, download, confirmation, deletion,\n * and replacement using both signed URLs and direct multipart uploads.\n */\nexport class StorageClientV0 extends MPFClient {\n constructor(options: StorageClientOptions) {\n super({\n baseUrl: `${options.baseUrl}/v0`,\n headers: options.headers,\n middlewares: options.middlewares,\n });\n }\n\n // ============================================================================\n // Signed URL Upload\n // ============================================================================\n\n /**\n * Get a signed URL for uploading a single file\n *\n * @param data - File name and MIME type\n * @param options - Optional request options\n * @returns Signed URL and file ID\n */\n async getUploadSignedUrl(\n data: GetUploadSignedUrlRequest,\n options?: RequestOptions,\n ): Promise<UploadSignedUrlResponse> {\n return this.post<UploadSignedUrlResponse>('/files/upload-url', data, options);\n }\n\n /**\n * Get signed URLs for uploading multiple files\n *\n * @param data - Array of file descriptors\n * @param options - Optional request options\n * @returns Array of signed URLs and file IDs\n */\n async getBatchUploadSignedUrls(\n data: GetBatchUploadSignedUrlsRequest,\n options?: RequestOptions,\n ): Promise<UploadSignedUrlResponse[]> {\n return this.post<UploadSignedUrlResponse[]>('/files/batch-upload-urls', data, options);\n }\n\n // ============================================================================\n // Direct Upload (multipart)\n // ============================================================================\n\n /**\n * Upload a single file via multipart form data\n *\n * @param file - The file or blob to upload\n * @param options - Optional request options\n * @returns File metadata\n */\n async uploadFile(\n file: Blob | File,\n options?: RequestOptions,\n ): Promise<FileMetadataResponse> {\n const form = new FormData();\n form.append('file', file);\n return this.multipartRequest<FileMetadataResponse>('POST', '/files/upload', form, options);\n }\n\n /**\n * Upload multiple files via multipart form data\n *\n * @param files - Array of files or blobs to upload\n * @param options - Optional request options\n * @returns Array of file metadata\n */\n async batchUploadFiles(\n files: (Blob | File)[],\n options?: RequestOptions,\n ): Promise<FileMetadataResponse[]> {\n const form = new FormData();\n for (const file of files) {\n form.append('files', file);\n }\n return this.multipartRequest<FileMetadataResponse[]>('POST', '/files/batch-upload', form, options);\n }\n\n // ============================================================================\n // Download\n // ============================================================================\n\n /**\n * Get a signed URL for downloading a file\n *\n * @param fileId - The file ID\n * @param options - Optional request options\n * @returns Signed download URL and optional metadata\n */\n async getDownloadSignedUrl(\n fileId: string,\n options?: RequestOptions,\n ): Promise<DownloadSignedUrlResponse> {\n return this.get<DownloadSignedUrlResponse>(`/files/${fileId}/download-url`, options);\n }\n\n /**\n * Get signed URLs for downloading multiple files\n *\n * @param data - Array of file IDs\n * @param options - Optional request options\n * @returns Array of signed download URLs\n */\n async getBatchDownloadSignedUrls(\n data: GetBatchDownloadSignedUrlsRequest,\n options?: RequestOptions,\n ): Promise<DownloadSignedUrlResponse[]> {\n return this.post<DownloadSignedUrlResponse[]>('/files/batch-download-urls', data, options);\n }\n\n // ============================================================================\n // Confirm\n // ============================================================================\n\n /**\n * Confirm a single file upload\n *\n * @param fileId - The file ID to confirm\n * @param options - Optional request options\n * @returns Updated file metadata\n */\n async confirmUpload(\n fileId: string,\n options?: RequestOptions,\n ): Promise<FileMetadataResponse> {\n return this.post<FileMetadataResponse>(`/files/${fileId}/confirm`, undefined, options);\n }\n\n /**\n * Confirm multiple file uploads\n *\n * @param data - Array of file IDs to confirm\n * @param options - Optional request options\n * @returns Array of updated file metadata\n */\n async batchConfirmUploads(\n data: BatchConfirmUploadsRequest,\n options?: RequestOptions,\n ): Promise<FileMetadataResponse[]> {\n return this.post<FileMetadataResponse[]>('/files/batch-confirm', data, options);\n }\n\n // ============================================================================\n // Delete\n // ============================================================================\n\n /**\n * Delete a single file\n *\n * @param fileId - The file ID to delete\n * @param options - Optional request options\n * @returns Deleted file metadata\n */\n async deleteFile(\n fileId: string,\n options?: RequestOptions,\n ): Promise<FileMetadataResponse> {\n return this.delete<FileMetadataResponse>(`/files/${fileId}`, undefined, options);\n }\n\n /**\n * Delete multiple files\n *\n * @param data - Array of file IDs to delete\n * @param options - Optional request options\n * @returns Array of deleted file metadata\n */\n async batchDeleteFiles(\n data: BatchDeleteFilesRequest,\n options?: RequestOptions,\n ): Promise<FileMetadataResponse[]> {\n return this.post<FileMetadataResponse[]>('/files/batch-delete', data, options);\n }\n\n // ============================================================================\n // Replace (multipart)\n // ============================================================================\n\n /**\n * Replace a single file via multipart form data\n *\n * @param fileId - The file ID to replace\n * @param file - The new file or blob\n * @param options - Optional request options\n * @returns Updated file metadata\n */\n async replaceFile(\n fileId: string,\n file: Blob | File,\n options?: RequestOptions,\n ): Promise<FileMetadataResponse> {\n const form = new FormData();\n form.append('file', file);\n return this.multipartRequest<FileMetadataResponse>('PUT', `/files/${fileId}`, form, options);\n }\n\n /**\n * Replace multiple files via multipart form data\n *\n * @param replacements - Array of file ID and new file pairs\n * @param options - Optional request options\n * @returns Array of updated file metadata\n */\n async batchReplaceFiles(\n replacements: BatchReplaceFilesEntry[],\n options?: RequestOptions,\n ): Promise<FileMetadataResponse[]> {\n const form = new FormData();\n for (const entry of replacements) {\n form.append('file_ids', entry.fileId);\n form.append('files', entry.file);\n }\n return this.multipartRequest<FileMetadataResponse[]>('POST', '/files/batch-replace', form, options);\n }\n\n // ============================================================================\n // Private helpers\n // ============================================================================\n\n /**\n * Execute a multipart/form-data request.\n *\n * Delegates to `MPFClient.request()` so multipart uploads get the same\n * middleware chain and retry loop as every other request. `request()` drops\n * the default JSON `Content-Type` for a `FormData` body so that fetch sets\n * `multipart/form-data` with the correct boundary string.\n *\n * `FormData` is re-readable, so a failed upload is safely retryable.\n */\n private async multipartRequest<T>(\n method: string,\n path: string,\n form: FormData,\n options?: RequestOptions,\n ): Promise<T> {\n return this.request<T>(\n path,\n {\n method,\n body: form,\n headers: options?.headers,\n signal: options?.signal,\n },\n options?.params,\n options,\n );\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/storage/index.ts","../src/storage/v0.ts"],"names":[],"mappings":"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","file":"chunk-HZWDNBJS.js","sourcesContent":["import { attachStandardMiddlewares } from '../client/core/client-factory.js';\nimport type { StorageClientOptions } from './types.js';\nimport { StorageClientV0 } from './v0.js';\n\n/**\n * Creates a Storage client for API v0\n *\n * @param options - Client configuration options\n * @returns StorageClientV0 instance\n *\n * @example\n * ```typescript\n * import { createStorageClient } from '@mosano-product-framework/sdk/storage';\n *\n * // Create authenticated storage client\n * const storage = createStorageClient({\n * baseUrl: 'https://api.example.com/storage',\n * tokenProvider: {\n * getAccessToken: () => localStorage.getItem('accessToken'),\n * onTokenExpired: () => { window.location.href = '/login'; },\n * },\n * });\n *\n * // Upload a file\n * const metadata = await storage.uploadFile(myFile);\n *\n * // Get a download URL\n * const { url } = await storage.getDownloadSignedUrl(metadata.id);\n * ```\n */\nexport function createStorageClient(options: StorageClientOptions): StorageClientV0 {\n const client = new StorageClientV0(options);\n\n attachStandardMiddlewares(client, {\n tokenProvider: options.tokenProvider,\n tenantProvider: options.tenantProvider,\n });\n\n return client;\n}\n\nexport { StorageClientV0 } from './v0.js';\nexport * from './types.js';\n","import { MPFClient } from '../client/core/client.js';\nimport type { RequestOptions } from '../client/core/types.js';\nimport type {\n StorageClientOptions,\n FileMetadataResponse,\n UploadSignedUrlResponse,\n DownloadSignedUrlResponse,\n GetUploadSignedUrlRequest,\n GetBatchUploadSignedUrlsRequest,\n GetBatchDownloadSignedUrlsRequest,\n BatchDeleteFilesRequest,\n BatchConfirmUploadsRequest,\n BatchReplaceFilesEntry,\n} from './types.js';\n\n/**\n * Storage API v0 client\n *\n * Provides methods for file upload, download, confirmation, deletion,\n * and replacement using both signed URLs and direct multipart uploads.\n */\nexport class StorageClientV0 extends MPFClient {\n constructor(options: StorageClientOptions) {\n super({\n baseUrl: `${options.baseUrl}/v0`,\n headers: options.headers,\n middlewares: options.middlewares,\n });\n }\n\n // ============================================================================\n // Signed URL Upload\n // ============================================================================\n\n /**\n * Get a signed URL for uploading a single file\n *\n * @param data - File name and MIME type\n * @param options - Optional request options\n * @returns Signed URL and file ID\n */\n async getUploadSignedUrl(\n data: GetUploadSignedUrlRequest,\n options?: RequestOptions,\n ): Promise<UploadSignedUrlResponse> {\n return this.post<UploadSignedUrlResponse>('/files/upload-url', data, options);\n }\n\n /**\n * Get signed URLs for uploading multiple files\n *\n * @param data - Array of file descriptors\n * @param options - Optional request options\n * @returns Array of signed URLs and file IDs\n */\n async getBatchUploadSignedUrls(\n data: GetBatchUploadSignedUrlsRequest,\n options?: RequestOptions,\n ): Promise<UploadSignedUrlResponse[]> {\n return this.post<UploadSignedUrlResponse[]>('/files/batch-upload-urls', data, options);\n }\n\n // ============================================================================\n // Direct Upload (multipart)\n // ============================================================================\n\n /**\n * Upload a single file via multipart form data\n *\n * @param file - The file or blob to upload\n * @param options - Optional request options\n * @returns File metadata\n */\n async uploadFile(\n file: Blob | File,\n options?: RequestOptions,\n ): Promise<FileMetadataResponse> {\n const form = new FormData();\n form.append('file', file);\n return this.multipartRequest<FileMetadataResponse>('POST', '/files/upload', form, options);\n }\n\n /**\n * Upload multiple files via multipart form data\n *\n * @param files - Array of files or blobs to upload\n * @param options - Optional request options\n * @returns Array of file metadata\n */\n async batchUploadFiles(\n files: (Blob | File)[],\n options?: RequestOptions,\n ): Promise<FileMetadataResponse[]> {\n const form = new FormData();\n for (const file of files) {\n form.append('files', file);\n }\n return this.multipartRequest<FileMetadataResponse[]>('POST', '/files/batch-upload', form, options);\n }\n\n // ============================================================================\n // Download\n // ============================================================================\n\n /**\n * Get a signed URL for downloading a file\n *\n * @param fileId - The file ID\n * @param options - Optional request options\n * @returns Signed download URL and optional metadata\n */\n async getDownloadSignedUrl(\n fileId: string,\n options?: RequestOptions,\n ): Promise<DownloadSignedUrlResponse> {\n return this.get<DownloadSignedUrlResponse>(`/files/${fileId}/download-url`, options);\n }\n\n /**\n * Get signed URLs for downloading multiple files\n *\n * @param data - Array of file IDs\n * @param options - Optional request options\n * @returns Array of signed download URLs\n */\n async getBatchDownloadSignedUrls(\n data: GetBatchDownloadSignedUrlsRequest,\n options?: RequestOptions,\n ): Promise<DownloadSignedUrlResponse[]> {\n return this.post<DownloadSignedUrlResponse[]>('/files/batch-download-urls', data, options);\n }\n\n // ============================================================================\n // Confirm\n // ============================================================================\n\n /**\n * Confirm a single file upload\n *\n * @param fileId - The file ID to confirm\n * @param options - Optional request options\n * @returns Updated file metadata\n */\n async confirmUpload(\n fileId: string,\n options?: RequestOptions,\n ): Promise<FileMetadataResponse> {\n return this.post<FileMetadataResponse>(`/files/${fileId}/confirm`, undefined, options);\n }\n\n /**\n * Confirm multiple file uploads\n *\n * @param data - Array of file IDs to confirm\n * @param options - Optional request options\n * @returns Array of updated file metadata\n */\n async batchConfirmUploads(\n data: BatchConfirmUploadsRequest,\n options?: RequestOptions,\n ): Promise<FileMetadataResponse[]> {\n return this.post<FileMetadataResponse[]>('/files/batch-confirm', data, options);\n }\n\n // ============================================================================\n // Delete\n // ============================================================================\n\n /**\n * Delete a single file\n *\n * @param fileId - The file ID to delete\n * @param options - Optional request options\n * @returns Deleted file metadata\n */\n async deleteFile(\n fileId: string,\n options?: RequestOptions,\n ): Promise<FileMetadataResponse> {\n return this.delete<FileMetadataResponse>(`/files/${fileId}`, undefined, options);\n }\n\n /**\n * Delete multiple files\n *\n * @param data - Array of file IDs to delete\n * @param options - Optional request options\n * @returns Array of deleted file metadata\n */\n async batchDeleteFiles(\n data: BatchDeleteFilesRequest,\n options?: RequestOptions,\n ): Promise<FileMetadataResponse[]> {\n return this.post<FileMetadataResponse[]>('/files/batch-delete', data, options);\n }\n\n // ============================================================================\n // Replace (multipart)\n // ============================================================================\n\n /**\n * Replace a single file via multipart form data\n *\n * @param fileId - The file ID to replace\n * @param file - The new file or blob\n * @param options - Optional request options\n * @returns Updated file metadata\n */\n async replaceFile(\n fileId: string,\n file: Blob | File,\n options?: RequestOptions,\n ): Promise<FileMetadataResponse> {\n const form = new FormData();\n form.append('file', file);\n return this.multipartRequest<FileMetadataResponse>('PUT', `/files/${fileId}`, form, options);\n }\n\n /**\n * Replace multiple files via multipart form data\n *\n * @param replacements - Array of file ID and new file pairs\n * @param options - Optional request options\n * @returns Array of updated file metadata\n */\n async batchReplaceFiles(\n replacements: BatchReplaceFilesEntry[],\n options?: RequestOptions,\n ): Promise<FileMetadataResponse[]> {\n const form = new FormData();\n for (const entry of replacements) {\n form.append('file_ids', entry.fileId);\n form.append('files', entry.file);\n }\n return this.multipartRequest<FileMetadataResponse[]>('POST', '/files/batch-replace', form, options);\n }\n\n // ============================================================================\n // Private helpers\n // ============================================================================\n\n /**\n * Execute a multipart/form-data request.\n *\n * Delegates to `MPFClient.request()` so multipart uploads get the same\n * middleware chain and retry loop as every other request. `request()` drops\n * the default JSON `Content-Type` for a `FormData` body so that fetch sets\n * `multipart/form-data` with the correct boundary string.\n *\n * `FormData` is re-readable, so a failed upload is safely retryable.\n */\n private async multipartRequest<T>(\n method: string,\n path: string,\n form: FormData,\n options?: RequestOptions,\n ): Promise<T> {\n return this.request<T>(\n path,\n {\n method,\n body: form,\n headers: options?.headers,\n signal: options?.signal,\n },\n options?.params,\n options,\n );\n }\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { tryDecodeAccessToken, tokenLifetimeSeconds, isExpired, DeadSessionError, MPFAuthError } from './chunk-AJWM5MDZ.js';
1
+ import { tryDecodeAccessToken, tokenLifetimeSeconds, isExpired, DeadSessionError, MPFAuthError } from './chunk-S5DKGPUY.js';
2
2
 
3
3
  // src/auth/tenant-selection.ts
4
4
  var DEFAULT_SELECTION_KEY = "mpf.tenant_selection";
@@ -617,5 +617,5 @@ function createSessionManager(options) {
617
617
  }
618
618
 
619
619
  export { REFRESH_TOKEN_SUPERSEDED, SessionManager, applyJitter, createCrossTabCoordinator, createDefaultStorage, createMemoryStorage, createSessionManager, createSessionStorage, createTenantSelection, isSupersededError };
620
- //# sourceMappingURL=chunk-7WAV52EO.js.map
621
- //# sourceMappingURL=chunk-7WAV52EO.js.map
620
+ //# sourceMappingURL=chunk-NM63E6OB.js.map
621
+ //# sourceMappingURL=chunk-NM63E6OB.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/auth/tenant-selection.ts","../src/auth/storage.ts","../src/auth/cross-tab.ts","../src/auth/session-manager.ts"],"names":[],"mappings":"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","file":"chunk-7WAV52EO.js","sourcesContent":["/**\n * Tenant selection — a dependency-free mutable leaf.\n *\n * Deliberately imports nothing but its own types, so it can be constructed\n * BEFORE any client and then shared by the identity, storage and GraphQL\n * clients without an import cycle:\n *\n * ```ts\n * const tenants = createTenantSelection();\n * const auth = createAuthClient({ baseUrl, tokenProvider, tenantProvider: tenants });\n * const storage = createStorageClient({ baseUrl, tokenProvider, tenantProvider: tenants });\n * tenants.set(tenantId); // both clients now scope to it, no reconstruction\n * ```\n */\n\nimport type { MPFRole } from './claims-types.js';\nimport type { MPFAuthStorage } from './storage.js';\n\n/**\n * The currently selected tenant.\n */\nexport interface TenantSelection {\n /** Selected tenant id, or `null` for the no-tenant (`drl`) path. */\n tenant: string | null;\n /**\n * Selected role. `undefined` lets the server apply that tenant's default\n * role (`tnts[].dfr`, else `tnts[].rls[0]`).\n */\n role?: MPFRole;\n}\n\n/**\n * A mutable, observable tenant selection.\n */\nexport interface TenantSelectionStore {\n /** Current selection. */\n get(): TenantSelection;\n /**\n * Select a tenant.\n *\n * Synchronous by design — selecting a tenant is a header change, so there is\n * no renew, no await and no network call.\n *\n * @param tenant - Tenant id, or `null` for the no-tenant path.\n * @param role - Optional role; omit to let the server apply the default.\n */\n set(tenant: string | null, role?: MPFRole): void;\n /** Reset to the no-tenant path. */\n clear(): void;\n /**\n * Observe changes. Returns an unsubscribe function.\n *\n * The listener is called only when the selection actually changes.\n */\n subscribe(listener: (selection: TenantSelection) => void): () => void;\n}\n\n/**\n * Persistence options for the tenant selection.\n *\n * The selected tenant IS persisted (unlike the access token). A tenant uuid the\n * user demonstrably has access to is not credential material, and losing the\n * selection on reload is a real UX regression: pick a tenant, refresh, land back\n * in the no-tenant state. Reading it synchronously at construction is also what\n * makes the provider's anti-flash hydration possible.\n *\n * Note this is a UX convenience only. A tampered value grants nothing — the\n * server re-derives tenant and role from the verified token on every request and\n * rejects a tenant the token does not hold.\n */\nexport interface TenantSelectionPersistence {\n /** Where to store it. Omit to disable persistence. */\n storage?: MPFAuthStorage;\n /** Storage key. @default 'mpf.tenant_selection' */\n storageKey?: string;\n}\n\nexport interface CreateTenantSelectionOptions extends TenantSelectionPersistence {\n /**\n * Initial selection. Takes precedence over a persisted value, so an app can\n * force a tenant (e.g. from a URL) without the stored one overriding it.\n */\n initial?: TenantSelection;\n}\n\nconst DEFAULT_SELECTION_KEY = 'mpf.tenant_selection';\n\nfunction readPersisted(\n storage: MPFAuthStorage | undefined,\n key: string,\n): TenantSelection | null {\n if (!storage) {\n return null;\n }\n\n try {\n const raw = storage.getItem(key);\n if (!raw) {\n return null;\n }\n const parsed = JSON.parse(raw) as Partial<TenantSelection>;\n if (typeof parsed.tenant !== 'string' || parsed.tenant === '') {\n return null;\n }\n return {\n tenant: parsed.tenant,\n ...(typeof parsed.role === 'string' && parsed.role !== '' ? { role: parsed.role } : {}),\n };\n } catch {\n // Corrupt or unreadable storage must never break client construction.\n return null;\n }\n}\n\n/**\n * Create a tenant selection store.\n *\n * Accepts either a bare initial selection (back-compatible) or an options\n * object enabling persistence.\n *\n * @example\n * ```ts\n * const tenants = createTenantSelection({ storage: createDefaultStorage() });\n * // survives a page reload\n * ```\n */\nexport function createTenantSelection(\n initialOrOptions?: TenantSelection | CreateTenantSelectionOptions,\n): TenantSelectionStore {\n const isOptions =\n initialOrOptions !== undefined &&\n !('tenant' in initialOrOptions);\n\n const options: CreateTenantSelectionOptions = isOptions\n ? (initialOrOptions as CreateTenantSelectionOptions)\n : { ...(initialOrOptions ? { initial: initialOrOptions as TenantSelection } : {}) };\n\n const storage = options.storage;\n const storageKey = options.storageKey ?? DEFAULT_SELECTION_KEY;\n const initial = options.initial ?? readPersisted(storage, storageKey) ?? undefined;\n\n let current: TenantSelection = {\n tenant: initial?.tenant ?? null,\n ...(initial?.role !== undefined ? { role: initial.role } : {}),\n };\n\n function persist(): void {\n if (!storage) {\n return;\n }\n try {\n if (current.tenant === null) {\n storage.removeItem(storageKey);\n } else {\n storage.setItem(storageKey, JSON.stringify(current));\n }\n } catch {\n // A full or blocked quota must not break tenant selection.\n }\n }\n\n const listeners = new Set<(selection: TenantSelection) => void>();\n\n function emit(): void {\n for (const listener of [...listeners]) {\n listener(current);\n }\n }\n\n return {\n get() {\n return current;\n },\n\n set(tenant, role) {\n if (current.tenant === tenant && current.role === role) {\n return;\n }\n current = { tenant, ...(role !== undefined ? { role } : {}) };\n persist();\n emit();\n },\n\n clear() {\n if (current.tenant === null && current.role === undefined) {\n return;\n }\n current = { tenant: null };\n persist();\n emit();\n },\n\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n","/**\n * Pluggable persistence for the session manager.\n *\n * Only the REFRESH token is persisted. The access token stays in memory: at a\n * 15-minute TTL, persisting it buys almost nothing and it is the more dangerous\n * of the two to leave sitting in `localStorage`.\n */\n\n/**\n * Minimal synchronous key/value store.\n *\n * Matches the `localStorage` shape so the browser default is a pass-through.\n */\nexport interface MPFAuthStorage {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\n/**\n * An in-memory store. Used automatically when `window` is absent (Node, SSR,\n * the e2e suite) and available explicitly for tests.\n */\nexport function createMemoryStorage(): MPFAuthStorage {\n const map = new Map<string, string>();\n return {\n getItem: (key) => map.get(key) ?? null,\n setItem: (key, value) => {\n map.set(key, value);\n },\n removeItem: (key) => {\n map.delete(key);\n },\n };\n}\n\n/**\n * `sessionStorage` when it is usable, an in-memory store otherwise.\n *\n * Per-tab and cleared when the tab closes, which is what a redirect flow wants:\n * see `createOAuthStateStore`.\n */\nexport function createSessionStorage(): MPFAuthStorage {\n if (typeof window === 'undefined') {\n return createMemoryStorage();\n }\n\n try {\n const probe = '__mpf_probe__';\n window.sessionStorage.setItem(probe, probe);\n window.sessionStorage.removeItem(probe);\n return window.sessionStorage;\n } catch {\n return createMemoryStorage();\n }\n}\n\n/**\n * `localStorage` when it is usable, an in-memory store otherwise.\n *\n * Access is probed rather than assumed: Safari in private mode and blocked\n * third-party contexts expose `localStorage` but throw on write.\n */\nexport function createDefaultStorage(): MPFAuthStorage {\n if (typeof window === 'undefined') {\n return createMemoryStorage();\n }\n\n try {\n const probe = '__mpf_probe__';\n window.localStorage.setItem(probe, probe);\n window.localStorage.removeItem(probe);\n return window.localStorage;\n } catch {\n return createMemoryStorage();\n }\n}\n","/**\n * Cross-tab refresh coordination.\n *\n * ## The problem, precisely\n *\n * Multiple tabs of ONE browser share one login and read the same refresh token\n * from origin-scoped `localStorage`. The refresh token is single-use and\n * rotates, so if two tabs renew concurrently one of them loses and its token is\n * dead. Separate browsers, machines or devices get separate session rows and\n * separate refresh tokens, so they are not a concern here.\n *\n * With proactive refresh every tab's timer fires at roughly the same moment, so\n * this is contended by N tabs at once, not two.\n *\n * ## Mechanism\n *\n * `navigator.locks` (Web Locks) is a standardized cross-tab mutex and needs no\n * dependency on the primary path. Locks auto-release when a tab is destroyed,\n * so a closed tab cannot deadlock the others. A polyfill is loaded only where\n * the API is missing (pre-15.4 Safari, non-secure contexts).\n *\n * A lock ALONE is insufficient: N tabs would queue and then each renew in turn,\n * which is the exact problem the lock was supposed to solve. The session manager\n * therefore does double-checked locking — re-reading shared state INSIDE the\n * lock and adopting instead of renewing when another tab got there first.\n *\n * ## Fanout: BroadcastChannel, not localStorage\n *\n * The winner's fresh ACCESS token is broadcast in memory over a\n * `BroadcastChannel` rather than persisted, so losing tabs can adopt it without\n * a network call while the access token still never touches `localStorage`.\n * Where `BroadcastChannel` is unavailable, losers fall back to renewing with the\n * rotated refresh token — correct, just less efficient.\n *\n * Everything here no-ops when `window` is absent, so Node and the e2e suite are\n * unaffected.\n */\n\n/** A token pair shared between tabs. */\nexport interface TokenBroadcast {\n accessToken: string;\n refreshToken: string;\n}\n\n/** The subset of `LockManager` this module uses. */\nexport interface LockManagerLike {\n request(\n name: string,\n options: { signal?: AbortSignal },\n callback: () => Promise<unknown>,\n ): Promise<unknown>;\n}\n\n/** The subset of `BroadcastChannel` this module uses. */\nexport interface BroadcastChannelLike {\n postMessage(message: unknown): void;\n addEventListener(type: 'message', listener: (event: { data: unknown }) => void): void;\n removeEventListener(type: 'message', listener: (event: { data: unknown }) => void): void;\n close(): void;\n}\n\n/** How the coordinator is providing mutual exclusion. */\nexport type CrossTabMode = 'native' | 'polyfill' | 'none';\n\nexport interface CrossTabCoordinator {\n /** How mutual exclusion is being provided. */\n readonly mode: CrossTabMode;\n /** Run `fn` holding the cross-tab refresh lock. */\n withLock<T>(fn: () => Promise<T>): Promise<T>;\n /** Tell other tabs about a fresh token pair. */\n publish(message: TokenBroadcast): void;\n /** Observe other tabs' fresh token pairs. Returns an unsubscribe function. */\n subscribe(listener: (message: TokenBroadcast) => void): () => void;\n /** Release resources. */\n close(): void;\n}\n\nexport interface CrossTabOptions {\n /** Lock name. @default 'mpf.refresh' */\n lockName?: string;\n /** BroadcastChannel name. @default 'mpf.auth' */\n channelName?: string;\n /**\n * How long to wait for the lock before giving up and proceeding without it.\n * @default 5000\n */\n lockTimeoutMs?: number;\n /** Injectable lock manager. Defaults to `navigator.locks`. For tests. */\n locks?: LockManagerLike | null;\n /**\n * Loads a Web Locks polyfill when the native API is absent. Resolve `null` to\n * decline. The default declines — no polyfill is bundled, keeping the primary\n * path dependency-free; a consumer targeting old Safari supplies one here.\n */\n loadPolyfill?: () => Promise<LockManagerLike | null>;\n /** Injectable channel factory. Defaults to `BroadcastChannel`. For tests. */\n channelFactory?: (name: string) => BroadcastChannelLike | null;\n}\n\nfunction defaultLocks(): LockManagerLike | null {\n if (typeof navigator === 'undefined') {\n return null;\n }\n const locks = (navigator as unknown as { locks?: LockManagerLike }).locks;\n return locks ?? null;\n}\n\nfunction defaultChannel(name: string): BroadcastChannelLike | null {\n if (typeof BroadcastChannel === 'undefined') {\n return null;\n }\n try {\n return new BroadcastChannel(name) as unknown as BroadcastChannelLike;\n } catch {\n return null;\n }\n}\n\n/**\n * An abort signal that fires after `ms`.\n *\n * Uses `AbortSignal.timeout` where available and falls back to a controller,\n * because the fallback also has to work under fake timers in tests.\n */\nfunction timeoutSignal(ms: number): AbortSignal {\n const controller = new AbortController();\n setTimeout(() => controller.abort(new Error(`Lock wait timed out after ${ms}ms`)), ms);\n return controller.signal;\n}\n\n/**\n * Apply symmetric jitter to a delay.\n *\n * Without this, N tabs computed the same proactive-renew deadline from the same\n * token and all wake on the same millisecond, then serialize through the lock\n * for no reason. Result is clamped at 0.\n *\n * @param baseMs - The unjittered delay.\n * @param jitterMs - Maximum deviation in either direction.\n * @param random - Injectable `Math.random` for tests.\n */\nexport function applyJitter(baseMs: number, jitterMs: number, random: () => number = Math.random): number {\n if (jitterMs <= 0) {\n return Math.max(0, baseMs);\n }\n const offset = (random() * 2 - 1) * jitterMs;\n return Math.max(0, baseMs + offset);\n}\n\n/**\n * Create a cross-tab coordinator.\n *\n * Resolution order for the lock: injected `locks` → `navigator.locks` →\n * `loadPolyfill()` → none (run without coordination, correct in a\n * single-context environment like Node).\n */\nexport function createCrossTabCoordinator(options: CrossTabOptions = {}): CrossTabCoordinator {\n const {\n lockName = 'mpf.refresh',\n channelName = 'mpf.auth',\n lockTimeoutMs = 5000,\n loadPolyfill = async () => null,\n channelFactory = defaultChannel,\n } = options;\n\n const browser = typeof window !== 'undefined';\n\n // `locks: null` explicitly declines; `undefined` means \"detect\".\n const injected = options.locks;\n let locks: LockManagerLike | null = injected !== undefined ? injected : browser ? defaultLocks() : null;\n let mode: CrossTabMode = locks ? (injected !== undefined ? 'native' : 'native') : 'none';\n\n // Polyfill resolution is async, so it is kicked off once and awaited by the\n // first withLock() call that needs it.\n let polyfillPromise: Promise<void> | null = null;\n if (!locks && browser && injected === undefined) {\n polyfillPromise = loadPolyfill()\n .then((loaded) => {\n if (loaded) {\n locks = loaded;\n mode = 'polyfill';\n }\n })\n .catch(() => {\n // No polyfill available; stay uncoordinated rather than failing auth.\n });\n }\n\n const channel = browser ? channelFactory(channelName) : null;\n const listeners = new Set<(message: TokenBroadcast) => void>();\n\n const onMessage = (event: { data: unknown }): void => {\n const data = event.data as Partial<TokenBroadcast> | null;\n if (\n !data ||\n typeof data.accessToken !== 'string' ||\n typeof data.refreshToken !== 'string'\n ) {\n return;\n }\n const message: TokenBroadcast = {\n accessToken: data.accessToken,\n refreshToken: data.refreshToken,\n };\n for (const listener of [...listeners]) {\n listener(message);\n }\n };\n\n channel?.addEventListener('message', onMessage);\n\n return {\n get mode() {\n return mode;\n },\n\n async withLock<T>(fn: () => Promise<T>): Promise<T> {\n if (polyfillPromise) {\n await polyfillPromise;\n polyfillPromise = null;\n }\n\n if (!locks) {\n // No cross-tab contention possible (or no API to use). Run directly.\n return fn();\n }\n\n let result: T;\n let ran = false;\n\n try {\n await locks.request(lockName, { signal: timeoutSignal(lockTimeoutMs) }, async () => {\n result = await fn();\n ran = true;\n });\n } catch (error) {\n if (ran) {\n // fn() itself threw, or the lock released oddly after running it.\n throw error;\n }\n // We never got the lock — most likely a frozen tab is holding it\n // mid-renew. Proceeding unlocked risks a duplicate renew, which the\n // server reports as REFRESH_TOKEN_SUPERSEDED and the session manager\n // recovers from. Hanging here would leave the app with no token at all,\n // which is strictly worse.\n return fn();\n }\n\n // `ran` is true whenever the callback completed; TS cannot see that.\n return result!;\n },\n\n publish(message: TokenBroadcast) {\n channel?.postMessage(message);\n },\n\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n\n close() {\n listeners.clear();\n channel?.removeEventListener('message', onMessage);\n channel?.close();\n },\n };\n}\n","/**\n * Session manager — single access token, single refresh token.\n *\n * There is deliberately no per-tenant token map, no `renewScoped`, and no\n * scoped/general token split: tenant is a request header now, so all of that\n * machinery is gone by construction rather than by choice.\n *\n * What remains, and why:\n * - **Coalescing** — the refresh token is single-use, so concurrent renewals\n * must serialize into one network call or they kill each other.\n * - **Proactive timer** — renew before expiry so requests never pay for it.\n * - **Wake handling** — `setTimeout` does not fire in frozen or discarded tabs,\n * so the timer alone is not a safety net. See `handleWake`.\n * - **Cross-tab lock** — N tabs of one browser share one refresh token.\n */\n\nimport { MPFAuthError } from '../client/core/errors.js';\nimport { tryDecodeAccessToken, isExpired, tokenLifetimeSeconds } from './claims.js';\nimport type { MPFClaims } from './claims-types.js';\nimport { DeadSessionError } from './errors.js';\nimport { createDefaultStorage, type MPFAuthStorage } from './storage.js';\nimport {\n applyJitter,\n createCrossTabCoordinator,\n type CrossTabCoordinator,\n type TokenBroadcast,\n} from './cross-tab.js';\n\n/** Outcome of {@link SessionManager.signOut}. */\nexport interface SignOutResult {\n /** Whether the server-side revoke succeeded. */\n revoked: boolean;\n /**\n * Why the revoke failed, when it did. Local state is cleared regardless, so\n * this is informational — surface it if you want to tell the user the session\n * may still be live elsewhere.\n */\n error?: Error;\n}\n\n/** A token pair as returned by `POST /v0/sessions/renew`. */\nexport interface TokenPair {\n access_token: string;\n refresh_token: string;\n}\n\n/**\n * The server's code when a renew loses the rotation race — another tab renewed\n * with the same refresh token first.\n *\n * This is recoverable: the winner's token is valid, so adopt it instead of\n * declaring the session dead.\n */\nexport const REFRESH_TOKEN_SUPERSEDED = 'REFRESH_TOKEN_SUPERSEDED';\n\n/** Seconds of headroom before expiry at which a token counts as stale. */\nconst DEFAULT_BUFFER_SECONDS = 120;\n\n/** Floor between two network renewals. */\nconst DEFAULT_MIN_RENEW_INTERVAL_MS = 5_000;\n\n/** Proactive-timer jitter, in milliseconds, applied symmetrically. */\nconst DEFAULT_JITTER_MS = 30_000;\n\nexport interface SessionManagerOptions {\n /**\n * Performs the network renewal. Injected rather than taking an `AuthClientV0`\n * so this module stays a leaf and cannot form an import cycle with the\n * identity client that consumes it.\n */\n renew: (refreshToken: string) => Promise<TokenPair>;\n\n /**\n * Revokes the session server-side. Wire to\n * `(refreshToken) => authClient.revokeSession({ refresh_token: refreshToken })`.\n *\n * Injected for the same reason as {@link renew}: it keeps `src/auth/` a leaf.\n *\n * Supplying it is what makes {@link SessionManager.signOut} revoke rather than\n * only clearing locally. Without it, `signOut()` still clears — a sign-out\n * button must always sign the user out — but the refresh token stays valid\n * server-side until it expires.\n */\n revoke?: (refreshToken: string) => Promise<unknown>;\n\n /** Where the refresh token is persisted. @default localStorage, else memory */\n storage?: MPFAuthStorage;\n\n /** Storage key for the refresh token. @default 'mpf.refresh_token' */\n storageKey?: string;\n\n /**\n * Seconds before expiry at which a token is considered stale.\n *\n * @default 120\n *\n * 120 rather than 60: at a 900s TTL, a request *starting* at `exp − 61s`\n * passes a 60s freshness check and then carries a token that dies mid-flight,\n * which storage uploads routinely will. The buffer must cover renew latency\n * plus the longest in-flight request, and browser clock skew is unbounded.\n *\n * Clamped per token — see {@link SessionManager.effectiveBufferSeconds}.\n */\n bufferSeconds?: number;\n\n /**\n * Minimum gap between two network renewals. Second line of defence against a\n * renew loop caused by a misconfigured server TTL.\n * @default 5000\n */\n minRenewIntervalMs?: number;\n\n /** Symmetric jitter on the proactive timer. @default 30000 */\n jitterMs?: number;\n\n /** Called when the session cannot be renewed and the user must log in again. */\n onDeadSession?: (error: DeadSessionError) => void;\n\n /** Called whenever the in-memory token pair changes. */\n onTokensChanged?: (tokens: TokenPair | null) => void;\n\n /**\n * Cross-tab coordinator. Pass `null` to disable coordination entirely.\n * @default createCrossTabCoordinator()\n */\n crossTab?: CrossTabCoordinator | null;\n\n /** Injectable clock, in ms. @default Date.now */\n now?: () => number;\n\n /** Injectable randomness for jitter. @default Math.random */\n random?: () => number;\n\n /**\n * Whether to attach `visibilitychange` / `online` listeners.\n * @default true when `window` exists\n */\n listenToWake?: boolean;\n}\n\n/**\n * Manages one session's tokens.\n */\nexport class SessionManager {\n private accessToken: string | null = null;\n private refreshToken: string | null = null;\n\n /** In-flight renewal, for coalescing. */\n private inflight: Promise<string> | null = null;\n\n /**\n * Gate that `getAccessToken()` awaits. Set by the wake path so that requests\n * queued during a wake renewal block behind it instead of shipping a token\n * that is already dead.\n */\n private wakeGate: Promise<void> | null = null;\n\n private timer: ReturnType<typeof setTimeout> | null = null;\n private lastRenewAt = 0;\n private destroyed = false;\n /**\n * Incremented on every `clear()`. A renewal captures it before its network\n * call and refuses to adopt a result from a superseded generation, so signing\n * out (or clearing) mid-renew cannot be undone by the renewal completing.\n */\n private generation = 0;\n\n private readonly storage: MPFAuthStorage;\n private readonly storageKey: string;\n private readonly bufferSeconds: number;\n private readonly minRenewIntervalMs: number;\n private readonly jitterMs: number;\n private readonly now: () => number;\n private readonly random: () => number;\n private readonly crossTab: CrossTabCoordinator | null;\n private readonly unsubscribeBroadcast: (() => void) | null = null;\n private readonly wakeListening: boolean;\n\n constructor(private readonly options: SessionManagerOptions) {\n this.storage = options.storage ?? createDefaultStorage();\n this.storageKey = options.storageKey ?? 'mpf.refresh_token';\n this.bufferSeconds = options.bufferSeconds ?? DEFAULT_BUFFER_SECONDS;\n this.minRenewIntervalMs = options.minRenewIntervalMs ?? DEFAULT_MIN_RENEW_INTERVAL_MS;\n this.jitterMs = options.jitterMs ?? DEFAULT_JITTER_MS;\n this.now = options.now ?? (() => Date.now());\n this.random = options.random ?? Math.random;\n\n this.crossTab =\n options.crossTab === null ? null : (options.crossTab ?? createCrossTabCoordinator());\n\n // Adopt a sibling tab's fresh tokens without a network call.\n if (this.crossTab) {\n this.unsubscribeBroadcast = this.crossTab.subscribe((message) => {\n this.adopt(message, { rebroadcast: false });\n });\n }\n\n this.refreshToken = this.storage.getItem(this.storageKey);\n\n this.wakeListening = options.listenToWake ?? typeof window !== 'undefined';\n if (this.wakeListening && typeof window !== 'undefined') {\n window.addEventListener('visibilitychange', this.onWakeEvent);\n window.addEventListener('online', this.onWakeEvent);\n }\n }\n\n // -------------------------------------------------------------------------\n // Public API\n // -------------------------------------------------------------------------\n\n /**\n * Adopt a freshly minted token pair (after sign-in, or after any flow that\n * changes memberships).\n */\n setTokens(tokens: TokenPair): void {\n this.adopt(\n { accessToken: tokens.access_token, refreshToken: tokens.refresh_token },\n { rebroadcast: true },\n );\n }\n\n /**\n * A usable access token, renewing first if necessary.\n *\n * Returns `null` only when there is no session at all.\n */\n async getAccessToken(): Promise<string | null> {\n // Block behind a wake renewal rather than shipping a dead token.\n if (this.wakeGate) {\n await this.wakeGate;\n }\n\n if (!this.accessToken) {\n return this.refreshToken ? this.renew() : null;\n }\n\n if (this.isStale()) {\n return this.renew();\n }\n\n return this.accessToken;\n }\n\n /** The current access token without renewing. */\n peekAccessToken(): string | null {\n return this.accessToken;\n }\n\n /** Decoded (UNVERIFIED — UX only) claims of the current access token. */\n getClaims(): MPFClaims | null {\n return tryDecodeAccessToken(this.accessToken);\n }\n\n /**\n * Renew the session. Coalesced: concurrent callers share one network call,\n * which is mandatory because the refresh token is single-use.\n */\n async renew(): Promise<string> {\n if (this.inflight) {\n return this.inflight;\n }\n\n this.inflight = this.renewGuarded().finally(() => {\n this.inflight = null;\n });\n\n return this.inflight;\n }\n\n /**\n * Sign out: revoke the session server-side, then clear it locally.\n *\n * **Never throws, and always clears.** A user who clicked sign-out must end up\n * signed out locally whatever the network did — a UI that refuses to log out\n * is worse than a token that outlives its client, and the failed revoke leaves\n * that token behind either way. The outcome is reported in the return value so\n * an app can say \"signed out, but we couldn't reach the server\" rather than\n * having to guess.\n *\n * Order is load-bearing: `revokeSession` is an *authenticated* endpoint, so\n * the revoke must go out **before** the access token is cleared. Reversing it\n * silently sends an unauthenticated request that cannot succeed.\n *\n * Idempotent: with no session, it is a no-op reporting `revoked: false`.\n *\n * This exists so consumers never need the raw refresh token. Reaching into\n * storage for it — `localStorage.getItem('mpf.refresh_token')` — hardcodes the\n * default key and breaks for anyone using a custom storage or key.\n */\n async signOut(): Promise<SignOutResult> {\n const refreshToken = this.refreshToken;\n const revoker = this.options.revoke;\n\n let revoked = false;\n let error: Error | undefined;\n\n if (refreshToken && revoker) {\n try {\n await revoker(refreshToken);\n revoked = true;\n } catch (caught) {\n error = caught instanceof Error ? caught : new Error(String(caught));\n }\n }\n\n // Unconditional: see the note above.\n this.clear();\n\n return { revoked, ...(error ? { error } : {}) };\n }\n\n /**\n * Forget the session locally. Stops timers and invalidates any in-flight\n * renewal so it cannot resurrect the session after it resolves.\n *\n * Prefer {@link signOut} for a user-initiated sign-out: this leaves the\n * refresh token valid server-side.\n */\n clear(): void {\n // Bump first: a renewal already in flight checks this before adopting its\n // result, so a proactive renew that resolves after sign-out cannot put the\n // tokens back. Without it, \"timer fires, user clicks sign out, renew\n // resolves\" leaves the user signed in again.\n this.generation += 1;\n\n this.accessToken = null;\n this.refreshToken = null;\n this.storage.removeItem(this.storageKey);\n this.cancelTimer();\n this.options.onTokensChanged?.(null);\n }\n\n /** Detach listeners and release the coordinator. */\n destroy(): void {\n this.destroyed = true;\n this.cancelTimer();\n this.unsubscribeBroadcast?.();\n if (this.wakeListening && typeof window !== 'undefined') {\n window.removeEventListener('visibilitychange', this.onWakeEvent);\n window.removeEventListener('online', this.onWakeEvent);\n }\n this.crossTab?.close();\n }\n\n /**\n * The buffer actually in force for the current token.\n *\n * The configured buffer is clamped to just under half the token's lifetime:\n * if the buffer were ≥ half the lifetime, a freshly minted token would be\n * \"stale\" the instant it arrived and the manager would renew-loop, hammering\n * `/sessions/renew` forever. A 60s-TTL token therefore gets a 29s buffer, not\n * the configured 120s.\n *\n * Falls back to the configured value when the token carries no `iat` and the\n * lifetime is unknowable.\n */\n effectiveBufferSeconds(): number {\n const claims = this.getClaims();\n if (!claims) {\n return this.bufferSeconds;\n }\n\n const lifetime = tokenLifetimeSeconds(claims);\n if (lifetime === undefined) {\n return this.bufferSeconds;\n }\n\n const ceiling = Math.max(0, Math.floor(lifetime / 2) - 1);\n return Math.min(this.bufferSeconds, ceiling);\n }\n\n // -------------------------------------------------------------------------\n // Internals\n // -------------------------------------------------------------------------\n\n private isStale(): boolean {\n const claims = this.getClaims();\n if (!claims) {\n // Undecodable token: treat as stale so we renew rather than send garbage.\n return true;\n }\n return isExpired(claims, this.effectiveBufferSeconds(), this.now());\n }\n\n private adopt(tokens: TokenBroadcast, opts: { rebroadcast: boolean }): void {\n this.accessToken = tokens.accessToken;\n this.refreshToken = tokens.refreshToken;\n this.storage.setItem(this.storageKey, tokens.refreshToken);\n\n this.scheduleProactiveRenew();\n\n this.options.onTokensChanged?.({\n access_token: tokens.accessToken,\n refresh_token: tokens.refreshToken,\n });\n\n if (opts.rebroadcast) {\n this.crossTab?.publish(tokens);\n }\n }\n\n private async renewGuarded(): Promise<string> {\n if (!this.refreshToken) {\n return this.die(new DeadSessionError('No refresh token available'));\n }\n\n // Floor between network renewals. If we still hold a token, hand it back\n // rather than renewing again inside the floor.\n const sinceLast = this.now() - this.lastRenewAt;\n if (this.lastRenewAt !== 0 && sinceLast < this.minRenewIntervalMs && this.accessToken) {\n return this.accessToken;\n }\n\n const startedWith = this.refreshToken;\n\n const run = async (): Promise<string> => {\n // Double-checked locking: re-read shared state INSIDE the lock. Without\n // this, N tabs queue on the lock and then each renew in turn, which is\n // exactly what the lock was meant to prevent.\n const persisted = this.storage.getItem(this.storageKey);\n if (persisted && persisted !== startedWith) {\n // Another tab renewed while we waited. Its access token arrives over\n // the broadcast channel, so we may already hold a fresh one.\n this.refreshToken = persisted;\n if (this.accessToken && !this.isStale()) {\n return this.accessToken;\n }\n }\n\n return this.networkRenew();\n };\n\n return this.crossTab ? this.crossTab.withLock(run) : run();\n }\n\n private async networkRenew(): Promise<string> {\n const refreshToken = this.refreshToken;\n if (!refreshToken) {\n return this.die(new DeadSessionError('No refresh token available'));\n }\n\n this.lastRenewAt = this.now();\n const generation = this.generation;\n\n let pair: TokenPair;\n try {\n pair = await this.options.renew(refreshToken);\n } catch (error) {\n // Losing the rotation race is recoverable, not fatal: a sibling tab\n // renewed first and its token is valid. Adopt rather than logging out.\n if (isSupersededError(error)) {\n const adopted = await this.adoptAfterSuperseded();\n if (adopted) {\n return adopted;\n }\n }\n\n return this.die(\n new DeadSessionError(\n 'Session renewal failed',\n error instanceof Error ? error : undefined,\n ),\n );\n }\n\n if (generation !== this.generation) {\n // The session was cleared while this renewal was in flight (sign-out being\n // the realistic case). Adopting now would sign the user back in.\n return pair.access_token;\n }\n\n this.adopt(\n { accessToken: pair.access_token, refreshToken: pair.refresh_token },\n { rebroadcast: true },\n );\n\n return pair.access_token;\n }\n\n /**\n * After a REFRESH_TOKEN_SUPERSEDED, take the winner's tokens.\n *\n * The winner's access token arrives over the broadcast channel; its refresh\n * token is in storage. If neither is usable there is nothing to adopt and the\n * caller declares the session dead.\n */\n private async adoptAfterSuperseded(): Promise<string | null> {\n const persisted = this.storage.getItem(this.storageKey);\n if (persisted) {\n this.refreshToken = persisted;\n }\n\n if (this.accessToken && !this.isStale()) {\n return this.accessToken;\n }\n\n return null;\n }\n\n private die(error: DeadSessionError): never {\n this.clear();\n this.options.onDeadSession?.(error);\n throw error;\n }\n\n private cancelTimer(): void {\n if (this.timer !== null) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n }\n\n /**\n * Schedule a renewal shortly before the token goes stale.\n *\n * Jittered so that N tabs computing the same deadline from the same token do\n * not all wake on the same millisecond.\n */\n private scheduleProactiveRenew(): void {\n this.cancelTimer();\n\n if (this.destroyed) {\n return;\n }\n\n const claims = this.getClaims();\n if (!claims) {\n return;\n }\n\n const staleAtMs = (claims.exp - this.effectiveBufferSeconds()) * 1000;\n const base = Math.max(0, staleAtMs - this.now());\n const delay = Math.max(\n this.minRenewIntervalMs,\n applyJitter(base, this.jitterMs, this.random),\n );\n\n this.timer = setTimeout(() => {\n this.timer = null;\n void this.renew().catch(() => {\n // renew() already reported via onDeadSession.\n });\n }, delay);\n }\n\n private readonly onWakeEvent = (): void => {\n if (typeof document !== 'undefined' && document.visibilityState === 'hidden') {\n return;\n }\n void this.handleWake();\n };\n\n /**\n * Wake path — the real safety net.\n *\n * `setTimeout` does not fire in frozen or discarded tabs (Chrome tab\n * freezing, iOS Safari), so a tab backgrounded for 20 minutes wakes holding a\n * guaranteed-dead token with its timer never having run.\n *\n * Merely rescheduling here is wrong: the delay computes negative, becomes\n * `setTimeout(0)`, and then races whatever the app fires on focus. Instead\n * renew immediately and publish a gate that `getAccessToken()` awaits, so\n * requests triggered by the same focus event queue behind the renewal rather\n * than each eating a 401.\n */\n private async handleWake(): Promise<void> {\n if (this.destroyed || !this.refreshToken) {\n return;\n }\n\n if (this.accessToken && !this.isStale()) {\n // Token survived the nap; the timer may still be stale after a freeze.\n this.scheduleProactiveRenew();\n return;\n }\n\n if (this.wakeGate) {\n return;\n }\n\n let release!: () => void;\n this.wakeGate = new Promise<void>((resolve) => {\n release = resolve;\n });\n\n try {\n await this.renew();\n } catch {\n // Reported through onDeadSession already.\n } finally {\n const gate = release;\n this.wakeGate = null;\n gate();\n }\n }\n}\n\n/**\n * Whether an error is the server's \"another tab renewed first\" signal.\n *\n * Checks `serverCode` because `MPFAuthError.code` is derived from the error\n * classification and collapses every 401 to `UNAUTHORIZED`.\n */\nexport function isSupersededError(error: unknown): boolean {\n if (error instanceof MPFAuthError) {\n return error.serverCode === REFRESH_TOKEN_SUPERSEDED;\n }\n if (error && typeof error === 'object' && 'code' in error) {\n return (error as { code?: unknown }).code === REFRESH_TOKEN_SUPERSEDED;\n }\n return false;\n}\n\n/**\n * Create a session manager.\n */\nexport function createSessionManager(options: SessionManagerOptions): SessionManager {\n return new SessionManager(options);\n}\n"]}
1
+ {"version":3,"sources":["../src/auth/tenant-selection.ts","../src/auth/storage.ts","../src/auth/cross-tab.ts","../src/auth/session-manager.ts"],"names":[],"mappings":"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","file":"chunk-NM63E6OB.js","sourcesContent":["/**\n * Tenant selection — a dependency-free mutable leaf.\n *\n * Deliberately imports nothing but its own types, so it can be constructed\n * BEFORE any client and then shared by the identity, storage and GraphQL\n * clients without an import cycle:\n *\n * ```ts\n * const tenants = createTenantSelection();\n * const auth = createAuthClient({ baseUrl, tokenProvider, tenantProvider: tenants });\n * const storage = createStorageClient({ baseUrl, tokenProvider, tenantProvider: tenants });\n * tenants.set(tenantId); // both clients now scope to it, no reconstruction\n * ```\n */\n\nimport type { MPFRole } from './claims-types.js';\nimport type { MPFAuthStorage } from './storage.js';\n\n/**\n * The currently selected tenant.\n */\nexport interface TenantSelection {\n /** Selected tenant id, or `null` for the no-tenant (`drl`) path. */\n tenant: string | null;\n /**\n * Selected role. `undefined` lets the server apply that tenant's default\n * role (`tnts[].dfr`, else `tnts[].rls[0]`).\n */\n role?: MPFRole;\n}\n\n/**\n * A mutable, observable tenant selection.\n */\nexport interface TenantSelectionStore {\n /** Current selection. */\n get(): TenantSelection;\n /**\n * Select a tenant.\n *\n * Synchronous by design — selecting a tenant is a header change, so there is\n * no renew, no await and no network call.\n *\n * @param tenant - Tenant id, or `null` for the no-tenant path.\n * @param role - Optional role; omit to let the server apply the default.\n */\n set(tenant: string | null, role?: MPFRole): void;\n /** Reset to the no-tenant path. */\n clear(): void;\n /**\n * Observe changes. Returns an unsubscribe function.\n *\n * The listener is called only when the selection actually changes.\n */\n subscribe(listener: (selection: TenantSelection) => void): () => void;\n}\n\n/**\n * Persistence options for the tenant selection.\n *\n * The selected tenant IS persisted (unlike the access token). A tenant uuid the\n * user demonstrably has access to is not credential material, and losing the\n * selection on reload is a real UX regression: pick a tenant, refresh, land back\n * in the no-tenant state. Reading it synchronously at construction is also what\n * makes the provider's anti-flash hydration possible.\n *\n * Note this is a UX convenience only. A tampered value grants nothing — the\n * server re-derives tenant and role from the verified token on every request and\n * rejects a tenant the token does not hold.\n */\nexport interface TenantSelectionPersistence {\n /** Where to store it. Omit to disable persistence. */\n storage?: MPFAuthStorage;\n /** Storage key. @default 'mpf.tenant_selection' */\n storageKey?: string;\n}\n\nexport interface CreateTenantSelectionOptions extends TenantSelectionPersistence {\n /**\n * Initial selection. Takes precedence over a persisted value, so an app can\n * force a tenant (e.g. from a URL) without the stored one overriding it.\n */\n initial?: TenantSelection;\n}\n\nconst DEFAULT_SELECTION_KEY = 'mpf.tenant_selection';\n\nfunction readPersisted(\n storage: MPFAuthStorage | undefined,\n key: string,\n): TenantSelection | null {\n if (!storage) {\n return null;\n }\n\n try {\n const raw = storage.getItem(key);\n if (!raw) {\n return null;\n }\n const parsed = JSON.parse(raw) as Partial<TenantSelection>;\n if (typeof parsed.tenant !== 'string' || parsed.tenant === '') {\n return null;\n }\n return {\n tenant: parsed.tenant,\n ...(typeof parsed.role === 'string' && parsed.role !== '' ? { role: parsed.role } : {}),\n };\n } catch {\n // Corrupt or unreadable storage must never break client construction.\n return null;\n }\n}\n\n/**\n * Create a tenant selection store.\n *\n * Accepts either a bare initial selection (back-compatible) or an options\n * object enabling persistence.\n *\n * @example\n * ```ts\n * const tenants = createTenantSelection({ storage: createDefaultStorage() });\n * // survives a page reload\n * ```\n */\nexport function createTenantSelection(\n initialOrOptions?: TenantSelection | CreateTenantSelectionOptions,\n): TenantSelectionStore {\n const isOptions =\n initialOrOptions !== undefined &&\n !('tenant' in initialOrOptions);\n\n const options: CreateTenantSelectionOptions = isOptions\n ? (initialOrOptions as CreateTenantSelectionOptions)\n : { ...(initialOrOptions ? { initial: initialOrOptions as TenantSelection } : {}) };\n\n const storage = options.storage;\n const storageKey = options.storageKey ?? DEFAULT_SELECTION_KEY;\n const initial = options.initial ?? readPersisted(storage, storageKey) ?? undefined;\n\n let current: TenantSelection = {\n tenant: initial?.tenant ?? null,\n ...(initial?.role !== undefined ? { role: initial.role } : {}),\n };\n\n function persist(): void {\n if (!storage) {\n return;\n }\n try {\n if (current.tenant === null) {\n storage.removeItem(storageKey);\n } else {\n storage.setItem(storageKey, JSON.stringify(current));\n }\n } catch {\n // A full or blocked quota must not break tenant selection.\n }\n }\n\n const listeners = new Set<(selection: TenantSelection) => void>();\n\n function emit(): void {\n for (const listener of [...listeners]) {\n listener(current);\n }\n }\n\n return {\n get() {\n return current;\n },\n\n set(tenant, role) {\n if (current.tenant === tenant && current.role === role) {\n return;\n }\n current = { tenant, ...(role !== undefined ? { role } : {}) };\n persist();\n emit();\n },\n\n clear() {\n if (current.tenant === null && current.role === undefined) {\n return;\n }\n current = { tenant: null };\n persist();\n emit();\n },\n\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n","/**\n * Pluggable persistence for the session manager.\n *\n * Only the REFRESH token is persisted. The access token stays in memory: at a\n * 15-minute TTL, persisting it buys almost nothing and it is the more dangerous\n * of the two to leave sitting in `localStorage`.\n */\n\n/**\n * Minimal synchronous key/value store.\n *\n * Matches the `localStorage` shape so the browser default is a pass-through.\n */\nexport interface MPFAuthStorage {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\n/**\n * An in-memory store. Used automatically when `window` is absent (Node, SSR,\n * the e2e suite) and available explicitly for tests.\n */\nexport function createMemoryStorage(): MPFAuthStorage {\n const map = new Map<string, string>();\n return {\n getItem: (key) => map.get(key) ?? null,\n setItem: (key, value) => {\n map.set(key, value);\n },\n removeItem: (key) => {\n map.delete(key);\n },\n };\n}\n\n/**\n * `sessionStorage` when it is usable, an in-memory store otherwise.\n *\n * Per-tab and cleared when the tab closes, which is what a redirect flow wants:\n * see `createOAuthStateStore`.\n */\nexport function createSessionStorage(): MPFAuthStorage {\n if (typeof window === 'undefined') {\n return createMemoryStorage();\n }\n\n try {\n const probe = '__mpf_probe__';\n window.sessionStorage.setItem(probe, probe);\n window.sessionStorage.removeItem(probe);\n return window.sessionStorage;\n } catch {\n return createMemoryStorage();\n }\n}\n\n/**\n * `localStorage` when it is usable, an in-memory store otherwise.\n *\n * Access is probed rather than assumed: Safari in private mode and blocked\n * third-party contexts expose `localStorage` but throw on write.\n */\nexport function createDefaultStorage(): MPFAuthStorage {\n if (typeof window === 'undefined') {\n return createMemoryStorage();\n }\n\n try {\n const probe = '__mpf_probe__';\n window.localStorage.setItem(probe, probe);\n window.localStorage.removeItem(probe);\n return window.localStorage;\n } catch {\n return createMemoryStorage();\n }\n}\n","/**\n * Cross-tab refresh coordination.\n *\n * ## The problem, precisely\n *\n * Multiple tabs of ONE browser share one login and read the same refresh token\n * from origin-scoped `localStorage`. The refresh token is single-use and\n * rotates, so if two tabs renew concurrently one of them loses and its token is\n * dead. Separate browsers, machines or devices get separate session rows and\n * separate refresh tokens, so they are not a concern here.\n *\n * With proactive refresh every tab's timer fires at roughly the same moment, so\n * this is contended by N tabs at once, not two.\n *\n * ## Mechanism\n *\n * `navigator.locks` (Web Locks) is a standardized cross-tab mutex and needs no\n * dependency on the primary path. Locks auto-release when a tab is destroyed,\n * so a closed tab cannot deadlock the others. A polyfill is loaded only where\n * the API is missing (pre-15.4 Safari, non-secure contexts).\n *\n * A lock ALONE is insufficient: N tabs would queue and then each renew in turn,\n * which is the exact problem the lock was supposed to solve. The session manager\n * therefore does double-checked locking — re-reading shared state INSIDE the\n * lock and adopting instead of renewing when another tab got there first.\n *\n * ## Fanout: BroadcastChannel, not localStorage\n *\n * The winner's fresh ACCESS token is broadcast in memory over a\n * `BroadcastChannel` rather than persisted, so losing tabs can adopt it without\n * a network call while the access token still never touches `localStorage`.\n * Where `BroadcastChannel` is unavailable, losers fall back to renewing with the\n * rotated refresh token — correct, just less efficient.\n *\n * Everything here no-ops when `window` is absent, so Node and the e2e suite are\n * unaffected.\n */\n\n/** A token pair shared between tabs. */\nexport interface TokenBroadcast {\n accessToken: string;\n refreshToken: string;\n}\n\n/** The subset of `LockManager` this module uses. */\nexport interface LockManagerLike {\n request(\n name: string,\n options: { signal?: AbortSignal },\n callback: () => Promise<unknown>,\n ): Promise<unknown>;\n}\n\n/** The subset of `BroadcastChannel` this module uses. */\nexport interface BroadcastChannelLike {\n postMessage(message: unknown): void;\n addEventListener(type: 'message', listener: (event: { data: unknown }) => void): void;\n removeEventListener(type: 'message', listener: (event: { data: unknown }) => void): void;\n close(): void;\n}\n\n/** How the coordinator is providing mutual exclusion. */\nexport type CrossTabMode = 'native' | 'polyfill' | 'none';\n\nexport interface CrossTabCoordinator {\n /** How mutual exclusion is being provided. */\n readonly mode: CrossTabMode;\n /** Run `fn` holding the cross-tab refresh lock. */\n withLock<T>(fn: () => Promise<T>): Promise<T>;\n /** Tell other tabs about a fresh token pair. */\n publish(message: TokenBroadcast): void;\n /** Observe other tabs' fresh token pairs. Returns an unsubscribe function. */\n subscribe(listener: (message: TokenBroadcast) => void): () => void;\n /** Release resources. */\n close(): void;\n}\n\nexport interface CrossTabOptions {\n /** Lock name. @default 'mpf.refresh' */\n lockName?: string;\n /** BroadcastChannel name. @default 'mpf.auth' */\n channelName?: string;\n /**\n * How long to wait for the lock before giving up and proceeding without it.\n * @default 5000\n */\n lockTimeoutMs?: number;\n /** Injectable lock manager. Defaults to `navigator.locks`. For tests. */\n locks?: LockManagerLike | null;\n /**\n * Loads a Web Locks polyfill when the native API is absent. Resolve `null` to\n * decline. The default declines — no polyfill is bundled, keeping the primary\n * path dependency-free; a consumer targeting old Safari supplies one here.\n */\n loadPolyfill?: () => Promise<LockManagerLike | null>;\n /** Injectable channel factory. Defaults to `BroadcastChannel`. For tests. */\n channelFactory?: (name: string) => BroadcastChannelLike | null;\n}\n\nfunction defaultLocks(): LockManagerLike | null {\n if (typeof navigator === 'undefined') {\n return null;\n }\n const locks = (navigator as unknown as { locks?: LockManagerLike }).locks;\n return locks ?? null;\n}\n\nfunction defaultChannel(name: string): BroadcastChannelLike | null {\n if (typeof BroadcastChannel === 'undefined') {\n return null;\n }\n try {\n return new BroadcastChannel(name) as unknown as BroadcastChannelLike;\n } catch {\n return null;\n }\n}\n\n/**\n * An abort signal that fires after `ms`.\n *\n * Uses `AbortSignal.timeout` where available and falls back to a controller,\n * because the fallback also has to work under fake timers in tests.\n */\nfunction timeoutSignal(ms: number): AbortSignal {\n const controller = new AbortController();\n setTimeout(() => controller.abort(new Error(`Lock wait timed out after ${ms}ms`)), ms);\n return controller.signal;\n}\n\n/**\n * Apply symmetric jitter to a delay.\n *\n * Without this, N tabs computed the same proactive-renew deadline from the same\n * token and all wake on the same millisecond, then serialize through the lock\n * for no reason. Result is clamped at 0.\n *\n * @param baseMs - The unjittered delay.\n * @param jitterMs - Maximum deviation in either direction.\n * @param random - Injectable `Math.random` for tests.\n */\nexport function applyJitter(baseMs: number, jitterMs: number, random: () => number = Math.random): number {\n if (jitterMs <= 0) {\n return Math.max(0, baseMs);\n }\n const offset = (random() * 2 - 1) * jitterMs;\n return Math.max(0, baseMs + offset);\n}\n\n/**\n * Create a cross-tab coordinator.\n *\n * Resolution order for the lock: injected `locks` → `navigator.locks` →\n * `loadPolyfill()` → none (run without coordination, correct in a\n * single-context environment like Node).\n */\nexport function createCrossTabCoordinator(options: CrossTabOptions = {}): CrossTabCoordinator {\n const {\n lockName = 'mpf.refresh',\n channelName = 'mpf.auth',\n lockTimeoutMs = 5000,\n loadPolyfill = async () => null,\n channelFactory = defaultChannel,\n } = options;\n\n const browser = typeof window !== 'undefined';\n\n // `locks: null` explicitly declines; `undefined` means \"detect\".\n const injected = options.locks;\n let locks: LockManagerLike | null = injected !== undefined ? injected : browser ? defaultLocks() : null;\n let mode: CrossTabMode = locks ? (injected !== undefined ? 'native' : 'native') : 'none';\n\n // Polyfill resolution is async, so it is kicked off once and awaited by the\n // first withLock() call that needs it.\n let polyfillPromise: Promise<void> | null = null;\n if (!locks && browser && injected === undefined) {\n polyfillPromise = loadPolyfill()\n .then((loaded) => {\n if (loaded) {\n locks = loaded;\n mode = 'polyfill';\n }\n })\n .catch(() => {\n // No polyfill available; stay uncoordinated rather than failing auth.\n });\n }\n\n const channel = browser ? channelFactory(channelName) : null;\n const listeners = new Set<(message: TokenBroadcast) => void>();\n\n const onMessage = (event: { data: unknown }): void => {\n const data = event.data as Partial<TokenBroadcast> | null;\n if (\n !data ||\n typeof data.accessToken !== 'string' ||\n typeof data.refreshToken !== 'string'\n ) {\n return;\n }\n const message: TokenBroadcast = {\n accessToken: data.accessToken,\n refreshToken: data.refreshToken,\n };\n for (const listener of [...listeners]) {\n listener(message);\n }\n };\n\n channel?.addEventListener('message', onMessage);\n\n return {\n get mode() {\n return mode;\n },\n\n async withLock<T>(fn: () => Promise<T>): Promise<T> {\n if (polyfillPromise) {\n await polyfillPromise;\n polyfillPromise = null;\n }\n\n if (!locks) {\n // No cross-tab contention possible (or no API to use). Run directly.\n return fn();\n }\n\n let result: T;\n let ran = false;\n\n try {\n await locks.request(lockName, { signal: timeoutSignal(lockTimeoutMs) }, async () => {\n result = await fn();\n ran = true;\n });\n } catch (error) {\n if (ran) {\n // fn() itself threw, or the lock released oddly after running it.\n throw error;\n }\n // We never got the lock — most likely a frozen tab is holding it\n // mid-renew. Proceeding unlocked risks a duplicate renew, which the\n // server reports as REFRESH_TOKEN_SUPERSEDED and the session manager\n // recovers from. Hanging here would leave the app with no token at all,\n // which is strictly worse.\n return fn();\n }\n\n // `ran` is true whenever the callback completed; TS cannot see that.\n return result!;\n },\n\n publish(message: TokenBroadcast) {\n channel?.postMessage(message);\n },\n\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n\n close() {\n listeners.clear();\n channel?.removeEventListener('message', onMessage);\n channel?.close();\n },\n };\n}\n","/**\n * Session manager — single access token, single refresh token.\n *\n * There is deliberately no per-tenant token map, no `renewScoped`, and no\n * scoped/general token split: tenant is a request header now, so all of that\n * machinery is gone by construction rather than by choice.\n *\n * What remains, and why:\n * - **Coalescing** — the refresh token is single-use, so concurrent renewals\n * must serialize into one network call or they kill each other.\n * - **Proactive timer** — renew before expiry so requests never pay for it.\n * - **Wake handling** — `setTimeout` does not fire in frozen or discarded tabs,\n * so the timer alone is not a safety net. See `handleWake`.\n * - **Cross-tab lock** — N tabs of one browser share one refresh token.\n */\n\nimport { MPFAuthError } from '../client/core/errors.js';\nimport { tryDecodeAccessToken, isExpired, tokenLifetimeSeconds } from './claims.js';\nimport type { MPFClaims } from './claims-types.js';\nimport { DeadSessionError } from './errors.js';\nimport { createDefaultStorage, type MPFAuthStorage } from './storage.js';\nimport {\n applyJitter,\n createCrossTabCoordinator,\n type CrossTabCoordinator,\n type TokenBroadcast,\n} from './cross-tab.js';\n\n/** Outcome of {@link SessionManager.signOut}. */\nexport interface SignOutResult {\n /** Whether the server-side revoke succeeded. */\n revoked: boolean;\n /**\n * Why the revoke failed, when it did. Local state is cleared regardless, so\n * this is informational — surface it if you want to tell the user the session\n * may still be live elsewhere.\n */\n error?: Error;\n}\n\n/** A token pair as returned by `POST /v0/sessions/renew`. */\nexport interface TokenPair {\n access_token: string;\n refresh_token: string;\n}\n\n/**\n * The server's code when a renew loses the rotation race — another tab renewed\n * with the same refresh token first.\n *\n * This is recoverable: the winner's token is valid, so adopt it instead of\n * declaring the session dead.\n */\nexport const REFRESH_TOKEN_SUPERSEDED = 'REFRESH_TOKEN_SUPERSEDED';\n\n/** Seconds of headroom before expiry at which a token counts as stale. */\nconst DEFAULT_BUFFER_SECONDS = 120;\n\n/** Floor between two network renewals. */\nconst DEFAULT_MIN_RENEW_INTERVAL_MS = 5_000;\n\n/** Proactive-timer jitter, in milliseconds, applied symmetrically. */\nconst DEFAULT_JITTER_MS = 30_000;\n\nexport interface SessionManagerOptions {\n /**\n * Performs the network renewal. Injected rather than taking an `AuthClientV0`\n * so this module stays a leaf and cannot form an import cycle with the\n * identity client that consumes it.\n */\n renew: (refreshToken: string) => Promise<TokenPair>;\n\n /**\n * Revokes the session server-side. Wire to\n * `(refreshToken) => authClient.revokeSession({ refresh_token: refreshToken })`.\n *\n * Injected for the same reason as {@link renew}: it keeps `src/auth/` a leaf.\n *\n * Supplying it is what makes {@link SessionManager.signOut} revoke rather than\n * only clearing locally. Without it, `signOut()` still clears — a sign-out\n * button must always sign the user out — but the refresh token stays valid\n * server-side until it expires.\n */\n revoke?: (refreshToken: string) => Promise<unknown>;\n\n /** Where the refresh token is persisted. @default localStorage, else memory */\n storage?: MPFAuthStorage;\n\n /** Storage key for the refresh token. @default 'mpf.refresh_token' */\n storageKey?: string;\n\n /**\n * Seconds before expiry at which a token is considered stale.\n *\n * @default 120\n *\n * 120 rather than 60: at a 900s TTL, a request *starting* at `exp − 61s`\n * passes a 60s freshness check and then carries a token that dies mid-flight,\n * which storage uploads routinely will. The buffer must cover renew latency\n * plus the longest in-flight request, and browser clock skew is unbounded.\n *\n * Clamped per token — see {@link SessionManager.effectiveBufferSeconds}.\n */\n bufferSeconds?: number;\n\n /**\n * Minimum gap between two network renewals. Second line of defence against a\n * renew loop caused by a misconfigured server TTL.\n * @default 5000\n */\n minRenewIntervalMs?: number;\n\n /** Symmetric jitter on the proactive timer. @default 30000 */\n jitterMs?: number;\n\n /** Called when the session cannot be renewed and the user must log in again. */\n onDeadSession?: (error: DeadSessionError) => void;\n\n /** Called whenever the in-memory token pair changes. */\n onTokensChanged?: (tokens: TokenPair | null) => void;\n\n /**\n * Cross-tab coordinator. Pass `null` to disable coordination entirely.\n * @default createCrossTabCoordinator()\n */\n crossTab?: CrossTabCoordinator | null;\n\n /** Injectable clock, in ms. @default Date.now */\n now?: () => number;\n\n /** Injectable randomness for jitter. @default Math.random */\n random?: () => number;\n\n /**\n * Whether to attach `visibilitychange` / `online` listeners.\n * @default true when `window` exists\n */\n listenToWake?: boolean;\n}\n\n/**\n * Manages one session's tokens.\n */\nexport class SessionManager {\n private accessToken: string | null = null;\n private refreshToken: string | null = null;\n\n /** In-flight renewal, for coalescing. */\n private inflight: Promise<string> | null = null;\n\n /**\n * Gate that `getAccessToken()` awaits. Set by the wake path so that requests\n * queued during a wake renewal block behind it instead of shipping a token\n * that is already dead.\n */\n private wakeGate: Promise<void> | null = null;\n\n private timer: ReturnType<typeof setTimeout> | null = null;\n private lastRenewAt = 0;\n private destroyed = false;\n /**\n * Incremented on every `clear()`. A renewal captures it before its network\n * call and refuses to adopt a result from a superseded generation, so signing\n * out (or clearing) mid-renew cannot be undone by the renewal completing.\n */\n private generation = 0;\n\n private readonly storage: MPFAuthStorage;\n private readonly storageKey: string;\n private readonly bufferSeconds: number;\n private readonly minRenewIntervalMs: number;\n private readonly jitterMs: number;\n private readonly now: () => number;\n private readonly random: () => number;\n private readonly crossTab: CrossTabCoordinator | null;\n private readonly unsubscribeBroadcast: (() => void) | null = null;\n private readonly wakeListening: boolean;\n\n constructor(private readonly options: SessionManagerOptions) {\n this.storage = options.storage ?? createDefaultStorage();\n this.storageKey = options.storageKey ?? 'mpf.refresh_token';\n this.bufferSeconds = options.bufferSeconds ?? DEFAULT_BUFFER_SECONDS;\n this.minRenewIntervalMs = options.minRenewIntervalMs ?? DEFAULT_MIN_RENEW_INTERVAL_MS;\n this.jitterMs = options.jitterMs ?? DEFAULT_JITTER_MS;\n this.now = options.now ?? (() => Date.now());\n this.random = options.random ?? Math.random;\n\n this.crossTab =\n options.crossTab === null ? null : (options.crossTab ?? createCrossTabCoordinator());\n\n // Adopt a sibling tab's fresh tokens without a network call.\n if (this.crossTab) {\n this.unsubscribeBroadcast = this.crossTab.subscribe((message) => {\n this.adopt(message, { rebroadcast: false });\n });\n }\n\n this.refreshToken = this.storage.getItem(this.storageKey);\n\n this.wakeListening = options.listenToWake ?? typeof window !== 'undefined';\n if (this.wakeListening && typeof window !== 'undefined') {\n window.addEventListener('visibilitychange', this.onWakeEvent);\n window.addEventListener('online', this.onWakeEvent);\n }\n }\n\n // -------------------------------------------------------------------------\n // Public API\n // -------------------------------------------------------------------------\n\n /**\n * Adopt a freshly minted token pair (after sign-in, or after any flow that\n * changes memberships).\n */\n setTokens(tokens: TokenPair): void {\n this.adopt(\n { accessToken: tokens.access_token, refreshToken: tokens.refresh_token },\n { rebroadcast: true },\n );\n }\n\n /**\n * A usable access token, renewing first if necessary.\n *\n * Returns `null` only when there is no session at all.\n */\n async getAccessToken(): Promise<string | null> {\n // Block behind a wake renewal rather than shipping a dead token.\n if (this.wakeGate) {\n await this.wakeGate;\n }\n\n if (!this.accessToken) {\n return this.refreshToken ? this.renew() : null;\n }\n\n if (this.isStale()) {\n return this.renew();\n }\n\n return this.accessToken;\n }\n\n /** The current access token without renewing. */\n peekAccessToken(): string | null {\n return this.accessToken;\n }\n\n /** Decoded (UNVERIFIED — UX only) claims of the current access token. */\n getClaims(): MPFClaims | null {\n return tryDecodeAccessToken(this.accessToken);\n }\n\n /**\n * Renew the session. Coalesced: concurrent callers share one network call,\n * which is mandatory because the refresh token is single-use.\n */\n async renew(): Promise<string> {\n if (this.inflight) {\n return this.inflight;\n }\n\n this.inflight = this.renewGuarded().finally(() => {\n this.inflight = null;\n });\n\n return this.inflight;\n }\n\n /**\n * Sign out: revoke the session server-side, then clear it locally.\n *\n * **Never throws, and always clears.** A user who clicked sign-out must end up\n * signed out locally whatever the network did — a UI that refuses to log out\n * is worse than a token that outlives its client, and the failed revoke leaves\n * that token behind either way. The outcome is reported in the return value so\n * an app can say \"signed out, but we couldn't reach the server\" rather than\n * having to guess.\n *\n * Order is load-bearing: `revokeSession` is an *authenticated* endpoint, so\n * the revoke must go out **before** the access token is cleared. Reversing it\n * silently sends an unauthenticated request that cannot succeed.\n *\n * Idempotent: with no session, it is a no-op reporting `revoked: false`.\n *\n * This exists so consumers never need the raw refresh token. Reaching into\n * storage for it — `localStorage.getItem('mpf.refresh_token')` — hardcodes the\n * default key and breaks for anyone using a custom storage or key.\n */\n async signOut(): Promise<SignOutResult> {\n const refreshToken = this.refreshToken;\n const revoker = this.options.revoke;\n\n let revoked = false;\n let error: Error | undefined;\n\n if (refreshToken && revoker) {\n try {\n await revoker(refreshToken);\n revoked = true;\n } catch (caught) {\n error = caught instanceof Error ? caught : new Error(String(caught));\n }\n }\n\n // Unconditional: see the note above.\n this.clear();\n\n return { revoked, ...(error ? { error } : {}) };\n }\n\n /**\n * Forget the session locally. Stops timers and invalidates any in-flight\n * renewal so it cannot resurrect the session after it resolves.\n *\n * Prefer {@link signOut} for a user-initiated sign-out: this leaves the\n * refresh token valid server-side.\n */\n clear(): void {\n // Bump first: a renewal already in flight checks this before adopting its\n // result, so a proactive renew that resolves after sign-out cannot put the\n // tokens back. Without it, \"timer fires, user clicks sign out, renew\n // resolves\" leaves the user signed in again.\n this.generation += 1;\n\n this.accessToken = null;\n this.refreshToken = null;\n this.storage.removeItem(this.storageKey);\n this.cancelTimer();\n this.options.onTokensChanged?.(null);\n }\n\n /** Detach listeners and release the coordinator. */\n destroy(): void {\n this.destroyed = true;\n this.cancelTimer();\n this.unsubscribeBroadcast?.();\n if (this.wakeListening && typeof window !== 'undefined') {\n window.removeEventListener('visibilitychange', this.onWakeEvent);\n window.removeEventListener('online', this.onWakeEvent);\n }\n this.crossTab?.close();\n }\n\n /**\n * The buffer actually in force for the current token.\n *\n * The configured buffer is clamped to just under half the token's lifetime:\n * if the buffer were ≥ half the lifetime, a freshly minted token would be\n * \"stale\" the instant it arrived and the manager would renew-loop, hammering\n * `/sessions/renew` forever. A 60s-TTL token therefore gets a 29s buffer, not\n * the configured 120s.\n *\n * Falls back to the configured value when the token carries no `iat` and the\n * lifetime is unknowable.\n */\n effectiveBufferSeconds(): number {\n const claims = this.getClaims();\n if (!claims) {\n return this.bufferSeconds;\n }\n\n const lifetime = tokenLifetimeSeconds(claims);\n if (lifetime === undefined) {\n return this.bufferSeconds;\n }\n\n const ceiling = Math.max(0, Math.floor(lifetime / 2) - 1);\n return Math.min(this.bufferSeconds, ceiling);\n }\n\n // -------------------------------------------------------------------------\n // Internals\n // -------------------------------------------------------------------------\n\n private isStale(): boolean {\n const claims = this.getClaims();\n if (!claims) {\n // Undecodable token: treat as stale so we renew rather than send garbage.\n return true;\n }\n return isExpired(claims, this.effectiveBufferSeconds(), this.now());\n }\n\n private adopt(tokens: TokenBroadcast, opts: { rebroadcast: boolean }): void {\n this.accessToken = tokens.accessToken;\n this.refreshToken = tokens.refreshToken;\n this.storage.setItem(this.storageKey, tokens.refreshToken);\n\n this.scheduleProactiveRenew();\n\n this.options.onTokensChanged?.({\n access_token: tokens.accessToken,\n refresh_token: tokens.refreshToken,\n });\n\n if (opts.rebroadcast) {\n this.crossTab?.publish(tokens);\n }\n }\n\n private async renewGuarded(): Promise<string> {\n if (!this.refreshToken) {\n return this.die(new DeadSessionError('No refresh token available'));\n }\n\n // Floor between network renewals. If we still hold a token, hand it back\n // rather than renewing again inside the floor.\n const sinceLast = this.now() - this.lastRenewAt;\n if (this.lastRenewAt !== 0 && sinceLast < this.minRenewIntervalMs && this.accessToken) {\n return this.accessToken;\n }\n\n const startedWith = this.refreshToken;\n\n const run = async (): Promise<string> => {\n // Double-checked locking: re-read shared state INSIDE the lock. Without\n // this, N tabs queue on the lock and then each renew in turn, which is\n // exactly what the lock was meant to prevent.\n const persisted = this.storage.getItem(this.storageKey);\n if (persisted && persisted !== startedWith) {\n // Another tab renewed while we waited. Its access token arrives over\n // the broadcast channel, so we may already hold a fresh one.\n this.refreshToken = persisted;\n if (this.accessToken && !this.isStale()) {\n return this.accessToken;\n }\n }\n\n return this.networkRenew();\n };\n\n return this.crossTab ? this.crossTab.withLock(run) : run();\n }\n\n private async networkRenew(): Promise<string> {\n const refreshToken = this.refreshToken;\n if (!refreshToken) {\n return this.die(new DeadSessionError('No refresh token available'));\n }\n\n this.lastRenewAt = this.now();\n const generation = this.generation;\n\n let pair: TokenPair;\n try {\n pair = await this.options.renew(refreshToken);\n } catch (error) {\n // Losing the rotation race is recoverable, not fatal: a sibling tab\n // renewed first and its token is valid. Adopt rather than logging out.\n if (isSupersededError(error)) {\n const adopted = await this.adoptAfterSuperseded();\n if (adopted) {\n return adopted;\n }\n }\n\n return this.die(\n new DeadSessionError(\n 'Session renewal failed',\n error instanceof Error ? error : undefined,\n ),\n );\n }\n\n if (generation !== this.generation) {\n // The session was cleared while this renewal was in flight (sign-out being\n // the realistic case). Adopting now would sign the user back in.\n return pair.access_token;\n }\n\n this.adopt(\n { accessToken: pair.access_token, refreshToken: pair.refresh_token },\n { rebroadcast: true },\n );\n\n return pair.access_token;\n }\n\n /**\n * After a REFRESH_TOKEN_SUPERSEDED, take the winner's tokens.\n *\n * The winner's access token arrives over the broadcast channel; its refresh\n * token is in storage. If neither is usable there is nothing to adopt and the\n * caller declares the session dead.\n */\n private async adoptAfterSuperseded(): Promise<string | null> {\n const persisted = this.storage.getItem(this.storageKey);\n if (persisted) {\n this.refreshToken = persisted;\n }\n\n if (this.accessToken && !this.isStale()) {\n return this.accessToken;\n }\n\n return null;\n }\n\n private die(error: DeadSessionError): never {\n this.clear();\n this.options.onDeadSession?.(error);\n throw error;\n }\n\n private cancelTimer(): void {\n if (this.timer !== null) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n }\n\n /**\n * Schedule a renewal shortly before the token goes stale.\n *\n * Jittered so that N tabs computing the same deadline from the same token do\n * not all wake on the same millisecond.\n */\n private scheduleProactiveRenew(): void {\n this.cancelTimer();\n\n if (this.destroyed) {\n return;\n }\n\n const claims = this.getClaims();\n if (!claims) {\n return;\n }\n\n const staleAtMs = (claims.exp - this.effectiveBufferSeconds()) * 1000;\n const base = Math.max(0, staleAtMs - this.now());\n const delay = Math.max(\n this.minRenewIntervalMs,\n applyJitter(base, this.jitterMs, this.random),\n );\n\n this.timer = setTimeout(() => {\n this.timer = null;\n void this.renew().catch(() => {\n // renew() already reported via onDeadSession.\n });\n }, delay);\n }\n\n private readonly onWakeEvent = (): void => {\n if (typeof document !== 'undefined' && document.visibilityState === 'hidden') {\n return;\n }\n void this.handleWake();\n };\n\n /**\n * Wake path — the real safety net.\n *\n * `setTimeout` does not fire in frozen or discarded tabs (Chrome tab\n * freezing, iOS Safari), so a tab backgrounded for 20 minutes wakes holding a\n * guaranteed-dead token with its timer never having run.\n *\n * Merely rescheduling here is wrong: the delay computes negative, becomes\n * `setTimeout(0)`, and then races whatever the app fires on focus. Instead\n * renew immediately and publish a gate that `getAccessToken()` awaits, so\n * requests triggered by the same focus event queue behind the renewal rather\n * than each eating a 401.\n */\n private async handleWake(): Promise<void> {\n if (this.destroyed || !this.refreshToken) {\n return;\n }\n\n if (this.accessToken && !this.isStale()) {\n // Token survived the nap; the timer may still be stale after a freeze.\n this.scheduleProactiveRenew();\n return;\n }\n\n if (this.wakeGate) {\n return;\n }\n\n let release!: () => void;\n this.wakeGate = new Promise<void>((resolve) => {\n release = resolve;\n });\n\n try {\n await this.renew();\n } catch {\n // Reported through onDeadSession already.\n } finally {\n const gate = release;\n this.wakeGate = null;\n gate();\n }\n }\n}\n\n/**\n * Whether an error is the server's \"another tab renewed first\" signal.\n *\n * Checks `serverCode` because `MPFAuthError.code` is derived from the error\n * classification and collapses every 401 to `UNAUTHORIZED`.\n */\nexport function isSupersededError(error: unknown): boolean {\n if (error instanceof MPFAuthError) {\n return error.serverCode === REFRESH_TOKEN_SUPERSEDED;\n }\n if (error && typeof error === 'object' && 'code' in error) {\n return (error as { code?: unknown }).code === REFRESH_TOKEN_SUPERSEDED;\n }\n return false;\n}\n\n/**\n * Create a session manager.\n */\nexport function createSessionManager(options: SessionManagerOptions): SessionManager {\n return new SessionManager(options);\n}\n"]}
@@ -1,6 +1,6 @@
1
- import { createTurnstileMiddleware, createAdminAuthMiddleware } from './chunk-K2ELAI2X.js';
2
- import { MPFClient, attachStandardMiddlewares } from './chunk-LRM6JJ63.js';
3
- import { __export, MPFValidationError } from './chunk-AJWM5MDZ.js';
1
+ import { createTurnstileMiddleware, createAdminAuthMiddleware } from './chunk-B3JXAPMM.js';
2
+ import { MPFClient, attachStandardMiddlewares } from './chunk-3OKR4MSV.js';
3
+ import { __export, MPFValidationError } from './chunk-S5DKGPUY.js';
4
4
 
5
5
  // src/identity/index.ts
6
6
  var identity_exports = {};
@@ -955,5 +955,5 @@ function createAdminClient(options) {
955
955
  }
956
956
 
957
957
  export { AuthClientV0, createAdminClient, createAuthClient, identity_exports, isMFARequired, isMFASetupRequired };
958
- //# sourceMappingURL=chunk-XAXFIIRT.js.map
959
- //# sourceMappingURL=chunk-XAXFIIRT.js.map
958
+ //# sourceMappingURL=chunk-R5KABORE.js.map
959
+ //# sourceMappingURL=chunk-R5KABORE.js.map