@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.
Files changed (35) hide show
  1. package/auth-components/AuthProvider.js +166 -27
  2. package/auth-components/SessionWarningDialog.js +2 -2
  3. package/auth-components/SignInForm.d.ts +2 -1
  4. package/auth-components/SignInForm.js +8 -2
  5. package/auth-components/SignUpForm.js +4 -1
  6. package/auth-core/client.d.ts +8 -1
  7. package/auth-core/client.js +142 -100
  8. package/auth-core/sso.d.ts +4 -9
  9. package/auth-core/sso.js +18 -58
  10. package/auth-core/token-storage.d.ts +12 -0
  11. package/auth-core/token-storage.js +42 -16
  12. package/auth-core/types.d.ts +1 -0
  13. package/components/console/ConsoleContent.js +30 -4
  14. package/components/console/graphAwareConfig.js +27 -17
  15. package/components/repositories/ActiveSubscriptions.js +18 -6
  16. package/components/repositories/BrowseRepositories.js +17 -5
  17. package/components/search/SearchContent.js +30 -4
  18. package/contexts/graph-context.js +23 -13
  19. package/hooks/use-streaming-query.js +43 -28
  20. package/index.d.ts +5 -2
  21. package/index.js +6 -2
  22. package/lib/index.d.ts +1 -0
  23. package/lib/index.js +1 -0
  24. package/lib/sdk-errors.d.ts +79 -0
  25. package/lib/sdk-errors.js +148 -0
  26. package/package.json +1 -1
  27. package/task-monitoring/hooks.js +23 -13
  28. package/task-monitoring/operationHooks.d.ts +21 -2
  29. package/task-monitoring/operationHooks.js +67 -17
  30. package/task-monitoring/operationMonitor.d.ts +10 -1
  31. package/task-monitoring/operationMonitor.js +109 -57
  32. package/task-monitoring/taskMonitor.d.ts +15 -1
  33. package/task-monitoring/taskMonitor.js +120 -73
  34. package/task-monitoring/types.d.ts +2 -1
  35. package/ui-components/layout/LandingFooter.js +2 -2
@@ -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 { getToken, getValidToken } from './token-storage';
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 token to requests
112
- const originalPost = (_b = this.client.post) === null || _b === void 0 ? void 0 : _b.bind(this.client);
113
- const originalPut = (_c = this.client.put) === null || _c === void 0 ? void 0 : _c.bind(this.client);
114
- const originalPatch = (_d = this.client.patch) === null || _d === void 0 ? void 0 : _d.bind(this.client);
115
- const originalDelete = (_e = this.client.delete) === null || _e === void 0 ? void 0 : _e.bind(this.client);
116
- // Helper to add JWT token to headers and handle 401 responses
117
- const wrapWithAuthAndErrorHandling = (originalMethod, _methodName) => {
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
- var _a, _b, _c;
122
- try {
123
- const authToken = await getAuthToken();
124
- const headers = Object.assign({}, options.headers);
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
- // Also wrap GET requests to add JWT token
152
- const originalGet = (_f = this.client.get) === null || _f === void 0 ? void 0 : _f.bind(this.client);
153
- // Override all methods with auth headers and error handling
154
- this.client.get = wrapWithAuthAndErrorHandling(originalGet, 'GET');
155
- this.client.post = wrapWithAuthAndErrorHandling(originalPost, 'POST');
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.data);
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
- // Check for error responses (4xx/5xx)
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.data);
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 response = await this.refreshSessionWithRetry();
362
- // Check if response has data property or if it IS the data
363
- const responseData = (response === null || response === void 0 ? void 0 : response.data) !== undefined ? response.data : response;
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
- // Attempt to refresh the session
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
- // Success - return the response
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.data;
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.data;
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
- const data = response.data;
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.data;
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.data);
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.data.options;
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.data);
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.data.options;
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.data);
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.data.options;
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.data;
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.data) === null || _a === void 0 ? void 0 : _a.passkeys) !== null && _b !== void 0 ? _b : []);
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.data.options;
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.data.codes;
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
- return {
678
- success: true,
679
- message: 'Password reset email sent if the account exists',
680
- };
714
+ unwrapSdk(response);
681
715
  }
682
716
  catch (error) {
683
- console.error('Forgot password error:', error);
684
- return {
685
- success: false,
686
- message: 'Failed to send password reset email',
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.data;
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
- return {
805
- success: true,
806
- message: 'Verification email sent if the account exists',
807
- };
844
+ unwrapSdk(response);
808
845
  }
809
846
  catch (error) {
810
- console.error('Resend verification email error:', error);
811
- return {
812
- success: false,
813
- message: 'Failed to send verification email',
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.data;
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.data;
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',
@@ -1,14 +1,9 @@
1
1
  import type { AppConfig, AuthUser } from './types';
2
2
  export interface SSORedirectUrlOptions {
3
3
  /**
4
- * Mirror the target app / return URL into this tab's sessionStorage as a
5
- * same-domain fallback for `handleSSOLogin`. Default true.
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, options?: SSORedirectUrlOptions): Promise<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, options = {}) {
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 || sessionStorageReturn);
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
- // Clean up session storage
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
- try {
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;