@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.
- package/README.md +827 -0
- package/README.react.md +348 -0
- package/dist/auth/claims-types.d.ts +89 -0
- package/dist/auth/claims.d.ts +125 -0
- package/dist/auth/cross-tab.d.ts +114 -0
- package/dist/auth/errors.d.ts +40 -0
- package/dist/auth/index.d.ts +18 -0
- package/dist/auth/index.js +5 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/oauth-state.d.ts +93 -0
- package/dist/auth/session-manager.d.ts +253 -0
- package/dist/auth/storage.d.ts +36 -0
- package/dist/auth/tenant-directory.d.ts +59 -0
- package/dist/auth/tenant-selection.d.ts +92 -0
- package/dist/chunk-7WAV52EO.js +621 -0
- package/dist/chunk-7WAV52EO.js.map +1 -0
- package/dist/chunk-AJWM5MDZ.js +410 -0
- package/dist/chunk-AJWM5MDZ.js.map +1 -0
- package/dist/chunk-EXPYHNPV.js +212 -0
- package/dist/chunk-EXPYHNPV.js.map +1 -0
- package/dist/chunk-GPWGOYCA.js +85 -0
- package/dist/chunk-GPWGOYCA.js.map +1 -0
- package/dist/chunk-GQJ3QQPH.js +339 -0
- package/dist/chunk-GQJ3QQPH.js.map +1 -0
- package/dist/chunk-K2ELAI2X.js +64 -0
- package/dist/chunk-K2ELAI2X.js.map +1 -0
- package/dist/chunk-LRM6JJ63.js +616 -0
- package/dist/chunk-LRM6JJ63.js.map +1 -0
- package/dist/chunk-XAXFIIRT.js +959 -0
- package/dist/chunk-XAXFIIRT.js.map +1 -0
- package/dist/client/core/client-factory.d.ts +61 -0
- package/dist/client/core/client.d.ts +144 -0
- package/dist/client/core/errors.d.ts +105 -0
- package/dist/client/core/index.d.ts +9 -0
- package/dist/client/core/middleware.d.ts +67 -0
- package/dist/client/core/types.d.ts +99 -0
- package/dist/client/graphql/client.d.ts +66 -0
- package/dist/client/graphql/factory.d.ts +84 -0
- package/dist/client/graphql/operation.d.ts +24 -0
- package/dist/client/graphql/types.d.ts +60 -0
- package/dist/client/graphql/ws-client.d.ts +116 -0
- package/dist/client/index.d.ts +17 -0
- package/dist/client/index.js +227 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/middlewares/admin-auth.d.ts +90 -0
- package/dist/client/middlewares/auth.d.ts +81 -0
- package/dist/client/middlewares/index.d.ts +12 -0
- package/dist/client/middlewares/logging.d.ts +102 -0
- package/dist/client/middlewares/retry.d.ts +138 -0
- package/dist/client/middlewares/tenant.d.ts +60 -0
- package/dist/client/middlewares/turnstile.d.ts +41 -0
- package/dist/client/peer-free.d.ts +25 -0
- package/dist/client/utils/url.d.ts +19 -0
- package/dist/identity/index.d.ts +85 -0
- package/dist/identity/index.js +6 -0
- package/dist/identity/index.js.map +1 -0
- package/dist/identity/types.d.ts +690 -0
- package/dist/identity/v0.d.ts +594 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/react/context.d.ts +47 -0
- package/dist/react/hooks.d.ts +120 -0
- package/dist/react/index.d.ts +19 -0
- package/dist/react/index.js +308 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/provider.d.ts +68 -0
- package/dist/react/store.d.ts +85 -0
- package/dist/storage/index.d.ts +31 -0
- package/dist/storage/index.js +5 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/types.d.ts +107 -0
- package/dist/storage/v0.d.ts +120 -0
- package/package.json +99 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The context value shared by the provider and every hook.
|
|
3
|
+
*
|
|
4
|
+
* Kept in its own module so `provider.tsx` and `hooks.ts` can both import it
|
|
5
|
+
* without a cycle.
|
|
6
|
+
*/
|
|
7
|
+
import type { MPFRole } from '../auth/claims-types.js';
|
|
8
|
+
import type { SessionManager, SignOutResult, TokenPair } from '../auth/session-manager.js';
|
|
9
|
+
import type { TenantSelectionStore } from '../auth/tenant-selection.js';
|
|
10
|
+
import type { AuthStore } from './store.js';
|
|
11
|
+
export interface MPFAuthContextValue {
|
|
12
|
+
store: AuthStore;
|
|
13
|
+
session: SessionManager;
|
|
14
|
+
tenants: TenantSelectionStore;
|
|
15
|
+
/**
|
|
16
|
+
* Select a tenant.
|
|
17
|
+
*
|
|
18
|
+
* **Synchronous. No await, no network, no renew.** Tenant is a request header
|
|
19
|
+
* now, so switching tenants is a local state change — that single property is
|
|
20
|
+
* the entire point of the header-based design.
|
|
21
|
+
*/
|
|
22
|
+
selectTenant(tenant: string | null, role?: MPFRole): void;
|
|
23
|
+
/** Adopt a fresh token pair (after sign-in). */
|
|
24
|
+
setTokens(tokens: TokenPair): void;
|
|
25
|
+
/**
|
|
26
|
+
* Sign out: revoke server-side (when a `revoke` was supplied) then clear
|
|
27
|
+
* locally. Never rejects; the outcome is in the result.
|
|
28
|
+
*/
|
|
29
|
+
signOut(): Promise<SignOutResult>;
|
|
30
|
+
/** Load the tenant list from the current token's claims. */
|
|
31
|
+
refreshTenants(): void;
|
|
32
|
+
/**
|
|
33
|
+
* Fetch tenant display names, if a loader was supplied.
|
|
34
|
+
*
|
|
35
|
+
* Resolves once the attempt has settled; failures are reported through state
|
|
36
|
+
* rather than thrown, because a switcher that cannot show names should still
|
|
37
|
+
* render (with ids) rather than crash.
|
|
38
|
+
*/
|
|
39
|
+
loadDirectory(): Promise<void>;
|
|
40
|
+
/** Whether a directory loader was supplied at all. */
|
|
41
|
+
readonly hasDirectoryLoader: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* `null` when no provider is mounted, which is what `useMPFAuthOptional()`
|
|
45
|
+
* reports without throwing.
|
|
46
|
+
*/
|
|
47
|
+
export declare const MPFAuthContext: import("react").Context<MPFAuthContextValue | null>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hooks over the auth store.
|
|
3
|
+
*
|
|
4
|
+
* Every read goes through `useSyncExternalStore`, so React owns the
|
|
5
|
+
* subscription and concurrent rendering cannot tear.
|
|
6
|
+
*/
|
|
7
|
+
import type { MPFClaims, MPFRole } from '../auth/claims-types.js';
|
|
8
|
+
import type { SignOutResult, TokenPair } from '../auth/session-manager.js';
|
|
9
|
+
import type { TenantSelection } from '../auth/tenant-selection.js';
|
|
10
|
+
import type { TenantDirectoryEntry } from '../auth/tenant-directory.js';
|
|
11
|
+
import { type MPFAuthContextValue } from './context.js';
|
|
12
|
+
import type { TenantDirectoryStatus, TenantEntry, TenantsStatus } from './store.js';
|
|
13
|
+
/**
|
|
14
|
+
* The auth context, or `null` when no provider is mounted.
|
|
15
|
+
*
|
|
16
|
+
* For components that must render both inside and outside an authenticated
|
|
17
|
+
* shell (a marketing header reused on a dashboard, say) without crashing.
|
|
18
|
+
*/
|
|
19
|
+
export declare function useMPFAuthOptional(): MPFAuthContextValue | null;
|
|
20
|
+
export interface UseAuthResult {
|
|
21
|
+
/** True once hydration has run — always true after the first render. */
|
|
22
|
+
hydrated: boolean;
|
|
23
|
+
/** True when an access token is present. */
|
|
24
|
+
authenticated: boolean;
|
|
25
|
+
/** Decoded (UNVERIFIED, UX-only) claims. */
|
|
26
|
+
claims: MPFClaims | null;
|
|
27
|
+
/** Await a usable access token, renewing if necessary. */
|
|
28
|
+
getAccessToken: () => Promise<string | null>;
|
|
29
|
+
/** Adopt a fresh token pair after sign-in. */
|
|
30
|
+
setTokens: (tokens: TokenPair) => void;
|
|
31
|
+
/**
|
|
32
|
+
* Sign out: revokes server-side (when the provider was given `revoke`) then
|
|
33
|
+
* clears locally. Resolves with `{ revoked, error? }` and never rejects —
|
|
34
|
+
* local state is always cleared, so a user who clicked sign-out ends up
|
|
35
|
+
* signed out regardless of the network. Does not navigate.
|
|
36
|
+
*/
|
|
37
|
+
signOut: () => Promise<SignOutResult>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Core auth state and actions.
|
|
41
|
+
*/
|
|
42
|
+
export declare function useAuth(): UseAuthResult;
|
|
43
|
+
/**
|
|
44
|
+
* Decoded claims of the current access token.
|
|
45
|
+
*
|
|
46
|
+
* ⚠️ UNVERIFIED and forgeable by a hostile client. UX only — never gate a
|
|
47
|
+
* security-relevant branch on this.
|
|
48
|
+
*/
|
|
49
|
+
export declare function useClaims(): MPFClaims | null;
|
|
50
|
+
export interface UseTenantsResult {
|
|
51
|
+
/**
|
|
52
|
+
* `'unloaded'` when there is no token, `'loaded'` otherwise.
|
|
53
|
+
*
|
|
54
|
+
* Only two states because this list comes from the token's claims: there is no
|
|
55
|
+
* fetch here, so there is nothing that can fail. The failure case that matters
|
|
56
|
+
* — "we could not find out what these tenants are called" — lives on
|
|
57
|
+
* {@link useTenantDirectory}, where it is real.
|
|
58
|
+
*/
|
|
59
|
+
status: TenantsStatus;
|
|
60
|
+
/**
|
|
61
|
+
* Selectable tenants, always driven by the claims. `name` is present only once
|
|
62
|
+
* the directory has loaded; render `name ?? id` so a switcher degrades to the
|
|
63
|
+
* id rather than to nothing.
|
|
64
|
+
*/
|
|
65
|
+
tenants: TenantEntry[];
|
|
66
|
+
/** Recompute from the current token's claims. */
|
|
67
|
+
refresh: () => void;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The tenants the current token grants access to.
|
|
71
|
+
*
|
|
72
|
+
* Synchronous and cannot fail: the answer is already in the access token, which
|
|
73
|
+
* is fresher than anything a round trip could tell us. Display names are a
|
|
74
|
+
* separate concern — see {@link useTenantDirectory}.
|
|
75
|
+
*/
|
|
76
|
+
export declare function useTenants(): UseTenantsResult;
|
|
77
|
+
export interface UseTenantDirectoryResult {
|
|
78
|
+
/**
|
|
79
|
+
* Tri-state, deliberately: `'loaded'` with no names ("this user belongs to no
|
|
80
|
+
* tenants") must never be confused with `'failed'` ("we could not find out").
|
|
81
|
+
* Collapsing those is how an empty switcher comes to silently mean "the server
|
|
82
|
+
* was unreachable".
|
|
83
|
+
*
|
|
84
|
+
* Stays `'unloaded'` when no `loadTenantDirectory` was supplied to the
|
|
85
|
+
* provider.
|
|
86
|
+
*/
|
|
87
|
+
status: TenantDirectoryStatus;
|
|
88
|
+
/** Fetched display metadata, keyed by tenant id. */
|
|
89
|
+
directory: Record<string, TenantDirectoryEntry>;
|
|
90
|
+
/** Why the fetch failed, when it did. */
|
|
91
|
+
error: Error | null;
|
|
92
|
+
/** Whether the provider was given a loader at all. */
|
|
93
|
+
available: boolean;
|
|
94
|
+
/** Fetch (or re-fetch) display names. */
|
|
95
|
+
load: () => Promise<void>;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Tenant display names.
|
|
99
|
+
*
|
|
100
|
+
* Opt-in: supply `loadTenantDirectory` to `<MPFAuthProvider>`. Without it
|
|
101
|
+
* `useTenants()` still lists every selectable tenant by id — a switcher works,
|
|
102
|
+
* it just shows ids.
|
|
103
|
+
*
|
|
104
|
+
* A failure here never removes tenants from `useTenants()`: the ids come from
|
|
105
|
+
* the token and stay selectable regardless.
|
|
106
|
+
*/
|
|
107
|
+
export declare function useTenantDirectory(): UseTenantDirectoryResult;
|
|
108
|
+
/**
|
|
109
|
+
* The current tenant selection.
|
|
110
|
+
*/
|
|
111
|
+
export declare function useCurrentTenant(): TenantSelection;
|
|
112
|
+
/**
|
|
113
|
+
* Select a tenant.
|
|
114
|
+
*
|
|
115
|
+
* **Synchronous — no await, no network, no renew.** Tenant is a request header,
|
|
116
|
+
* so switching is a local state change and the next request simply carries a
|
|
117
|
+
* different header. This is the entire payoff of the header-based design; a test
|
|
118
|
+
* asserts it fires zero fetches.
|
|
119
|
+
*/
|
|
120
|
+
export declare function useSelectTenant(): (tenant: string | null, role?: MPFRole) => void;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React bindings for the MPF SDK.
|
|
3
|
+
*
|
|
4
|
+
* **Dependency-free apart from React itself** — no TanStack Query, no zustand,
|
|
5
|
+
* no router. State is held in a small external store read through
|
|
6
|
+
* `useSyncExternalStore`, so there are no peer-dependency constraints on how a
|
|
7
|
+
* consumer manages the rest of its data, and SWR / Apollo / plain-fetch users
|
|
8
|
+
* are all equally supported.
|
|
9
|
+
*
|
|
10
|
+
* This subpath imports only from `src/auth/` and public core types, never into
|
|
11
|
+
* `src/identity/` or `src/storage/` internals, so splitting it into its own package later is a
|
|
12
|
+
* directory move.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
export { MPFAuthProvider, type MPFAuthProviderProps } from './provider.js';
|
|
17
|
+
export { useAuth, useClaims, useTenants, useTenantDirectory, useCurrentTenant, useSelectTenant, useMPFAuthOptional, type UseAuthResult, type UseTenantsResult, type UseTenantDirectoryResult, } from './hooks.js';
|
|
18
|
+
export { MPFAuthContext, type MPFAuthContextValue } from './context.js';
|
|
19
|
+
export { createAuthStore, createInitialState, type AuthState, type AuthStore, type TenantEntry, type TenantsStatus, type TenantDirectoryStatus, } from './store.js';
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { createDefaultStorage, createTenantSelection, SessionManager } from '../chunk-7WAV52EO.js';
|
|
2
|
+
import { tryDecodeAccessToken, listTenants, defaultRoleForTenant, rolesForTenant } from '../chunk-AJWM5MDZ.js';
|
|
3
|
+
import { createContext, useRef, useEffect, useMemo, useContext, useCallback, useSyncExternalStore } from 'react';
|
|
4
|
+
import { jsx } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
var MPFAuthContext = createContext(null);
|
|
7
|
+
|
|
8
|
+
// src/react/store.ts
|
|
9
|
+
function createInitialState(overrides = {}) {
|
|
10
|
+
return {
|
|
11
|
+
hydrated: false,
|
|
12
|
+
authenticated: false,
|
|
13
|
+
claims: null,
|
|
14
|
+
selection: { tenant: null },
|
|
15
|
+
tenantsStatus: "unloaded",
|
|
16
|
+
tenants: [],
|
|
17
|
+
directoryStatus: "unloaded",
|
|
18
|
+
directory: {},
|
|
19
|
+
directoryError: null,
|
|
20
|
+
...overrides
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function createAuthStore(initial) {
|
|
24
|
+
let state = initial;
|
|
25
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
26
|
+
return {
|
|
27
|
+
getSnapshot() {
|
|
28
|
+
return state;
|
|
29
|
+
},
|
|
30
|
+
subscribe(listener) {
|
|
31
|
+
listeners.add(listener);
|
|
32
|
+
return () => {
|
|
33
|
+
listeners.delete(listener);
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
setState(patch) {
|
|
37
|
+
let changed = false;
|
|
38
|
+
for (const key of Object.keys(patch)) {
|
|
39
|
+
if (state[key] !== patch[key]) {
|
|
40
|
+
changed = true;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (!changed) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
state = { ...state, ...patch };
|
|
48
|
+
for (const listener of [...listeners]) {
|
|
49
|
+
listener();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function tenantsFromToken(token, directory = {}) {
|
|
55
|
+
const claims = tryDecodeAccessToken(token);
|
|
56
|
+
if (!claims) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
return listTenants(claims).map((id) => {
|
|
60
|
+
const meta = directory[id];
|
|
61
|
+
const defaultRole = defaultRoleForTenant(claims, id);
|
|
62
|
+
return {
|
|
63
|
+
id,
|
|
64
|
+
roles: rolesForTenant(claims, id),
|
|
65
|
+
...defaultRole !== void 0 ? { defaultRole } : {},
|
|
66
|
+
...meta?.name !== void 0 ? { name: meta.name } : {},
|
|
67
|
+
...meta?.slug !== void 0 ? { slug: meta.slug } : {}
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
var warnedDeadSession = false;
|
|
72
|
+
function defaultOnDeadSession() {
|
|
73
|
+
if (warnedDeadSession) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
warnedDeadSession = true;
|
|
77
|
+
console.warn(
|
|
78
|
+
"[MPF SDK] The session is no longer valid and the user must sign in again. Pass `onDeadSession` to <MPFAuthProvider> to handle this (e.g. redirect to your login route). This SDK never navigates on your behalf."
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
function MPFAuthProvider(props) {
|
|
82
|
+
const {
|
|
83
|
+
children,
|
|
84
|
+
renew,
|
|
85
|
+
revoke,
|
|
86
|
+
storage,
|
|
87
|
+
onDeadSession = defaultOnDeadSession,
|
|
88
|
+
tenantProvider,
|
|
89
|
+
sessionOptions,
|
|
90
|
+
session: injectedSession,
|
|
91
|
+
loadTenantDirectory,
|
|
92
|
+
autoLoadTenantDirectory = true
|
|
93
|
+
} = props;
|
|
94
|
+
const value = useRef(null);
|
|
95
|
+
if (value.current === null) {
|
|
96
|
+
const resolvedStorage = storage ?? createDefaultStorage();
|
|
97
|
+
const tenants = tenantProvider ?? createTenantSelection({ storage: resolvedStorage });
|
|
98
|
+
const session = injectedSession ?? new SessionManager({
|
|
99
|
+
...sessionOptions,
|
|
100
|
+
renew,
|
|
101
|
+
...revoke ? { revoke } : {},
|
|
102
|
+
storage: resolvedStorage,
|
|
103
|
+
onDeadSession: (error) => onDeadSession(error)
|
|
104
|
+
});
|
|
105
|
+
const initialToken = session.peekAccessToken();
|
|
106
|
+
const initialState = createInitialState({
|
|
107
|
+
hydrated: true,
|
|
108
|
+
authenticated: initialToken !== null,
|
|
109
|
+
claims: session.getClaims(),
|
|
110
|
+
selection: tenants.get(),
|
|
111
|
+
...initialToken ? { tenantsStatus: "loaded", tenants: tenantsFromToken(initialToken) } : {}
|
|
112
|
+
});
|
|
113
|
+
const store = createAuthStore(initialState);
|
|
114
|
+
const syncFromSession = () => {
|
|
115
|
+
const token = session.peekAccessToken();
|
|
116
|
+
store.setState({
|
|
117
|
+
authenticated: token !== null,
|
|
118
|
+
claims: session.getClaims()
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
value.current = {
|
|
122
|
+
store,
|
|
123
|
+
session,
|
|
124
|
+
tenants,
|
|
125
|
+
selectTenant(tenant, role) {
|
|
126
|
+
tenants.set(tenant, role);
|
|
127
|
+
},
|
|
128
|
+
setTokens(tokens) {
|
|
129
|
+
session.setTokens(tokens);
|
|
130
|
+
syncFromSession();
|
|
131
|
+
store.setState({
|
|
132
|
+
tenantsStatus: "loaded",
|
|
133
|
+
tenants: tenantsFromToken(
|
|
134
|
+
tokens.access_token,
|
|
135
|
+
store.getSnapshot().directory
|
|
136
|
+
)
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
async signOut() {
|
|
140
|
+
const result = await session.signOut();
|
|
141
|
+
tenants.clear();
|
|
142
|
+
store.setState({
|
|
143
|
+
authenticated: false,
|
|
144
|
+
claims: null,
|
|
145
|
+
tenantsStatus: "unloaded",
|
|
146
|
+
tenants: [],
|
|
147
|
+
// Names belong to the signed-out user; drop them with the session.
|
|
148
|
+
directoryStatus: "unloaded",
|
|
149
|
+
directory: {},
|
|
150
|
+
directoryError: null
|
|
151
|
+
});
|
|
152
|
+
return result;
|
|
153
|
+
},
|
|
154
|
+
refreshTenants() {
|
|
155
|
+
const token = session.peekAccessToken();
|
|
156
|
+
if (!token) {
|
|
157
|
+
store.setState({ tenantsStatus: "unloaded", tenants: [] });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
store.setState({
|
|
161
|
+
tenantsStatus: "loaded",
|
|
162
|
+
tenants: tenantsFromToken(token, store.getSnapshot().directory)
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
hasDirectoryLoader: loadTenantDirectory !== void 0,
|
|
166
|
+
async loadDirectory() {
|
|
167
|
+
if (!loadTenantDirectory) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const token = session.peekAccessToken();
|
|
171
|
+
if (!token) {
|
|
172
|
+
store.setState({
|
|
173
|
+
directoryStatus: "failed",
|
|
174
|
+
directoryError: new Error(
|
|
175
|
+
"No access token; cannot load tenant display names"
|
|
176
|
+
)
|
|
177
|
+
});
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const ids = tenantsFromToken(token).map((t) => t.id);
|
|
181
|
+
if (ids.length === 0) {
|
|
182
|
+
store.setState({
|
|
183
|
+
directoryStatus: "loaded",
|
|
184
|
+
directory: {},
|
|
185
|
+
directoryError: null
|
|
186
|
+
});
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
store.setState({ directoryStatus: "loading", directoryError: null });
|
|
190
|
+
try {
|
|
191
|
+
const entries = await loadTenantDirectory(ids);
|
|
192
|
+
const directory = {};
|
|
193
|
+
for (const entry of entries) {
|
|
194
|
+
if (entry && typeof entry.id === "string") {
|
|
195
|
+
directory[entry.id] = entry;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
store.setState({
|
|
199
|
+
directoryStatus: "loaded",
|
|
200
|
+
directory,
|
|
201
|
+
directoryError: null,
|
|
202
|
+
// Re-merge names into the claims-derived list.
|
|
203
|
+
tenants: tenantsFromToken(session.peekAccessToken(), directory)
|
|
204
|
+
});
|
|
205
|
+
} catch (error) {
|
|
206
|
+
store.setState({
|
|
207
|
+
directoryStatus: "failed",
|
|
208
|
+
directoryError: error instanceof Error ? error : new Error(String(error))
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
const ctx = value.current;
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
const unsubscribeTenants = ctx.tenants.subscribe((selection) => {
|
|
217
|
+
ctx.store.setState({ selection });
|
|
218
|
+
});
|
|
219
|
+
return () => {
|
|
220
|
+
unsubscribeTenants();
|
|
221
|
+
};
|
|
222
|
+
}, [ctx]);
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
ctx.store.setState({
|
|
225
|
+
authenticated: ctx.session.peekAccessToken() !== null,
|
|
226
|
+
claims: ctx.session.getClaims()
|
|
227
|
+
});
|
|
228
|
+
}, [ctx]);
|
|
229
|
+
const authenticated = ctx.store.getSnapshot().authenticated;
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
if (!autoLoadTenantDirectory || !ctx.hasDirectoryLoader || !authenticated) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (ctx.store.getSnapshot().directoryStatus !== "unloaded") {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
void ctx.loadDirectory();
|
|
238
|
+
}, [ctx, autoLoadTenantDirectory, authenticated]);
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
return () => {
|
|
241
|
+
ctx.session.destroy();
|
|
242
|
+
};
|
|
243
|
+
}, [ctx]);
|
|
244
|
+
const memoized = useMemo(() => ctx, [ctx]);
|
|
245
|
+
return /* @__PURE__ */ jsx(MPFAuthContext.Provider, { value: memoized, children });
|
|
246
|
+
}
|
|
247
|
+
function useMPFAuthOptional() {
|
|
248
|
+
return useContext(MPFAuthContext);
|
|
249
|
+
}
|
|
250
|
+
function useRequiredContext(hook) {
|
|
251
|
+
const ctx = useContext(MPFAuthContext);
|
|
252
|
+
if (!ctx) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
`${hook}() requires a <MPFAuthProvider> ancestor. Use useMPFAuthOptional() if the component can render without one.`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
return ctx;
|
|
258
|
+
}
|
|
259
|
+
function useAuthState(hook, select) {
|
|
260
|
+
const ctx = useRequiredContext(hook);
|
|
261
|
+
return useSyncExternalStore(
|
|
262
|
+
ctx.store.subscribe,
|
|
263
|
+
() => select(ctx.store.getSnapshot()),
|
|
264
|
+
() => select(ctx.store.getSnapshot())
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
function useAuth() {
|
|
268
|
+
const ctx = useRequiredContext("useAuth");
|
|
269
|
+
const hydrated = useAuthState("useAuth", (s) => s.hydrated);
|
|
270
|
+
const authenticated = useAuthState("useAuth", (s) => s.authenticated);
|
|
271
|
+
const claims = useAuthState("useAuth", (s) => s.claims);
|
|
272
|
+
const getAccessToken = useCallback(() => ctx.session.getAccessToken(), [ctx]);
|
|
273
|
+
const setTokens = useCallback((tokens) => ctx.setTokens(tokens), [ctx]);
|
|
274
|
+
const signOut = useCallback(() => ctx.signOut(), [ctx]);
|
|
275
|
+
return { hydrated, authenticated, claims, getAccessToken, setTokens, signOut };
|
|
276
|
+
}
|
|
277
|
+
function useClaims() {
|
|
278
|
+
return useAuthState("useClaims", (s) => s.claims);
|
|
279
|
+
}
|
|
280
|
+
function useTenants() {
|
|
281
|
+
const ctx = useRequiredContext("useTenants");
|
|
282
|
+
const status = useAuthState("useTenants", (s) => s.tenantsStatus);
|
|
283
|
+
const tenants = useAuthState("useTenants", (s) => s.tenants);
|
|
284
|
+
const refresh = useCallback(() => ctx.refreshTenants(), [ctx]);
|
|
285
|
+
return { status, tenants, refresh };
|
|
286
|
+
}
|
|
287
|
+
function useTenantDirectory() {
|
|
288
|
+
const ctx = useRequiredContext("useTenantDirectory");
|
|
289
|
+
const status = useAuthState("useTenantDirectory", (s) => s.directoryStatus);
|
|
290
|
+
const directory = useAuthState("useTenantDirectory", (s) => s.directory);
|
|
291
|
+
const error = useAuthState("useTenantDirectory", (s) => s.directoryError);
|
|
292
|
+
const load = useCallback(() => ctx.loadDirectory(), [ctx]);
|
|
293
|
+
return { status, directory, error, available: ctx.hasDirectoryLoader, load };
|
|
294
|
+
}
|
|
295
|
+
function useCurrentTenant() {
|
|
296
|
+
return useAuthState("useCurrentTenant", (s) => s.selection);
|
|
297
|
+
}
|
|
298
|
+
function useSelectTenant() {
|
|
299
|
+
const ctx = useRequiredContext("useSelectTenant");
|
|
300
|
+
return useCallback(
|
|
301
|
+
(tenant, role) => ctx.selectTenant(tenant, role),
|
|
302
|
+
[ctx]
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export { MPFAuthContext, MPFAuthProvider, createAuthStore, createInitialState, useAuth, useClaims, useCurrentTenant, useMPFAuthOptional, useSelectTenant, useTenantDirectory, useTenants };
|
|
307
|
+
//# sourceMappingURL=index.js.map
|
|
308
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/react/context.ts","../../src/react/store.ts","../../src/react/provider.tsx","../../src/react/hooks.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * The context value shared by the provider and every hook.\n *\n * Kept in its own module so `provider.tsx` and `hooks.ts` can both import it\n * without a cycle.\n */\n\nimport { createContext } from 'react';\n\nimport type { MPFRole } from '../auth/claims-types.js';\nimport type { SessionManager, SignOutResult, TokenPair } from '../auth/session-manager.js';\nimport type { TenantSelectionStore } from '../auth/tenant-selection.js';\nimport type { AuthStore } from './store.js';\n\nexport interface MPFAuthContextValue {\n store: AuthStore;\n session: SessionManager;\n tenants: TenantSelectionStore;\n /**\n * Select a tenant.\n *\n * **Synchronous. No await, no network, no renew.** Tenant is a request header\n * now, so switching tenants is a local state change — that single property is\n * the entire point of the header-based design.\n */\n selectTenant(tenant: string | null, role?: MPFRole): void;\n /** Adopt a fresh token pair (after sign-in). */\n setTokens(tokens: TokenPair): void;\n /**\n * Sign out: revoke server-side (when a `revoke` was supplied) then clear\n * locally. Never rejects; the outcome is in the result.\n */\n signOut(): Promise<SignOutResult>;\n /** Load the tenant list from the current token's claims. */\n refreshTenants(): void;\n /**\n * Fetch tenant display names, if a loader was supplied.\n *\n * Resolves once the attempt has settled; failures are reported through state\n * rather than thrown, because a switcher that cannot show names should still\n * render (with ids) rather than crash.\n */\n loadDirectory(): Promise<void>;\n /** Whether a directory loader was supplied at all. */\n readonly hasDirectoryLoader: boolean;\n}\n\n/**\n * `null` when no provider is mounted, which is what `useMPFAuthOptional()`\n * reports without throwing.\n */\nexport const MPFAuthContext = createContext<MPFAuthContextValue | null>(null);\n","/**\n * A tiny external store for `useSyncExternalStore`.\n *\n * Zero dependencies and no tearing: `useSyncExternalStore` is React's own\n * contract for reading external mutable state, so concurrent rendering cannot\n * observe two different snapshots within one commit. A `useState` +\n * `useEffect` mirror would.\n *\n * `getSnapshot` must return a **referentially stable** value while nothing has\n * changed, or React re-renders forever. Every mutation here therefore builds\n * exactly one new state object and every non-mutation returns the existing one.\n */\n\nimport type { MPFClaims } from '../auth/claims-types.js';\nimport type { TenantSelection } from '../auth/tenant-selection.js';\nimport type { TenantDirectoryEntry } from '../auth/tenant-directory.js';\n\n/**\n * Whether the selectable-tenant list is known.\n *\n * Only two states, because this list comes from the access token's claims: if\n * there is a token the answer is known, and if there is not, it is not. There is\n * no fetch, so there is nothing to fail. The thing that CAN fail is the display\n * name lookup — see {@link TenantDirectoryStatus}, which is where the\n * \"loaded-but-empty vs failed\" distinction actually earns its keep.\n */\nexport type TenantsStatus = 'unloaded' | 'loaded';\n\n/** Whether tenant display names have been fetched. */\nexport type TenantDirectoryStatus = 'unloaded' | 'loading' | 'loaded' | 'failed';\n\n/** A tenant as surfaced to the UI. */\nexport interface TenantEntry {\n id: string;\n /** Every role held in this tenant. */\n roles: string[];\n /**\n * That tenant's default role: its explicit `dfr` claim when the token carries\n * one, otherwise `roles[0]`. Absent only when the tenant grants no roles.\n *\n * Surfaced separately because `roles[0]` is no longer authoritative — a token\n * may name a default that is not the first role listed.\n */\n defaultRole?: string;\n /**\n * Display name, present only once the directory has loaded. Render\n * `name ?? id` so a switcher degrades to the id rather than to nothing.\n */\n name?: string;\n /** URL-friendly slug, present only once the directory has loaded. */\n slug?: string;\n}\n\nexport interface AuthState {\n /** True once hydration has run. Always true after provider construction. */\n hydrated: boolean;\n /** True when an access token is present. */\n authenticated: boolean;\n /** Decoded (UNVERIFIED, UX-only) claims of the current access token. */\n claims: MPFClaims | null;\n /** Current tenant selection. */\n selection: TenantSelection;\n /** Whether the selectable-tenant list is known (i.e. whether a token exists). */\n tenantsStatus: TenantsStatus;\n /** Selectable tenants, meaningful only when `tenantsStatus === 'loaded'`. */\n tenants: TenantEntry[];\n\n /**\n * Display-name lookup state.\n *\n * Tri-state on purpose: \"this user belongs to zero tenants\" and \"we could not\n * find out what they are called\" must never be indistinguishable. Collapsing\n * them is a real bug class — an empty switcher that silently means \"the server\n * was unreachable\".\n */\n directoryStatus: TenantDirectoryStatus;\n /** Fetched display metadata, keyed by tenant id. */\n directory: Record<string, TenantDirectoryEntry>;\n /** Why the directory fetch failed, when it did. */\n directoryError: Error | null;\n}\n\nexport interface AuthStore {\n getSnapshot(): AuthState;\n subscribe(listener: () => void): () => void;\n setState(patch: Partial<AuthState>): void;\n}\n\nexport function createInitialState(overrides: Partial<AuthState> = {}): AuthState {\n return {\n hydrated: false,\n authenticated: false,\n claims: null,\n selection: { tenant: null },\n tenantsStatus: 'unloaded',\n tenants: [],\n directoryStatus: 'unloaded',\n directory: {},\n directoryError: null,\n ...overrides,\n };\n}\n\n/**\n * Create an auth store.\n */\nexport function createAuthStore(initial: AuthState): AuthStore {\n let state = initial;\n const listeners = new Set<() => void>();\n\n return {\n getSnapshot() {\n return state;\n },\n\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n\n setState(patch) {\n // Skip no-op updates so `getSnapshot` stays referentially stable and\n // React does not re-render for nothing.\n let changed = false;\n for (const key of Object.keys(patch) as Array<keyof AuthState>) {\n if (state[key] !== patch[key]) {\n changed = true;\n break;\n }\n }\n if (!changed) {\n return;\n }\n\n state = { ...state, ...patch };\n for (const listener of [...listeners]) {\n listener();\n }\n },\n };\n}\n","/**\n * `<MPFAuthProvider>` — wires a `SessionManager` and a `TenantSelectionStore`\n * into a React tree.\n */\n\nimport { useEffect, useMemo, useRef, type ReactNode } from 'react';\n\nimport type { MPFRole } from '../auth/claims-types.js';\nimport {\n tryDecodeAccessToken,\n listTenants,\n rolesForTenant,\n defaultRoleForTenant,\n} from '../auth/claims.js';\nimport { createDefaultStorage, type MPFAuthStorage } from '../auth/storage.js';\nimport {\n SessionManager,\n type SessionManagerOptions,\n type TokenPair,\n} from '../auth/session-manager.js';\nimport {\n createTenantSelection,\n type TenantSelectionStore,\n} from '../auth/tenant-selection.js';\nimport type { DeadSessionError } from '../auth/errors.js';\nimport type {\n TenantDirectoryEntry,\n TenantDirectoryLoader,\n} from '../auth/tenant-directory.js';\nimport { MPFAuthContext, type MPFAuthContextValue } from './context.js';\nimport { createAuthStore, createInitialState, type AuthState, type TenantEntry } from './store.js';\n\nexport interface MPFAuthProviderProps {\n children?: ReactNode;\n\n /**\n * Performs the network renewal. Wire to\n * `(refreshToken) => authClient.renewSession({ refresh_token: refreshToken })`.\n */\n renew: SessionManagerOptions['renew'];\n\n /**\n * Revokes the session server-side on sign-out. Wire to\n * `(refreshToken) => authClient.revokeSession({ refresh_token: refreshToken })`.\n *\n * Without it `signOut()` still clears local state — a sign-out button must\n * always sign the user out — but the refresh token stays valid server-side\n * until it expires. Supply it.\n */\n revoke?: SessionManagerOptions['revoke'];\n\n /** Storage for the refresh token and tenant selection. @default localStorage */\n storage?: MPFAuthStorage;\n\n /**\n * Called when the session is dead and the user must sign in again.\n *\n * A **callback, never a navigation**. Libraries do not navigate: routing is\n * the application's concern and hard-coding `window.location` here would\n * couple the SDK to one router and one URL scheme. Defaults to a one-time\n * `console.warn`.\n */\n onDeadSession?: (error: DeadSessionError) => void;\n\n /**\n * Pre-built tenant selection store. Supply this when non-React code\n * (a bare `createAuthClient`) must share the same selection.\n */\n tenantProvider?: TenantSelectionStore;\n\n /** Overrides forwarded to the session manager (buffer, jitter, ...). */\n sessionOptions?: Omit<SessionManagerOptions, 'renew' | 'storage' | 'onDeadSession'>;\n\n /** Pre-built session manager. Mainly for tests. Takes precedence. */\n session?: SessionManager;\n\n /**\n * Fetches tenant display names.\n *\n * Optional: without it `useTenants()` still returns every selectable tenant\n * (ids and roles, straight from the token) and a switcher renders ids. Supply\n * it to get names.\n *\n * Injected rather than built in so the React layer never depends on the\n * GraphQL client. Use `TENANT_DIRECTORY_QUERY` with whichever client the app\n * already has, and query it with `{ tenant: null }` — the `tenantless` role has\n * the select permission a switcher needs.\n */\n loadTenantDirectory?: TenantDirectoryLoader;\n\n /**\n * Whether to fetch display names automatically once a token is present.\n * @default true when `loadTenantDirectory` is supplied\n */\n autoLoadTenantDirectory?: boolean;\n}\n\n/**\n * Build the selectable-tenant list from decoded claims, merging any display\n * metadata already fetched.\n */\nfunction tenantsFromToken(\n token: string | null,\n directory: Record<string, TenantDirectoryEntry> = {},\n): TenantEntry[] {\n const claims = tryDecodeAccessToken(token);\n if (!claims) {\n return [];\n }\n\n // Driven by the CLAIMS, never by the directory: a tenant the token does not\n // grant must never appear in a switcher just because its name was fetched.\n return listTenants(claims).map((id) => {\n const meta = directory[id];\n const defaultRole = defaultRoleForTenant(claims, id);\n return {\n id,\n roles: rolesForTenant(claims, id),\n ...(defaultRole !== undefined ? { defaultRole } : {}),\n ...(meta?.name !== undefined ? { name: meta.name } : {}),\n ...(meta?.slug !== undefined ? { slug: meta.slug } : {}),\n };\n });\n}\n\nlet warnedDeadSession = false;\n\nfunction defaultOnDeadSession(): void {\n if (warnedDeadSession) {\n return;\n }\n warnedDeadSession = true;\n console.warn(\n '[MPF SDK] The session is no longer valid and the user must sign in again. ' +\n 'Pass `onDeadSession` to <MPFAuthProvider> to handle this (e.g. redirect to ' +\n 'your login route). This SDK never navigates on your behalf.',\n );\n}\n\n/** @internal test seam */\nexport function resetDeadSessionWarning(): void {\n warnedDeadSession = false;\n}\n\nexport function MPFAuthProvider(props: MPFAuthProviderProps): JSX.Element {\n const {\n children,\n renew,\n revoke,\n storage,\n onDeadSession = defaultOnDeadSession,\n tenantProvider,\n sessionOptions,\n session: injectedSession,\n loadTenantDirectory,\n autoLoadTenantDirectory = true,\n } = props;\n\n // Everything below is built ONCE, during the first render, and never\n // recreated: a new SessionManager per render would restart timers and lose\n // in-flight renewals.\n const value = useRef<MPFAuthContextValue | null>(null);\n\n if (value.current === null) {\n const resolvedStorage = storage ?? createDefaultStorage();\n\n const tenants =\n tenantProvider ?? createTenantSelection({ storage: resolvedStorage });\n\n const session =\n injectedSession ??\n new SessionManager({\n ...sessionOptions,\n renew,\n ...(revoke ? { revoke } : {}),\n storage: resolvedStorage,\n onDeadSession: (error) => onDeadSession(error),\n });\n\n // Hydrate SYNCHRONOUSLY, before the first paint. An effect-based hydration\n // renders one frame as unauthenticated and produces the classic login-form\n // flash on reload — which is also what makes persisting the tenant\n // selection actually visible to the user.\n const initialToken = session.peekAccessToken();\n const initialState: AuthState = createInitialState({\n hydrated: true,\n authenticated: initialToken !== null,\n claims: session.getClaims(),\n selection: tenants.get(),\n ...(initialToken\n ? { tenantsStatus: 'loaded' as const, tenants: tenantsFromToken(initialToken) }\n : {}),\n });\n\n const store = createAuthStore(initialState);\n\n const syncFromSession = (): void => {\n const token = session.peekAccessToken();\n store.setState({\n authenticated: token !== null,\n claims: session.getClaims(),\n });\n };\n\n value.current = {\n store,\n session,\n tenants,\n\n selectTenant(tenant: string | null, role?: MPFRole) {\n // Synchronous by construction: a header change, nothing more.\n tenants.set(tenant, role);\n },\n\n setTokens(tokens: TokenPair) {\n session.setTokens(tokens);\n syncFromSession();\n store.setState({\n tenantsStatus: 'loaded',\n tenants: tenantsFromToken(\n tokens.access_token,\n store.getSnapshot().directory,\n ),\n });\n },\n\n async signOut() {\n // Full sequence: revoke server-side, then clear. The manager owns the\n // ordering (revoke is an authenticated call, so it must precede the\n // clear) and never throws.\n const result = await session.signOut();\n tenants.clear();\n store.setState({\n authenticated: false,\n claims: null,\n tenantsStatus: 'unloaded',\n tenants: [],\n // Names belong to the signed-out user; drop them with the session.\n directoryStatus: 'unloaded',\n directory: {},\n directoryError: null,\n });\n return result;\n },\n\n refreshTenants() {\n const token = session.peekAccessToken();\n if (!token) {\n store.setState({ tenantsStatus: 'unloaded', tenants: [] });\n return;\n }\n store.setState({\n tenantsStatus: 'loaded',\n tenants: tenantsFromToken(token, store.getSnapshot().directory),\n });\n },\n\n hasDirectoryLoader: loadTenantDirectory !== undefined,\n\n async loadDirectory() {\n if (!loadTenantDirectory) {\n return;\n }\n\n const token = session.peekAccessToken();\n if (!token) {\n store.setState({\n directoryStatus: 'failed',\n directoryError: new Error(\n 'No access token; cannot load tenant display names',\n ),\n });\n return;\n }\n\n const ids = tenantsFromToken(token).map((t) => t.id);\n if (ids.length === 0) {\n // No tenants to name. This is 'loaded' with nothing in it, which is\n // emphatically NOT the same as 'failed'.\n store.setState({\n directoryStatus: 'loaded',\n directory: {},\n directoryError: null,\n });\n return;\n }\n\n store.setState({ directoryStatus: 'loading', directoryError: null });\n\n try {\n const entries = await loadTenantDirectory(ids);\n const directory: Record<string, TenantDirectoryEntry> = {};\n for (const entry of entries) {\n if (entry && typeof entry.id === 'string') {\n directory[entry.id] = entry;\n }\n }\n\n store.setState({\n directoryStatus: 'loaded',\n directory,\n directoryError: null,\n // Re-merge names into the claims-derived list.\n tenants: tenantsFromToken(session.peekAccessToken(), directory),\n });\n } catch (error) {\n // A name lookup failure must not empty the switcher: the ids remain\n // selectable and the UI degrades to showing them.\n store.setState({\n directoryStatus: 'failed',\n directoryError: error instanceof Error ? error : new Error(String(error)),\n });\n }\n },\n };\n }\n\n const ctx = value.current;\n\n // Keep the store in step with external mutations: token renewals (including\n // ones driven by another tab) and tenant selections made outside React.\n useEffect(() => {\n const unsubscribeTenants = ctx.tenants.subscribe((selection) => {\n ctx.store.setState({ selection });\n });\n\n return () => {\n unsubscribeTenants();\n };\n }, [ctx]);\n\n useEffect(() => {\n // The session manager reports token changes through onTokensChanged, which\n // is owned by whoever constructed it. Poll-free alternative: re-read on\n // mount and rely on setTokens/signOut for the rest.\n ctx.store.setState({\n authenticated: ctx.session.peekAccessToken() !== null,\n claims: ctx.session.getClaims(),\n });\n }, [ctx]);\n\n // Fetch display names once, after mount, when a token is present. In an\n // effect rather than during construction because it is a network call — the\n // synchronous hydration above must not be blocked on it.\n const authenticated = ctx.store.getSnapshot().authenticated;\n useEffect(() => {\n if (!autoLoadTenantDirectory || !ctx.hasDirectoryLoader || !authenticated) {\n return;\n }\n if (ctx.store.getSnapshot().directoryStatus !== 'unloaded') {\n return;\n }\n void ctx.loadDirectory();\n }, [ctx, autoLoadTenantDirectory, authenticated]);\n\n useEffect(() => {\n return () => {\n ctx.session.destroy();\n };\n }, [ctx]);\n\n const memoized = useMemo(() => ctx, [ctx]);\n\n return <MPFAuthContext.Provider value={memoized}>{children}</MPFAuthContext.Provider>;\n}\n","/**\n * Hooks over the auth store.\n *\n * Every read goes through `useSyncExternalStore`, so React owns the\n * subscription and concurrent rendering cannot tear.\n */\n\nimport { useCallback, useContext, useSyncExternalStore } from 'react';\n\nimport type { MPFClaims, MPFRole } from '../auth/claims-types.js';\nimport type { SignOutResult, TokenPair } from '../auth/session-manager.js';\nimport type { TenantSelection } from '../auth/tenant-selection.js';\nimport type { TenantDirectoryEntry } from '../auth/tenant-directory.js';\nimport { MPFAuthContext, type MPFAuthContextValue } from './context.js';\nimport type {\n AuthState,\n TenantDirectoryStatus,\n TenantEntry,\n TenantsStatus,\n} from './store.js';\n\n/**\n * The auth context, or `null` when no provider is mounted.\n *\n * For components that must render both inside and outside an authenticated\n * shell (a marketing header reused on a dashboard, say) without crashing.\n */\nexport function useMPFAuthOptional(): MPFAuthContextValue | null {\n return useContext(MPFAuthContext);\n}\n\nfunction useRequiredContext(hook: string): MPFAuthContextValue {\n const ctx = useContext(MPFAuthContext);\n if (!ctx) {\n throw new Error(\n `${hook}() requires a <MPFAuthProvider> ancestor. ` +\n 'Use useMPFAuthOptional() if the component can render without one.',\n );\n }\n return ctx;\n}\n\n/** Subscribe to a slice of auth state. */\nfunction useAuthState<T>(hook: string, select: (state: AuthState) => T): T {\n const ctx = useRequiredContext(hook);\n return useSyncExternalStore(\n ctx.store.subscribe,\n () => select(ctx.store.getSnapshot()),\n () => select(ctx.store.getSnapshot()),\n );\n}\n\nexport interface UseAuthResult {\n /** True once hydration has run — always true after the first render. */\n hydrated: boolean;\n /** True when an access token is present. */\n authenticated: boolean;\n /** Decoded (UNVERIFIED, UX-only) claims. */\n claims: MPFClaims | null;\n /** Await a usable access token, renewing if necessary. */\n getAccessToken: () => Promise<string | null>;\n /** Adopt a fresh token pair after sign-in. */\n setTokens: (tokens: TokenPair) => void;\n /**\n * Sign out: revokes server-side (when the provider was given `revoke`) then\n * clears locally. Resolves with `{ revoked, error? }` and never rejects —\n * local state is always cleared, so a user who clicked sign-out ends up\n * signed out regardless of the network. Does not navigate.\n */\n signOut: () => Promise<SignOutResult>;\n}\n\n/**\n * Core auth state and actions.\n */\nexport function useAuth(): UseAuthResult {\n const ctx = useRequiredContext('useAuth');\n\n const hydrated = useAuthState('useAuth', (s) => s.hydrated);\n const authenticated = useAuthState('useAuth', (s) => s.authenticated);\n const claims = useAuthState('useAuth', (s) => s.claims);\n\n const getAccessToken = useCallback(() => ctx.session.getAccessToken(), [ctx]);\n const setTokens = useCallback((tokens: TokenPair) => ctx.setTokens(tokens), [ctx]);\n const signOut = useCallback(() => ctx.signOut(), [ctx]);\n\n return { hydrated, authenticated, claims, getAccessToken, setTokens, signOut };\n}\n\n/**\n * Decoded claims of the current access token.\n *\n * ⚠️ UNVERIFIED and forgeable by a hostile client. UX only — never gate a\n * security-relevant branch on this.\n */\nexport function useClaims(): MPFClaims | null {\n return useAuthState('useClaims', (s) => s.claims);\n}\n\nexport interface UseTenantsResult {\n /**\n * `'unloaded'` when there is no token, `'loaded'` otherwise.\n *\n * Only two states because this list comes from the token's claims: there is no\n * fetch here, so there is nothing that can fail. The failure case that matters\n * — \"we could not find out what these tenants are called\" — lives on\n * {@link useTenantDirectory}, where it is real.\n */\n status: TenantsStatus;\n /**\n * Selectable tenants, always driven by the claims. `name` is present only once\n * the directory has loaded; render `name ?? id` so a switcher degrades to the\n * id rather than to nothing.\n */\n tenants: TenantEntry[];\n /** Recompute from the current token's claims. */\n refresh: () => void;\n}\n\n/**\n * The tenants the current token grants access to.\n *\n * Synchronous and cannot fail: the answer is already in the access token, which\n * is fresher than anything a round trip could tell us. Display names are a\n * separate concern — see {@link useTenantDirectory}.\n */\nexport function useTenants(): UseTenantsResult {\n const ctx = useRequiredContext('useTenants');\n\n const status = useAuthState('useTenants', (s) => s.tenantsStatus);\n const tenants = useAuthState('useTenants', (s) => s.tenants);\n const refresh = useCallback(() => ctx.refreshTenants(), [ctx]);\n\n return { status, tenants, refresh };\n}\n\nexport interface UseTenantDirectoryResult {\n /**\n * Tri-state, deliberately: `'loaded'` with no names (\"this user belongs to no\n * tenants\") must never be confused with `'failed'` (\"we could not find out\").\n * Collapsing those is how an empty switcher comes to silently mean \"the server\n * was unreachable\".\n *\n * Stays `'unloaded'` when no `loadTenantDirectory` was supplied to the\n * provider.\n */\n status: TenantDirectoryStatus;\n /** Fetched display metadata, keyed by tenant id. */\n directory: Record<string, TenantDirectoryEntry>;\n /** Why the fetch failed, when it did. */\n error: Error | null;\n /** Whether the provider was given a loader at all. */\n available: boolean;\n /** Fetch (or re-fetch) display names. */\n load: () => Promise<void>;\n}\n\n/**\n * Tenant display names.\n *\n * Opt-in: supply `loadTenantDirectory` to `<MPFAuthProvider>`. Without it\n * `useTenants()` still lists every selectable tenant by id — a switcher works,\n * it just shows ids.\n *\n * A failure here never removes tenants from `useTenants()`: the ids come from\n * the token and stay selectable regardless.\n */\nexport function useTenantDirectory(): UseTenantDirectoryResult {\n const ctx = useRequiredContext('useTenantDirectory');\n\n const status = useAuthState('useTenantDirectory', (s) => s.directoryStatus);\n const directory = useAuthState('useTenantDirectory', (s) => s.directory);\n const error = useAuthState('useTenantDirectory', (s) => s.directoryError);\n const load = useCallback(() => ctx.loadDirectory(), [ctx]);\n\n return { status, directory, error, available: ctx.hasDirectoryLoader, load };\n}\n\n/**\n * The current tenant selection.\n */\nexport function useCurrentTenant(): TenantSelection {\n return useAuthState('useCurrentTenant', (s) => s.selection);\n}\n\n/**\n * Select a tenant.\n *\n * **Synchronous — no await, no network, no renew.** Tenant is a request header,\n * so switching is a local state change and the next request simply carries a\n * different header. This is the entire payoff of the header-based design; a test\n * asserts it fires zero fetches.\n */\nexport function useSelectTenant(): (tenant: string | null, role?: MPFRole) => void {\n const ctx = useRequiredContext('useSelectTenant');\n return useCallback(\n (tenant: string | null, role?: MPFRole) => ctx.selectTenant(tenant, role),\n [ctx],\n );\n}\n"]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<MPFAuthProvider>` — wires a `SessionManager` and a `TenantSelectionStore`
|
|
3
|
+
* into a React tree.
|
|
4
|
+
*/
|
|
5
|
+
import { type ReactNode } from 'react';
|
|
6
|
+
import { type MPFAuthStorage } from '../auth/storage.js';
|
|
7
|
+
import { SessionManager, type SessionManagerOptions } from '../auth/session-manager.js';
|
|
8
|
+
import { type TenantSelectionStore } from '../auth/tenant-selection.js';
|
|
9
|
+
import type { DeadSessionError } from '../auth/errors.js';
|
|
10
|
+
import type { TenantDirectoryLoader } from '../auth/tenant-directory.js';
|
|
11
|
+
export interface MPFAuthProviderProps {
|
|
12
|
+
children?: ReactNode;
|
|
13
|
+
/**
|
|
14
|
+
* Performs the network renewal. Wire to
|
|
15
|
+
* `(refreshToken) => authClient.renewSession({ refresh_token: refreshToken })`.
|
|
16
|
+
*/
|
|
17
|
+
renew: SessionManagerOptions['renew'];
|
|
18
|
+
/**
|
|
19
|
+
* Revokes the session server-side on sign-out. Wire to
|
|
20
|
+
* `(refreshToken) => authClient.revokeSession({ refresh_token: refreshToken })`.
|
|
21
|
+
*
|
|
22
|
+
* Without it `signOut()` still clears local state — a sign-out button must
|
|
23
|
+
* always sign the user out — but the refresh token stays valid server-side
|
|
24
|
+
* until it expires. Supply it.
|
|
25
|
+
*/
|
|
26
|
+
revoke?: SessionManagerOptions['revoke'];
|
|
27
|
+
/** Storage for the refresh token and tenant selection. @default localStorage */
|
|
28
|
+
storage?: MPFAuthStorage;
|
|
29
|
+
/**
|
|
30
|
+
* Called when the session is dead and the user must sign in again.
|
|
31
|
+
*
|
|
32
|
+
* A **callback, never a navigation**. Libraries do not navigate: routing is
|
|
33
|
+
* the application's concern and hard-coding `window.location` here would
|
|
34
|
+
* couple the SDK to one router and one URL scheme. Defaults to a one-time
|
|
35
|
+
* `console.warn`.
|
|
36
|
+
*/
|
|
37
|
+
onDeadSession?: (error: DeadSessionError) => void;
|
|
38
|
+
/**
|
|
39
|
+
* Pre-built tenant selection store. Supply this when non-React code
|
|
40
|
+
* (a bare `createAuthClient`) must share the same selection.
|
|
41
|
+
*/
|
|
42
|
+
tenantProvider?: TenantSelectionStore;
|
|
43
|
+
/** Overrides forwarded to the session manager (buffer, jitter, ...). */
|
|
44
|
+
sessionOptions?: Omit<SessionManagerOptions, 'renew' | 'storage' | 'onDeadSession'>;
|
|
45
|
+
/** Pre-built session manager. Mainly for tests. Takes precedence. */
|
|
46
|
+
session?: SessionManager;
|
|
47
|
+
/**
|
|
48
|
+
* Fetches tenant display names.
|
|
49
|
+
*
|
|
50
|
+
* Optional: without it `useTenants()` still returns every selectable tenant
|
|
51
|
+
* (ids and roles, straight from the token) and a switcher renders ids. Supply
|
|
52
|
+
* it to get names.
|
|
53
|
+
*
|
|
54
|
+
* Injected rather than built in so the React layer never depends on the
|
|
55
|
+
* GraphQL client. Use `TENANT_DIRECTORY_QUERY` with whichever client the app
|
|
56
|
+
* already has, and query it with `{ tenant: null }` — the `tenantless` role has
|
|
57
|
+
* the select permission a switcher needs.
|
|
58
|
+
*/
|
|
59
|
+
loadTenantDirectory?: TenantDirectoryLoader;
|
|
60
|
+
/**
|
|
61
|
+
* Whether to fetch display names automatically once a token is present.
|
|
62
|
+
* @default true when `loadTenantDirectory` is supplied
|
|
63
|
+
*/
|
|
64
|
+
autoLoadTenantDirectory?: boolean;
|
|
65
|
+
}
|
|
66
|
+
/** @internal test seam */
|
|
67
|
+
export declare function resetDeadSessionWarning(): void;
|
|
68
|
+
export declare function MPFAuthProvider(props: MPFAuthProviderProps): JSX.Element;
|