@robosystems/core 0.11.1 → 0.11.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/auth-components/AuthProvider.js +166 -27
- package/auth-components/SessionWarningDialog.js +2 -2
- package/auth-components/SignInForm.d.ts +2 -1
- package/auth-components/SignInForm.js +8 -2
- package/auth-components/SignUpForm.js +4 -1
- package/auth-core/client.d.ts +8 -1
- package/auth-core/client.js +142 -100
- package/auth-core/sso.d.ts +4 -9
- package/auth-core/sso.js +18 -58
- package/auth-core/token-storage.d.ts +12 -0
- package/auth-core/token-storage.js +42 -16
- package/auth-core/types.d.ts +1 -0
- package/components/console/ConsoleContent.js +30 -4
- package/components/console/graphAwareConfig.js +27 -17
- package/components/repositories/ActiveSubscriptions.js +18 -6
- package/components/repositories/BrowseRepositories.js +17 -5
- package/components/search/SearchContent.js +30 -4
- package/contexts/graph-context.js +23 -13
- package/hooks/use-streaming-query.js +43 -28
- package/index.d.ts +5 -2
- package/index.js +6 -2
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/sdk-errors.d.ts +79 -0
- package/lib/sdk-errors.js +148 -0
- package/package.json +1 -1
- package/task-monitoring/hooks.js +23 -13
- package/task-monitoring/operationHooks.d.ts +21 -2
- package/task-monitoring/operationHooks.js +67 -17
- package/task-monitoring/operationMonitor.d.ts +10 -1
- package/task-monitoring/operationMonitor.js +109 -57
- package/task-monitoring/taskMonitor.d.ts +15 -1
- package/task-monitoring/taskMonitor.js +120 -73
- package/task-monitoring/types.d.ts +2 -1
- package/ui-components/layout/LandingFooter.js +2 -2
package/auth-core/client.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
var _a;
|
|
2
2
|
import { checkPasswordStrength, client, completeSsoAuth, createUserApiKey, deleteUserPasskey, forgotPassword, generateSsoToken, getAuthProviders, getCurrentAuthUser, getInvitationPreview, getMfaOptions, getMfaStatus, getPasskeyLoginOptions, getPasskeyReauthOptions, getPasskeyRegistrationOptions, getPasswordPolicy, listUserApiKeys, listUserPasskeys, loginUser, logoutUser, refreshAuthSession, regenerateMfaRecoveryCodes, registerUser, resendVerificationEmail, resetPassword, revokeUserApiKey, ssoTokenExchange, validateResetToken, verifyEmail, verifyMfa, verifyPasskeyLogin, verifyPasskeyRegistration, } from '@robosystems/client';
|
|
3
3
|
import * as sdkClientsModule from '@robosystems/client/clients';
|
|
4
|
-
import {
|
|
4
|
+
import { ApiError, isSessionRejection, isTransientError, unwrapSdk, } from '../lib/sdk-errors';
|
|
5
|
+
import { getRefreshToken, getToken, getValidToken } from './token-storage';
|
|
5
6
|
// Global config hook for the clients package's lazy default singleton
|
|
6
7
|
// (`import { clients } from '@robosystems/client/clients'`). We must
|
|
7
8
|
// set this BEFORE the singleton is first accessed — the singleton reads the
|
|
@@ -26,7 +27,45 @@ const ERROR_CACHE_TTL_MS = 5 * 1000; // 5 seconds - allow reasonable retry delay
|
|
|
26
27
|
* instead.
|
|
27
28
|
*/
|
|
28
29
|
const APP_SOURCE_HEADER = 'X-App-Source';
|
|
30
|
+
/**
|
|
31
|
+
* Marks the shared SDK client as already wrapped. Every auth client instance
|
|
32
|
+
* configures the *global* SDK client; wrapping its methods once per instance
|
|
33
|
+
* would stack a layer per construction (per render in a careless caller).
|
|
34
|
+
*/
|
|
35
|
+
const AUTH_WRAPPED = Symbol.for('@robosystems/core:auth-wrapped');
|
|
36
|
+
export { isSessionRejection, isTransientError };
|
|
29
37
|
const appSourceHeaders = (appSource) => appSource ? { [APP_SOURCE_HEADER]: appSource } : undefined;
|
|
38
|
+
/**
|
|
39
|
+
* For the email endpoints (forgot password, resend verification): the
|
|
40
|
+
* failures worth telling the user about. Null for a refusal the caller
|
|
41
|
+
* reports as "sent".
|
|
42
|
+
*
|
|
43
|
+
* Forgot password is unauthenticated and must not reveal whether the address
|
|
44
|
+
* has an account, so it surfaces only what says nothing about the account:
|
|
45
|
+
* rate limiting and a request that never arrived. A 5xx is not surfaced —
|
|
46
|
+
* only the existing-account path does work that can fail. Resend requires a
|
|
47
|
+
* session, so it can also report an outage and an expired session.
|
|
48
|
+
*/
|
|
49
|
+
const deliveryFailure = (error, { authenticated }) => {
|
|
50
|
+
if (!(error instanceof ApiError)) {
|
|
51
|
+
return authenticated ? 'Failed to send email. Please try again.' : null;
|
|
52
|
+
}
|
|
53
|
+
if (error.status === 429) {
|
|
54
|
+
return 'Too many requests. Please wait a few minutes and try again.';
|
|
55
|
+
}
|
|
56
|
+
if (error.isNetworkError) {
|
|
57
|
+
return 'Unable to reach the server. Check your connection and try again.';
|
|
58
|
+
}
|
|
59
|
+
if (!authenticated)
|
|
60
|
+
return null;
|
|
61
|
+
if (error.status === 401) {
|
|
62
|
+
return 'Your session has expired. Please sign in again.';
|
|
63
|
+
}
|
|
64
|
+
if (error.status >= 500) {
|
|
65
|
+
return 'The server ran into a problem. Please try again in a moment.';
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
};
|
|
30
69
|
// Custom error class for token expiry
|
|
31
70
|
export class TokenExpiredError extends Error {
|
|
32
71
|
constructor(message = 'Token expired') {
|
|
@@ -108,54 +147,32 @@ export class RoboSystemsAuthClient {
|
|
|
108
147
|
console.warn('[AuthClient] Failed to seed SDK extensions config:', err);
|
|
109
148
|
}
|
|
110
149
|
}
|
|
111
|
-
// Wrap the client methods to add JWT
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
150
|
+
// Wrap the client methods to add the JWT to every request — once per
|
|
151
|
+
// SDK client, however many auth clients are constructed.
|
|
152
|
+
const sdkClient = this.client;
|
|
153
|
+
if (sdkClient[AUTH_WRAPPED])
|
|
154
|
+
return;
|
|
155
|
+
sdkClient[AUTH_WRAPPED] = true;
|
|
156
|
+
// Errors are not handled here: the SDK resolves `{ error }` on HTTP
|
|
157
|
+
// failures rather than throwing, so each caller unwraps its own result
|
|
158
|
+
// (`unwrapSdk`) and decides what a given status means.
|
|
159
|
+
const withAuthHeader = (originalMethod) => {
|
|
118
160
|
if (!originalMethod)
|
|
119
161
|
return undefined;
|
|
120
162
|
return async (options) => {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (authToken) {
|
|
126
|
-
headers['Authorization'] = `Bearer ${authToken}`;
|
|
127
|
-
}
|
|
128
|
-
const enhancedOptions = Object.assign(Object.assign({}, options), { headers });
|
|
129
|
-
const result = await originalMethod(enhancedOptions);
|
|
130
|
-
return result;
|
|
131
|
-
}
|
|
132
|
-
catch (error) {
|
|
133
|
-
// Check for 401 Unauthorized errors
|
|
134
|
-
if ((error === null || error === void 0 ? void 0 : error.status) === 401 ||
|
|
135
|
-
((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) === 401 ||
|
|
136
|
-
((_b = error === null || error === void 0 ? void 0 : error.message) === null || _b === void 0 ? void 0 : _b.toLowerCase().includes('unauthorized')) ||
|
|
137
|
-
((_c = error === null || error === void 0 ? void 0 : error.message) === null || _c === void 0 ? void 0 : _c.toLowerCase().includes('401'))) {
|
|
138
|
-
// Clear cached auth data
|
|
139
|
-
this.clearAuthCache();
|
|
140
|
-
// Clear token from storage
|
|
141
|
-
const { clearToken } = await import('./token-storage');
|
|
142
|
-
clearToken();
|
|
143
|
-
// Throw custom error that can be caught by UI
|
|
144
|
-
throw new TokenExpiredError('Your session has expired. Please log in again.');
|
|
145
|
-
}
|
|
146
|
-
// Re-throw other errors
|
|
147
|
-
throw error;
|
|
163
|
+
const authToken = await getAuthToken();
|
|
164
|
+
const headers = Object.assign({}, options.headers);
|
|
165
|
+
if (authToken) {
|
|
166
|
+
headers['Authorization'] = `Bearer ${authToken}`;
|
|
148
167
|
}
|
|
168
|
+
return originalMethod(Object.assign(Object.assign({}, options), { headers }));
|
|
149
169
|
};
|
|
150
170
|
};
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
this.client.
|
|
155
|
-
this.client.
|
|
156
|
-
this.client.put = wrapWithAuthAndErrorHandling(originalPut, 'PUT');
|
|
157
|
-
this.client.patch = wrapWithAuthAndErrorHandling(originalPatch, 'PATCH');
|
|
158
|
-
this.client.delete = wrapWithAuthAndErrorHandling(originalDelete, 'DELETE');
|
|
171
|
+
this.client.get = withAuthHeader((_b = this.client.get) === null || _b === void 0 ? void 0 : _b.bind(this.client));
|
|
172
|
+
this.client.post = withAuthHeader((_c = this.client.post) === null || _c === void 0 ? void 0 : _c.bind(this.client));
|
|
173
|
+
this.client.put = withAuthHeader((_d = this.client.put) === null || _d === void 0 ? void 0 : _d.bind(this.client));
|
|
174
|
+
this.client.patch = withAuthHeader((_e = this.client.patch) === null || _e === void 0 ? void 0 : _e.bind(this.client));
|
|
175
|
+
this.client.delete = withAuthHeader((_f = this.client.delete) === null || _f === void 0 ? void 0 : _f.bind(this.client));
|
|
159
176
|
}
|
|
160
177
|
/**
|
|
161
178
|
* Push the latest auth token into the SDK extensions global config.
|
|
@@ -188,7 +205,7 @@ export class RoboSystemsAuthClient {
|
|
|
188
205
|
client: this.client,
|
|
189
206
|
body: { email, password },
|
|
190
207
|
});
|
|
191
|
-
return this.finalizeAuthResponse(response
|
|
208
|
+
return this.finalizeAuthResponse(unwrapSdk(response));
|
|
192
209
|
}
|
|
193
210
|
/**
|
|
194
211
|
* Validate an auth-shaped SDK response, store the session token when one
|
|
@@ -240,12 +257,7 @@ export class RoboSystemsAuthClient {
|
|
|
240
257
|
},
|
|
241
258
|
headers: appSourceHeaders(options === null || options === void 0 ? void 0 : options.appSource),
|
|
242
259
|
});
|
|
243
|
-
|
|
244
|
-
if (response.error) {
|
|
245
|
-
const errorData = response.error;
|
|
246
|
-
throw new Error((errorData === null || errorData === void 0 ? void 0 : errorData.detail) || 'Registration failed');
|
|
247
|
-
}
|
|
248
|
-
const sdkResponse = this.validateSDKAuthResponse(response.data);
|
|
260
|
+
const sdkResponse = this.validateSDKAuthResponse(unwrapSdk(response));
|
|
249
261
|
// Store JWT token with expiry information if present in response
|
|
250
262
|
if (sdkResponse.token) {
|
|
251
263
|
const { storeToken } = await import('./token-storage');
|
|
@@ -346,7 +358,7 @@ export class RoboSystemsAuthClient {
|
|
|
346
358
|
const response = await getCurrentAuthUser({
|
|
347
359
|
client: this.client,
|
|
348
360
|
});
|
|
349
|
-
const data = this.validateSDKCurrentUserResponse(response
|
|
361
|
+
const data = this.validateSDKCurrentUserResponse(unwrapSdk(response));
|
|
350
362
|
const user = data.user;
|
|
351
363
|
this.lastAuthCheck = { timestamp: Date.now(), result: user };
|
|
352
364
|
return user;
|
|
@@ -358,9 +370,25 @@ export class RoboSystemsAuthClient {
|
|
|
358
370
|
}
|
|
359
371
|
}
|
|
360
372
|
async refreshSession() {
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
373
|
+
const sentToken = getRefreshToken();
|
|
374
|
+
let responseData;
|
|
375
|
+
try {
|
|
376
|
+
responseData = await this.refreshSessionWithRetry();
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
// Tabs of one app share the stored token. When another tab renewed it
|
|
380
|
+
// first, this refusal is for the token it replaced: adopt the renewed
|
|
381
|
+
// one instead of ending the session.
|
|
382
|
+
const currentToken = getRefreshToken();
|
|
383
|
+
if (!isTransientError(error) &&
|
|
384
|
+
currentToken !== null &&
|
|
385
|
+
currentToken !== sentToken) {
|
|
386
|
+
this.clearAuthCache();
|
|
387
|
+
const user = await this.getCurrentUser();
|
|
388
|
+
return { user, success: true, token: currentToken };
|
|
389
|
+
}
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
364
392
|
const sdkResponse = this.validateSDKAuthResponse(responseData);
|
|
365
393
|
// Store new JWT token with expiry information if present in response
|
|
366
394
|
if (sdkResponse.token) {
|
|
@@ -385,21 +413,31 @@ export class RoboSystemsAuthClient {
|
|
|
385
413
|
};
|
|
386
414
|
}
|
|
387
415
|
/**
|
|
388
|
-
* Refresh session with exponential backoff retry logic
|
|
416
|
+
* Refresh session with exponential backoff retry logic.
|
|
417
|
+
*
|
|
418
|
+
* The request presents the stored token even when it has just expired:
|
|
419
|
+
* the API renews a recently-expired token within its grace window, and
|
|
420
|
+
* the data-call path deliberately stops sending it at expiry. A refusal
|
|
421
|
+
* (401/403) is final; only network errors, 429 and 5xx are retried.
|
|
389
422
|
*/
|
|
390
423
|
async refreshSessionWithRetry() {
|
|
391
424
|
let lastError = null;
|
|
392
425
|
for (let attempt = 0; attempt <= this.MAX_REFRESH_RETRIES; attempt++) {
|
|
393
426
|
try {
|
|
394
|
-
|
|
427
|
+
const refreshToken = getRefreshToken();
|
|
395
428
|
const response = await refreshAuthSession({
|
|
396
429
|
client: this.client,
|
|
430
|
+
headers: refreshToken
|
|
431
|
+
? { Authorization: `Bearer ${refreshToken}` }
|
|
432
|
+
: undefined,
|
|
397
433
|
});
|
|
398
|
-
|
|
399
|
-
return response;
|
|
434
|
+
return unwrapSdk(response);
|
|
400
435
|
}
|
|
401
436
|
catch (error) {
|
|
402
437
|
lastError = error;
|
|
438
|
+
if (!isTransientError(error)) {
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
403
441
|
// If this is the last attempt, throw the error
|
|
404
442
|
if (attempt === this.MAX_REFRESH_RETRIES) {
|
|
405
443
|
break;
|
|
@@ -425,7 +463,7 @@ export class RoboSystemsAuthClient {
|
|
|
425
463
|
description: request.permissions.join(', '), // Map permissions to description for now
|
|
426
464
|
},
|
|
427
465
|
});
|
|
428
|
-
const sdkResponse = response
|
|
466
|
+
const sdkResponse = unwrapSdk(response);
|
|
429
467
|
return {
|
|
430
468
|
id: sdkResponse.api_key.id,
|
|
431
469
|
name: sdkResponse.api_key.name,
|
|
@@ -442,7 +480,7 @@ export class RoboSystemsAuthClient {
|
|
|
442
480
|
const response = await listUserApiKeys({
|
|
443
481
|
client: this.client,
|
|
444
482
|
});
|
|
445
|
-
const sdkResponse = response
|
|
483
|
+
const sdkResponse = unwrapSdk(response);
|
|
446
484
|
return sdkResponse.api_keys.map((apiKey) => ({
|
|
447
485
|
id: apiKey.id,
|
|
448
486
|
name: apiKey.name,
|
|
@@ -454,24 +492,23 @@ export class RoboSystemsAuthClient {
|
|
|
454
492
|
}));
|
|
455
493
|
}
|
|
456
494
|
async revokeAPIKey(keyId) {
|
|
457
|
-
await revokeUserApiKey({
|
|
495
|
+
unwrapSdk(await revokeUserApiKey({
|
|
458
496
|
client: this.client,
|
|
459
497
|
path: { api_key_id: keyId },
|
|
460
|
-
});
|
|
498
|
+
}));
|
|
461
499
|
}
|
|
462
500
|
async generateSSOToken() {
|
|
463
501
|
const response = await generateSsoToken({
|
|
464
502
|
client: this.client,
|
|
465
503
|
});
|
|
466
|
-
|
|
467
|
-
return data;
|
|
504
|
+
return unwrapSdk(response);
|
|
468
505
|
}
|
|
469
506
|
async ssoExchange(token, targetApp) {
|
|
470
507
|
const response = await ssoTokenExchange({
|
|
471
508
|
client: this.client,
|
|
472
509
|
body: { token, target_app: targetApp },
|
|
473
510
|
});
|
|
474
|
-
const data = response
|
|
511
|
+
const data = unwrapSdk(response);
|
|
475
512
|
return {
|
|
476
513
|
session_id: data.session_id,
|
|
477
514
|
};
|
|
@@ -481,7 +518,7 @@ export class RoboSystemsAuthClient {
|
|
|
481
518
|
client: this.client,
|
|
482
519
|
body: { session_id: sessionId },
|
|
483
520
|
});
|
|
484
|
-
const sdkResponse = this.validateSDKAuthResponse(response
|
|
521
|
+
const sdkResponse = this.validateSDKAuthResponse(unwrapSdk(response));
|
|
485
522
|
// Store JWT token with expiry information if present in response
|
|
486
523
|
if (sdkResponse.token) {
|
|
487
524
|
const { storeToken } = await import('./token-storage');
|
|
@@ -542,7 +579,7 @@ export class RoboSystemsAuthClient {
|
|
|
542
579
|
client: this.client,
|
|
543
580
|
body: { mfa_token: mfaToken },
|
|
544
581
|
});
|
|
545
|
-
return response.
|
|
582
|
+
return unwrapSdk(response).options;
|
|
546
583
|
}
|
|
547
584
|
/** Second-factor step: complete with an assertion or a recovery code. */
|
|
548
585
|
async verifyMfa(mfaToken, input) {
|
|
@@ -554,12 +591,12 @@ export class RoboSystemsAuthClient {
|
|
|
554
591
|
recovery_code: input.recoveryCode,
|
|
555
592
|
},
|
|
556
593
|
});
|
|
557
|
-
return this.finalizeAuthResponse(response
|
|
594
|
+
return this.finalizeAuthResponse(unwrapSdk(response));
|
|
558
595
|
}
|
|
559
596
|
/** Passwordless login: usernameless assertion options. */
|
|
560
597
|
async getPasskeyLoginOptions() {
|
|
561
598
|
const response = await getPasskeyLoginOptions({ client: this.client });
|
|
562
|
-
return response.
|
|
599
|
+
return unwrapSdk(response).options;
|
|
563
600
|
}
|
|
564
601
|
/** Passwordless login: assertion → session. */
|
|
565
602
|
async completePasskeyLogin(assertion) {
|
|
@@ -567,7 +604,7 @@ export class RoboSystemsAuthClient {
|
|
|
567
604
|
client: this.client,
|
|
568
605
|
body: { assertion },
|
|
569
606
|
});
|
|
570
|
-
return this.finalizeAuthResponse(response
|
|
607
|
+
return this.finalizeAuthResponse(unwrapSdk(response));
|
|
571
608
|
}
|
|
572
609
|
/**
|
|
573
610
|
* Begin enrollment. The settings lane must carry a fresh re-auth proof —
|
|
@@ -585,7 +622,7 @@ export class RoboSystemsAuthClient {
|
|
|
585
622
|
assertion: proof === null || proof === void 0 ? void 0 : proof.assertion,
|
|
586
623
|
},
|
|
587
624
|
});
|
|
588
|
-
return response.
|
|
625
|
+
return unwrapSdk(response).options;
|
|
589
626
|
}
|
|
590
627
|
/**
|
|
591
628
|
* Finish enrollment. First passkey returns recovery codes (once); in the
|
|
@@ -602,7 +639,7 @@ export class RoboSystemsAuthClient {
|
|
|
602
639
|
mfa_token: options === null || options === void 0 ? void 0 : options.mfaToken,
|
|
603
640
|
},
|
|
604
641
|
});
|
|
605
|
-
const data = response
|
|
642
|
+
const data = unwrapSdk(response);
|
|
606
643
|
let auth;
|
|
607
644
|
if (data.auth) {
|
|
608
645
|
auth = await this.finalizeAuthResponse(data.auth);
|
|
@@ -617,20 +654,20 @@ export class RoboSystemsAuthClient {
|
|
|
617
654
|
async listPasskeys() {
|
|
618
655
|
var _a, _b;
|
|
619
656
|
const response = await listUserPasskeys({ client: this.client });
|
|
620
|
-
return ((_b = (_a = response
|
|
657
|
+
return ((_b = (_a = unwrapSdk(response)) === null || _a === void 0 ? void 0 : _a.passkeys) !== null && _b !== void 0 ? _b : []);
|
|
621
658
|
}
|
|
622
659
|
/** Fresh-assertion options for destructive lifecycle actions. */
|
|
623
660
|
async getPasskeyReauthOptions() {
|
|
624
661
|
const response = await getPasskeyReauthOptions({ client: this.client });
|
|
625
|
-
return response.
|
|
662
|
+
return unwrapSdk(response).options;
|
|
626
663
|
}
|
|
627
664
|
/** Remove a passkey; exactly one re-auth proof must be supplied. */
|
|
628
665
|
async deletePasskey(passkeyId, proof) {
|
|
629
|
-
await deleteUserPasskey({
|
|
666
|
+
unwrapSdk(await deleteUserPasskey({
|
|
630
667
|
client: this.client,
|
|
631
668
|
path: { passkey_id: passkeyId },
|
|
632
669
|
body: { password: proof.password, assertion: proof.assertion },
|
|
633
|
-
});
|
|
670
|
+
}));
|
|
634
671
|
}
|
|
635
672
|
/** MFA posture for the settings surface; null on any failure. */
|
|
636
673
|
async getMfaStatus() {
|
|
@@ -656,7 +693,7 @@ export class RoboSystemsAuthClient {
|
|
|
656
693
|
client: this.client,
|
|
657
694
|
body: { password: proof.password, assertion: proof.assertion },
|
|
658
695
|
});
|
|
659
|
-
return response.
|
|
696
|
+
return unwrapSdk(response).codes;
|
|
660
697
|
}
|
|
661
698
|
// Clear request deduplication cache (useful after login/logout)
|
|
662
699
|
clearAuthCache() {
|
|
@@ -669,23 +706,26 @@ export class RoboSystemsAuthClient {
|
|
|
669
706
|
*/
|
|
670
707
|
async forgotPassword(email, options) {
|
|
671
708
|
try {
|
|
672
|
-
await forgotPassword({
|
|
709
|
+
const response = await forgotPassword({
|
|
673
710
|
client: this.client,
|
|
674
711
|
body: { email },
|
|
675
712
|
headers: appSourceHeaders(options === null || options === void 0 ? void 0 : options.appSource),
|
|
676
713
|
});
|
|
677
|
-
|
|
678
|
-
success: true,
|
|
679
|
-
message: 'Password reset email sent if the account exists',
|
|
680
|
-
};
|
|
714
|
+
unwrapSdk(response);
|
|
681
715
|
}
|
|
682
716
|
catch (error) {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
}
|
|
717
|
+
const failure = deliveryFailure(error, { authenticated: false });
|
|
718
|
+
if (failure) {
|
|
719
|
+
console.error('Forgot password error:', error);
|
|
720
|
+
return { success: false, message: failure };
|
|
721
|
+
}
|
|
688
722
|
}
|
|
723
|
+
// Any other outcome reads as sent: whether the address has an account
|
|
724
|
+
// must not be observable from this form.
|
|
725
|
+
return {
|
|
726
|
+
success: true,
|
|
727
|
+
message: 'Password reset email sent if the account exists',
|
|
728
|
+
};
|
|
689
729
|
}
|
|
690
730
|
/**
|
|
691
731
|
* Reset password using token
|
|
@@ -737,7 +777,7 @@ export class RoboSystemsAuthClient {
|
|
|
737
777
|
client: this.client,
|
|
738
778
|
query: { token },
|
|
739
779
|
});
|
|
740
|
-
const data = response
|
|
780
|
+
const data = unwrapSdk(response);
|
|
741
781
|
return {
|
|
742
782
|
valid: (data === null || data === void 0 ? void 0 : data.valid) === true,
|
|
743
783
|
email: data === null || data === void 0 ? void 0 : data.email,
|
|
@@ -796,23 +836,25 @@ export class RoboSystemsAuthClient {
|
|
|
796
836
|
*/
|
|
797
837
|
async resendVerificationEmail(email, options) {
|
|
798
838
|
try {
|
|
799
|
-
await resendVerificationEmail({
|
|
839
|
+
const response = await resendVerificationEmail({
|
|
800
840
|
client: this.client,
|
|
801
841
|
body: { email },
|
|
802
842
|
headers: appSourceHeaders(options === null || options === void 0 ? void 0 : options.appSource),
|
|
803
843
|
});
|
|
804
|
-
|
|
805
|
-
success: true,
|
|
806
|
-
message: 'Verification email sent if the account exists',
|
|
807
|
-
};
|
|
844
|
+
unwrapSdk(response);
|
|
808
845
|
}
|
|
809
846
|
catch (error) {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
}
|
|
847
|
+
const failure = deliveryFailure(error, { authenticated: true });
|
|
848
|
+
if (failure) {
|
|
849
|
+
console.error('Resend verification email error:', error);
|
|
850
|
+
return { success: false, message: failure };
|
|
851
|
+
}
|
|
815
852
|
}
|
|
853
|
+
// Any other refusal (e.g. already verified) reads as sent.
|
|
854
|
+
return {
|
|
855
|
+
success: true,
|
|
856
|
+
message: 'Verification email sent if the account exists',
|
|
857
|
+
};
|
|
816
858
|
}
|
|
817
859
|
/**
|
|
818
860
|
* Get password policy requirements
|
|
@@ -822,7 +864,7 @@ export class RoboSystemsAuthClient {
|
|
|
822
864
|
const response = await getPasswordPolicy({
|
|
823
865
|
client: this.client,
|
|
824
866
|
});
|
|
825
|
-
const data = response
|
|
867
|
+
const data = unwrapSdk(response);
|
|
826
868
|
return {
|
|
827
869
|
minLength: (data === null || data === void 0 ? void 0 : data.min_length) || 8,
|
|
828
870
|
requireUppercase: (data === null || data === void 0 ? void 0 : data.require_uppercase) || false,
|
|
@@ -878,7 +920,7 @@ export class RoboSystemsAuthClient {
|
|
|
878
920
|
client: this.client,
|
|
879
921
|
body: { password, email },
|
|
880
922
|
});
|
|
881
|
-
const data = response
|
|
923
|
+
const data = unwrapSdk(response);
|
|
882
924
|
return {
|
|
883
925
|
score: (data === null || data === void 0 ? void 0 : data.score) || 0,
|
|
884
926
|
strength: (data === null || data === void 0 ? void 0 : data.strength) || 'very-weak',
|
package/auth-core/sso.d.ts
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import type { AppConfig, AuthUser } from './types';
|
|
2
2
|
export interface SSORedirectUrlOptions {
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Pass false when the URL is being handed to a *different* tab: the hints
|
|
8
|
-
* are unreadable there (separate tab, separate origin) and would linger in
|
|
9
|
-
* this tab, where a later same-tab SSO completion with no `returnUrl`
|
|
10
|
-
* param would pick up the stale path and redirect somewhere the user
|
|
11
|
-
* never asked to go.
|
|
4
|
+
* @deprecated No effect. The return path travels only in the handoff URL;
|
|
5
|
+
* a sessionStorage copy was never readable by the target app (every app is
|
|
6
|
+
* its own origin) and misrouted later handoffs into the *source* app.
|
|
12
7
|
*/
|
|
13
8
|
persistSessionHints?: boolean;
|
|
14
9
|
}
|
|
@@ -26,7 +21,7 @@ export declare class SSOManager {
|
|
|
26
21
|
/**
|
|
27
22
|
* Get SSO redirect URL using secure POST-based flow
|
|
28
23
|
*/
|
|
29
|
-
getSSORedirectUrl(targetApp: string, returnUrl?: string,
|
|
24
|
+
getSSORedirectUrl(targetApp: string, returnUrl?: string, _options?: SSORedirectUrlOptions): Promise<string>;
|
|
30
25
|
/**
|
|
31
26
|
* Handle SSO login using secure POST-based flow
|
|
32
27
|
*/
|
package/auth-core/sso.js
CHANGED
|
@@ -31,6 +31,18 @@ const sanitizeReturnUrl = (returnUrl) => {
|
|
|
31
31
|
return null;
|
|
32
32
|
return returnUrl;
|
|
33
33
|
};
|
|
34
|
+
/** Keys an older version wrote; cleared on every handoff completion. */
|
|
35
|
+
const LEGACY_SSO_HINT_KEYS = ['sso_target_app', 'sso_return_url'];
|
|
36
|
+
const clearLegacySsoHints = () => {
|
|
37
|
+
try {
|
|
38
|
+
for (const key of LEGACY_SSO_HINT_KEYS)
|
|
39
|
+
sessionStorage.removeItem(key);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
// Storage access error - continue silently
|
|
43
|
+
debugLog('sessionStorage cleanup failed', error);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
34
46
|
export class SSOManager {
|
|
35
47
|
constructor(apiUrl) {
|
|
36
48
|
this.authClient = new RoboSystemsAuthClient(apiUrl);
|
|
@@ -55,8 +67,7 @@ export class SSOManager {
|
|
|
55
67
|
/**
|
|
56
68
|
* Get SSO redirect URL using secure POST-based flow
|
|
57
69
|
*/
|
|
58
|
-
async getSSORedirectUrl(targetApp, returnUrl,
|
|
59
|
-
const { persistSessionHints = true } = options;
|
|
70
|
+
async getSSORedirectUrl(targetApp, returnUrl, _options = {}) {
|
|
60
71
|
const ssoData = await this.generateSSOToken();
|
|
61
72
|
const appConfig = APP_CONFIGS[targetApp];
|
|
62
73
|
if (!appConfig) {
|
|
@@ -71,20 +82,6 @@ export class SSOManager {
|
|
|
71
82
|
if (returnUrl) {
|
|
72
83
|
url.searchParams.set('returnUrl', returnUrl);
|
|
73
84
|
}
|
|
74
|
-
// Store only non-sensitive metadata in sessionStorage as backup for same-domain cases
|
|
75
|
-
// Note: We avoid storing the session_id in sessionStorage for security
|
|
76
|
-
if (persistSessionHints) {
|
|
77
|
-
try {
|
|
78
|
-
sessionStorage.setItem('sso_target_app', targetApp);
|
|
79
|
-
if (returnUrl) {
|
|
80
|
-
sessionStorage.setItem('sso_return_url', returnUrl);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
catch (error) {
|
|
84
|
-
// Storage error - continue without backup storage
|
|
85
|
-
debugLog('sessionStorage write failed during SSO URL generation', error);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
85
|
return url.toString();
|
|
89
86
|
}
|
|
90
87
|
/**
|
|
@@ -94,19 +91,11 @@ export class SSOManager {
|
|
|
94
91
|
// Check for session ID from URL parameters (cross-domain compatible)
|
|
95
92
|
const urlParams = new URLSearchParams(window.location.search);
|
|
96
93
|
const sessionId = urlParams.get('session_id');
|
|
94
|
+
// The return path comes only from the handoff URL. With none, the
|
|
95
|
+
// caller's own default landing applies.
|
|
97
96
|
const returnUrl = urlParams.get('returnUrl');
|
|
98
|
-
// Check sessionStorage for return URL fallback (same-domain case)
|
|
99
|
-
// Note: We don't store session_id in sessionStorage for security
|
|
100
|
-
let sessionStorageReturn = null;
|
|
101
|
-
try {
|
|
102
|
-
sessionStorageReturn = sessionStorage.getItem('sso_return_url');
|
|
103
|
-
}
|
|
104
|
-
catch (error) {
|
|
105
|
-
// Storage access error - continue without fallback
|
|
106
|
-
debugLog('sessionStorage read failed during SSO login', error);
|
|
107
|
-
}
|
|
108
97
|
const finalSessionId = sessionId; // Only use URL parameter for session ID
|
|
109
|
-
const finalReturnUrl = sanitizeReturnUrl(returnUrl
|
|
98
|
+
const finalReturnUrl = sanitizeReturnUrl(returnUrl);
|
|
110
99
|
if (!finalSessionId) {
|
|
111
100
|
return null;
|
|
112
101
|
}
|
|
@@ -121,15 +110,7 @@ export class SSOManager {
|
|
|
121
110
|
newUrl.searchParams.delete('returnUrl');
|
|
122
111
|
window.history.replaceState({}, '', newUrl.toString());
|
|
123
112
|
}
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
sessionStorage.removeItem('sso_target_app');
|
|
127
|
-
sessionStorage.removeItem('sso_return_url');
|
|
128
|
-
}
|
|
129
|
-
catch (error) {
|
|
130
|
-
// Storage access error - continue silently
|
|
131
|
-
debugLog('sessionStorage cleanup failed during SSO success', error);
|
|
132
|
-
}
|
|
113
|
+
clearLegacySsoHints();
|
|
133
114
|
// Handle return URL if provided
|
|
134
115
|
if (finalReturnUrl && finalReturnUrl !== window.location.pathname) {
|
|
135
116
|
// Delay navigation to allow authentication to complete
|
|
@@ -147,14 +128,7 @@ export class SSOManager {
|
|
|
147
128
|
newUrl.searchParams.delete('returnUrl');
|
|
148
129
|
window.history.replaceState({}, '', newUrl.toString());
|
|
149
130
|
}
|
|
150
|
-
|
|
151
|
-
sessionStorage.removeItem('sso_target_app');
|
|
152
|
-
sessionStorage.removeItem('sso_return_url');
|
|
153
|
-
}
|
|
154
|
-
catch (error) {
|
|
155
|
-
// Storage access error - continue silently
|
|
156
|
-
debugLog('sessionStorage cleanup failed during SSO error', error);
|
|
157
|
-
}
|
|
131
|
+
clearLegacySsoHints();
|
|
158
132
|
// Log errors for monitoring in production (sanitized)
|
|
159
133
|
if (process.env.NODE_ENV === 'development') {
|
|
160
134
|
console.error('SSO login failed:', this.sanitizeErrorForLogging(error));
|
|
@@ -249,20 +223,6 @@ export class SSOManager {
|
|
|
249
223
|
if (returnUrl) {
|
|
250
224
|
targetUrl.searchParams.set('returnUrl', returnUrl);
|
|
251
225
|
}
|
|
252
|
-
// Store only non-sensitive metadata in sessionStorage as backup for same-domain cases
|
|
253
|
-
// Note: We avoid storing the session_id in sessionStorage for security
|
|
254
|
-
if (typeof window !== 'undefined') {
|
|
255
|
-
try {
|
|
256
|
-
sessionStorage.setItem('sso_target_app', targetApp);
|
|
257
|
-
if (returnUrl) {
|
|
258
|
-
sessionStorage.setItem('sso_return_url', returnUrl);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
catch (error) {
|
|
262
|
-
// Storage error - continue without backup storage
|
|
263
|
-
debugLog('sessionStorage write failed during SSO navigation', error);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
226
|
// Navigate to target app
|
|
267
227
|
window.location.href = targetUrl.toString();
|
|
268
228
|
}
|
|
@@ -10,6 +10,13 @@ export interface TokenData {
|
|
|
10
10
|
refreshThreshold?: number;
|
|
11
11
|
}
|
|
12
12
|
export type TokenStatus = 'valid' | 'warning' | 'expired';
|
|
13
|
+
/**
|
|
14
|
+
* How long past its local expiry a token is kept. The API renews a
|
|
15
|
+
* recently-expired token at `/v1/auth/refresh` within this window, so the
|
|
16
|
+
* token must survive locally until then — for the refresh call only. Data
|
|
17
|
+
* calls stop sending it at expiry.
|
|
18
|
+
*/
|
|
19
|
+
export declare const TOKEN_REFRESH_GRACE_MS: number;
|
|
13
20
|
/**
|
|
14
21
|
* Store JWT token in localStorage
|
|
15
22
|
* @param token - The JWT token from login response
|
|
@@ -56,3 +63,8 @@ export declare function handleAuthResponse(response: any): void;
|
|
|
56
63
|
* @returns Valid token or null if expired
|
|
57
64
|
*/
|
|
58
65
|
export declare function getValidToken(): Promise<string | null>;
|
|
66
|
+
/**
|
|
67
|
+
* The token to present to `/v1/auth/refresh`: the stored token while it is
|
|
68
|
+
* unexpired or within the server's refresh grace after expiry, else null.
|
|
69
|
+
*/
|
|
70
|
+
export declare function getRefreshToken(): string | null;
|