spry-apps-dropdown 2.0.3 → 3.0.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 +92 -2
- package/dist/AccountList.d.ts +21 -0
- package/dist/ProfileMenu.d.ts +5 -2
- package/dist/ProfileMenuConnected.d.ts +3 -2
- package/dist/SpryAuthProvider.d.ts +15 -0
- package/dist/TopBar.d.ts +2 -1
- package/dist/accountStorage.d.ts +104 -0
- package/dist/constants.d.ts +44 -0
- package/dist/index.cjs +16 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +2965 -1627
- package/dist/index.js.map +1 -1
- package/dist/keycloakLogout.d.ts +58 -0
- package/dist/syncBridge.d.ts +8 -0
- package/dist/tokenUtils.d.ts +58 -0
- package/dist/types.d.ts +56 -0
- package/dist/useAccountManager.d.ts +28 -0
- package/dist/useSpryAccountManager.d.ts +8 -0
- package/dist/useSpryAuth.d.ts +1 -0
- package/dist/userManagerPool.d.ts +84 -0
- package/package.json +9 -5
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main logout: Remove account from localStorage, then redirect to soft-logout endpoint
|
|
3
|
+
* Always redirects back to the app after logout
|
|
4
|
+
*
|
|
5
|
+
* @param accountId - The account ID to remove
|
|
6
|
+
* @param keycloakConfig - Keycloak configuration
|
|
7
|
+
* @param redirectUrl - (Optional) URL to redirect back to after logout (defaults to window.location.origin)
|
|
8
|
+
*/
|
|
9
|
+
export declare function mainLogout(accountId: string, keycloakConfig: KeycloakConfig, redirectUrl?: string): void;
|
|
10
|
+
import type { KeycloakConfig, OIDCUser } from './types';
|
|
11
|
+
export interface LogoutResult {
|
|
12
|
+
success: boolean;
|
|
13
|
+
message: string;
|
|
14
|
+
remainingAccounts: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Clear Keycloak SSO session via fetch (without redirect)
|
|
18
|
+
* Used before signinRedirect to handle SSO collision when adding new account
|
|
19
|
+
*
|
|
20
|
+
* Important: Soft-logout clears Keycloak SSO cookies without revoking tokens.
|
|
21
|
+
* This preserves offline refresh tokens for multi-account switching.
|
|
22
|
+
*
|
|
23
|
+
* @param user - Current OIDC user with refresh_token
|
|
24
|
+
* @param keycloakConfig - Keycloak configuration
|
|
25
|
+
* @returns Success boolean
|
|
26
|
+
*/
|
|
27
|
+
export declare function clearSSOSessionBeforeAddAccount(_user: OIDCUser | null, keycloakConfig: KeycloakConfig): Promise<boolean>;
|
|
28
|
+
/**
|
|
29
|
+
* Sign out a single account and optionally switch to another
|
|
30
|
+
* Uses silent token revocation (no redirect) to logout without interrupting the user.
|
|
31
|
+
* Removes the account from local storage after successful logout.
|
|
32
|
+
*
|
|
33
|
+
* Only redirects if this is the LAST account AND revocation fails.
|
|
34
|
+
*
|
|
35
|
+
* @param user - The OIDC user object containing id_token and refresh_token
|
|
36
|
+
* @param keycloakConfig - Keycloak configuration
|
|
37
|
+
* @param accountId - The account ID to remove
|
|
38
|
+
* @returns LogoutResult with success status and remaining account count
|
|
39
|
+
*/
|
|
40
|
+
export declare function logoutAccount(user: OIDCUser, keycloakConfig: KeycloakConfig, accountId: string): Promise<LogoutResult>;
|
|
41
|
+
/**
|
|
42
|
+
* Sign out all accounts and clear all sessions
|
|
43
|
+
* This destroys the entire multi-account session
|
|
44
|
+
*/
|
|
45
|
+
export declare function logoutAllAccounts(accounts: Array<{
|
|
46
|
+
accountId: string;
|
|
47
|
+
user: OIDCUser;
|
|
48
|
+
}>, keycloakConfig: KeycloakConfig): Promise<LogoutResult>;
|
|
49
|
+
/**
|
|
50
|
+
* Soft logout: Remove an account from local storage without calling Keycloak
|
|
51
|
+
* Use this when Keycloak endpoint is unavailable but you want to clear local session
|
|
52
|
+
*/
|
|
53
|
+
export declare function logoutAccountLocally(accountId: string): LogoutResult;
|
|
54
|
+
/**
|
|
55
|
+
* Check if logout succeeded but we should still update the UI
|
|
56
|
+
* (e.g., Keycloak endpoint failed but we removed locally)
|
|
57
|
+
*/
|
|
58
|
+
export declare function isLogoutPartiallySuccessful(result: LogoutResult): boolean;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SyncBridgeClient } from './types';
|
|
2
|
+
interface SyncBridgeOptions {
|
|
3
|
+
bridgeUrl: string;
|
|
4
|
+
timeoutMs?: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function createSyncBridgeClient(options: SyncBridgeOptions | null | undefined): SyncBridgeClient | null;
|
|
7
|
+
export declare function getBridgeStorageKey(): string;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JWT token validation and inspection utilities
|
|
3
|
+
* Handles checking token expiry, decoding payloads, and refresh token validation
|
|
4
|
+
*/
|
|
5
|
+
import type { OIDCUser, StoredAccount } from './types';
|
|
6
|
+
/**
|
|
7
|
+
* Decode a JWT token without validation (client-side only)
|
|
8
|
+
* WARNING: Does not validate signature. For verification, rely on server.
|
|
9
|
+
*/
|
|
10
|
+
export declare function decodeToken(token: string): Record<string, unknown> | null;
|
|
11
|
+
/**
|
|
12
|
+
* Check if an access token has expired with a buffer
|
|
13
|
+
* Returns true if token is expired OR expiring within buffer seconds
|
|
14
|
+
*/
|
|
15
|
+
export declare function isTokenExpired(token: string | undefined, bufferSeconds?: number): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Check if a refresh token has expired with a buffer
|
|
18
|
+
* Similar to isTokenExpired but with longer buffer (5 minutes)
|
|
19
|
+
*/
|
|
20
|
+
export declare function isRefreshTokenExpired(token: string | undefined, bufferSeconds?: number): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Get the expiry timestamp of a token (in milliseconds)
|
|
23
|
+
* Returns null if token is invalid or missing exp claim
|
|
24
|
+
*/
|
|
25
|
+
export declare function getTokenExpiryTime(token: string | undefined): number | null;
|
|
26
|
+
/**
|
|
27
|
+
* Get the remaining lifetime of a token in seconds
|
|
28
|
+
* Returns 0 or negative if expired
|
|
29
|
+
*/
|
|
30
|
+
export declare function getTokenTimeRemaining(token: string | undefined): number;
|
|
31
|
+
/**
|
|
32
|
+
* Extract user info from OIDC User object
|
|
33
|
+
* Returns email, name, and avatar for display
|
|
34
|
+
*/
|
|
35
|
+
export declare function extractUserInfo(user: OIDCUser): {
|
|
36
|
+
email: string;
|
|
37
|
+
name: string;
|
|
38
|
+
avatar: string | undefined;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Check if a stored account's tokens are stale
|
|
42
|
+
* This checks both access token and refresh token expiry
|
|
43
|
+
*/
|
|
44
|
+
export declare function isAccountStale(account: StoredAccount): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Check if we need to refresh the access token for a user
|
|
47
|
+
* This is different from checking if it's stale; we refresh proactively
|
|
48
|
+
*/
|
|
49
|
+
export declare function shouldRefreshAccessToken(token: string | undefined): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Check if we need to refresh the refresh token
|
|
52
|
+
* Refresh tokens have longer buffer time
|
|
53
|
+
*/
|
|
54
|
+
export declare function shouldRefreshRefreshToken(token: string | undefined): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Check if an access token is valid right now (ignoring buffer)
|
|
57
|
+
*/
|
|
58
|
+
export declare function isAccessTokenValid(token: string | undefined): boolean;
|
package/dist/types.d.ts
CHANGED
|
@@ -78,4 +78,60 @@ export interface TopBarProps {
|
|
|
78
78
|
appsCacheTime?: number;
|
|
79
79
|
profileRefetchInterval?: number;
|
|
80
80
|
profileCacheTime?: number;
|
|
81
|
+
accountManager?: UseAccountManagerReturn;
|
|
82
|
+
}
|
|
83
|
+
export interface KeycloakConfig {
|
|
84
|
+
authority: string;
|
|
85
|
+
client_id: string;
|
|
86
|
+
redirect_uri?: string;
|
|
87
|
+
post_logout_redirect_uri?: string;
|
|
88
|
+
}
|
|
89
|
+
export interface OIDCUser {
|
|
90
|
+
access_token: string;
|
|
91
|
+
refresh_token?: string;
|
|
92
|
+
id_token?: string;
|
|
93
|
+
expires_in?: number;
|
|
94
|
+
profile?: {
|
|
95
|
+
sub: string;
|
|
96
|
+
email?: string;
|
|
97
|
+
name?: string;
|
|
98
|
+
given_name?: string;
|
|
99
|
+
family_name?: string;
|
|
100
|
+
picture?: string;
|
|
101
|
+
[key: string]: unknown;
|
|
102
|
+
};
|
|
103
|
+
[key: string]: unknown;
|
|
104
|
+
}
|
|
105
|
+
export interface StoredAccountMetadata {
|
|
106
|
+
accountId: string;
|
|
107
|
+
lastUsed: number;
|
|
108
|
+
isStale: boolean;
|
|
109
|
+
createdAt: number;
|
|
110
|
+
}
|
|
111
|
+
export interface StoredAccount extends StoredAccountMetadata {
|
|
112
|
+
user: OIDCUser;
|
|
113
|
+
}
|
|
114
|
+
export interface AccountsState {
|
|
115
|
+
accounts: StoredAccount[];
|
|
116
|
+
activeAccountId: string | null;
|
|
117
|
+
lastUpdated: number;
|
|
118
|
+
}
|
|
119
|
+
export interface SyncBridgeClient {
|
|
120
|
+
getAccountsState: () => Promise<AccountsState | null>;
|
|
121
|
+
setAccountsState: (state: AccountsState) => Promise<void>;
|
|
122
|
+
}
|
|
123
|
+
export interface UseAccountManagerOptions {
|
|
124
|
+
keycloakConfig: KeycloakConfig;
|
|
125
|
+
initialUser: OIDCUser | null | undefined;
|
|
126
|
+
onAccountSwitch: (user: OIDCUser) => void;
|
|
127
|
+
signinRedirect?: (args?: import('oidc-client-ts').SigninRedirectArgs) => Promise<void>;
|
|
128
|
+
syncBridge?: SyncBridgeClient | null;
|
|
129
|
+
}
|
|
130
|
+
export interface UseAccountManagerReturn {
|
|
131
|
+
accounts: StoredAccount[];
|
|
132
|
+
activeAccount: StoredAccount | null;
|
|
133
|
+
switchAccount: (accountId: string) => Promise<void>;
|
|
134
|
+
addNewAccount: () => Promise<void>;
|
|
135
|
+
removeAccount: (accountId: string) => Promise<void>;
|
|
136
|
+
refreshAccountToken: (accountId: string) => Promise<void>;
|
|
81
137
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAccountManager Hook
|
|
3
|
+
* Core integration point for multi-account switching with Keycloak
|
|
4
|
+
*
|
|
5
|
+
* Manages account storage, switching, and token refresh
|
|
6
|
+
* Handles redirect-based add account flow with state flags
|
|
7
|
+
*
|
|
8
|
+
* Redirect Flow (5 steps):
|
|
9
|
+
* 1. Save state before redirect (current user, flags, return URL)
|
|
10
|
+
* 2. Clear SSO session via fetch, then signinRedirect
|
|
11
|
+
* 3. Keycloak redirects back, primary auth provider (ReactKeycloak) processes callback
|
|
12
|
+
* 4. On mount, detect flag, wait for initialUser, add to accounts, call onAccountSwitch
|
|
13
|
+
* 5. Navigate to return URL
|
|
14
|
+
*/
|
|
15
|
+
import type { UseAccountManagerOptions, UseAccountManagerReturn } from './types';
|
|
16
|
+
/**
|
|
17
|
+
* Hook for managing multi-account switching with redirect-based add account flow
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* ```
|
|
21
|
+
* const accountManager = useAccountManager({
|
|
22
|
+
* keycloakConfig: { authority: '...', client_id: '...', redirect_uri: '...' },
|
|
23
|
+
* initialUser: keycloak.user, // From @react-keycloak/web
|
|
24
|
+
* onAccountSwitch: (user) => updateKeycloakTokens(user.access_token, user.refresh_token)
|
|
25
|
+
* })
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function useAccountManager(options: UseAccountManagerOptions): UseAccountManagerReturn;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function useSpryAccountManager(): {
|
|
2
|
+
removeAccount: (accountId: string) => Promise<void>;
|
|
3
|
+
accounts: import("./types").StoredAccount[];
|
|
4
|
+
activeAccount: import("./types").StoredAccount | null;
|
|
5
|
+
switchAccount: (accountId: string) => Promise<void>;
|
|
6
|
+
addNewAccount: () => Promise<void>;
|
|
7
|
+
refreshAccountToken: (accountId: string) => Promise<void>;
|
|
8
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useSpryAuth(): import("react-oidc-context").AuthContextProps;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main logout handler: removes account from localStorage and redirects to soft-logout endpoint
|
|
3
|
+
* @param accountId - The account ID to remove
|
|
4
|
+
* @param keycloakConfig - Keycloak configuration
|
|
5
|
+
* @param redirectUrl - (Optional) URL to redirect back to after logout (defaults to window.location.origin)
|
|
6
|
+
*/
|
|
7
|
+
export declare function handleMainLogout(accountId: string, keycloakConfig: KeycloakConfig, redirectUrl?: string): void;
|
|
8
|
+
/**
|
|
9
|
+
* UserManager pool for managing per-account Keycloak token refresh
|
|
10
|
+
* Each account has its own UserManager instance for isolated token refresh lifecycle
|
|
11
|
+
*
|
|
12
|
+
* Redirect Flow:
|
|
13
|
+
* - Uses signinRedirect for adding accounts (not popup)
|
|
14
|
+
* - State flags in localStorage track redirect progress
|
|
15
|
+
* - On callback, useAccountManager detects the flag and processes new account
|
|
16
|
+
*/
|
|
17
|
+
import { UserManager } from 'oidc-client-ts';
|
|
18
|
+
import type { KeycloakConfig, OIDCUser } from './types';
|
|
19
|
+
/**
|
|
20
|
+
* Create a new UserManager instance for a specific Keycloak realm
|
|
21
|
+
*/
|
|
22
|
+
export declare function createUserManager(keycloakConfig: KeycloakConfig, accountId: string): Promise<UserManager>;
|
|
23
|
+
/**
|
|
24
|
+
* Maintain a map of UserManager instances, one per account
|
|
25
|
+
* This allows each account to manage its own token refresh independently
|
|
26
|
+
*/
|
|
27
|
+
declare class UserManagerPoolImpl {
|
|
28
|
+
private pool;
|
|
29
|
+
getOrCreate(keycloakConfig: KeycloakConfig, accountId: string): Promise<UserManager>;
|
|
30
|
+
remove(accountId: string): void;
|
|
31
|
+
clear(): void;
|
|
32
|
+
has(accountId: string): boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare const userManagerPool: UserManagerPoolImpl;
|
|
35
|
+
/**
|
|
36
|
+
* Refresh the access token for a specific account using signinSilent
|
|
37
|
+
* Updates the stored account with new tokens on success
|
|
38
|
+
*/
|
|
39
|
+
export declare function refreshTokenForAccount(accountId: string, user: OIDCUser, keycloakConfig: KeycloakConfig): Promise<OIDCUser | null>;
|
|
40
|
+
/**
|
|
41
|
+
* Handle token expiry by marking account as stale
|
|
42
|
+
* The next time user tries to switch to this account, they'll see the stale badge
|
|
43
|
+
* and can click to trigger signinPopup with prompt=login
|
|
44
|
+
*/
|
|
45
|
+
export declare function handleTokenExpired(accountId: string): void;
|
|
46
|
+
/**
|
|
47
|
+
* Initiate sign-in redirect for adding a new account
|
|
48
|
+
*
|
|
49
|
+
* This is a fallback function used when signinRedirect is not available from context.
|
|
50
|
+
* The standard flow uses addNewAccount in useAccountManager which handles soft-logout + signinRedirect.
|
|
51
|
+
*/
|
|
52
|
+
export declare function signinRedirectForNewAccount(keycloakConfig: KeycloakConfig, tempAccountId: string): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Revoke a token for a specific account (silent, no redirect)
|
|
55
|
+
* Uses Keycloak's token revocation endpoint to invalidate the refresh token
|
|
56
|
+
*
|
|
57
|
+
* For multi-account scenarios, use this instead of signoutRedirect to avoid the logout page
|
|
58
|
+
*/
|
|
59
|
+
export declare function revokeTokenForAccount(user: OIDCUser, keycloakConfig: KeycloakConfig): Promise<boolean>;
|
|
60
|
+
/**
|
|
61
|
+
* Sign out a specific account from Keycloak (in-browser redirect)
|
|
62
|
+
* This invalidates the Keycloak session for that account
|
|
63
|
+
*
|
|
64
|
+
* WARNING: This redirects to Keycloak logout page. Only use this when logging out the last account
|
|
65
|
+
* or when you want to redirect to login page. For multi-account logout, use revokeTokenForAccount instead.
|
|
66
|
+
*/
|
|
67
|
+
export declare function signoutAccountFromKeycloak(user: OIDCUser, keycloakConfig: KeycloakConfig, tempAccountId: string): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Validate if an account's tokens can be refreshed
|
|
70
|
+
* Returns true if the refresh token is still valid (not expired)
|
|
71
|
+
*/
|
|
72
|
+
export declare function canRefreshAccount(user: OIDCUser): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Get a list of accounts that can be refreshed
|
|
75
|
+
* Useful for background refresh tasks
|
|
76
|
+
*/
|
|
77
|
+
export declare function getRefreshableAccounts(accounts: Array<{
|
|
78
|
+
accountId: string;
|
|
79
|
+
user: OIDCUser;
|
|
80
|
+
}>): Array<{
|
|
81
|
+
accountId: string;
|
|
82
|
+
user: OIDCUser;
|
|
83
|
+
}>;
|
|
84
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spry-apps-dropdown",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "React components for Spry apps dropdown and profile menu with dynamic API integration - Google-style UI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -32,13 +32,15 @@
|
|
|
32
32
|
"author": "",
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@mui/material": "^6.0.0",
|
|
36
|
-
"@mui/icons-material": "^6.0.0",
|
|
37
|
-
"framer-motion": "^11.0.0",
|
|
38
35
|
"@emotion/react": "^11.11.0",
|
|
39
|
-
"@emotion/styled": "^11.11.0"
|
|
36
|
+
"@emotion/styled": "^11.11.0",
|
|
37
|
+
"@mui/icons-material": "^6.0.0",
|
|
38
|
+
"@mui/material": "^6.0.0",
|
|
39
|
+
"framer-motion": "^11.0.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
+
"oidc-client-ts": "^1.0.0",
|
|
43
|
+
"react-oidc-context": "^3.3.0",
|
|
42
44
|
"react": "^18.0.0 || ^19.0.0",
|
|
43
45
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
44
46
|
},
|
|
@@ -46,6 +48,8 @@
|
|
|
46
48
|
"@types/react": "^19.0.1",
|
|
47
49
|
"@types/react-dom": "^19.0.1",
|
|
48
50
|
"@vitejs/plugin-react": "^4.3.4",
|
|
51
|
+
"oidc-client-ts": "^3.4.1",
|
|
52
|
+
"react-oidc-context": "^3.3.0",
|
|
49
53
|
"typescript": "^5.9.3",
|
|
50
54
|
"vite": "^7.2.4"
|
|
51
55
|
}
|