@mosano-product-framework/sdk 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +827 -0
  2. package/README.react.md +348 -0
  3. package/dist/auth/claims-types.d.ts +89 -0
  4. package/dist/auth/claims.d.ts +125 -0
  5. package/dist/auth/cross-tab.d.ts +114 -0
  6. package/dist/auth/errors.d.ts +40 -0
  7. package/dist/auth/index.d.ts +18 -0
  8. package/dist/auth/index.js +5 -0
  9. package/dist/auth/index.js.map +1 -0
  10. package/dist/auth/oauth-state.d.ts +93 -0
  11. package/dist/auth/session-manager.d.ts +253 -0
  12. package/dist/auth/storage.d.ts +36 -0
  13. package/dist/auth/tenant-directory.d.ts +59 -0
  14. package/dist/auth/tenant-selection.d.ts +92 -0
  15. package/dist/chunk-7WAV52EO.js +621 -0
  16. package/dist/chunk-7WAV52EO.js.map +1 -0
  17. package/dist/chunk-AJWM5MDZ.js +410 -0
  18. package/dist/chunk-AJWM5MDZ.js.map +1 -0
  19. package/dist/chunk-EXPYHNPV.js +212 -0
  20. package/dist/chunk-EXPYHNPV.js.map +1 -0
  21. package/dist/chunk-GPWGOYCA.js +85 -0
  22. package/dist/chunk-GPWGOYCA.js.map +1 -0
  23. package/dist/chunk-GQJ3QQPH.js +339 -0
  24. package/dist/chunk-GQJ3QQPH.js.map +1 -0
  25. package/dist/chunk-K2ELAI2X.js +64 -0
  26. package/dist/chunk-K2ELAI2X.js.map +1 -0
  27. package/dist/chunk-LRM6JJ63.js +616 -0
  28. package/dist/chunk-LRM6JJ63.js.map +1 -0
  29. package/dist/chunk-XAXFIIRT.js +959 -0
  30. package/dist/chunk-XAXFIIRT.js.map +1 -0
  31. package/dist/client/core/client-factory.d.ts +61 -0
  32. package/dist/client/core/client.d.ts +144 -0
  33. package/dist/client/core/errors.d.ts +105 -0
  34. package/dist/client/core/index.d.ts +9 -0
  35. package/dist/client/core/middleware.d.ts +67 -0
  36. package/dist/client/core/types.d.ts +99 -0
  37. package/dist/client/graphql/client.d.ts +66 -0
  38. package/dist/client/graphql/factory.d.ts +84 -0
  39. package/dist/client/graphql/operation.d.ts +24 -0
  40. package/dist/client/graphql/types.d.ts +60 -0
  41. package/dist/client/graphql/ws-client.d.ts +116 -0
  42. package/dist/client/index.d.ts +17 -0
  43. package/dist/client/index.js +227 -0
  44. package/dist/client/index.js.map +1 -0
  45. package/dist/client/middlewares/admin-auth.d.ts +90 -0
  46. package/dist/client/middlewares/auth.d.ts +81 -0
  47. package/dist/client/middlewares/index.d.ts +12 -0
  48. package/dist/client/middlewares/logging.d.ts +102 -0
  49. package/dist/client/middlewares/retry.d.ts +138 -0
  50. package/dist/client/middlewares/tenant.d.ts +60 -0
  51. package/dist/client/middlewares/turnstile.d.ts +41 -0
  52. package/dist/client/peer-free.d.ts +25 -0
  53. package/dist/client/utils/url.d.ts +19 -0
  54. package/dist/identity/index.d.ts +85 -0
  55. package/dist/identity/index.js +6 -0
  56. package/dist/identity/index.js.map +1 -0
  57. package/dist/identity/types.d.ts +690 -0
  58. package/dist/identity/v0.d.ts +594 -0
  59. package/dist/index.d.ts +50 -0
  60. package/dist/index.js +24 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/react/context.d.ts +47 -0
  63. package/dist/react/hooks.d.ts +120 -0
  64. package/dist/react/index.d.ts +19 -0
  65. package/dist/react/index.js +308 -0
  66. package/dist/react/index.js.map +1 -0
  67. package/dist/react/provider.d.ts +68 -0
  68. package/dist/react/store.d.ts +85 -0
  69. package/dist/storage/index.d.ts +31 -0
  70. package/dist/storage/index.js +5 -0
  71. package/dist/storage/index.js.map +1 -0
  72. package/dist/storage/types.d.ts +107 -0
  73. package/dist/storage/v0.d.ts +120 -0
  74. package/package.json +99 -0
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Errors for the auth layer.
3
+ */
4
+ import { MPFError } from '../client/core/errors.js';
5
+ /**
6
+ * Base error for access-token claim decoding failures.
7
+ */
8
+ export declare class MPFClaimsError extends MPFError {
9
+ constructor(message: string, code?: string, details?: unknown);
10
+ }
11
+ /**
12
+ * Thrown when a token's claim-set version has no registered parser.
13
+ *
14
+ * Carries the offending version so a client can distinguish "this app is older
15
+ * than the server" (a deploy-ordering problem, usually fixed by reloading) from
16
+ * "this token is garbage" (a real auth failure).
17
+ */
18
+ export declare class MPFUnsupportedClaimsVersionError extends MPFClaimsError {
19
+ /** The `v` value found in the token, or null when it was absent/non-numeric. */
20
+ readonly version: number | null;
21
+ constructor(version: number | null);
22
+ toJSON(): Record<string, unknown>;
23
+ }
24
+ /**
25
+ * Thrown when the session can no longer be renewed and the user must
26
+ * re-authenticate.
27
+ *
28
+ * This is a terminal state: the refresh token was rejected, absent, or the
29
+ * renew endpoint failed in a non-recoverable way.
30
+ */
31
+ export declare class DeadSessionError extends MPFError {
32
+ /** The underlying error that killed the session, when there was one. */
33
+ readonly cause?: Error;
34
+ constructor(message?: string, cause?: Error);
35
+ toJSON(): Record<string, unknown>;
36
+ }
37
+ /**
38
+ * Type guard for {@link DeadSessionError}.
39
+ */
40
+ export declare function isDeadSession(error: unknown): error is DeadSessionError;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Auth layer: token claims, tenant selection, session lifecycle.
3
+ *
4
+ * ⚠️ Claim decoding in this module DECODES BUT DOES NOT VERIFY. Its output is
5
+ * UX-only and is forgeable by a hostile client. See the security block at the
6
+ * top of `claims.ts`.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ export type { MPFRole, MPFClaims, MPFClaimsV1, MPFTenantClaim, ClaimsParser, } from './claims-types.js';
11
+ export { decodeAccessToken, tryDecodeAccessToken, listTenants, rolesForTenant, defaultRoleForTenant, canSelect, expiresAt, isExpired, tokenLifetimeSeconds, clearClaimsCache, registerClaimsParser, supportedClaimsVersions, } from './claims.js';
12
+ export { MPFClaimsError, MPFUnsupportedClaimsVersionError, DeadSessionError, isDeadSession, } from './errors.js';
13
+ export { createTenantSelection, type TenantSelection, type TenantSelectionStore, type TenantSelectionPersistence, type CreateTenantSelectionOptions, } from './tenant-selection.js';
14
+ export { createDefaultStorage, createSessionStorage, createMemoryStorage, type MPFAuthStorage, } from './storage.js';
15
+ export { createOAuthStateStore, type OAuthStateStore, type OAuthStateStoreOptions, } from './oauth-state.js';
16
+ export { createCrossTabCoordinator, applyJitter, type CrossTabCoordinator, type CrossTabMode, type CrossTabOptions, type TokenBroadcast, type LockManagerLike, type BroadcastChannelLike, } from './cross-tab.js';
17
+ export { TENANT_DIRECTORY_QUERY, type TenantDirectoryEntry, type TenantDirectoryLoader, } from './tenant-directory.js';
18
+ export { SessionManager, createSessionManager, isSupersededError, REFRESH_TOKEN_SUPERSEDED, type SessionManagerOptions, type SignOutResult, type TokenPair, } from './session-manager.js';
@@ -0,0 +1,5 @@
1
+ export { TENANT_DIRECTORY_QUERY, createOAuthStateStore } from '../chunk-GPWGOYCA.js';
2
+ export { REFRESH_TOKEN_SUPERSEDED, SessionManager, applyJitter, createCrossTabCoordinator, createDefaultStorage, createMemoryStorage, createSessionManager, createSessionStorage, createTenantSelection, isSupersededError } from '../chunk-7WAV52EO.js';
3
+ export { DeadSessionError, MPFClaimsError, MPFUnsupportedClaimsVersionError, canSelect, clearClaimsCache, decodeAccessToken, defaultRoleForTenant, expiresAt, isDeadSession, isExpired, listTenants, registerClaimsParser, rolesForTenant, supportedClaimsVersions, tokenLifetimeSeconds, tryDecodeAccessToken } from '../chunk-AJWM5MDZ.js';
4
+ //# sourceMappingURL=index.js.map
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * OAuth `state` persistence — CSRF protection for the Google OAuth flow.
3
+ *
4
+ * ## What the state is for
5
+ *
6
+ * Without it, an OAuth callback route accepts *any* `code` presented to it. An
7
+ * attacker who obtains an authorization code for their own account can send the
8
+ * victim to `.../callback?code=<attacker's code>`; the victim's browser posts it,
9
+ * the server exchanges it, and the victim is now signed in **as the attacker** —
10
+ * whose account the attacker still controls. Anything the victim then does
11
+ * (entering payment details, uploading documents, accepting an invitation)
12
+ * happens inside the attacker's account.
13
+ *
14
+ * That is not theoretical: it was confirmed exploitable end to end in the
15
+ * reference boilerplate, whose callback route discarded `state` and auto-posted
16
+ * `code` on page load with no user interaction.
17
+ *
18
+ * The state closes it: the server mints an unguessable value, the client stores
19
+ * it before redirecting, and the server accepts the callback only if the value
20
+ * comes back. An attacker cannot produce a state that matches the victim's
21
+ * browser storage.
22
+ *
23
+ * ## Why this store exists
24
+ *
25
+ * The state must survive a **full-page redirect to Google and back**, so it
26
+ * cannot live in memory or in React state. That leaves web storage, and
27
+ * "remember to persist a value across a redirect" is precisely the step the
28
+ * boilerplate omitted. This makes it one call each way.
29
+ *
30
+ * Sending the state stays explicit: `GoogleOAuthCallbackRequest.state` is
31
+ * required, so TypeScript rejects a callback that forgets it. This store solves
32
+ * *where to keep it*, not *whether to send it* — a security parameter should not
33
+ * be invisibly injected.
34
+ */
35
+ import { type MPFAuthStorage } from './storage.js';
36
+ export interface OAuthStateStore {
37
+ /** Persist the state returned by `getGoogleOAuthURL`, before redirecting. */
38
+ save(state: string): void;
39
+ /**
40
+ * Read and **clear** the stored state, for sending on the callback.
41
+ *
42
+ * Single-use by design: a state that could be replayed is not much better than
43
+ * no state at all, and the server also rejects reuse.
44
+ */
45
+ take(): string | null;
46
+ /** Read without clearing. For diagnostics; prefer `take()` in the flow. */
47
+ peek(): string | null;
48
+ /** Discard any pending state (e.g. the user abandoned the flow). */
49
+ clear(): void;
50
+ }
51
+ export interface OAuthStateStoreOptions {
52
+ /**
53
+ * Where to keep the pending state.
54
+ *
55
+ * Defaults to `sessionStorage`, which is the right default for a redirect
56
+ * flow: it is scoped to the tab that started it, so a state minted in one tab
57
+ * cannot be consumed by another, and it is cleared when the tab closes.
58
+ *
59
+ * **Limitation worth knowing:** `sessionStorage` is per-tab, so a callback
60
+ * that lands in a *different* tab (a provider opening a new tab, or the user
61
+ * moving the URL) will not find the state and the callback will be rejected —
62
+ * failing closed, which is the correct direction, but it will look like a
63
+ * spurious error. Pass `createDefaultStorage()` (localStorage) if your flow
64
+ * can land in another tab, accepting that the state is then visible to every
65
+ * tab on the origin.
66
+ *
67
+ * A cookie is not offered: one set from JavaScript cannot be `httpOnly`, so it
68
+ * is no less readable than web storage while adding `SameSite` pitfalls on the
69
+ * cross-site return leg.
70
+ */
71
+ storage?: MPFAuthStorage;
72
+ /** Storage key. @default 'mpf.oauth_state' */
73
+ storageKey?: string;
74
+ }
75
+ /**
76
+ * Create a store for the pending OAuth state.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * const oauthState = createOAuthStateStore();
81
+ * const auth = createAuthClient({ baseUrl, oauthStateStore: oauthState });
82
+ *
83
+ * // Starting the flow — the client saves the state for you.
84
+ * const { url } = await auth.getGoogleOAuthURL({ redirect_uri });
85
+ * window.location.href = url;
86
+ *
87
+ * // On the callback route, send it back explicitly.
88
+ * const state = oauthState.take();
89
+ * if (!state) throw new Error('No pending OAuth flow in this tab');
90
+ * const tokens = await auth.handleGoogleOAuthCallback({ code, redirect_uri, state });
91
+ * ```
92
+ */
93
+ export declare function createOAuthStateStore(options?: OAuthStateStoreOptions): OAuthStateStore;
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Session manager — single access token, single refresh token.
3
+ *
4
+ * There is deliberately no per-tenant token map, no `renewScoped`, and no
5
+ * scoped/general token split: tenant is a request header now, so all of that
6
+ * machinery is gone by construction rather than by choice.
7
+ *
8
+ * What remains, and why:
9
+ * - **Coalescing** — the refresh token is single-use, so concurrent renewals
10
+ * must serialize into one network call or they kill each other.
11
+ * - **Proactive timer** — renew before expiry so requests never pay for it.
12
+ * - **Wake handling** — `setTimeout` does not fire in frozen or discarded tabs,
13
+ * so the timer alone is not a safety net. See `handleWake`.
14
+ * - **Cross-tab lock** — N tabs of one browser share one refresh token.
15
+ */
16
+ import type { MPFClaims } from './claims-types.js';
17
+ import { DeadSessionError } from './errors.js';
18
+ import { type MPFAuthStorage } from './storage.js';
19
+ import { type CrossTabCoordinator } from './cross-tab.js';
20
+ /** Outcome of {@link SessionManager.signOut}. */
21
+ export interface SignOutResult {
22
+ /** Whether the server-side revoke succeeded. */
23
+ revoked: boolean;
24
+ /**
25
+ * Why the revoke failed, when it did. Local state is cleared regardless, so
26
+ * this is informational — surface it if you want to tell the user the session
27
+ * may still be live elsewhere.
28
+ */
29
+ error?: Error;
30
+ }
31
+ /** A token pair as returned by `POST /v0/sessions/renew`. */
32
+ export interface TokenPair {
33
+ access_token: string;
34
+ refresh_token: string;
35
+ }
36
+ /**
37
+ * The server's code when a renew loses the rotation race — another tab renewed
38
+ * with the same refresh token first.
39
+ *
40
+ * This is recoverable: the winner's token is valid, so adopt it instead of
41
+ * declaring the session dead.
42
+ */
43
+ export declare const REFRESH_TOKEN_SUPERSEDED = "REFRESH_TOKEN_SUPERSEDED";
44
+ export interface SessionManagerOptions {
45
+ /**
46
+ * Performs the network renewal. Injected rather than taking an `AuthClientV0`
47
+ * so this module stays a leaf and cannot form an import cycle with the
48
+ * identity client that consumes it.
49
+ */
50
+ renew: (refreshToken: string) => Promise<TokenPair>;
51
+ /**
52
+ * Revokes the session server-side. Wire to
53
+ * `(refreshToken) => authClient.revokeSession({ refresh_token: refreshToken })`.
54
+ *
55
+ * Injected for the same reason as {@link renew}: it keeps `src/auth/` a leaf.
56
+ *
57
+ * Supplying it is what makes {@link SessionManager.signOut} revoke rather than
58
+ * only clearing locally. Without it, `signOut()` still clears — a sign-out
59
+ * button must always sign the user out — but the refresh token stays valid
60
+ * server-side until it expires.
61
+ */
62
+ revoke?: (refreshToken: string) => Promise<unknown>;
63
+ /** Where the refresh token is persisted. @default localStorage, else memory */
64
+ storage?: MPFAuthStorage;
65
+ /** Storage key for the refresh token. @default 'mpf.refresh_token' */
66
+ storageKey?: string;
67
+ /**
68
+ * Seconds before expiry at which a token is considered stale.
69
+ *
70
+ * @default 120
71
+ *
72
+ * 120 rather than 60: at a 900s TTL, a request *starting* at `exp − 61s`
73
+ * passes a 60s freshness check and then carries a token that dies mid-flight,
74
+ * which storage uploads routinely will. The buffer must cover renew latency
75
+ * plus the longest in-flight request, and browser clock skew is unbounded.
76
+ *
77
+ * Clamped per token — see {@link SessionManager.effectiveBufferSeconds}.
78
+ */
79
+ bufferSeconds?: number;
80
+ /**
81
+ * Minimum gap between two network renewals. Second line of defence against a
82
+ * renew loop caused by a misconfigured server TTL.
83
+ * @default 5000
84
+ */
85
+ minRenewIntervalMs?: number;
86
+ /** Symmetric jitter on the proactive timer. @default 30000 */
87
+ jitterMs?: number;
88
+ /** Called when the session cannot be renewed and the user must log in again. */
89
+ onDeadSession?: (error: DeadSessionError) => void;
90
+ /** Called whenever the in-memory token pair changes. */
91
+ onTokensChanged?: (tokens: TokenPair | null) => void;
92
+ /**
93
+ * Cross-tab coordinator. Pass `null` to disable coordination entirely.
94
+ * @default createCrossTabCoordinator()
95
+ */
96
+ crossTab?: CrossTabCoordinator | null;
97
+ /** Injectable clock, in ms. @default Date.now */
98
+ now?: () => number;
99
+ /** Injectable randomness for jitter. @default Math.random */
100
+ random?: () => number;
101
+ /**
102
+ * Whether to attach `visibilitychange` / `online` listeners.
103
+ * @default true when `window` exists
104
+ */
105
+ listenToWake?: boolean;
106
+ }
107
+ /**
108
+ * Manages one session's tokens.
109
+ */
110
+ export declare class SessionManager {
111
+ private readonly options;
112
+ private accessToken;
113
+ private refreshToken;
114
+ /** In-flight renewal, for coalescing. */
115
+ private inflight;
116
+ /**
117
+ * Gate that `getAccessToken()` awaits. Set by the wake path so that requests
118
+ * queued during a wake renewal block behind it instead of shipping a token
119
+ * that is already dead.
120
+ */
121
+ private wakeGate;
122
+ private timer;
123
+ private lastRenewAt;
124
+ private destroyed;
125
+ /**
126
+ * Incremented on every `clear()`. A renewal captures it before its network
127
+ * call and refuses to adopt a result from a superseded generation, so signing
128
+ * out (or clearing) mid-renew cannot be undone by the renewal completing.
129
+ */
130
+ private generation;
131
+ private readonly storage;
132
+ private readonly storageKey;
133
+ private readonly bufferSeconds;
134
+ private readonly minRenewIntervalMs;
135
+ private readonly jitterMs;
136
+ private readonly now;
137
+ private readonly random;
138
+ private readonly crossTab;
139
+ private readonly unsubscribeBroadcast;
140
+ private readonly wakeListening;
141
+ constructor(options: SessionManagerOptions);
142
+ /**
143
+ * Adopt a freshly minted token pair (after sign-in, or after any flow that
144
+ * changes memberships).
145
+ */
146
+ setTokens(tokens: TokenPair): void;
147
+ /**
148
+ * A usable access token, renewing first if necessary.
149
+ *
150
+ * Returns `null` only when there is no session at all.
151
+ */
152
+ getAccessToken(): Promise<string | null>;
153
+ /** The current access token without renewing. */
154
+ peekAccessToken(): string | null;
155
+ /** Decoded (UNVERIFIED — UX only) claims of the current access token. */
156
+ getClaims(): MPFClaims | null;
157
+ /**
158
+ * Renew the session. Coalesced: concurrent callers share one network call,
159
+ * which is mandatory because the refresh token is single-use.
160
+ */
161
+ renew(): Promise<string>;
162
+ /**
163
+ * Sign out: revoke the session server-side, then clear it locally.
164
+ *
165
+ * **Never throws, and always clears.** A user who clicked sign-out must end up
166
+ * signed out locally whatever the network did — a UI that refuses to log out
167
+ * is worse than a token that outlives its client, and the failed revoke leaves
168
+ * that token behind either way. The outcome is reported in the return value so
169
+ * an app can say "signed out, but we couldn't reach the server" rather than
170
+ * having to guess.
171
+ *
172
+ * Order is load-bearing: `revokeSession` is an *authenticated* endpoint, so
173
+ * the revoke must go out **before** the access token is cleared. Reversing it
174
+ * silently sends an unauthenticated request that cannot succeed.
175
+ *
176
+ * Idempotent: with no session, it is a no-op reporting `revoked: false`.
177
+ *
178
+ * This exists so consumers never need the raw refresh token. Reaching into
179
+ * storage for it — `localStorage.getItem('mpf.refresh_token')` — hardcodes the
180
+ * default key and breaks for anyone using a custom storage or key.
181
+ */
182
+ signOut(): Promise<SignOutResult>;
183
+ /**
184
+ * Forget the session locally. Stops timers and invalidates any in-flight
185
+ * renewal so it cannot resurrect the session after it resolves.
186
+ *
187
+ * Prefer {@link signOut} for a user-initiated sign-out: this leaves the
188
+ * refresh token valid server-side.
189
+ */
190
+ clear(): void;
191
+ /** Detach listeners and release the coordinator. */
192
+ destroy(): void;
193
+ /**
194
+ * The buffer actually in force for the current token.
195
+ *
196
+ * The configured buffer is clamped to just under half the token's lifetime:
197
+ * if the buffer were ≥ half the lifetime, a freshly minted token would be
198
+ * "stale" the instant it arrived and the manager would renew-loop, hammering
199
+ * `/sessions/renew` forever. A 60s-TTL token therefore gets a 29s buffer, not
200
+ * the configured 120s.
201
+ *
202
+ * Falls back to the configured value when the token carries no `iat` and the
203
+ * lifetime is unknowable.
204
+ */
205
+ effectiveBufferSeconds(): number;
206
+ private isStale;
207
+ private adopt;
208
+ private renewGuarded;
209
+ private networkRenew;
210
+ /**
211
+ * After a REFRESH_TOKEN_SUPERSEDED, take the winner's tokens.
212
+ *
213
+ * The winner's access token arrives over the broadcast channel; its refresh
214
+ * token is in storage. If neither is usable there is nothing to adopt and the
215
+ * caller declares the session dead.
216
+ */
217
+ private adoptAfterSuperseded;
218
+ private die;
219
+ private cancelTimer;
220
+ /**
221
+ * Schedule a renewal shortly before the token goes stale.
222
+ *
223
+ * Jittered so that N tabs computing the same deadline from the same token do
224
+ * not all wake on the same millisecond.
225
+ */
226
+ private scheduleProactiveRenew;
227
+ private readonly onWakeEvent;
228
+ /**
229
+ * Wake path — the real safety net.
230
+ *
231
+ * `setTimeout` does not fire in frozen or discarded tabs (Chrome tab
232
+ * freezing, iOS Safari), so a tab backgrounded for 20 minutes wakes holding a
233
+ * guaranteed-dead token with its timer never having run.
234
+ *
235
+ * Merely rescheduling here is wrong: the delay computes negative, becomes
236
+ * `setTimeout(0)`, and then races whatever the app fires on focus. Instead
237
+ * renew immediately and publish a gate that `getAccessToken()` awaits, so
238
+ * requests triggered by the same focus event queue behind the renewal rather
239
+ * than each eating a 401.
240
+ */
241
+ private handleWake;
242
+ }
243
+ /**
244
+ * Whether an error is the server's "another tab renewed first" signal.
245
+ *
246
+ * Checks `serverCode` because `MPFAuthError.code` is derived from the error
247
+ * classification and collapses every 401 to `UNAUTHORIZED`.
248
+ */
249
+ export declare function isSupersededError(error: unknown): boolean;
250
+ /**
251
+ * Create a session manager.
252
+ */
253
+ export declare function createSessionManager(options: SessionManagerOptions): SessionManager;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Pluggable persistence for the session manager.
3
+ *
4
+ * Only the REFRESH token is persisted. The access token stays in memory: at a
5
+ * 15-minute TTL, persisting it buys almost nothing and it is the more dangerous
6
+ * of the two to leave sitting in `localStorage`.
7
+ */
8
+ /**
9
+ * Minimal synchronous key/value store.
10
+ *
11
+ * Matches the `localStorage` shape so the browser default is a pass-through.
12
+ */
13
+ export interface MPFAuthStorage {
14
+ getItem(key: string): string | null;
15
+ setItem(key: string, value: string): void;
16
+ removeItem(key: string): void;
17
+ }
18
+ /**
19
+ * An in-memory store. Used automatically when `window` is absent (Node, SSR,
20
+ * the e2e suite) and available explicitly for tests.
21
+ */
22
+ export declare function createMemoryStorage(): MPFAuthStorage;
23
+ /**
24
+ * `sessionStorage` when it is usable, an in-memory store otherwise.
25
+ *
26
+ * Per-tab and cleared when the tab closes, which is what a redirect flow wants:
27
+ * see `createOAuthStateStore`.
28
+ */
29
+ export declare function createSessionStorage(): MPFAuthStorage;
30
+ /**
31
+ * `localStorage` when it is usable, an in-memory store otherwise.
32
+ *
33
+ * Access is probed rather than assumed: Safari in private mode and blocked
34
+ * third-party contexts expose `localStorage` but throw on write.
35
+ */
36
+ export declare function createDefaultStorage(): MPFAuthStorage;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Tenant display metadata — names and slugs.
3
+ *
4
+ * ## Why this is separate from the claim set
5
+ *
6
+ * The access token's `tnts` array carries tenant **ids and roles only**. That is
7
+ * the authoritative, freshest answer to "which tenants may this user select",
8
+ * and it needs no network call — but it renders a switcher full of UUIDs.
9
+ *
10
+ * Names live in the database (`identity.tenants`), so fetching them is a real
11
+ * network operation that can really fail. Keeping it in its own module, behind
12
+ * its own state, means the two questions stay distinguishable:
13
+ *
14
+ * - "which tenants can I select?" — claims, synchronous, cannot fail
15
+ * - "what are they called?" — network, asynchronous, can fail
16
+ *
17
+ * Merging them would make a failed name lookup indistinguishable from having no
18
+ * tenants, which is the exact bug class the tri-state exists to prevent.
19
+ *
20
+ * ## Reachability
21
+ *
22
+ * The `tenantless` Hasura role has select permissions on `identity_tenants`
23
+ * scoped to `X-Hasura-User-Id`, so a signed-in user with NO tenant selected can
24
+ * read the names of the tenants they belong to. That is precisely the switcher's
25
+ * situation.
26
+ */
27
+ /** Display metadata for one tenant. */
28
+ export interface TenantDirectoryEntry {
29
+ id: string;
30
+ name?: string;
31
+ slug?: string;
32
+ }
33
+ /**
34
+ * Loads display metadata for the given tenant ids.
35
+ *
36
+ * Injected rather than implemented here so that neither this module nor the
37
+ * React layer has to depend on the GraphQL client — which keeps `src/auth/` a
38
+ * leaf and the React layer free of `src/client/graphql/`. Use
39
+ * {@link TENANT_DIRECTORY_QUERY} with whichever GraphQL client the app already
40
+ * has.
41
+ */
42
+ export type TenantDirectoryLoader = (tenantIds: string[]) => Promise<TenantDirectoryEntry[]>;
43
+ /**
44
+ * A ready-made query for the loader.
45
+ *
46
+ * Runs under the `tenantless` role — i.e. with no `X-MPF-Tenant` header — which
47
+ * is why a switcher can populate itself before any tenant is selected.
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * import { TENANT_DIRECTORY_QUERY } from '@mosano-product-framework/sdk/auth';
52
+ *
53
+ * const loadTenantDirectory = async () => {
54
+ * const data = await gql.query(TENANT_DIRECTORY_QUERY, undefined, { tenant: null });
55
+ * return data.identity_tenants;
56
+ * };
57
+ * ```
58
+ */
59
+ export declare const TENANT_DIRECTORY_QUERY = "query MPFTenantDirectory {\n identity_tenants {\n id\n name\n slug\n }\n}";
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Tenant selection — a dependency-free mutable leaf.
3
+ *
4
+ * Deliberately imports nothing but its own types, so it can be constructed
5
+ * BEFORE any client and then shared by the identity, storage and GraphQL
6
+ * clients without an import cycle:
7
+ *
8
+ * ```ts
9
+ * const tenants = createTenantSelection();
10
+ * const auth = createAuthClient({ baseUrl, tokenProvider, tenantProvider: tenants });
11
+ * const storage = createStorageClient({ baseUrl, tokenProvider, tenantProvider: tenants });
12
+ * tenants.set(tenantId); // both clients now scope to it, no reconstruction
13
+ * ```
14
+ */
15
+ import type { MPFRole } from './claims-types.js';
16
+ import type { MPFAuthStorage } from './storage.js';
17
+ /**
18
+ * The currently selected tenant.
19
+ */
20
+ export interface TenantSelection {
21
+ /** Selected tenant id, or `null` for the no-tenant (`drl`) path. */
22
+ tenant: string | null;
23
+ /**
24
+ * Selected role. `undefined` lets the server apply that tenant's default
25
+ * role (`tnts[].dfr`, else `tnts[].rls[0]`).
26
+ */
27
+ role?: MPFRole;
28
+ }
29
+ /**
30
+ * A mutable, observable tenant selection.
31
+ */
32
+ export interface TenantSelectionStore {
33
+ /** Current selection. */
34
+ get(): TenantSelection;
35
+ /**
36
+ * Select a tenant.
37
+ *
38
+ * Synchronous by design — selecting a tenant is a header change, so there is
39
+ * no renew, no await and no network call.
40
+ *
41
+ * @param tenant - Tenant id, or `null` for the no-tenant path.
42
+ * @param role - Optional role; omit to let the server apply the default.
43
+ */
44
+ set(tenant: string | null, role?: MPFRole): void;
45
+ /** Reset to the no-tenant path. */
46
+ clear(): void;
47
+ /**
48
+ * Observe changes. Returns an unsubscribe function.
49
+ *
50
+ * The listener is called only when the selection actually changes.
51
+ */
52
+ subscribe(listener: (selection: TenantSelection) => void): () => void;
53
+ }
54
+ /**
55
+ * Persistence options for the tenant selection.
56
+ *
57
+ * The selected tenant IS persisted (unlike the access token). A tenant uuid the
58
+ * user demonstrably has access to is not credential material, and losing the
59
+ * selection on reload is a real UX regression: pick a tenant, refresh, land back
60
+ * in the no-tenant state. Reading it synchronously at construction is also what
61
+ * makes the provider's anti-flash hydration possible.
62
+ *
63
+ * Note this is a UX convenience only. A tampered value grants nothing — the
64
+ * server re-derives tenant and role from the verified token on every request and
65
+ * rejects a tenant the token does not hold.
66
+ */
67
+ export interface TenantSelectionPersistence {
68
+ /** Where to store it. Omit to disable persistence. */
69
+ storage?: MPFAuthStorage;
70
+ /** Storage key. @default 'mpf.tenant_selection' */
71
+ storageKey?: string;
72
+ }
73
+ export interface CreateTenantSelectionOptions extends TenantSelectionPersistence {
74
+ /**
75
+ * Initial selection. Takes precedence over a persisted value, so an app can
76
+ * force a tenant (e.g. from a URL) without the stored one overriding it.
77
+ */
78
+ initial?: TenantSelection;
79
+ }
80
+ /**
81
+ * Create a tenant selection store.
82
+ *
83
+ * Accepts either a bare initial selection (back-compatible) or an options
84
+ * object enabling persistence.
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * const tenants = createTenantSelection({ storage: createDefaultStorage() });
89
+ * // survives a page reload
90
+ * ```
91
+ */
92
+ export declare function createTenantSelection(initialOrOptions?: TenantSelection | CreateTenantSelectionOptions): TenantSelectionStore;