@aifabrix/miso-client 3.1.1 → 3.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 +1 -1
- package/dist/express/client-token-endpoint.d.ts +76 -0
- package/dist/express/client-token-endpoint.d.ts.map +1 -0
- package/dist/express/client-token-endpoint.js +108 -0
- package/dist/express/client-token-endpoint.js.map +1 -0
- package/dist/express/index.d.ts +2 -1
- package/dist/express/index.d.ts.map +1 -1
- package/dist/express/index.js +8 -3
- package/dist/express/index.js.map +1 -1
- package/dist/express/response-middleware.d.ts.map +1 -1
- package/dist/express/response-middleware.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -1
- package/dist/index.js.map +1 -1
- package/dist/services/auth.service.js +5 -5
- package/dist/services/auth.service.js.map +1 -1
- package/dist/services/browser-permission.service.d.ts +60 -0
- package/dist/services/browser-permission.service.d.ts.map +1 -0
- package/dist/services/browser-permission.service.js +159 -0
- package/dist/services/browser-permission.service.js.map +1 -0
- package/dist/services/browser-role.service.d.ts +60 -0
- package/dist/services/browser-role.service.d.ts.map +1 -0
- package/dist/services/browser-role.service.js +159 -0
- package/dist/services/browser-role.service.js.map +1 -0
- package/dist/services/cache.service.d.ts.map +1 -1
- package/dist/services/cache.service.js +4 -0
- package/dist/services/cache.service.js.map +1 -1
- package/dist/services/logger.service.d.ts +4 -0
- package/dist/services/logger.service.d.ts.map +1 -1
- package/dist/services/logger.service.js +21 -0
- package/dist/services/logger.service.js.map +1 -1
- package/dist/types/data-client.types.d.ts +1 -0
- package/dist/types/data-client.types.d.ts.map +1 -1
- package/dist/types/data-client.types.js.map +1 -1
- package/dist/utils/audit-log-queue.d.ts +4 -0
- package/dist/utils/audit-log-queue.d.ts.map +1 -1
- package/dist/utils/audit-log-queue.js +22 -2
- package/dist/utils/audit-log-queue.js.map +1 -1
- package/dist/utils/auth-strategy.js +2 -2
- package/dist/utils/browser-jwt-decoder.d.ts +20 -0
- package/dist/utils/browser-jwt-decoder.d.ts.map +1 -0
- package/dist/utils/browser-jwt-decoder.js +75 -0
- package/dist/utils/browser-jwt-decoder.js.map +1 -0
- package/dist/utils/controller-url-resolver.d.ts +16 -0
- package/dist/utils/controller-url-resolver.d.ts.map +1 -1
- package/dist/utils/controller-url-resolver.js +12 -0
- package/dist/utils/controller-url-resolver.js.map +1 -1
- package/dist/utils/data-client-audit.d.ts +24 -0
- package/dist/utils/data-client-audit.d.ts.map +1 -0
- package/dist/utils/data-client-audit.js +138 -0
- package/dist/utils/data-client-audit.js.map +1 -0
- package/dist/utils/data-client-auth.d.ts +59 -0
- package/dist/utils/data-client-auth.d.ts.map +1 -0
- package/dist/utils/data-client-auth.js +427 -0
- package/dist/utils/data-client-auth.js.map +1 -0
- package/dist/utils/data-client-auto-init.d.ts +66 -0
- package/dist/utils/data-client-auto-init.d.ts.map +1 -0
- package/dist/utils/data-client-auto-init.js +215 -0
- package/dist/utils/data-client-auto-init.js.map +1 -0
- package/dist/utils/data-client-cache.d.ts +36 -0
- package/dist/utils/data-client-cache.d.ts.map +1 -0
- package/dist/utils/data-client-cache.js +55 -0
- package/dist/utils/data-client-cache.js.map +1 -0
- package/dist/utils/data-client-redirect.d.ts +22 -0
- package/dist/utils/data-client-redirect.d.ts.map +1 -0
- package/dist/utils/data-client-redirect.js +345 -0
- package/dist/utils/data-client-redirect.js.map +1 -0
- package/dist/utils/data-client-request.d.ts +32 -0
- package/dist/utils/data-client-request.d.ts.map +1 -0
- package/dist/utils/data-client-request.js +309 -0
- package/dist/utils/data-client-request.js.map +1 -0
- package/dist/utils/data-client-utils.d.ts +49 -0
- package/dist/utils/data-client-utils.d.ts.map +1 -0
- package/dist/utils/data-client-utils.js +139 -0
- package/dist/utils/data-client-utils.js.map +1 -0
- package/dist/utils/data-client.d.ts +103 -29
- package/dist/utils/data-client.d.ts.map +1 -1
- package/dist/utils/data-client.js +321 -774
- package/dist/utils/data-client.js.map +1 -1
- package/dist/utils/internal-http-client.d.ts.map +1 -1
- package/dist/utils/internal-http-client.js +7 -3
- package/dist/utils/internal-http-client.js.map +1 -1
- package/package.json +9 -2
|
@@ -3,135 +3,23 @@
|
|
|
3
3
|
* DataClient - Browser-compatible HTTP client wrapper around MisoClient
|
|
4
4
|
* Provides enhanced HTTP capabilities with ISO 27001 compliance, caching, retry, and more
|
|
5
5
|
*/
|
|
6
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
-
};
|
|
9
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
7
|
exports.DataClient = void 0;
|
|
11
8
|
exports.dataClient = dataClient;
|
|
12
9
|
const index_1 = require("../index");
|
|
13
|
-
const data_client_types_1 = require("../types/data-client.types");
|
|
14
10
|
const data_masker_1 = require("./data-masker");
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
*/
|
|
28
|
-
function getLocalStorage(key) {
|
|
29
|
-
if (!isBrowser())
|
|
30
|
-
return null;
|
|
31
|
-
try {
|
|
32
|
-
return globalThis.localStorage.getItem(key);
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Set value in localStorage (browser only)
|
|
40
|
-
*/
|
|
41
|
-
function setLocalStorage(key, value) {
|
|
42
|
-
if (!isBrowser())
|
|
43
|
-
return;
|
|
44
|
-
try {
|
|
45
|
-
globalThis.localStorage.setItem(key, value);
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
// Ignore localStorage errors (SSR, private browsing, etc.)
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Remove value from localStorage (browser only)
|
|
53
|
-
*/
|
|
54
|
-
function removeLocalStorage(key) {
|
|
55
|
-
if (!isBrowser())
|
|
56
|
-
return;
|
|
57
|
-
try {
|
|
58
|
-
globalThis.localStorage.removeItem(key);
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
// Ignore localStorage errors (SSR, private browsing, etc.)
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Extract userId from JWT token
|
|
66
|
-
*/
|
|
67
|
-
function extractUserIdFromToken(token) {
|
|
68
|
-
try {
|
|
69
|
-
const decoded = jsonwebtoken_1.default.decode(token);
|
|
70
|
-
if (!decoded)
|
|
71
|
-
return null;
|
|
72
|
-
return (decoded.sub || decoded.userId || decoded.user_id || decoded.id);
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Calculate cache key from endpoint and options
|
|
80
|
-
*/
|
|
81
|
-
function generateCacheKey(endpoint, options) {
|
|
82
|
-
const method = options?.method || "GET";
|
|
83
|
-
const body = options?.body ? JSON.stringify(options.body) : "";
|
|
84
|
-
return `data-client:${method}:${endpoint}:${body}`;
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Truncate large payloads before masking (performance optimization)
|
|
88
|
-
*/
|
|
89
|
-
function truncatePayload(data, maxSize) {
|
|
90
|
-
const json = JSON.stringify(data);
|
|
91
|
-
if (json.length <= maxSize) {
|
|
92
|
-
return { data, truncated: false };
|
|
93
|
-
}
|
|
94
|
-
return {
|
|
95
|
-
data: { _message: "Payload truncated for performance", _size: json.length },
|
|
96
|
-
truncated: true,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Calculate request/response sizes
|
|
101
|
-
*/
|
|
102
|
-
function calculateSize(data) {
|
|
103
|
-
try {
|
|
104
|
-
return JSON.stringify(data).length;
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
return 0;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Check if error is retryable
|
|
112
|
-
*/
|
|
113
|
-
function isRetryableError(statusCode, _error) {
|
|
114
|
-
if (!statusCode)
|
|
115
|
-
return true; // Network errors are retryable
|
|
116
|
-
if (statusCode >= 500)
|
|
117
|
-
return true; // Server errors
|
|
118
|
-
if (statusCode === 408)
|
|
119
|
-
return true; // Timeout
|
|
120
|
-
if (statusCode === 429)
|
|
121
|
-
return true; // Rate limit
|
|
122
|
-
if (statusCode === 401 || statusCode === 403)
|
|
123
|
-
return false; // Auth errors
|
|
124
|
-
if (statusCode >= 400 && statusCode < 500)
|
|
125
|
-
return false; // Client errors
|
|
126
|
-
return false;
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Calculate exponential backoff delay
|
|
130
|
-
*/
|
|
131
|
-
function calculateBackoffDelay(attempt, baseDelay, maxDelay) {
|
|
132
|
-
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
|
|
133
|
-
return delay + Math.random() * 1000; // Add jitter
|
|
134
|
-
}
|
|
11
|
+
const data_client_utils_1 = require("./data-client-utils");
|
|
12
|
+
const data_client_cache_1 = require("./data-client-cache");
|
|
13
|
+
const data_client_request_1 = require("./data-client-request");
|
|
14
|
+
const data_client_auth_1 = require("./data-client-auth");
|
|
15
|
+
const data_client_redirect_1 = require("./data-client-redirect");
|
|
16
|
+
const browser_permission_service_1 = require("../services/browser-permission.service");
|
|
17
|
+
const browser_role_service_1 = require("../services/browser-role.service");
|
|
18
|
+
const cache_service_1 = require("../services/cache.service");
|
|
19
|
+
const http_client_1 = require("../utils/http-client");
|
|
20
|
+
const internal_http_client_1 = require("../utils/internal-http-client");
|
|
21
|
+
const logger_service_1 = require("../services/logger.service");
|
|
22
|
+
const redis_service_1 = require("../services/redis.service");
|
|
135
23
|
class DataClient {
|
|
136
24
|
constructor(config) {
|
|
137
25
|
this.misoClient = null;
|
|
@@ -145,6 +33,8 @@ class DataClient {
|
|
|
145
33
|
cacheHits: 0,
|
|
146
34
|
cacheMisses: 0,
|
|
147
35
|
};
|
|
36
|
+
this.permissionService = null;
|
|
37
|
+
this.roleService = null;
|
|
148
38
|
this.config = {
|
|
149
39
|
tokenKeys: ["token", "accessToken", "authToken"],
|
|
150
40
|
loginUrl: "/login",
|
|
@@ -172,7 +62,7 @@ class DataClient {
|
|
|
172
62
|
};
|
|
173
63
|
// Security: Warn if clientSecret is provided in browser environment
|
|
174
64
|
// This is a security risk as clientSecret should never be exposed in client-side code
|
|
175
|
-
if (isBrowser() && this.config.misoConfig?.clientSecret) {
|
|
65
|
+
if ((0, data_client_utils_1.isBrowser)() && this.config.misoConfig?.clientSecret) {
|
|
176
66
|
console.warn("⚠️ SECURITY WARNING: clientSecret detected in browser environment. " +
|
|
177
67
|
"Client secrets should NEVER be exposed in client-side code. " +
|
|
178
68
|
"Use the client token pattern instead (clientToken + onClientTokenRefresh). " +
|
|
@@ -180,68 +70,79 @@ class DataClient {
|
|
|
180
70
|
}
|
|
181
71
|
// Initialize MisoClient if config provided
|
|
182
72
|
if (this.config.misoConfig) {
|
|
183
|
-
|
|
73
|
+
// Automatically bridge DataClient.getEnvironmentToken() to MisoClient
|
|
74
|
+
// This allows MisoClient's logger service to get client tokens automatically
|
|
75
|
+
// Users don't need to manually provide onClientTokenRefresh!
|
|
76
|
+
const misoConfigWithRefresh = {
|
|
77
|
+
...this.config.misoConfig,
|
|
78
|
+
// Only auto-bridge if:
|
|
79
|
+
// 1. User hasn't provided onClientTokenRefresh (allow override)
|
|
80
|
+
// 2. We're in browser (server-side uses clientSecret)
|
|
81
|
+
// 3. No clientSecret provided (would use that instead)
|
|
82
|
+
onClientTokenRefresh: this.config.misoConfig.onClientTokenRefresh ||
|
|
83
|
+
((0, data_client_utils_1.isBrowser)() && !this.config.misoConfig.clientSecret
|
|
84
|
+
? async () => {
|
|
85
|
+
const token = await this.getEnvironmentToken();
|
|
86
|
+
if (!token) {
|
|
87
|
+
throw new Error("Failed to get client token");
|
|
88
|
+
}
|
|
89
|
+
// Get expiration from localStorage (set by getEnvironmentToken)
|
|
90
|
+
const expiresAtStr = (0, data_client_utils_1.getLocalStorage)("miso:client-token-expires-at");
|
|
91
|
+
const expiresAt = expiresAtStr
|
|
92
|
+
? parseInt(expiresAtStr, 10)
|
|
93
|
+
: Date.now() + 3600000; // Default 1 hour
|
|
94
|
+
const expiresIn = Math.floor((expiresAt - Date.now()) / 1000);
|
|
95
|
+
return {
|
|
96
|
+
token,
|
|
97
|
+
expiresIn: expiresIn > 0 ? expiresIn : 3600, // Default 1 hour if invalid
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
: undefined),
|
|
101
|
+
};
|
|
102
|
+
this.misoClient = new index_1.MisoClient(misoConfigWithRefresh);
|
|
184
103
|
}
|
|
185
104
|
// Initialize DataMasker with config path if provided
|
|
186
105
|
if (this.config.misoConfig?.sensitiveFieldsConfig) {
|
|
187
106
|
data_masker_1.DataMasker.setConfigPath(this.config.misoConfig.sensitiveFieldsConfig);
|
|
188
107
|
}
|
|
108
|
+
// Initialize browser-compatible permission and role services
|
|
109
|
+
// These services need HttpClient and CacheService, so they're initialized after MisoClient
|
|
110
|
+
if (this.misoClient && this.config.misoConfig) {
|
|
111
|
+
// Create InternalHttpClient first (base HTTP functionality)
|
|
112
|
+
const internalClient = new internal_http_client_1.InternalHttpClient(this.config.misoConfig);
|
|
113
|
+
// Create Redis service (will be undefined for browser, but needed for LoggerService)
|
|
114
|
+
const redis = new redis_service_1.RedisService(this.config.misoConfig.redis);
|
|
115
|
+
// Create LoggerService with InternalHttpClient (needs httpClient.request() and httpClient.config)
|
|
116
|
+
const logger = new logger_service_1.LoggerService(internalClient, redis);
|
|
117
|
+
// Create HttpClient that wraps InternalHttpClient with logger
|
|
118
|
+
const httpClient = new http_client_1.HttpClient(this.config.misoConfig, logger);
|
|
119
|
+
// Update LoggerService to use the new HttpClient (for logging)
|
|
120
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
121
|
+
logger.httpClient = httpClient;
|
|
122
|
+
// Create CacheService without Redis (in-memory only for browser)
|
|
123
|
+
const cacheService = new cache_service_1.CacheService(undefined);
|
|
124
|
+
// Create browser-compatible services
|
|
125
|
+
this.permissionService = new browser_permission_service_1.BrowserPermissionService(httpClient, cacheService);
|
|
126
|
+
this.roleService = new browser_role_service_1.BrowserRoleService(httpClient, cacheService);
|
|
127
|
+
}
|
|
189
128
|
}
|
|
190
129
|
/**
|
|
191
130
|
* Get authentication token from localStorage
|
|
192
131
|
*/
|
|
193
132
|
getToken() {
|
|
194
|
-
|
|
195
|
-
return null;
|
|
196
|
-
const keys = this.config.tokenKeys || ["token", "accessToken", "authToken"];
|
|
197
|
-
for (const key of keys) {
|
|
198
|
-
const token = getLocalStorage(key);
|
|
199
|
-
if (token)
|
|
200
|
-
return token;
|
|
201
|
-
}
|
|
202
|
-
return null;
|
|
133
|
+
return (0, data_client_auth_1.getToken)(this.config.tokenKeys);
|
|
203
134
|
}
|
|
204
135
|
/**
|
|
205
136
|
* Check if client token is available (from localStorage cache or config)
|
|
206
137
|
*/
|
|
207
138
|
hasClientToken() {
|
|
208
|
-
|
|
209
|
-
// Server-side: check if misoClient config has clientSecret
|
|
210
|
-
if (this.misoClient && this.config.misoConfig?.clientSecret) {
|
|
211
|
-
return true;
|
|
212
|
-
}
|
|
213
|
-
return false;
|
|
214
|
-
}
|
|
215
|
-
// Browser-side: check localStorage cache
|
|
216
|
-
const cachedToken = getLocalStorage("miso:client-token");
|
|
217
|
-
if (cachedToken) {
|
|
218
|
-
const expiresAtStr = getLocalStorage("miso:client-token-expires-at");
|
|
219
|
-
if (expiresAtStr) {
|
|
220
|
-
const expiresAt = parseInt(expiresAtStr, 10);
|
|
221
|
-
if (expiresAt > Date.now()) {
|
|
222
|
-
return true; // Valid cached token
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
// Check config token
|
|
227
|
-
if (this.config.misoConfig?.clientToken) {
|
|
228
|
-
return true;
|
|
229
|
-
}
|
|
230
|
-
// Check if misoClient config has onClientTokenRefresh callback (browser pattern)
|
|
231
|
-
if (this.config.misoConfig?.onClientTokenRefresh) {
|
|
232
|
-
return true; // Has means to get client token
|
|
233
|
-
}
|
|
234
|
-
// Check if misoClient config has clientSecret (server-side fallback)
|
|
235
|
-
if (this.config.misoConfig?.clientSecret) {
|
|
236
|
-
return true;
|
|
237
|
-
}
|
|
238
|
-
return false;
|
|
139
|
+
return (0, data_client_auth_1.hasClientToken)(this.misoClient, this.config.misoConfig);
|
|
239
140
|
}
|
|
240
141
|
/**
|
|
241
142
|
* Check if any authentication token is available (user token OR client token)
|
|
242
143
|
*/
|
|
243
144
|
hasAnyToken() {
|
|
244
|
-
return this.
|
|
145
|
+
return (0, data_client_auth_1.hasAnyToken)(this.config.tokenKeys, this.misoClient, this.config.misoConfig);
|
|
245
146
|
}
|
|
246
147
|
/**
|
|
247
148
|
* Get client token for requests
|
|
@@ -249,31 +150,7 @@ class DataClient {
|
|
|
249
150
|
* @returns Client token string or null if unavailable
|
|
250
151
|
*/
|
|
251
152
|
async getClientToken() {
|
|
252
|
-
|
|
253
|
-
// Server-side: return null (client token handled by MisoClient)
|
|
254
|
-
return null;
|
|
255
|
-
}
|
|
256
|
-
// Check localStorage cache first
|
|
257
|
-
const cachedToken = getLocalStorage("miso:client-token");
|
|
258
|
-
const expiresAtStr = getLocalStorage("miso:client-token-expires-at");
|
|
259
|
-
if (cachedToken && expiresAtStr) {
|
|
260
|
-
const expiresAt = parseInt(expiresAtStr, 10);
|
|
261
|
-
if (expiresAt > Date.now()) {
|
|
262
|
-
return cachedToken; // Valid cached token
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
// Check config token
|
|
266
|
-
if (this.config.misoConfig?.clientToken) {
|
|
267
|
-
return this.config.misoConfig.clientToken;
|
|
268
|
-
}
|
|
269
|
-
// Try to get via getEnvironmentToken() (may throw if unavailable)
|
|
270
|
-
try {
|
|
271
|
-
return await this.getEnvironmentToken();
|
|
272
|
-
}
|
|
273
|
-
catch (error) {
|
|
274
|
-
console.warn("Failed to get client token:", error);
|
|
275
|
-
return null;
|
|
276
|
-
}
|
|
153
|
+
return (0, data_client_auth_1.getClientToken)(this.config.misoConfig, this.config.baseUrl, () => this.getEnvironmentToken());
|
|
277
154
|
}
|
|
278
155
|
/**
|
|
279
156
|
* Build controller URL from configuration
|
|
@@ -281,19 +158,7 @@ class DataClient {
|
|
|
281
158
|
* @returns Controller base URL or null if not configured
|
|
282
159
|
*/
|
|
283
160
|
getControllerUrl() {
|
|
284
|
-
|
|
285
|
-
return null;
|
|
286
|
-
}
|
|
287
|
-
// Browser: prefer controllerPublicUrl, fallback to controllerUrl
|
|
288
|
-
if (isBrowser()) {
|
|
289
|
-
return this.config.misoConfig.controllerPublicUrl ||
|
|
290
|
-
this.config.misoConfig.controllerUrl ||
|
|
291
|
-
null;
|
|
292
|
-
}
|
|
293
|
-
// Server: prefer controllerPrivateUrl, fallback to controllerUrl
|
|
294
|
-
return this.config.misoConfig.controllerPrivateUrl ||
|
|
295
|
-
this.config.misoConfig.controllerUrl ||
|
|
296
|
-
null;
|
|
161
|
+
return (0, data_client_auth_1.getControllerUrl)(this.config.misoConfig);
|
|
297
162
|
}
|
|
298
163
|
/**
|
|
299
164
|
* Check if user is authenticated
|
|
@@ -307,71 +172,7 @@ class DataClient {
|
|
|
307
172
|
* @param redirectUrl - Optional redirect URL to return to after login (defaults to current page URL)
|
|
308
173
|
*/
|
|
309
174
|
async redirectToLogin(redirectUrl) {
|
|
310
|
-
|
|
311
|
-
return;
|
|
312
|
-
// Get redirect URL - use provided URL or current page URL
|
|
313
|
-
const currentUrl = globalThis.window.location.href;
|
|
314
|
-
const finalRedirectUrl = redirectUrl || currentUrl;
|
|
315
|
-
// Build controller URL
|
|
316
|
-
const controllerUrl = this.getControllerUrl();
|
|
317
|
-
if (!controllerUrl) {
|
|
318
|
-
// Fallback to static loginUrl if controller URL not configured
|
|
319
|
-
const loginUrl = this.config.loginUrl || "/login";
|
|
320
|
-
const fullUrl = /^https?:\/\//i.test(loginUrl)
|
|
321
|
-
? loginUrl
|
|
322
|
-
: `${globalThis.window.location.origin}${loginUrl.startsWith("/") ? loginUrl : `/${loginUrl}`}`;
|
|
323
|
-
globalThis.window.location.href = fullUrl;
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
try {
|
|
327
|
-
// Get client token
|
|
328
|
-
const clientToken = await this.getClientToken();
|
|
329
|
-
// Build login endpoint URL with query parameters
|
|
330
|
-
const loginEndpoint = `${controllerUrl}/api/v1/auth/login`;
|
|
331
|
-
const url = new URL(loginEndpoint);
|
|
332
|
-
url.searchParams.set("redirect", finalRedirectUrl);
|
|
333
|
-
// Build headers
|
|
334
|
-
const headers = {
|
|
335
|
-
"Content-Type": "application/json",
|
|
336
|
-
};
|
|
337
|
-
// Add x-client-token header if available
|
|
338
|
-
if (clientToken) {
|
|
339
|
-
headers["x-client-token"] = clientToken;
|
|
340
|
-
}
|
|
341
|
-
// Make fetch request
|
|
342
|
-
const response = await fetch(url.toString(), {
|
|
343
|
-
method: "GET",
|
|
344
|
-
headers,
|
|
345
|
-
credentials: "include", // Include cookies for CORS
|
|
346
|
-
});
|
|
347
|
-
if (!response.ok) {
|
|
348
|
-
throw new Error(`Login request failed: ${response.status} ${response.statusText}`);
|
|
349
|
-
}
|
|
350
|
-
const data = (await response.json());
|
|
351
|
-
// Extract loginUrl (support both nested and flat formats)
|
|
352
|
-
const loginUrl = data.data?.loginUrl || data.loginUrl;
|
|
353
|
-
if (loginUrl) {
|
|
354
|
-
// Redirect to the login URL returned by controller
|
|
355
|
-
globalThis.window.location.href = loginUrl;
|
|
356
|
-
}
|
|
357
|
-
else {
|
|
358
|
-
// Fallback if loginUrl not in response
|
|
359
|
-
const fallbackLoginUrl = this.config.loginUrl || "/login";
|
|
360
|
-
const fullUrl = /^https?:\/\//i.test(fallbackLoginUrl)
|
|
361
|
-
? fallbackLoginUrl
|
|
362
|
-
: `${globalThis.window.location.origin}${fallbackLoginUrl.startsWith("/") ? fallbackLoginUrl : `/${fallbackLoginUrl}`}`;
|
|
363
|
-
globalThis.window.location.href = fullUrl;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
catch (error) {
|
|
367
|
-
// On error, fallback to static loginUrl
|
|
368
|
-
console.error("Failed to get login URL from controller:", error);
|
|
369
|
-
const loginUrl = this.config.loginUrl || "/login";
|
|
370
|
-
const fullUrl = /^https?:\/\//i.test(loginUrl)
|
|
371
|
-
? loginUrl
|
|
372
|
-
: `${globalThis.window.location.origin}${loginUrl.startsWith("/") ? loginUrl : `/${loginUrl}`}`;
|
|
373
|
-
globalThis.window.location.href = fullUrl;
|
|
374
|
-
}
|
|
175
|
+
return (0, data_client_redirect_1.redirectToLogin)(this.config, () => this.getClientToken(), redirectUrl);
|
|
375
176
|
}
|
|
376
177
|
/**
|
|
377
178
|
* Logout user and redirect
|
|
@@ -379,70 +180,7 @@ class DataClient {
|
|
|
379
180
|
* @param redirectUrl - Optional redirect URL after logout (defaults to logoutUrl or loginUrl)
|
|
380
181
|
*/
|
|
381
182
|
async logout(redirectUrl) {
|
|
382
|
-
|
|
383
|
-
return;
|
|
384
|
-
const token = this.getToken();
|
|
385
|
-
// Build controller URL
|
|
386
|
-
const controllerUrl = this.getControllerUrl();
|
|
387
|
-
// Call logout API if controller URL available and token exists
|
|
388
|
-
if (controllerUrl && token) {
|
|
389
|
-
try {
|
|
390
|
-
// Get client token
|
|
391
|
-
const clientToken = await this.getClientToken();
|
|
392
|
-
// Build logout endpoint URL
|
|
393
|
-
const logoutEndpoint = `${controllerUrl}/api/v1/auth/logout`;
|
|
394
|
-
// Build headers
|
|
395
|
-
const headers = {
|
|
396
|
-
"Content-Type": "application/json",
|
|
397
|
-
};
|
|
398
|
-
// Add x-client-token header if available
|
|
399
|
-
if (clientToken) {
|
|
400
|
-
headers["x-client-token"] = clientToken;
|
|
401
|
-
}
|
|
402
|
-
// Make fetch request
|
|
403
|
-
const response = await fetch(logoutEndpoint, {
|
|
404
|
-
method: "POST",
|
|
405
|
-
headers,
|
|
406
|
-
body: JSON.stringify({ token }),
|
|
407
|
-
credentials: "include", // Include cookies for CORS
|
|
408
|
-
});
|
|
409
|
-
if (!response.ok) {
|
|
410
|
-
// Log error but continue with cleanup (logout should always clear local state)
|
|
411
|
-
const errorText = await response.text().catch(() => "Unknown error");
|
|
412
|
-
console.error(`Logout API call failed: ${response.status} ${response.statusText}. ${errorText}`);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
catch (error) {
|
|
416
|
-
// Log error but continue with cleanup (logout should always clear local state)
|
|
417
|
-
console.error("Logout API call failed:", error);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
// Clear tokens from localStorage (always, even if API call failed)
|
|
421
|
-
const keys = this.config.tokenKeys || ["token", "accessToken", "authToken"];
|
|
422
|
-
keys.forEach(key => {
|
|
423
|
-
try {
|
|
424
|
-
const storage = globalThis.localStorage;
|
|
425
|
-
if (storage) {
|
|
426
|
-
storage.removeItem(key);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
catch (e) {
|
|
430
|
-
// Ignore localStorage errors (SSR, private browsing, etc.)
|
|
431
|
-
}
|
|
432
|
-
});
|
|
433
|
-
// Clear HTTP cache
|
|
434
|
-
this.clearCache();
|
|
435
|
-
// Determine redirect URL: redirectUrl param > logoutUrl config > loginUrl config > '/login'
|
|
436
|
-
const finalRedirectUrl = redirectUrl ||
|
|
437
|
-
this.config.logoutUrl ||
|
|
438
|
-
this.config.loginUrl ||
|
|
439
|
-
"/login";
|
|
440
|
-
// Construct full URL
|
|
441
|
-
const fullUrl = /^https?:\/\//i.test(finalRedirectUrl)
|
|
442
|
-
? finalRedirectUrl
|
|
443
|
-
: `${globalThis.window.location.origin}${finalRedirectUrl.startsWith("/") ? finalRedirectUrl : `/${finalRedirectUrl}`}`;
|
|
444
|
-
// Redirect
|
|
445
|
-
globalThis.window.location.href = fullUrl;
|
|
183
|
+
return (0, data_client_auth_1.logout)(this.config, () => this.getToken(), () => this.getClientToken(), () => this.clearCache(), redirectUrl);
|
|
446
184
|
}
|
|
447
185
|
/**
|
|
448
186
|
* Set interceptors
|
|
@@ -488,7 +226,7 @@ class DataClient {
|
|
|
488
226
|
* Get request metrics
|
|
489
227
|
*/
|
|
490
228
|
getMetrics() {
|
|
491
|
-
const responseTimes = this.metrics.responseTimes.sort((a, b) => a - b);
|
|
229
|
+
const responseTimes = (this.metrics.responseTimes || []).sort((a, b) => a - b);
|
|
492
230
|
const len = responseTimes.length;
|
|
493
231
|
return {
|
|
494
232
|
totalRequests: this.metrics.totalRequests,
|
|
@@ -512,141 +250,21 @@ class DataClient {
|
|
|
512
250
|
: 0,
|
|
513
251
|
};
|
|
514
252
|
}
|
|
515
|
-
/**
|
|
516
|
-
* Check if endpoint should skip audit logging
|
|
517
|
-
*/
|
|
518
|
-
shouldSkipAudit(endpoint) {
|
|
519
|
-
if (!this.config.audit?.enabled)
|
|
520
|
-
return true;
|
|
521
|
-
const skipEndpoints = this.config.audit.skipEndpoints || [];
|
|
522
|
-
return skipEndpoints.some((skip) => endpoint.includes(skip));
|
|
523
|
-
}
|
|
524
|
-
/**
|
|
525
|
-
* Log audit event (ISO 27001 compliance)
|
|
526
|
-
* Skips audit logging if no authentication token is available (user token OR client token)
|
|
527
|
-
*/
|
|
528
|
-
async logAuditEvent(method, url, statusCode, duration, requestSize, responseSize, error, requestHeaders, responseHeaders, requestBody, responseBody) {
|
|
529
|
-
if (this.shouldSkipAudit(url) || !this.misoClient)
|
|
530
|
-
return;
|
|
531
|
-
// Skip audit logging if no authentication token is available
|
|
532
|
-
// This prevents 401 errors when attempting to audit log unauthenticated requests
|
|
533
|
-
if (!this.hasAnyToken()) {
|
|
534
|
-
// Silently skip audit logging for unauthenticated requests
|
|
535
|
-
// This is expected behavior and prevents 401 errors
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
try {
|
|
539
|
-
const token = this.getToken();
|
|
540
|
-
const userId = token ? extractUserIdFromToken(token) : undefined;
|
|
541
|
-
const auditLevel = this.config.audit?.level || "standard";
|
|
542
|
-
// Build audit context based on level
|
|
543
|
-
const auditContext = {
|
|
544
|
-
method,
|
|
545
|
-
url,
|
|
546
|
-
statusCode,
|
|
547
|
-
duration,
|
|
548
|
-
};
|
|
549
|
-
if (userId) {
|
|
550
|
-
auditContext.userId = userId;
|
|
551
|
-
}
|
|
552
|
-
// Minimal level: only basic info
|
|
553
|
-
if (auditLevel === "minimal") {
|
|
554
|
-
await this.misoClient.log.audit(`http.request.${method.toLowerCase()}`, url, auditContext, { token: token || undefined });
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
// Standard/Detailed/Full levels: include headers and bodies (masked)
|
|
558
|
-
const maxResponseSize = this.config.audit?.maxResponseSize || 10000;
|
|
559
|
-
const maxMaskingSize = this.config.audit?.maxMaskingSize || 50000;
|
|
560
|
-
// Truncate and mask request body
|
|
561
|
-
let maskedRequestBody = undefined;
|
|
562
|
-
if (requestBody !== undefined) {
|
|
563
|
-
const truncated = truncatePayload(requestBody, maxMaskingSize);
|
|
564
|
-
if (!truncated.truncated) {
|
|
565
|
-
maskedRequestBody = data_masker_1.DataMasker.maskSensitiveData(truncated.data);
|
|
566
|
-
}
|
|
567
|
-
else {
|
|
568
|
-
maskedRequestBody = truncated.data;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
// Truncate and mask response body (for standard, detailed, full levels)
|
|
572
|
-
let maskedResponseBody = undefined;
|
|
573
|
-
if (responseBody !== undefined) {
|
|
574
|
-
const truncated = truncatePayload(responseBody, maxResponseSize);
|
|
575
|
-
if (!truncated.truncated) {
|
|
576
|
-
maskedResponseBody = data_masker_1.DataMasker.maskSensitiveData(truncated.data);
|
|
577
|
-
}
|
|
578
|
-
else {
|
|
579
|
-
maskedResponseBody = truncated.data;
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
// Mask headers
|
|
583
|
-
const maskedRequestHeaders = requestHeaders
|
|
584
|
-
? data_masker_1.DataMasker.maskSensitiveData(requestHeaders)
|
|
585
|
-
: undefined;
|
|
586
|
-
const maskedResponseHeaders = responseHeaders
|
|
587
|
-
? data_masker_1.DataMasker.maskSensitiveData(responseHeaders)
|
|
588
|
-
: undefined;
|
|
589
|
-
// Add to context based on level (standard, detailed, full all include headers/bodies)
|
|
590
|
-
if (maskedRequestHeaders)
|
|
591
|
-
auditContext.requestHeaders = maskedRequestHeaders;
|
|
592
|
-
if (maskedResponseHeaders)
|
|
593
|
-
auditContext.responseHeaders = maskedResponseHeaders;
|
|
594
|
-
if (maskedRequestBody !== undefined)
|
|
595
|
-
auditContext.requestBody = maskedRequestBody;
|
|
596
|
-
if (maskedResponseBody !== undefined)
|
|
597
|
-
auditContext.responseBody = maskedResponseBody;
|
|
598
|
-
// Add sizes for detailed/full levels
|
|
599
|
-
if (auditLevel === "detailed" || auditLevel === "full") {
|
|
600
|
-
if (requestSize !== undefined)
|
|
601
|
-
auditContext.requestSize = requestSize;
|
|
602
|
-
if (responseSize !== undefined)
|
|
603
|
-
auditContext.responseSize = responseSize;
|
|
604
|
-
}
|
|
605
|
-
if (error) {
|
|
606
|
-
const maskedError = data_masker_1.DataMasker.maskSensitiveData({
|
|
607
|
-
message: error.message,
|
|
608
|
-
name: error.name,
|
|
609
|
-
stack: error.stack,
|
|
610
|
-
});
|
|
611
|
-
auditContext.error = maskedError;
|
|
612
|
-
}
|
|
613
|
-
await this.misoClient.log.audit(`http.request.${method.toLowerCase()}`, url, auditContext, { token: token || undefined });
|
|
614
|
-
}
|
|
615
|
-
catch (auditError) {
|
|
616
|
-
// Handle audit logging errors gracefully
|
|
617
|
-
// Don't fail main request if audit logging fails
|
|
618
|
-
const error = auditError;
|
|
619
|
-
const statusCode = error.statusCode || error.response?.status;
|
|
620
|
-
if (statusCode === 401) {
|
|
621
|
-
// User not authenticated - this is expected for unauthenticated requests
|
|
622
|
-
// Silently skip to avoid noise (we already check hasAnyToken() before attempting)
|
|
623
|
-
// This catch block handles edge cases where token becomes unavailable between check and audit call
|
|
624
|
-
}
|
|
625
|
-
else {
|
|
626
|
-
// Other errors - log warning but don't fail request
|
|
627
|
-
console.warn("Failed to log audit event:", auditError);
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
253
|
/**
|
|
632
254
|
* Make HTTP request with all features (caching, retry, deduplication, audit)
|
|
633
255
|
*/
|
|
634
256
|
async request(method, endpoint, options) {
|
|
635
257
|
const startTime = Date.now();
|
|
636
258
|
const fullUrl = `${this.config.baseUrl}${endpoint}`;
|
|
637
|
-
const cacheKey =
|
|
259
|
+
const cacheKey = (0, data_client_cache_1.getCacheKeyForRequest)(endpoint, options);
|
|
638
260
|
const isGetRequest = method.toUpperCase() === "GET";
|
|
639
|
-
const cacheEnabled = (this.config.cache
|
|
640
|
-
isGetRequest &&
|
|
641
|
-
(options?.cache?.enabled !== false);
|
|
261
|
+
const cacheEnabled = (0, data_client_cache_1.isCacheEnabled)(method, this.config.cache, options);
|
|
642
262
|
// Check cache for GET requests
|
|
643
263
|
if (cacheEnabled) {
|
|
644
|
-
const cached = this.cache.
|
|
645
|
-
if (cached
|
|
646
|
-
|
|
647
|
-
return cached.data;
|
|
264
|
+
const cached = (0, data_client_cache_1.getCachedEntry)(this.cache, cacheKey, this.metrics);
|
|
265
|
+
if (cached !== null) {
|
|
266
|
+
return cached;
|
|
648
267
|
}
|
|
649
|
-
this.metrics.cacheMisses++;
|
|
650
268
|
}
|
|
651
269
|
// Check for duplicate concurrent requests (only for GET requests)
|
|
652
270
|
if (isGetRequest) {
|
|
@@ -656,7 +274,7 @@ class DataClient {
|
|
|
656
274
|
}
|
|
657
275
|
}
|
|
658
276
|
// Create request promise
|
|
659
|
-
const requestPromise =
|
|
277
|
+
const requestPromise = (0, data_client_request_1.executeHttpRequest)(method, fullUrl, endpoint, this.config, this.cache, cacheKey, cacheEnabled, startTime, this.misoClient, () => this.hasAnyToken(), () => this.getToken(), () => this.handleAuthError(), this.interceptors, this.metrics, options);
|
|
660
278
|
// Store pending request (only for GET requests)
|
|
661
279
|
if (isGetRequest) {
|
|
662
280
|
this.pendingRequests.set(cacheKey, requestPromise);
|
|
@@ -672,221 +290,11 @@ class DataClient {
|
|
|
672
290
|
}
|
|
673
291
|
}
|
|
674
292
|
}
|
|
675
|
-
/**
|
|
676
|
-
* Execute HTTP request with retry logic
|
|
677
|
-
*/
|
|
678
|
-
async executeRequest(method, fullUrl, endpoint, options, cacheKey, cacheEnabled, startTime) {
|
|
679
|
-
const maxRetries = options?.retries !== undefined
|
|
680
|
-
? options.retries
|
|
681
|
-
: this.config.retry?.maxRetries || 3;
|
|
682
|
-
const retryEnabled = this.config.retry?.enabled !== false;
|
|
683
|
-
let lastError;
|
|
684
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
685
|
-
try {
|
|
686
|
-
const response = await this.makeFetchRequest(method, fullUrl, options);
|
|
687
|
-
const duration = Date.now() - startTime;
|
|
688
|
-
// Update metrics
|
|
689
|
-
this.metrics.totalRequests++;
|
|
690
|
-
this.metrics.responseTimes.push(duration);
|
|
691
|
-
// Parse response
|
|
692
|
-
const data = await this.parseResponse(response);
|
|
693
|
-
// Cache successful GET responses
|
|
694
|
-
if (cacheEnabled && response.ok) {
|
|
695
|
-
const ttl = options?.cache?.ttl || this.config.cache?.defaultTTL || 300;
|
|
696
|
-
this.cache.set(cacheKey, {
|
|
697
|
-
data,
|
|
698
|
-
expiresAt: Date.now() + ttl * 1000,
|
|
699
|
-
key: cacheKey,
|
|
700
|
-
});
|
|
701
|
-
// Enforce max cache size
|
|
702
|
-
if (this.cache.size > (this.config.cache?.maxSize || 100)) {
|
|
703
|
-
const firstKey = this.cache.keys().next().value;
|
|
704
|
-
if (firstKey)
|
|
705
|
-
this.cache.delete(firstKey);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
// Apply response interceptor
|
|
709
|
-
if (this.interceptors.onResponse) {
|
|
710
|
-
return await this.interceptors.onResponse(response, data);
|
|
711
|
-
}
|
|
712
|
-
// Audit logging
|
|
713
|
-
if (!options?.skipAudit) {
|
|
714
|
-
const requestSize = options?.body
|
|
715
|
-
? calculateSize(options.body)
|
|
716
|
-
: undefined;
|
|
717
|
-
const responseSize = calculateSize(data);
|
|
718
|
-
await this.logAuditEvent(method, endpoint, response.status, duration, requestSize, responseSize, undefined, this.extractHeaders(options?.headers), this.extractHeaders(response.headers), options?.body, data);
|
|
719
|
-
}
|
|
720
|
-
// Handle error responses
|
|
721
|
-
if (!response.ok) {
|
|
722
|
-
if (response.status === 401) {
|
|
723
|
-
this.handleAuthError();
|
|
724
|
-
throw new data_client_types_1.AuthenticationError("Authentication required", response);
|
|
725
|
-
}
|
|
726
|
-
throw new data_client_types_1.ApiError(`Request failed with status ${response.status}`, response.status, response);
|
|
727
|
-
}
|
|
728
|
-
return data;
|
|
729
|
-
}
|
|
730
|
-
catch (error) {
|
|
731
|
-
lastError = error;
|
|
732
|
-
const duration = Date.now() - startTime;
|
|
733
|
-
// Extract statusCode from error (handle AuthenticationError and ApiError)
|
|
734
|
-
const errorObj = error;
|
|
735
|
-
let statusCode = errorObj.statusCode;
|
|
736
|
-
// Explicitly check for AuthenticationError (401) - don't retry auth errors
|
|
737
|
-
if (errorObj.name === "AuthenticationError" || statusCode === 401) {
|
|
738
|
-
statusCode = 401;
|
|
739
|
-
}
|
|
740
|
-
// Check if retryable - 401/403 errors should never retry
|
|
741
|
-
const isRetryable = statusCode !== 401 &&
|
|
742
|
-
statusCode !== 403 &&
|
|
743
|
-
retryEnabled &&
|
|
744
|
-
attempt < maxRetries &&
|
|
745
|
-
isRetryableError(statusCode, error);
|
|
746
|
-
if (!isRetryable) {
|
|
747
|
-
this.metrics.totalFailures++;
|
|
748
|
-
// Audit log error
|
|
749
|
-
if (!options?.skipAudit) {
|
|
750
|
-
await this.logAuditEvent(method, endpoint, error.statusCode || 0, duration, options?.body ? calculateSize(options.body) : undefined, undefined, error, this.extractHeaders(options?.headers), undefined, options?.body, undefined);
|
|
751
|
-
}
|
|
752
|
-
// Apply error interceptor
|
|
753
|
-
if (this.interceptors.onError) {
|
|
754
|
-
throw await this.interceptors.onError(error);
|
|
755
|
-
}
|
|
756
|
-
throw error;
|
|
757
|
-
}
|
|
758
|
-
// Calculate backoff delay
|
|
759
|
-
const baseDelay = this.config.retry?.baseDelay || 1000;
|
|
760
|
-
const maxDelay = this.config.retry?.maxDelay || 10000;
|
|
761
|
-
const delay = calculateBackoffDelay(attempt, baseDelay, maxDelay);
|
|
762
|
-
// Wait before retry
|
|
763
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
// All retries exhausted
|
|
767
|
-
this.metrics.totalFailures++;
|
|
768
|
-
if (lastError) {
|
|
769
|
-
if (this.interceptors.onError) {
|
|
770
|
-
throw await this.interceptors.onError(lastError);
|
|
771
|
-
}
|
|
772
|
-
throw lastError;
|
|
773
|
-
}
|
|
774
|
-
throw new Error("Request failed after retries");
|
|
775
|
-
}
|
|
776
|
-
/**
|
|
777
|
-
* Make fetch request with timeout and authentication
|
|
778
|
-
*/
|
|
779
|
-
async makeFetchRequest(method, url, options) {
|
|
780
|
-
// Build headers
|
|
781
|
-
const headers = new Headers(this.config.defaultHeaders);
|
|
782
|
-
if (options?.headers) {
|
|
783
|
-
if (options.headers instanceof Headers) {
|
|
784
|
-
options.headers.forEach((value, key) => headers.set(key, value));
|
|
785
|
-
}
|
|
786
|
-
else {
|
|
787
|
-
Object.entries(options.headers).forEach(([key, value]) => {
|
|
788
|
-
headers.set(key, String(value));
|
|
789
|
-
});
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
// Add authentication
|
|
793
|
-
if (!options?.skipAuth) {
|
|
794
|
-
const token = this.getToken();
|
|
795
|
-
if (token) {
|
|
796
|
-
headers.set("Authorization", `Bearer ${token}`);
|
|
797
|
-
}
|
|
798
|
-
// Note: MisoClient client-token is handled server-side, not in browser
|
|
799
|
-
}
|
|
800
|
-
// Create abort controller for timeout
|
|
801
|
-
const timeout = options?.timeout || this.config.timeout || 30000;
|
|
802
|
-
const controller = new AbortController();
|
|
803
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
804
|
-
// Merge signals
|
|
805
|
-
const signal = options?.signal
|
|
806
|
-
? this.mergeSignals(controller.signal, options.signal)
|
|
807
|
-
: controller.signal;
|
|
808
|
-
try {
|
|
809
|
-
const response = await fetch(url, {
|
|
810
|
-
method,
|
|
811
|
-
headers,
|
|
812
|
-
body: options?.body,
|
|
813
|
-
signal,
|
|
814
|
-
...options,
|
|
815
|
-
});
|
|
816
|
-
clearTimeout(timeoutId);
|
|
817
|
-
return response;
|
|
818
|
-
}
|
|
819
|
-
catch (error) {
|
|
820
|
-
clearTimeout(timeoutId);
|
|
821
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
822
|
-
throw new data_client_types_1.TimeoutError(`Request timeout after ${timeout}ms`, timeout);
|
|
823
|
-
}
|
|
824
|
-
throw new data_client_types_1.NetworkError(`Network error: ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : undefined);
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
/**
|
|
828
|
-
* Merge AbortSignals
|
|
829
|
-
*/
|
|
830
|
-
mergeSignals(signal1, signal2) {
|
|
831
|
-
const controller = new AbortController();
|
|
832
|
-
const abort = () => {
|
|
833
|
-
controller.abort();
|
|
834
|
-
signal1.removeEventListener("abort", abort);
|
|
835
|
-
signal2.removeEventListener("abort", abort);
|
|
836
|
-
};
|
|
837
|
-
if (signal1.aborted || signal2.aborted) {
|
|
838
|
-
controller.abort();
|
|
839
|
-
return controller.signal;
|
|
840
|
-
}
|
|
841
|
-
signal1.addEventListener("abort", abort);
|
|
842
|
-
signal2.addEventListener("abort", abort);
|
|
843
|
-
return controller.signal;
|
|
844
|
-
}
|
|
845
|
-
/**
|
|
846
|
-
* Parse response based on content type
|
|
847
|
-
*/
|
|
848
|
-
async parseResponse(response) {
|
|
849
|
-
const contentType = response.headers.get("content-type") || "";
|
|
850
|
-
if (contentType.includes("application/json")) {
|
|
851
|
-
return (await response.json());
|
|
852
|
-
}
|
|
853
|
-
if (contentType.includes("text/")) {
|
|
854
|
-
return (await response.text());
|
|
855
|
-
}
|
|
856
|
-
return (await response.blob());
|
|
857
|
-
}
|
|
858
|
-
/**
|
|
859
|
-
* Extract headers from Headers object or Record
|
|
860
|
-
*/
|
|
861
|
-
extractHeaders(headers) {
|
|
862
|
-
if (!headers)
|
|
863
|
-
return undefined;
|
|
864
|
-
if (headers instanceof Headers) {
|
|
865
|
-
const result = {};
|
|
866
|
-
headers.forEach((value, key) => {
|
|
867
|
-
result[key] = value;
|
|
868
|
-
});
|
|
869
|
-
return result;
|
|
870
|
-
}
|
|
871
|
-
if (Array.isArray(headers)) {
|
|
872
|
-
const result = {};
|
|
873
|
-
headers.forEach(([key, value]) => {
|
|
874
|
-
result[key] = String(value);
|
|
875
|
-
});
|
|
876
|
-
return result;
|
|
877
|
-
}
|
|
878
|
-
// Convert Record<string, string | readonly string[]> to Record<string, string>
|
|
879
|
-
const result = {};
|
|
880
|
-
Object.entries(headers).forEach(([key, value]) => {
|
|
881
|
-
result[key] = Array.isArray(value) ? value.join(", ") : String(value);
|
|
882
|
-
});
|
|
883
|
-
return result;
|
|
884
|
-
}
|
|
885
293
|
/**
|
|
886
294
|
* Handle authentication error
|
|
887
295
|
*/
|
|
888
296
|
handleAuthError() {
|
|
889
|
-
if (isBrowser()) {
|
|
297
|
+
if ((0, data_client_utils_1.isBrowser)()) {
|
|
890
298
|
// Fire and forget - redirect doesn't need to complete before throwing error
|
|
891
299
|
this.redirectToLogin().catch((error) => {
|
|
892
300
|
console.error("Failed to redirect to login:", error);
|
|
@@ -968,6 +376,245 @@ class DataClient {
|
|
|
968
376
|
});
|
|
969
377
|
return this.request("DELETE", endpoint, finalOptions);
|
|
970
378
|
}
|
|
379
|
+
// ==================== AUTHORIZATION METHODS ====================
|
|
380
|
+
/**
|
|
381
|
+
* Get user permissions (uses token from localStorage if not provided)
|
|
382
|
+
* @param token - Optional user authentication token (auto-retrieved from localStorage if not provided)
|
|
383
|
+
* @returns Array of permission strings
|
|
384
|
+
*/
|
|
385
|
+
async getPermissions(token) {
|
|
386
|
+
if (!this.misoClient || !this.permissionService) {
|
|
387
|
+
return [];
|
|
388
|
+
}
|
|
389
|
+
const userToken = token || this.getToken();
|
|
390
|
+
if (!userToken) {
|
|
391
|
+
return [];
|
|
392
|
+
}
|
|
393
|
+
return this.permissionService.getPermissions(userToken);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Check if user has specific permission
|
|
397
|
+
* @param permission - Permission to check
|
|
398
|
+
* @param token - Optional user authentication token (auto-retrieved from localStorage if not provided)
|
|
399
|
+
* @returns True if user has the permission
|
|
400
|
+
*/
|
|
401
|
+
async hasPermission(permission, token) {
|
|
402
|
+
if (!this.misoClient || !this.permissionService) {
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
const userToken = token || this.getToken();
|
|
406
|
+
if (!userToken) {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
return this.permissionService.hasPermission(userToken, permission);
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Check if user has any of the specified permissions
|
|
413
|
+
* @param permissions - Permissions to check
|
|
414
|
+
* @param token - Optional user authentication token (auto-retrieved from localStorage if not provided)
|
|
415
|
+
* @returns True if user has any of the permissions
|
|
416
|
+
*/
|
|
417
|
+
async hasAnyPermission(permissions, token) {
|
|
418
|
+
if (!this.misoClient || !this.permissionService) {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
const userToken = token || this.getToken();
|
|
422
|
+
if (!userToken) {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
return this.permissionService.hasAnyPermission(userToken, permissions);
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Check if user has all of the specified permissions
|
|
429
|
+
* @param permissions - Permissions to check
|
|
430
|
+
* @param token - Optional user authentication token (auto-retrieved from localStorage if not provided)
|
|
431
|
+
* @returns True if user has all of the permissions
|
|
432
|
+
*/
|
|
433
|
+
async hasAllPermissions(permissions, token) {
|
|
434
|
+
if (!this.misoClient || !this.permissionService) {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
const userToken = token || this.getToken();
|
|
438
|
+
if (!userToken) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
return this.permissionService.hasAllPermissions(userToken, permissions);
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Force refresh permissions from controller (bypass cache)
|
|
445
|
+
* @param token - Optional user authentication token (auto-retrieved from localStorage if not provided)
|
|
446
|
+
* @returns Array of permission strings
|
|
447
|
+
*/
|
|
448
|
+
async refreshPermissions(token) {
|
|
449
|
+
if (!this.misoClient || !this.permissionService) {
|
|
450
|
+
return [];
|
|
451
|
+
}
|
|
452
|
+
const userToken = token || this.getToken();
|
|
453
|
+
if (!userToken) {
|
|
454
|
+
return [];
|
|
455
|
+
}
|
|
456
|
+
return this.permissionService.refreshPermissions(userToken);
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Clear cached permissions for a user
|
|
460
|
+
* @param token - Optional user authentication token (auto-retrieved from localStorage if not provided)
|
|
461
|
+
*/
|
|
462
|
+
async clearPermissionsCache(token) {
|
|
463
|
+
if (!this.misoClient || !this.permissionService) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const userToken = token || this.getToken();
|
|
467
|
+
if (!userToken) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
return this.permissionService.clearPermissionsCache(userToken);
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Get user roles (uses token from localStorage if not provided)
|
|
474
|
+
* @param token - Optional user authentication token (auto-retrieved from localStorage if not provided)
|
|
475
|
+
* @returns Array of role strings
|
|
476
|
+
*/
|
|
477
|
+
async getRoles(token) {
|
|
478
|
+
if (!this.misoClient || !this.roleService) {
|
|
479
|
+
return [];
|
|
480
|
+
}
|
|
481
|
+
const userToken = token || this.getToken();
|
|
482
|
+
if (!userToken) {
|
|
483
|
+
return [];
|
|
484
|
+
}
|
|
485
|
+
return this.roleService.getRoles(userToken);
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Check if user has specific role
|
|
489
|
+
* @param role - Role to check
|
|
490
|
+
* @param token - Optional user authentication token (auto-retrieved from localStorage if not provided)
|
|
491
|
+
* @returns True if user has the role
|
|
492
|
+
*/
|
|
493
|
+
async hasRole(role, token) {
|
|
494
|
+
if (!this.misoClient || !this.roleService) {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
const userToken = token || this.getToken();
|
|
498
|
+
if (!userToken) {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
return this.roleService.hasRole(userToken, role);
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Check if user has any of the specified roles
|
|
505
|
+
* @param roles - Roles to check
|
|
506
|
+
* @param token - Optional user authentication token (auto-retrieved from localStorage if not provided)
|
|
507
|
+
* @returns True if user has any of the roles
|
|
508
|
+
*/
|
|
509
|
+
async hasAnyRole(roles, token) {
|
|
510
|
+
if (!this.misoClient || !this.roleService) {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
const userToken = token || this.getToken();
|
|
514
|
+
if (!userToken) {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
return this.roleService.hasAnyRole(userToken, roles);
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Check if user has all of the specified roles
|
|
521
|
+
* @param roles - Roles to check
|
|
522
|
+
* @param token - Optional user authentication token (auto-retrieved from localStorage if not provided)
|
|
523
|
+
* @returns True if user has all of the roles
|
|
524
|
+
*/
|
|
525
|
+
async hasAllRoles(roles, token) {
|
|
526
|
+
if (!this.misoClient || !this.roleService) {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
const userToken = token || this.getToken();
|
|
530
|
+
if (!userToken) {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
return this.roleService.hasAllRoles(userToken, roles);
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Force refresh roles from controller (bypass cache)
|
|
537
|
+
* @param token - Optional user authentication token (auto-retrieved from localStorage if not provided)
|
|
538
|
+
* @returns Array of role strings
|
|
539
|
+
*/
|
|
540
|
+
async refreshRoles(token) {
|
|
541
|
+
if (!this.misoClient || !this.roleService) {
|
|
542
|
+
return [];
|
|
543
|
+
}
|
|
544
|
+
const userToken = token || this.getToken();
|
|
545
|
+
if (!userToken) {
|
|
546
|
+
return [];
|
|
547
|
+
}
|
|
548
|
+
return this.roleService.refreshRoles(userToken);
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Clear cached roles for a user
|
|
552
|
+
* @param token - Optional user authentication token (auto-retrieved from localStorage if not provided)
|
|
553
|
+
*/
|
|
554
|
+
async clearRolesCache(token) {
|
|
555
|
+
if (!this.misoClient || !this.roleService) {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const userToken = token || this.getToken();
|
|
559
|
+
if (!userToken) {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
return this.roleService.clearRolesCache(userToken);
|
|
563
|
+
}
|
|
564
|
+
// ==================== AUTHENTICATION METHODS ====================
|
|
565
|
+
/**
|
|
566
|
+
* Validate token (uses localStorage token if not provided)
|
|
567
|
+
* @param token - Optional user authentication token (auto-retrieved from localStorage if not provided)
|
|
568
|
+
* @returns True if token is valid
|
|
569
|
+
*/
|
|
570
|
+
async validateToken(token) {
|
|
571
|
+
if (!this.misoClient) {
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
const userToken = token || this.getToken();
|
|
575
|
+
if (!userToken) {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
return this.misoClient.validateToken(userToken);
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Get user info from token (uses localStorage token if not provided)
|
|
582
|
+
* @param token - Optional user authentication token (auto-retrieved from localStorage if not provided)
|
|
583
|
+
* @returns User info or null if not authenticated
|
|
584
|
+
*/
|
|
585
|
+
async getUser(token) {
|
|
586
|
+
if (!this.misoClient) {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
const userToken = token || this.getToken();
|
|
590
|
+
if (!userToken) {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
return this.misoClient.getUser(userToken);
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Get user info from API endpoint (uses localStorage token if not provided)
|
|
597
|
+
* @param token - Optional user authentication token (auto-retrieved from localStorage if not provided)
|
|
598
|
+
* @returns User info or null if not authenticated
|
|
599
|
+
*/
|
|
600
|
+
async getUserInfo(token) {
|
|
601
|
+
if (!this.misoClient) {
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
const userToken = token || this.getToken();
|
|
605
|
+
if (!userToken) {
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
return this.misoClient.getUserInfo(userToken);
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Check if authenticated (alias for validateToken)
|
|
612
|
+
* @param token - Optional user authentication token (auto-retrieved from localStorage if not provided)
|
|
613
|
+
* @returns True if authenticated
|
|
614
|
+
*/
|
|
615
|
+
async isAuthenticatedAsync(token) {
|
|
616
|
+
return this.validateToken(token);
|
|
617
|
+
}
|
|
971
618
|
/**
|
|
972
619
|
* Get environment token (browser-side)
|
|
973
620
|
* Checks localStorage cache first, then calls backend endpoint if needed
|
|
@@ -977,93 +624,7 @@ class DataClient {
|
|
|
977
624
|
* @throws Error if token fetch fails
|
|
978
625
|
*/
|
|
979
626
|
async getEnvironmentToken() {
|
|
980
|
-
|
|
981
|
-
throw new Error("getEnvironmentToken() is only available in browser environment");
|
|
982
|
-
}
|
|
983
|
-
const cacheKey = "miso:client-token";
|
|
984
|
-
const expiresAtKey = "miso:client-token-expires-at";
|
|
985
|
-
// Check cache first
|
|
986
|
-
const cachedToken = getLocalStorage(cacheKey);
|
|
987
|
-
const expiresAtStr = getLocalStorage(expiresAtKey);
|
|
988
|
-
if (cachedToken && expiresAtStr) {
|
|
989
|
-
const expiresAt = parseInt(expiresAtStr, 10);
|
|
990
|
-
const now = Date.now();
|
|
991
|
-
// If token is still valid, return cached token
|
|
992
|
-
if (expiresAt > now) {
|
|
993
|
-
return cachedToken;
|
|
994
|
-
}
|
|
995
|
-
// Token expired, remove from cache
|
|
996
|
-
removeLocalStorage(cacheKey);
|
|
997
|
-
removeLocalStorage(expiresAtKey);
|
|
998
|
-
}
|
|
999
|
-
// Cache miss or expired - fetch from backend
|
|
1000
|
-
const clientTokenUri = this.config.misoConfig?.clientTokenUri || "/api/v1/auth/client-token";
|
|
1001
|
-
// Build full URL
|
|
1002
|
-
const fullUrl = /^https?:\/\//i.test(clientTokenUri)
|
|
1003
|
-
? clientTokenUri
|
|
1004
|
-
: `${this.config.baseUrl}${clientTokenUri}`;
|
|
1005
|
-
try {
|
|
1006
|
-
// Make request to backend endpoint
|
|
1007
|
-
const response = await fetch(fullUrl, {
|
|
1008
|
-
method: "POST",
|
|
1009
|
-
headers: {
|
|
1010
|
-
"Content-Type": "application/json",
|
|
1011
|
-
},
|
|
1012
|
-
credentials: "include", // Include cookies for CORS
|
|
1013
|
-
});
|
|
1014
|
-
if (!response.ok) {
|
|
1015
|
-
const errorText = await response.text();
|
|
1016
|
-
throw new Error(`Failed to get environment token: ${response.status} ${response.statusText}. ${errorText}`);
|
|
1017
|
-
}
|
|
1018
|
-
const data = (await response.json());
|
|
1019
|
-
// Extract token from response (support both nested and flat formats)
|
|
1020
|
-
const token = data.data?.token || data.token || data.accessToken || data.access_token;
|
|
1021
|
-
if (!token || typeof token !== "string") {
|
|
1022
|
-
throw new Error("Invalid response format: token not found in response");
|
|
1023
|
-
}
|
|
1024
|
-
// Calculate expiration time (default to 1 hour if not provided)
|
|
1025
|
-
const expiresIn = data.data?.expiresIn || data.expiresIn || data.expires_in || 3600;
|
|
1026
|
-
const expiresAt = Date.now() + expiresIn * 1000;
|
|
1027
|
-
// Cache token
|
|
1028
|
-
setLocalStorage(cacheKey, token);
|
|
1029
|
-
setLocalStorage(expiresAtKey, expiresAt.toString());
|
|
1030
|
-
// Log audit event if misoClient available
|
|
1031
|
-
if (this.misoClient && !this.shouldSkipAudit(clientTokenUri)) {
|
|
1032
|
-
try {
|
|
1033
|
-
await this.misoClient.log.audit("client.token.request.success", clientTokenUri, {
|
|
1034
|
-
method: "POST",
|
|
1035
|
-
url: clientTokenUri,
|
|
1036
|
-
statusCode: response.status,
|
|
1037
|
-
cached: false,
|
|
1038
|
-
}, {});
|
|
1039
|
-
}
|
|
1040
|
-
catch (auditError) {
|
|
1041
|
-
// Silently fail audit logging to avoid breaking requests
|
|
1042
|
-
console.warn("Failed to log audit event:", auditError);
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
return token;
|
|
1046
|
-
}
|
|
1047
|
-
catch (error) {
|
|
1048
|
-
// Log audit event for error if misoClient available
|
|
1049
|
-
if (this.misoClient && !this.shouldSkipAudit(clientTokenUri)) {
|
|
1050
|
-
try {
|
|
1051
|
-
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1052
|
-
await this.misoClient.log.audit("client.token.request.failed", clientTokenUri, {
|
|
1053
|
-
method: "POST",
|
|
1054
|
-
url: clientTokenUri,
|
|
1055
|
-
statusCode: 0,
|
|
1056
|
-
error: errorMessage,
|
|
1057
|
-
cached: false,
|
|
1058
|
-
}, {});
|
|
1059
|
-
}
|
|
1060
|
-
catch (auditError) {
|
|
1061
|
-
// Silently fail audit logging to avoid breaking requests
|
|
1062
|
-
console.warn("Failed to log audit event:", auditError);
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
throw error;
|
|
1066
|
-
}
|
|
627
|
+
return (0, data_client_auth_1.getEnvironmentToken)(this.config, this.misoClient);
|
|
1067
628
|
}
|
|
1068
629
|
/**
|
|
1069
630
|
* Get client token information (browser-side)
|
|
@@ -1072,21 +633,7 @@ class DataClient {
|
|
|
1072
633
|
* @returns Client token info or null if token not available
|
|
1073
634
|
*/
|
|
1074
635
|
getClientTokenInfo() {
|
|
1075
|
-
|
|
1076
|
-
return null;
|
|
1077
|
-
}
|
|
1078
|
-
// Try to get token from cache first
|
|
1079
|
-
const cachedToken = getLocalStorage("miso:client-token");
|
|
1080
|
-
if (cachedToken) {
|
|
1081
|
-
return (0, token_utils_1.extractClientTokenInfo)(cachedToken);
|
|
1082
|
-
}
|
|
1083
|
-
// Try to get token from config (if provided)
|
|
1084
|
-
const configToken = this.config.misoConfig?.clientToken;
|
|
1085
|
-
if (configToken) {
|
|
1086
|
-
return (0, token_utils_1.extractClientTokenInfo)(configToken);
|
|
1087
|
-
}
|
|
1088
|
-
// No token available
|
|
1089
|
-
return null;
|
|
636
|
+
return (0, data_client_auth_1.getClientTokenInfo)(this.config.misoConfig);
|
|
1090
637
|
}
|
|
1091
638
|
}
|
|
1092
639
|
exports.DataClient = DataClient;
|