@enterprisestandard/react 0.0.5-beta.20260115.1 → 0.0.5-beta.20260115.3
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/dist/group-store.js +127 -0
- package/dist/iam.js +680 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +144 -3672
- package/dist/session-store.js +105 -0
- package/dist/sso-server.d.ts +1 -1
- package/dist/sso-server.d.ts.map +1 -1
- package/dist/sso-server.js +46 -0
- package/dist/sso.js +820 -0
- package/dist/tenant-server.js +6 -0
- package/dist/tenant.js +324 -0
- package/dist/types/base-user.js +1 -0
- package/dist/types/enterprise-user.js +1 -0
- package/dist/types/oidc-schema.js +328 -0
- package/dist/types/scim-schema.js +519 -0
- package/dist/types/standard-schema.js +1 -0
- package/dist/types/user.js +1 -0
- package/dist/types/workload-schema.js +208 -0
- package/dist/ui/sign-in-loading.js +8 -0
- package/dist/ui/signed-in.js +8 -0
- package/dist/ui/signed-out.js +8 -0
- package/dist/ui/sso-provider.js +275 -0
- package/dist/user-store.js +114 -0
- package/dist/utils.js +23 -0
- package/dist/vault.js +22 -0
- package/dist/workload-server.d.ts +1 -1
- package/dist/workload-server.d.ts.map +1 -1
- package/dist/workload-server.js +167 -0
- package/dist/workload-token-store.js +95 -0
- package/dist/workload.js +691 -0
- package/package.json +1 -1
- package/dist/index.js.map +0 -29
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a StandardSchemaV1 for validating JWT Assertion Claims.
|
|
3
|
+
* @param vendor - The name of the vendor creating this schema
|
|
4
|
+
* @returns A StandardSchemaV1 instance for JWT Assertion Claims validation
|
|
5
|
+
*/
|
|
6
|
+
export function jwtAssertionClaimsSchema(vendor) {
|
|
7
|
+
return {
|
|
8
|
+
'~standard': {
|
|
9
|
+
version: 1,
|
|
10
|
+
vendor,
|
|
11
|
+
validate: (value) => {
|
|
12
|
+
if (typeof value !== 'object' || value === null) {
|
|
13
|
+
return {
|
|
14
|
+
issues: [
|
|
15
|
+
{
|
|
16
|
+
message: 'Expected an object',
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const claims = value;
|
|
22
|
+
const issues = [];
|
|
23
|
+
const result = { ...claims };
|
|
24
|
+
// Validate required string fields
|
|
25
|
+
const requiredStringFields = ['iss', 'sub'];
|
|
26
|
+
for (const field of requiredStringFields) {
|
|
27
|
+
if (field in claims) {
|
|
28
|
+
if (typeof claims[field] !== 'string') {
|
|
29
|
+
issues.push({
|
|
30
|
+
message: `${field} must be a string`,
|
|
31
|
+
path: [field],
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
issues.push({
|
|
37
|
+
message: `${field} is required`,
|
|
38
|
+
path: [field],
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Validate optional aud field (can be string or array of strings)
|
|
43
|
+
if ('aud' in claims && claims.aud !== undefined) {
|
|
44
|
+
const aud = claims.aud;
|
|
45
|
+
if (typeof aud !== 'string' && !Array.isArray(aud)) {
|
|
46
|
+
issues.push({
|
|
47
|
+
message: 'aud must be a string or array of strings',
|
|
48
|
+
path: ['aud'],
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
else if (Array.isArray(aud) && !aud.every((a) => typeof a === 'string')) {
|
|
52
|
+
issues.push({
|
|
53
|
+
message: 'aud array must contain only strings',
|
|
54
|
+
path: ['aud'],
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Validate optional string fields
|
|
59
|
+
const optionalStringFields = ['jti', 'scope'];
|
|
60
|
+
for (const field of optionalStringFields) {
|
|
61
|
+
if (field in claims && claims[field] !== undefined) {
|
|
62
|
+
if (typeof claims[field] !== 'string') {
|
|
63
|
+
issues.push({
|
|
64
|
+
message: `${field} must be a string`,
|
|
65
|
+
path: [field],
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Validate required number fields
|
|
71
|
+
const requiredNumberFields = ['exp', 'iat'];
|
|
72
|
+
for (const field of requiredNumberFields) {
|
|
73
|
+
if (field in claims) {
|
|
74
|
+
if (typeof claims[field] !== 'number') {
|
|
75
|
+
issues.push({
|
|
76
|
+
message: `${field} must be a number`,
|
|
77
|
+
path: [field],
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
issues.push({
|
|
83
|
+
message: `${field} is required`,
|
|
84
|
+
path: [field],
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (issues.length > 0) {
|
|
89
|
+
return { issues };
|
|
90
|
+
}
|
|
91
|
+
return { value: result };
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Creates a StandardSchemaV1 for validating Workload Token Responses.
|
|
98
|
+
* @param vendor - The name of the vendor creating this schema
|
|
99
|
+
* @returns A StandardSchemaV1 instance for Workload Token Response validation
|
|
100
|
+
*/
|
|
101
|
+
export function workloadTokenResponseSchema(vendor) {
|
|
102
|
+
return {
|
|
103
|
+
'~standard': {
|
|
104
|
+
version: 1,
|
|
105
|
+
vendor,
|
|
106
|
+
validate: (value) => {
|
|
107
|
+
if (typeof value !== 'object' || value === null) {
|
|
108
|
+
return {
|
|
109
|
+
issues: [
|
|
110
|
+
{
|
|
111
|
+
message: 'Expected an object',
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const response = value;
|
|
117
|
+
const issues = [];
|
|
118
|
+
const result = {};
|
|
119
|
+
// Check required 'access_token' parameter
|
|
120
|
+
if ('access_token' in response) {
|
|
121
|
+
if (typeof response.access_token === 'string') {
|
|
122
|
+
result.access_token = response.access_token;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
issues.push({
|
|
126
|
+
message: 'access_token must be a string',
|
|
127
|
+
path: ['access_token'],
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
issues.push({
|
|
133
|
+
message: 'access_token is required',
|
|
134
|
+
path: ['access_token'],
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
// Check required 'token_type' parameter
|
|
138
|
+
if ('token_type' in response) {
|
|
139
|
+
if (typeof response.token_type === 'string') {
|
|
140
|
+
result.token_type = response.token_type;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
issues.push({
|
|
144
|
+
message: 'token_type must be a string',
|
|
145
|
+
path: ['token_type'],
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
issues.push({
|
|
151
|
+
message: 'token_type is required',
|
|
152
|
+
path: ['token_type'],
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
// Optional string fields
|
|
156
|
+
if ('scope' in response) {
|
|
157
|
+
if (typeof response.scope === 'string' || response.scope === undefined) {
|
|
158
|
+
result.scope = response.scope;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
issues.push({
|
|
162
|
+
message: 'scope must be a string',
|
|
163
|
+
path: ['scope'],
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if ('refresh_token' in response) {
|
|
168
|
+
if (typeof response.refresh_token === 'string' || response.refresh_token === undefined) {
|
|
169
|
+
result.refresh_token = response.refresh_token;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
issues.push({
|
|
173
|
+
message: 'refresh_token must be a string',
|
|
174
|
+
path: ['refresh_token'],
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if ('expires' in response) {
|
|
179
|
+
if (typeof response.expires === 'string' || response.expires === undefined) {
|
|
180
|
+
result.expires = response.expires;
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
issues.push({
|
|
184
|
+
message: 'expires must be a string',
|
|
185
|
+
path: ['expires'],
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Optional number field
|
|
190
|
+
if ('expires_in' in response) {
|
|
191
|
+
if (typeof response.expires_in === 'number' || response.expires_in === undefined) {
|
|
192
|
+
result.expires_in = response.expires_in;
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
issues.push({
|
|
196
|
+
message: 'expires_in must be a number',
|
|
197
|
+
path: ['expires_in'],
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (issues.length > 0) {
|
|
202
|
+
return { issues };
|
|
203
|
+
}
|
|
204
|
+
return { value: result };
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useUser } from '..';
|
|
3
|
+
export function SignInLoading({ complete = false, children }) {
|
|
4
|
+
const { isLoading } = useUser();
|
|
5
|
+
if (isLoading && !complete)
|
|
6
|
+
return _jsx(_Fragment, { children: children });
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useUser } from '..';
|
|
3
|
+
export function SignedOut({ children }) {
|
|
4
|
+
const { user, isLoading } = useUser();
|
|
5
|
+
if (user || isLoading)
|
|
6
|
+
return null;
|
|
7
|
+
return _jsx(_Fragment, { children: children });
|
|
8
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
|
3
|
+
const CTX = createContext(undefined);
|
|
4
|
+
const generateStorageKey = (tenantId) => {
|
|
5
|
+
return `es-sso-user-${tenantId
|
|
6
|
+
.replace(/[^a-zA-Z0-9]/g, '-')
|
|
7
|
+
.replace(/-+/g, '-')
|
|
8
|
+
.replace(/^-|-$/g, '')}`;
|
|
9
|
+
};
|
|
10
|
+
export function SSOProvider({ tenantId, storage = 'memory', storageKey, userUrl, tokenUrl, refreshUrl, disableListener = false, children, }) {
|
|
11
|
+
const [user, setUserState] = useState(null);
|
|
12
|
+
const [isLoading, setIsLoading] = useState(!!userUrl);
|
|
13
|
+
const actualStorageKey = storageKey || (tenantId ? generateStorageKey(tenantId) : 'es-sso-user');
|
|
14
|
+
const isValidUser = useCallback((user) => {
|
|
15
|
+
if (!user || !tenantId)
|
|
16
|
+
return true;
|
|
17
|
+
return user.sso?.tenant?.id === tenantId;
|
|
18
|
+
}, [tenantId]);
|
|
19
|
+
const loadUserFromStorage = useCallback(() => {
|
|
20
|
+
if (storage === 'memory' || typeof window === 'undefined')
|
|
21
|
+
return null;
|
|
22
|
+
try {
|
|
23
|
+
const storageObject = storage === 'local' ? localStorage : sessionStorage;
|
|
24
|
+
const userData = storageObject.getItem(actualStorageKey);
|
|
25
|
+
if (!userData)
|
|
26
|
+
return null;
|
|
27
|
+
const parsedUser = JSON.parse(userData);
|
|
28
|
+
if (parsedUser.sso?.expires) {
|
|
29
|
+
parsedUser.sso.expires = new Date(parsedUser.sso.expires);
|
|
30
|
+
}
|
|
31
|
+
return isValidUser(parsedUser) ? parsedUser : null;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
console.error('Error loading user from storage:', error);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}, [storage, actualStorageKey, isValidUser]);
|
|
38
|
+
const saveUserToStorage = useCallback((user) => {
|
|
39
|
+
if (storage === 'memory' || typeof window === 'undefined')
|
|
40
|
+
return;
|
|
41
|
+
try {
|
|
42
|
+
const storageObject = storage === 'local' ? localStorage : sessionStorage;
|
|
43
|
+
if (user === null) {
|
|
44
|
+
storageObject.removeItem(actualStorageKey);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
storageObject.setItem(actualStorageKey, JSON.stringify(user));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
console.error('Error saving user to storage:', error);
|
|
52
|
+
}
|
|
53
|
+
}, [storage, actualStorageKey]);
|
|
54
|
+
const setUser = useCallback((newUser) => {
|
|
55
|
+
if (newUser && !isValidUser(newUser))
|
|
56
|
+
return;
|
|
57
|
+
setUserState(newUser);
|
|
58
|
+
saveUserToStorage(newUser);
|
|
59
|
+
}, [isValidUser, saveUserToStorage]);
|
|
60
|
+
const fetchUserFromUrl = useCallback(async () => {
|
|
61
|
+
if (!userUrl)
|
|
62
|
+
return;
|
|
63
|
+
setIsLoading(true);
|
|
64
|
+
try {
|
|
65
|
+
const response = await fetch(userUrl);
|
|
66
|
+
if (response.status === 401) {
|
|
67
|
+
setUserState(null);
|
|
68
|
+
saveUserToStorage(null);
|
|
69
|
+
setIsLoading(false);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
throw new Error(`Failed to fetch user: ${response.status} ${response.statusText}`);
|
|
74
|
+
}
|
|
75
|
+
const userData = (await response.json());
|
|
76
|
+
if (userData.sso?.expires && typeof userData.sso.expires === 'string') {
|
|
77
|
+
userData.sso.expires = new Date(userData.sso.expires);
|
|
78
|
+
}
|
|
79
|
+
if (isValidUser(userData)) {
|
|
80
|
+
setUserState(userData);
|
|
81
|
+
saveUserToStorage(userData);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
console.error('Error fetching user from URL:', error);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
setIsLoading(false);
|
|
89
|
+
}
|
|
90
|
+
}, [userUrl, isValidUser, saveUserToStorage]);
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
const storedUser = loadUserFromStorage();
|
|
93
|
+
if (storedUser) {
|
|
94
|
+
setUserState(storedUser);
|
|
95
|
+
}
|
|
96
|
+
if (userUrl) {
|
|
97
|
+
fetchUserFromUrl();
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
setIsLoading(false);
|
|
101
|
+
}
|
|
102
|
+
}, [loadUserFromStorage, userUrl, fetchUserFromUrl]);
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (disableListener || storage === 'memory')
|
|
105
|
+
return;
|
|
106
|
+
const handleStorageChange = (event) => {
|
|
107
|
+
if (event.key !== actualStorageKey)
|
|
108
|
+
return;
|
|
109
|
+
if (event.newValue === null) {
|
|
110
|
+
setUserState(null);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
try {
|
|
114
|
+
const parsedUser = JSON.parse(event.newValue);
|
|
115
|
+
if (parsedUser.sso?.expires) {
|
|
116
|
+
parsedUser.sso.expires = new Date(parsedUser.sso.expires);
|
|
117
|
+
}
|
|
118
|
+
if (isValidUser(parsedUser)) {
|
|
119
|
+
setUserState(parsedUser);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
console.error('Error parsing user from storage event:', error);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const handleLogout = () => {
|
|
128
|
+
setUserState(null);
|
|
129
|
+
};
|
|
130
|
+
window.addEventListener('storage', handleStorageChange);
|
|
131
|
+
window.addEventListener('es-sso-logout', handleLogout);
|
|
132
|
+
return () => {
|
|
133
|
+
window.removeEventListener('storage', handleStorageChange);
|
|
134
|
+
window.removeEventListener('es-sso-logout', handleLogout);
|
|
135
|
+
};
|
|
136
|
+
}, [disableListener, storage, actualStorageKey, isValidUser]);
|
|
137
|
+
const contextValue = {
|
|
138
|
+
user,
|
|
139
|
+
setUser,
|
|
140
|
+
isLoading,
|
|
141
|
+
tokenUrl,
|
|
142
|
+
refreshUrl,
|
|
143
|
+
};
|
|
144
|
+
return _jsx(CTX.Provider, { value: contextValue, children: children });
|
|
145
|
+
}
|
|
146
|
+
export function useUser() {
|
|
147
|
+
const context = useContext(CTX);
|
|
148
|
+
if (context === undefined) {
|
|
149
|
+
throw new Error('useUser must be used within a SSOProvider');
|
|
150
|
+
}
|
|
151
|
+
return context;
|
|
152
|
+
}
|
|
153
|
+
export function useToken() {
|
|
154
|
+
const context = useContext(CTX);
|
|
155
|
+
if (context === undefined) {
|
|
156
|
+
throw new Error('useToken must be used within a SSOProvider');
|
|
157
|
+
}
|
|
158
|
+
const { tokenUrl, refreshUrl } = context;
|
|
159
|
+
if (!tokenUrl || !refreshUrl) {
|
|
160
|
+
throw new Error('useToken requires that a "tokenUrl" and "refreshUrl" be set in the SSOProvider');
|
|
161
|
+
}
|
|
162
|
+
const [token, setToken] = useState(null);
|
|
163
|
+
const [expires, setExpires] = useState(null);
|
|
164
|
+
const [isLoading, setIsLoading] = useState(!!tokenUrl);
|
|
165
|
+
const [error, setError] = useState(null);
|
|
166
|
+
const fetchJwt = useCallback(async (url) => {
|
|
167
|
+
setIsLoading(true);
|
|
168
|
+
setError(null);
|
|
169
|
+
try {
|
|
170
|
+
const response = await fetch(url);
|
|
171
|
+
if (response.status === 401) {
|
|
172
|
+
context.setUser(null);
|
|
173
|
+
setToken(null);
|
|
174
|
+
setExpires(null);
|
|
175
|
+
setIsLoading(false);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (!response.ok) {
|
|
179
|
+
throw new Error(`Failed to fetch JWT: ${response.status} ${response.statusText}`);
|
|
180
|
+
}
|
|
181
|
+
const data = (await response.json());
|
|
182
|
+
setToken(data.token);
|
|
183
|
+
setExpires(new Date(data.expires));
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
187
|
+
setError(error);
|
|
188
|
+
setToken(null);
|
|
189
|
+
setExpires(null);
|
|
190
|
+
console.error('Error fetching JWT:', error);
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
setIsLoading(false);
|
|
194
|
+
}
|
|
195
|
+
}, [context]);
|
|
196
|
+
const refresh = useCallback(async () => {
|
|
197
|
+
const url = refreshUrl || tokenUrl;
|
|
198
|
+
if (!url) {
|
|
199
|
+
console.warn('No tokenUrl or refreshUrl provided');
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
await fetchJwt(url);
|
|
203
|
+
}, [refreshUrl, tokenUrl, fetchJwt]);
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
if (!tokenUrl) {
|
|
206
|
+
setIsLoading(false);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
fetchJwt(tokenUrl);
|
|
210
|
+
}, [tokenUrl, fetchJwt]);
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (!expires || !refreshUrl)
|
|
213
|
+
return;
|
|
214
|
+
const checkExpiration = () => {
|
|
215
|
+
const now = new Date();
|
|
216
|
+
const timeUntilExpiry = expires.getTime() - now.getTime();
|
|
217
|
+
// Refresh 1 minute before expiration
|
|
218
|
+
if (timeUntilExpiry <= 60000 && timeUntilExpiry > 0) {
|
|
219
|
+
refresh();
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
// Check immediately
|
|
223
|
+
checkExpiration();
|
|
224
|
+
// Check every 30 seconds
|
|
225
|
+
const interval = setInterval(checkExpiration, 30000);
|
|
226
|
+
return () => clearInterval(interval);
|
|
227
|
+
}, [expires, refreshUrl, refresh]);
|
|
228
|
+
return {
|
|
229
|
+
token,
|
|
230
|
+
isLoading,
|
|
231
|
+
error,
|
|
232
|
+
refresh,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
export async function logout(logoutUrl) {
|
|
236
|
+
try {
|
|
237
|
+
// Make AJAX logout call
|
|
238
|
+
const response = await fetch(logoutUrl, {
|
|
239
|
+
headers: { Accept: 'application/json' },
|
|
240
|
+
});
|
|
241
|
+
if (!response.ok) {
|
|
242
|
+
return { success: false, error: `HTTP ${response.status}` };
|
|
243
|
+
}
|
|
244
|
+
const data = await response.json();
|
|
245
|
+
if (!data.success) {
|
|
246
|
+
return { success: false, error: data.message || 'Logout failed' };
|
|
247
|
+
}
|
|
248
|
+
// Clear managed storage keys (all es-sso-user-* keys)
|
|
249
|
+
if (typeof window !== 'undefined') {
|
|
250
|
+
// Clear localStorage
|
|
251
|
+
for (let i = localStorage.length - 1; i >= 0; i--) {
|
|
252
|
+
const key = localStorage.key(i);
|
|
253
|
+
if (key?.startsWith('es-sso-user')) {
|
|
254
|
+
localStorage.removeItem(key);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Clear sessionStorage
|
|
258
|
+
for (let i = sessionStorage.length - 1; i >= 0; i--) {
|
|
259
|
+
const key = sessionStorage.key(i);
|
|
260
|
+
if (key?.startsWith('es-sso-user')) {
|
|
261
|
+
sessionStorage.removeItem(key);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Dispatch custom event for same-tab state updates
|
|
265
|
+
window.dispatchEvent(new CustomEvent('es-sso-logout'));
|
|
266
|
+
}
|
|
267
|
+
return { success: true };
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
return {
|
|
271
|
+
success: false,
|
|
272
|
+
error: error instanceof Error ? error.message : 'Network error',
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User storage for persisting user profiles from SSO authentication.
|
|
3
|
+
*
|
|
4
|
+
* User stores are optional - the package works with JWT cookies alone.
|
|
5
|
+
* User stores are useful when you want to:
|
|
6
|
+
* - Cache user profiles for fast lookup
|
|
7
|
+
* - Store users close to your application (in-memory, Redis, etc.)
|
|
8
|
+
* - Avoid custom IAM/SCIM integration for simple use cases
|
|
9
|
+
*
|
|
10
|
+
* ## When to Use UserStore vs IAM
|
|
11
|
+
*
|
|
12
|
+
* **Use UserStore when:**
|
|
13
|
+
* - You just need fast user lookups without external systems
|
|
14
|
+
* - Users are managed by an external IdP and you just cache them locally
|
|
15
|
+
* - You want simple in-memory or Redis storage
|
|
16
|
+
*
|
|
17
|
+
* **Use IAM (SCIM) when:**
|
|
18
|
+
* - You need to provision users to an external identity provider
|
|
19
|
+
* - You need custom user attributes beyond what SSO provides
|
|
20
|
+
* - You need to sync users with enterprise directories
|
|
21
|
+
*
|
|
22
|
+
* ## Example Usage
|
|
23
|
+
*
|
|
24
|
+
* ```typescript
|
|
25
|
+
* import { sso, InMemoryUserStore } from '@enterprisestandard/react/server';
|
|
26
|
+
*
|
|
27
|
+
* const userStore = new InMemoryUserStore();
|
|
28
|
+
*
|
|
29
|
+
* const auth = sso({
|
|
30
|
+
* // ... other config
|
|
31
|
+
* user_store: userStore,
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* // Later, look up users
|
|
35
|
+
* const user = await userStore.get('user-sub-id');
|
|
36
|
+
* const userByEmail = await userStore.getByEmail('user@example.com');
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* In-memory user store implementation using Maps.
|
|
41
|
+
*
|
|
42
|
+
* Suitable for:
|
|
43
|
+
* - Development and testing
|
|
44
|
+
* - Single-server deployments
|
|
45
|
+
* - Applications without high availability requirements
|
|
46
|
+
*
|
|
47
|
+
* NOT suitable for:
|
|
48
|
+
* - Multi-server deployments (users not shared)
|
|
49
|
+
* - High availability scenarios (users lost on restart)
|
|
50
|
+
* - Production applications with distributed architecture
|
|
51
|
+
*
|
|
52
|
+
* For production, implement UserStore with Redis or a database.
|
|
53
|
+
*
|
|
54
|
+
* @template TExtended - Type-safe custom data that consumers can add to users
|
|
55
|
+
*/
|
|
56
|
+
export class InMemoryUserStore {
|
|
57
|
+
constructor() {
|
|
58
|
+
/** Primary storage: sub -> user */
|
|
59
|
+
this.users = new Map();
|
|
60
|
+
/** Secondary index: email -> sub */
|
|
61
|
+
this.emailIndex = new Map();
|
|
62
|
+
/** Secondary index: userName -> sub */
|
|
63
|
+
this.userNameIndex = new Map();
|
|
64
|
+
}
|
|
65
|
+
async get(sub) {
|
|
66
|
+
return this.users.get(sub) ?? null;
|
|
67
|
+
}
|
|
68
|
+
async getByEmail(email) {
|
|
69
|
+
const sub = this.emailIndex.get(email.toLowerCase());
|
|
70
|
+
if (!sub)
|
|
71
|
+
return null;
|
|
72
|
+
return this.users.get(sub) ?? null;
|
|
73
|
+
}
|
|
74
|
+
async getByUserName(userName) {
|
|
75
|
+
const sub = this.userNameIndex.get(userName.toLowerCase());
|
|
76
|
+
if (!sub)
|
|
77
|
+
return null;
|
|
78
|
+
return this.users.get(sub) ?? null;
|
|
79
|
+
}
|
|
80
|
+
async upsert(user) {
|
|
81
|
+
const existing = this.users.get(user.id);
|
|
82
|
+
// Clean up old indexes if email or userName changed
|
|
83
|
+
if (existing) {
|
|
84
|
+
if (existing.email && existing.email.toLowerCase() !== user.email?.toLowerCase()) {
|
|
85
|
+
this.emailIndex.delete(existing.email.toLowerCase());
|
|
86
|
+
}
|
|
87
|
+
if (existing.userName && existing.userName.toLowerCase() !== user.userName?.toLowerCase()) {
|
|
88
|
+
this.userNameIndex.delete(existing.userName.toLowerCase());
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Store the user
|
|
92
|
+
this.users.set(user.id, user);
|
|
93
|
+
// Update indexes
|
|
94
|
+
if (user.email) {
|
|
95
|
+
this.emailIndex.set(user.email.toLowerCase(), user.id);
|
|
96
|
+
}
|
|
97
|
+
if (user.userName) {
|
|
98
|
+
this.userNameIndex.set(user.userName.toLowerCase(), user.id);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async delete(sub) {
|
|
102
|
+
const user = this.users.get(sub);
|
|
103
|
+
if (user) {
|
|
104
|
+
// Clean up indexes
|
|
105
|
+
if (user.email) {
|
|
106
|
+
this.emailIndex.delete(user.email.toLowerCase());
|
|
107
|
+
}
|
|
108
|
+
if (user.userName) {
|
|
109
|
+
this.userNameIndex.delete(user.userName.toLowerCase());
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
this.users.delete(sub);
|
|
113
|
+
}
|
|
114
|
+
}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
let defaultInstance;
|
|
2
|
+
export function must(value, message = 'Assertion failed. Required value is null or undefined.') {
|
|
3
|
+
if (value === undefined || value === null) {
|
|
4
|
+
throw new Error(message);
|
|
5
|
+
}
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
export function setDefaultInstance(es) {
|
|
9
|
+
defaultInstance = es;
|
|
10
|
+
}
|
|
11
|
+
export function getDefaultInstance() {
|
|
12
|
+
return defaultInstance;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* If an es is defined, then return it, otherwise return the defaultEnterpriseStandard
|
|
16
|
+
*/
|
|
17
|
+
export function getES(es) {
|
|
18
|
+
if (es)
|
|
19
|
+
return es;
|
|
20
|
+
if (defaultInstance)
|
|
21
|
+
return defaultInstance;
|
|
22
|
+
throw new Error(`TODO standardize the error message when there isn't a default EntepriseStandard`);
|
|
23
|
+
}
|
package/dist/vault.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function vault(url) {
|
|
2
|
+
async function getFullSecret(path, token) {
|
|
3
|
+
const resp = await fetch(`${url}/${path}`, { headers: { 'X-Vault-Token': token } });
|
|
4
|
+
if (resp.status !== 200) {
|
|
5
|
+
throw new Error(`Vault returned invalid status, ${resp.status}: '${resp.statusText}' from URL: ${url}`);
|
|
6
|
+
}
|
|
7
|
+
try {
|
|
8
|
+
const secret = await resp.json();
|
|
9
|
+
return secret.data;
|
|
10
|
+
}
|
|
11
|
+
catch (cause) {
|
|
12
|
+
throw new Error('Error retrieving secret', { cause });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
url,
|
|
17
|
+
getFullSecret,
|
|
18
|
+
getSecret: async (path, token) => {
|
|
19
|
+
return (await getFullSecret(path, token)).data;
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workload-server.d.ts","sourceRoot":"","sources":["../src/workload-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,GAAG,CAAC;
|
|
1
|
+
{"version":3,"file":"workload-server.d.ts","sourceRoot":"","sources":["../src/workload-server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,GAAG,CAAC;AAC5C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAErE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAcnD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAMlH;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CAI/F;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAarH;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAI/F;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAIlG"}
|