@acorex-platform/framework-client 0.0.59 → 0.0.60

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 (38) hide show
  1. package/fesm2022/acorex-platform-framework-client-auth.mjs +397 -93
  2. package/fesm2022/acorex-platform-framework-client-auth.mjs.map +1 -1
  3. package/fesm2022/acorex-platform-framework-client-common.mjs +33 -31
  4. package/fesm2022/acorex-platform-framework-client-common.mjs.map +1 -1
  5. package/fesm2022/acorex-platform-framework-client-layout-components.mjs +12 -4
  6. package/fesm2022/acorex-platform-framework-client-layout-components.mjs.map +1 -1
  7. package/fesm2022/{acorex-platform-framework-client-layout-entity-acorex-platform-framework-client-layout-entity-BBuYugjl.mjs → acorex-platform-framework-client-layout-entity-acorex-platform-framework-client-layout-entity-D0jDsA6M.mjs} +80 -13
  8. package/fesm2022/acorex-platform-framework-client-layout-entity-acorex-platform-framework-client-layout-entity-D0jDsA6M.mjs.map +1 -0
  9. package/fesm2022/{acorex-platform-framework-client-layout-entity-attachments-page.component-DZDf_nVq.mjs → acorex-platform-framework-client-layout-entity-attachments-page.component-Bdvd1iYt.mjs} +2 -2
  10. package/fesm2022/{acorex-platform-framework-client-layout-entity-attachments-page.component-DZDf_nVq.mjs.map → acorex-platform-framework-client-layout-entity-attachments-page.component-Bdvd1iYt.mjs.map} +1 -1
  11. package/fesm2022/{acorex-platform-framework-client-layout-entity-bulk-entity.command-CFFv3jDs.mjs → acorex-platform-framework-client-layout-entity-bulk-entity.command-CsTP8YHv.mjs} +2 -2
  12. package/fesm2022/{acorex-platform-framework-client-layout-entity-bulk-entity.command-CFFv3jDs.mjs.map → acorex-platform-framework-client-layout-entity-bulk-entity.command-CsTP8YHv.mjs.map} +1 -1
  13. package/fesm2022/{acorex-platform-framework-client-layout-entity-gallery-page.component-BDNPJg_6.mjs → acorex-platform-framework-client-layout-entity-gallery-page.component-UvMk0GbU.mjs} +2 -2
  14. package/fesm2022/{acorex-platform-framework-client-layout-entity-gallery-page.component-BDNPJg_6.mjs.map → acorex-platform-framework-client-layout-entity-gallery-page.component-UvMk0GbU.mjs.map} +1 -1
  15. package/fesm2022/acorex-platform-framework-client-layout-entity.mjs +1 -1
  16. package/fesm2022/acorex-platform-framework-client-layout-widget-core.mjs +165 -28
  17. package/fesm2022/acorex-platform-framework-client-layout-widget-core.mjs.map +1 -1
  18. package/fesm2022/{acorex-platform-framework-client-layout-widgets-acorex-platform-framework-client-layout-widgets-Rv0K9r_e.mjs → acorex-platform-framework-client-layout-widgets-acorex-platform-framework-client-layout-widgets-KrDsAa3H.mjs} +555 -121
  19. package/fesm2022/acorex-platform-framework-client-layout-widgets-acorex-platform-framework-client-layout-widgets-KrDsAa3H.mjs.map +1 -0
  20. package/fesm2022/{acorex-platform-framework-client-layout-widgets-repeater-widget-column.component-Dzqn3rGa.mjs → acorex-platform-framework-client-layout-widgets-repeater-widget-column.component-BDlo6nTx.mjs} +2 -2
  21. package/fesm2022/{acorex-platform-framework-client-layout-widgets-repeater-widget-column.component-Dzqn3rGa.mjs.map → acorex-platform-framework-client-layout-widgets-repeater-widget-column.component-BDlo6nTx.mjs.map} +1 -1
  22. package/fesm2022/{acorex-platform-framework-client-layout-widgets-text-block-widget-designer.component-C97rrrmu.mjs → acorex-platform-framework-client-layout-widgets-text-block-widget-designer.component-By3wntCB.mjs} +2 -2
  23. package/fesm2022/{acorex-platform-framework-client-layout-widgets-text-block-widget-designer.component-C97rrrmu.mjs.map → acorex-platform-framework-client-layout-widgets-text-block-widget-designer.component-By3wntCB.mjs.map} +1 -1
  24. package/fesm2022/acorex-platform-framework-client-layout-widgets.mjs +1 -1
  25. package/fesm2022/acorex-platform-framework-client-themes-default.mjs +4 -4
  26. package/fesm2022/acorex-platform-framework-client-themes-default.mjs.map +1 -1
  27. package/layout/builder/README.md +10 -0
  28. package/layout/widget-core/README.md +47 -3
  29. package/layout/widgets/README.md +8 -2
  30. package/package.json +2 -2
  31. package/types/acorex-platform-framework-client-auth.d.ts +60 -1
  32. package/types/acorex-platform-framework-client-common.d.ts +3 -2
  33. package/types/acorex-platform-framework-client-layout-components.d.ts +3 -1
  34. package/types/acorex-platform-framework-client-layout-entity.d.ts +8 -1
  35. package/types/acorex-platform-framework-client-layout-widget-core.d.ts +39 -0
  36. package/types/acorex-platform-framework-client-layout-widgets.d.ts +25 -5
  37. package/fesm2022/acorex-platform-framework-client-layout-entity-acorex-platform-framework-client-layout-entity-BBuYugjl.mjs.map +0 -1
  38. package/fesm2022/acorex-platform-framework-client-layout-widgets-acorex-platform-framework-client-layout-widgets-Rv0K9r_e.mjs.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { InjectionToken, Injector, Injectable, signal, inject, Input, Directive, Optional, Inject, NgModule, input, output, ChangeDetectionStrategy, Component } from '@angular/core';
3
- import { map, BehaviorSubject, shareReplay, defaultIfEmpty, switchMap, filter, distinctUntilChanged, of, from, combineLatest, first } from 'rxjs';
3
+ import { map, BehaviorSubject, shareReplay, defaultIfEmpty, switchMap, filter, distinctUntilChanged, combineLatest, of, from, first } from 'rxjs';
4
4
  import { AXPDebugService, AXPBroadcastEventService, AXP_SESSION_SERVICE, AXP_EXPRESSION_EVALUATOR_SCOPE_PROVIDER, provideAppStartUpTask, AXP_APP_STARTUP_PRIORITY } from '@acorex-platform/framework-client/core';
5
5
  import { Router } from '@angular/router';
6
6
  import { isEmpty } from 'lodash-es';
@@ -445,82 +445,136 @@ function withMemoryRefreshLock(run) {
445
445
  }
446
446
  //#endregion
447
447
 
448
- //#region ---- Max active device sessions (client) ----
449
- /** Stable API / ForbiddenException code from AuthService. */
450
- const MAX_ACTIVE_DEVICE_SESSIONS_CODE = 'MAX_ACTIVE_DEVICE_SESSIONS';
451
- const PENDING_TENANT_STORAGE_KEY = 'axp.auth.sessionLimit.pendingTenant';
452
- function textContainsMaxSessionsCode(value) {
453
- if (typeof value === 'string') {
454
- return value === MAX_ACTIVE_DEVICE_SESSIONS_CODE || value.includes(MAX_ACTIVE_DEVICE_SESSIONS_CODE);
455
- }
456
- if (Array.isArray(value)) {
457
- return value.some((item) => textContainsMaxSessionsCode(item));
458
- }
459
- return false;
460
- }
448
+ //#region ----- Auth refresh failure classification -----
461
449
  /**
462
- * Detects tenant max concurrent device-session rejection from command / HTTP shapes.
463
- * Must recognize {@link Error} subclasses (e.g. AuthApiError) via `.message`.
450
+ * Stable server codes for definitive refresh/session death (prefer over message text).
451
+ * Keep in sync with `AUTH_REFRESH_ERROR_CODES` / `SESSION_MEMBERSHIP_DENIED` on the server.
464
452
  */
465
- function isMaxActiveDeviceSessionsError(error) {
453
+ const DEFINITIVE_AUTH_REFRESH_CODES = [
454
+ 'REFRESH_MISSING',
455
+ 'REFRESH_EXPIRED',
456
+ 'REFRESH_REVOKED',
457
+ 'REFRESH_REUSE',
458
+ 'SESSION_REVOKED',
459
+ 'USER_NOT_FOUND',
460
+ 'SESSION_MEMBERSHIP_DENIED',
461
+ ];
462
+ /**
463
+ * Backward-compatible message patterns when older servers omit `code`.
464
+ * Do NOT treat bare HTTP 401/403 / Redis unavailable / 5xx as definitive.
465
+ */
466
+ const DEFINITIVE_AUTH_REFRESH_PATTERNS = [
467
+ /refresh token reuse detected/i,
468
+ /refresh token has been revoked/i,
469
+ /refreshToken is required/i,
470
+ /invalid or expired refresh token/i,
471
+ /invalid refresh token/i,
472
+ /session has been revoked/i,
473
+ /user not found/i,
474
+ /SESSION_MEMBERSHIP_DENIED/i,
475
+ /you do not have access to this organization/i,
476
+ /you do not have access to this application/i,
477
+ ];
478
+ function collectErrorText(error) {
466
479
  if (error == null) {
467
- return false;
480
+ return '';
468
481
  }
469
482
  if (typeof error === 'string') {
470
- return textContainsMaxSessionsCode(error);
483
+ return error;
471
484
  }
472
- if (typeof error !== 'object') {
473
- return false;
485
+ if (error instanceof Error) {
486
+ return error.message;
474
487
  }
475
- // Error.message is reliable for AuthApiError / Error thrown from UpdateSession.
476
- if (error instanceof Error && textContainsMaxSessionsCode(error.message)) {
477
- return true;
488
+ if (typeof error !== 'object') {
489
+ return String(error);
478
490
  }
479
491
  const err = error;
480
- const candidates = [err['message'], err['error'], err['code']];
481
- const nestedError = err['error'];
482
- if (nestedError && typeof nestedError === 'object') {
483
- const nested = nestedError;
484
- candidates.push(nested['message'], nested['error'], nested['code']);
492
+ const parts = [];
493
+ for (const key of ['message', 'error', 'code', 'statusText']) {
494
+ const value = err[key];
495
+ if (typeof value === 'string') {
496
+ parts.push(value);
497
+ }
498
+ }
499
+ const nested = err['error'];
500
+ if (nested && typeof nested === 'object') {
501
+ parts.push(collectErrorText(nested));
485
502
  }
486
503
  const apiError = err['apiError'];
487
504
  if (apiError && typeof apiError === 'object') {
488
- const api = apiError;
489
- candidates.push(api['message'], api['error'], api['code']);
505
+ parts.push(collectErrorText(apiError));
490
506
  }
491
- return candidates.some((value) => textContainsMaxSessionsCode(value));
507
+ return parts.filter(Boolean).join(' ');
492
508
  }
493
- function storeSessionLimitPendingTenant(tenant) {
494
- try {
495
- sessionStorage.setItem(PENDING_TENANT_STORAGE_KEY, JSON.stringify(tenant));
509
+ function collectErrorCodes(error) {
510
+ if (error == null || typeof error !== 'object') {
511
+ return typeof error === 'string' && DEFINITIVE_AUTH_REFRESH_CODES.includes(error)
512
+ ? [error]
513
+ : [];
496
514
  }
497
- catch {
498
- // Ignore quota / private-mode failures; route state may still carry the tenant.
515
+ const err = error;
516
+ const codes = [];
517
+ for (const key of ['code', 'error']) {
518
+ const value = err[key];
519
+ if (typeof value === 'string' && value.trim()) {
520
+ codes.push(value.trim());
521
+ }
522
+ }
523
+ const apiError = err['apiError'];
524
+ if (apiError && typeof apiError === 'object') {
525
+ codes.push(...collectErrorCodes(apiError));
526
+ }
527
+ const nested = err['error'];
528
+ if (nested && typeof nested === 'object') {
529
+ codes.push(...collectErrorCodes(nested));
499
530
  }
531
+ const details = err['details'];
532
+ if (details && typeof details === 'object') {
533
+ codes.push(...collectErrorCodes(details));
534
+ }
535
+ return codes;
500
536
  }
501
- function readSessionLimitPendingTenant() {
502
- try {
503
- const raw = sessionStorage.getItem(PENDING_TENANT_STORAGE_KEY);
504
- if (!raw) {
505
- return null;
506
- }
507
- const parsed = JSON.parse(raw);
508
- if (!parsed?.id || typeof parsed.id !== 'string') {
509
- return null;
510
- }
511
- return parsed;
537
+ /**
538
+ * True only when refresh failed because the refresh cookie / session family is dead.
539
+ * Network / 5xx / Redis unavailable / empty transient results → false.
540
+ */
541
+ function isDefinitiveAuthRefreshFailure(error) {
542
+ const codes = collectErrorCodes(error);
543
+ if (codes.some((code) => DEFINITIVE_AUTH_REFRESH_CODES.includes(code))) {
544
+ return true;
512
545
  }
513
- catch {
514
- return null;
546
+ const text = collectErrorText(error);
547
+ if (!text.trim()) {
548
+ return false;
549
+ }
550
+ // Do not treat generic "Authentication failed" / Redis unavailable as definitive.
551
+ if (/session store \(redis\) is unavailable/i.test(text)) {
552
+ return false;
515
553
  }
554
+ return DEFINITIVE_AUTH_REFRESH_PATTERNS.some((pattern) => pattern.test(text));
516
555
  }
517
- function clearSessionLimitPendingTenant() {
518
- try {
519
- sessionStorage.removeItem(PENDING_TENANT_STORAGE_KEY);
556
+ //#endregion
557
+
558
+ //#region ----- Soft restore authorization status -----
559
+ /**
560
+ * After soft restore: keep identity without promoting to Authorized when
561
+ * access policy could not be loaded (transient API/refresh failure).
562
+ */
563
+ function resolveStatusAfterSoftRestore(options) {
564
+ if (!options.hasUser) {
565
+ return AXPSessionStatus.Unauthenticated;
520
566
  }
521
- catch {
522
- // ignore
567
+ if (options.hasCompleteWorkspace && options.policyLoadSucceeded) {
568
+ return AXPSessionStatus.Authorized;
523
569
  }
570
+ return AXPSessionStatus.Authenticated;
571
+ }
572
+ /**
573
+ * Soft-restore left the session Authenticated (policy unavailable).
574
+ * Retry access-policy load after connectivity / successful token refresh recovers.
575
+ */
576
+ function shouldRetryAccessPolicyAuthorization(status) {
577
+ return status === AXPSessionStatus.Authenticated;
524
578
  }
525
579
  //#endregion
526
580
 
@@ -539,7 +593,13 @@ class AXPSessionService {
539
593
  /** In-memory catalog caches — shared by guards and chooser UI (one List* per bind). */
540
594
  this.tenantsListCache = null;
541
595
  this.applicationsListCache = null;
596
+ /** Bumps when catalogs must re-fetch (e.g. after tenant-request approve). */
597
+ this.catalogEpoch$ = new BehaviorSubject(0);
542
598
  this.signoutInFlight = null;
599
+ /** Coalesces soft-restore → Authorized promotion after connectivity recovers. */
600
+ this.accessPolicyRetryInFlight = null;
601
+ this.softRestoreOnlineRecoveryAttached = false;
602
+ this.softRestoreOnlineRecoveryHandler = null;
543
603
  this.status = new BehaviorSubject(AXPSessionStatus.Unauthenticated);
544
604
  this.status$ = this.status.asObservable().pipe(shareReplay(1));
545
605
  // Add loading state to prevent premature redirects
@@ -568,7 +628,10 @@ class AXPSessionService {
568
628
  this.isAuthorized$ = this.status$.pipe(map((status) => status === AXPSessionStatus.Authorized), shareReplay(1));
569
629
  this.isReadyForAppScopedApi$ = this.isAuthorized$.pipe(map((authorized) => authorized && this.hasSessionCredentials()), distinctUntilChanged(), shareReplay(1));
570
630
  /** Tenant list for the signed-in user; skips the API when there is no cookie session. */
571
- this.tenants$ = this.isAuthenticated$.pipe(distinctUntilChanged(), switchMap((authenticated) => {
631
+ this.tenants$ = combineLatest([
632
+ this.isAuthenticated$,
633
+ this.catalogEpoch$,
634
+ ]).pipe(switchMap(([authenticated]) => {
572
635
  if (!authenticated || !this.hasSessionCredentials()) {
573
636
  return of([]);
574
637
  }
@@ -658,9 +721,12 @@ class AXPSessionService {
658
721
  const strategyName = sessionData.strategy;
659
722
  const strategy = strategyName ? this.authStrategyRegistry.get(strategyName) : null;
660
723
  if (!strategy) {
661
- this.clearSession();
662
- this.status.next(AXPSessionStatus.Unauthorized);
663
- traceAuthRestore('Strategy missing — cleared', () => ({ strategyName }));
724
+ // Keep AXP_SESSION — wiping here permanently logs the user out on a
725
+ // registry timing race. Soft-restore; interceptor can refresh later.
726
+ warnAuthLifecycle('Restore strategy missing — soft restore (kept session)', () => ({
727
+ strategyName,
728
+ }));
729
+ await this.softRestoreFromStoredSession(sessionData);
664
730
  return;
665
731
  }
666
732
  // Keep subjects warm for refresh context, but do NOT mark Authenticated yet —
@@ -674,23 +740,48 @@ class AXPSessionService {
674
740
  applicationId: sessionData.application?.id,
675
741
  strategy: strategyName,
676
742
  }));
677
- // Cross-tab exclusive refresh — avoids concurrent jti reuse / sid revoke.
678
- const refreshResult = await this.refreshCookieSessionExclusive(strategyName, strategy);
679
- if (!refreshResult?.succeed || !refreshResult.data?.user) {
680
- this.clearSession();
681
- this.status.next(AXPSessionStatus.Unauthorized);
682
- traceAuthRestore('Refresh failed — cleared');
683
- return;
743
+ try {
744
+ // Cross-tab exclusive refresh — avoids concurrent jti reuse / sid revoke.
745
+ const refreshResult = await this.refreshCookieSessionExclusive(strategyName, strategy);
746
+ if (refreshResult?.succeed && refreshResult.data?.user) {
747
+ await this.applySignInResult(strategyName, refreshResult);
748
+ traceAuthRestore('Restore complete', () => ({
749
+ ms: Date.now() - startedAt,
750
+ status: this.status.value,
751
+ tenantId: this.tenant?.id,
752
+ applicationId: this.application?.id,
753
+ }));
754
+ return;
755
+ }
756
+ // succeed:false without throw — usually lock/metadata race; do not kick out.
757
+ warnAuthLifecycle('Restore refresh returned empty — soft restore (kept session)');
758
+ await this.softRestoreFromStoredSession(sessionData);
759
+ }
760
+ catch (error) {
761
+ if (isDefinitiveAuthRefreshFailure(error)) {
762
+ this.clearSession();
763
+ this.status.next(AXPSessionStatus.Unauthorized);
764
+ traceAuthRestore('Restore refresh rejected — cleared', () => ({
765
+ error: error instanceof Error ? error.message : String(error),
766
+ }));
767
+ return;
768
+ }
769
+ // Network / 5xx / aborted XHR after HMR reload — keep local session.
770
+ warnAuthLifecycle('Restore refresh transient failure — soft restore (kept session)', () => ({
771
+ error: error instanceof Error ? error.message : String(error),
772
+ }));
773
+ await this.softRestoreFromStoredSession(sessionData);
684
774
  }
685
- await this.applySignInResult(strategyName, refreshResult);
686
- traceAuthRestore('Restore complete', () => ({
687
- ms: Date.now() - startedAt,
688
- status: this.status.value,
689
- tenantId: this.tenant?.id,
690
- applicationId: this.application?.id,
691
- }));
692
775
  }
693
776
  catch (error) {
777
+ const sessionData = this.getSessionData();
778
+ if (sessionData?.user && sessionData.cookieSession && !isDefinitiveAuthRefreshFailure(error)) {
779
+ warnAuthLifecycle('Restore error — soft restore (kept session)', () => ({
780
+ error: error instanceof Error ? error.message : String(error),
781
+ }));
782
+ await this.softRestoreFromStoredSession(sessionData);
783
+ return;
784
+ }
694
785
  this.clearSession();
695
786
  this.status.next(AXPSessionStatus.Unauthorized);
696
787
  traceAuthRestore('Restore error — cleared', () => ({
@@ -701,6 +792,122 @@ class AXPSessionService {
701
792
  this.isLoading.next(false);
702
793
  }
703
794
  }
795
+ /**
796
+ * Trust persisted AXP_SESSION after a non-definitive restore refresh failure.
797
+ * Keeps identity; promotes to Authorized only when access policy load succeeds.
798
+ */
799
+ async softRestoreFromStoredSession(sessionData) {
800
+ this.currentUserSubject.next(sessionData.user ?? null);
801
+ this.currentTenantSubject.next(sessionData.tenant ?? null);
802
+ this.currentApplicationSubject.next(sessionData.application ?? null);
803
+ const policyLoadSucceeded = await this.tryLoadAccessPolicyForSoftRestore();
804
+ const nextStatus = resolveStatusAfterSoftRestore({
805
+ hasUser: !!sessionData.user,
806
+ hasCompleteWorkspace: this.hasCompleteSessionContext(),
807
+ policyLoadSucceeded,
808
+ });
809
+ this.status.next(nextStatus);
810
+ traceAuthRestore('Soft restore status', () => ({
811
+ status: nextStatus,
812
+ policyLoadSucceeded,
813
+ tenantId: sessionData.tenant?.id ?? null,
814
+ applicationId: sessionData.application?.id ?? null,
815
+ }));
816
+ if (shouldRetryAccessPolicyAuthorization(nextStatus)) {
817
+ this.armSoftRestorePolicyRecovery();
818
+ }
819
+ }
820
+ /**
821
+ * Soft-restore policy load: success = both loaders completed without throw
822
+ * (empty permission/feature lists still count as a successful load).
823
+ */
824
+ async tryLoadAccessPolicyForSoftRestore() {
825
+ if (!this.hasCompleteSessionContext()) {
826
+ this.permissionsSubject.next([]);
827
+ this.featuresSubject.next([]);
828
+ return false;
829
+ }
830
+ let permissionsOk = false;
831
+ let featuresOk = false;
832
+ try {
833
+ const permissions = await this.permissionLoader.getList(this.getContext());
834
+ this.permissionsSubject.next(permissions ?? []);
835
+ permissionsOk = true;
836
+ }
837
+ catch (error) {
838
+ warnAuthLifecycle('Soft restore permissions load failed — stay Authenticated', () => ({
839
+ error: error instanceof Error ? error.message : String(error),
840
+ }));
841
+ this.permissionsSubject.next([]);
842
+ }
843
+ try {
844
+ const features = await this.featureLoader.getList(this.getContext());
845
+ this.featuresSubject.next(features ?? []);
846
+ featuresOk = true;
847
+ }
848
+ catch (error) {
849
+ warnAuthLifecycle('Soft restore features load failed — stay Authenticated', () => ({
850
+ error: error instanceof Error ? error.message : String(error),
851
+ }));
852
+ this.featuresSubject.next([]);
853
+ }
854
+ return permissionsOk && featuresOk;
855
+ }
856
+ /**
857
+ * Retries access-policy load after a soft restore that stayed Authenticated.
858
+ * On success with complete workspace, promotes to Authorized (no logout on failure).
859
+ * Coalesces concurrent callers (refresh / online / successful API).
860
+ */
861
+ async retryAccessPolicyAuthorization() {
862
+ if (!this.user) {
863
+ return false;
864
+ }
865
+ if (!shouldRetryAccessPolicyAuthorization(this.status.value)) {
866
+ return this.status.value === AXPSessionStatus.Authorized;
867
+ }
868
+ if (this.accessPolicyRetryInFlight) {
869
+ return this.accessPolicyRetryInFlight;
870
+ }
871
+ this.accessPolicyRetryInFlight = (async () => {
872
+ const policyLoadSucceeded = await this.tryLoadAccessPolicyForSoftRestore();
873
+ const nextStatus = resolveStatusAfterSoftRestore({
874
+ hasUser: true,
875
+ hasCompleteWorkspace: this.hasCompleteSessionContext(),
876
+ policyLoadSucceeded,
877
+ });
878
+ this.status.next(nextStatus);
879
+ if (nextStatus === AXPSessionStatus.Authorized) {
880
+ this.disarmSoftRestorePolicyRecovery();
881
+ }
882
+ return nextStatus === AXPSessionStatus.Authorized;
883
+ })().finally(() => {
884
+ this.accessPolicyRetryInFlight = null;
885
+ });
886
+ return this.accessPolicyRetryInFlight;
887
+ }
888
+ /**
889
+ * Bounded recovery after soft-restore Authenticated: browser `online` once per arm.
890
+ * Successful token refresh also retries (see {@link refreshToken}).
891
+ */
892
+ armSoftRestorePolicyRecovery() {
893
+ if (this.softRestoreOnlineRecoveryAttached || typeof window === 'undefined') {
894
+ return;
895
+ }
896
+ this.softRestoreOnlineRecoveryAttached = true;
897
+ this.softRestoreOnlineRecoveryHandler = () => {
898
+ void this.retryAccessPolicyAuthorization();
899
+ };
900
+ window.addEventListener('online', this.softRestoreOnlineRecoveryHandler);
901
+ }
902
+ disarmSoftRestorePolicyRecovery() {
903
+ if (this.softRestoreOnlineRecoveryAttached &&
904
+ this.softRestoreOnlineRecoveryHandler &&
905
+ typeof window !== 'undefined') {
906
+ window.removeEventListener('online', this.softRestoreOnlineRecoveryHandler);
907
+ }
908
+ this.softRestoreOnlineRecoveryAttached = false;
909
+ this.softRestoreOnlineRecoveryHandler = null;
910
+ }
704
911
  async signin(credentials) {
705
912
  this.isLoading.next(true);
706
913
  const startedAt = Date.now();
@@ -835,6 +1042,7 @@ class AXPSessionService {
835
1042
  const result = await withAuthRefreshLock(() => strategy.updateToken(params));
836
1043
  if (result?.succeed) {
837
1044
  if (params?.['code']) {
1045
+ // OAuth code exchange replaces identity metadata; wipe stale local fields first.
838
1046
  this.clearSession();
839
1047
  clearOAuthPendingStrategy();
840
1048
  }
@@ -863,30 +1071,28 @@ class AXPSessionService {
863
1071
  }));
864
1072
  }
865
1073
  else {
866
- this.status.next(AXPSessionStatus.Unauthenticated);
867
- throw new Error(`Invalid Username or Password`);
1074
+ throw new Error('UpdateSession failed');
868
1075
  }
869
1076
  }
870
1077
  catch (error) {
871
1078
  traceAuthLifecycleError('UpdateSession failed', () => ({
872
1079
  error: error instanceof Error ? error.message : String(error),
873
1080
  }));
874
- warnAuthWorkspace('UpdateSession failed — clearing session (logout UX)', () => ({
1081
+ warnAuthWorkspace('UpdateSession failed — identity session kept', () => ({
875
1082
  error: error instanceof Error ? error.message : String(error),
876
1083
  tenantId: params?.['tenant']?.id ?? params?.['tenantId'] ?? null,
877
1084
  applicationId: params?.['application']?.id ?? params?.['applicationId'] ?? null,
878
1085
  }));
879
- // Failed workspace bind must not leave a half-authenticated cookie session
880
- // (that causes splash restore loops after refresh).
881
- // Exception: max device sessions — keep identity session so the user can
882
- // terminate another device and retry tenant bind.
883
- if (isMaxActiveDeviceSessionsError(error)) {
884
- throw error;
885
- }
886
1086
  if (params?.['code']) {
887
1087
  clearOAuthPendingStrategy();
888
1088
  }
889
- this.clearSession();
1089
+ // Workspace bind failure (tenant/app/network/membership) must not log the user out.
1090
+ // Restore subjects from AXP_SESSION after the optimistic application clear above.
1091
+ this.syncContextSubjectsFromStorage();
1092
+ const stored = this.getSessionData();
1093
+ if (stored?.user && stored.cookieSession) {
1094
+ await this.syncSessionAuthorization();
1095
+ }
890
1096
  throw error;
891
1097
  }
892
1098
  finally {
@@ -954,21 +1160,31 @@ class AXPSessionService {
954
1160
  try {
955
1161
  const result = await this.refreshCookieSessionExclusive(strategyName, strategy);
956
1162
  if (!result.succeed || !result.data?.user) {
957
- warnAuthLifecycle('Token refresh failed — clearing session', () => ({
1163
+ // Empty result is usually a lock/metadata race — not proof the cookie is dead.
1164
+ warnAuthLifecycle('Token refresh returned empty — session kept', () => ({
958
1165
  strategy: strategyName,
959
1166
  }));
960
- this.clearSession();
961
- this.status.next(AXPSessionStatus.Expired);
962
1167
  throw new Error('Token refresh failed');
963
1168
  }
1169
+ // Soft restore may have left Authenticated; promote once API/refresh works again.
1170
+ if (shouldRetryAccessPolicyAuthorization(this.status.value)) {
1171
+ await this.retryAccessPolicyAuthorization();
1172
+ }
964
1173
  return true;
965
1174
  }
966
1175
  catch (error) {
967
- warnAuthLifecycle('Token refresh error — clearing session', () => ({
968
- error: error instanceof Error ? error.message : String(error),
969
- }));
970
- this.clearSession();
971
- this.status.next(AXPSessionStatus.Expired);
1176
+ if (isDefinitiveAuthRefreshFailure(error)) {
1177
+ warnAuthLifecycle('Token refresh definitive failure — clearing session', () => ({
1178
+ error: error instanceof Error ? error.message : String(error),
1179
+ }));
1180
+ this.clearSession();
1181
+ this.status.next(AXPSessionStatus.Expired);
1182
+ }
1183
+ else {
1184
+ warnAuthLifecycle('Token refresh transient error — session kept', () => ({
1185
+ error: error instanceof Error ? error.message : String(error),
1186
+ }));
1187
+ }
972
1188
  throw error;
973
1189
  }
974
1190
  }
@@ -1128,6 +1344,14 @@ class AXPSessionService {
1128
1344
  this.tenantsListCache = null;
1129
1345
  this.applicationsListCache = null;
1130
1346
  }
1347
+ /**
1348
+ * Drops ListTenants / ListApplications caches and re-emits {@link tenants$}.
1349
+ * Call after membership/tenant availability changes (e.g. approve tenant request).
1350
+ */
1351
+ invalidateCatalogCaches() {
1352
+ this.clearCatalogCaches();
1353
+ this.catalogEpoch$.next(this.catalogEpoch$.value + 1);
1354
+ }
1131
1355
  async signInComplete() {
1132
1356
  // Ensure we have the required data
1133
1357
  if (!this.user) {
@@ -1222,6 +1446,7 @@ class AXPSessionService {
1222
1446
  }
1223
1447
  }
1224
1448
  clearSession() {
1449
+ this.disarmSoftRestorePolicyRecovery();
1225
1450
  this.invalidateAccessPolicyCache();
1226
1451
  this.clearCatalogCaches();
1227
1452
  this.status.next(AXPSessionStatus.Unauthenticated);
@@ -2246,6 +2471,85 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
2246
2471
  args: [{ providedIn: 'root' }]
2247
2472
  }] });
2248
2473
 
2474
+ //#region ---- Max active device sessions (client) ----
2475
+ /** Stable API / ForbiddenException code from AuthService. */
2476
+ const MAX_ACTIVE_DEVICE_SESSIONS_CODE = 'MAX_ACTIVE_DEVICE_SESSIONS';
2477
+ const PENDING_TENANT_STORAGE_KEY = 'axp.auth.sessionLimit.pendingTenant';
2478
+ function textContainsMaxSessionsCode(value) {
2479
+ if (typeof value === 'string') {
2480
+ return value === MAX_ACTIVE_DEVICE_SESSIONS_CODE || value.includes(MAX_ACTIVE_DEVICE_SESSIONS_CODE);
2481
+ }
2482
+ if (Array.isArray(value)) {
2483
+ return value.some((item) => textContainsMaxSessionsCode(item));
2484
+ }
2485
+ return false;
2486
+ }
2487
+ /**
2488
+ * Detects tenant max concurrent device-session rejection from command / HTTP shapes.
2489
+ * Must recognize {@link Error} subclasses (e.g. AuthApiError) via `.message`.
2490
+ */
2491
+ function isMaxActiveDeviceSessionsError(error) {
2492
+ if (error == null) {
2493
+ return false;
2494
+ }
2495
+ if (typeof error === 'string') {
2496
+ return textContainsMaxSessionsCode(error);
2497
+ }
2498
+ if (typeof error !== 'object') {
2499
+ return false;
2500
+ }
2501
+ // Error.message is reliable for AuthApiError / Error thrown from UpdateSession.
2502
+ if (error instanceof Error && textContainsMaxSessionsCode(error.message)) {
2503
+ return true;
2504
+ }
2505
+ const err = error;
2506
+ const candidates = [err['message'], err['error'], err['code']];
2507
+ const nestedError = err['error'];
2508
+ if (nestedError && typeof nestedError === 'object') {
2509
+ const nested = nestedError;
2510
+ candidates.push(nested['message'], nested['error'], nested['code']);
2511
+ }
2512
+ const apiError = err['apiError'];
2513
+ if (apiError && typeof apiError === 'object') {
2514
+ const api = apiError;
2515
+ candidates.push(api['message'], api['error'], api['code']);
2516
+ }
2517
+ return candidates.some((value) => textContainsMaxSessionsCode(value));
2518
+ }
2519
+ function storeSessionLimitPendingTenant(tenant) {
2520
+ try {
2521
+ sessionStorage.setItem(PENDING_TENANT_STORAGE_KEY, JSON.stringify(tenant));
2522
+ }
2523
+ catch {
2524
+ // Ignore quota / private-mode failures; route state may still carry the tenant.
2525
+ }
2526
+ }
2527
+ function readSessionLimitPendingTenant() {
2528
+ try {
2529
+ const raw = sessionStorage.getItem(PENDING_TENANT_STORAGE_KEY);
2530
+ if (!raw) {
2531
+ return null;
2532
+ }
2533
+ const parsed = JSON.parse(raw);
2534
+ if (!parsed?.id || typeof parsed.id !== 'string') {
2535
+ return null;
2536
+ }
2537
+ return parsed;
2538
+ }
2539
+ catch {
2540
+ return null;
2541
+ }
2542
+ }
2543
+ function clearSessionLimitPendingTenant() {
2544
+ try {
2545
+ sessionStorage.removeItem(PENDING_TENANT_STORAGE_KEY);
2546
+ }
2547
+ catch {
2548
+ // ignore
2549
+ }
2550
+ }
2551
+ //#endregion
2552
+
2249
2553
  const AXP_USER_AVATAR_CACHE_PORT = new InjectionToken('AXP_USER_AVATAR_CACHE_PORT');
2250
2554
 
2251
2555
  //#region ---- User profile identity service ----
@@ -2495,5 +2799,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
2495
2799
  * Generated bundle index. Do not edit.
2496
2800
  */
2497
2801
 
2498
- export { AUTH_SESSION_TRACE_NS, AUTH_SESSION_TRACE_TITLE, AXPAuthGuard, AXPAuthModule, AXPAuthStrategy, AXPAuthStrategyRegistryService, AXPFeatureDirective, AXPFeatureEvaluatorScopeProvider, AXPFeatureGuard, AXPLoginChallengeComponentBase, AXPLoginChallengeProvider, AXPPermissionDefinitionBuilder, AXPPermissionDefinitionGroupBuilder, AXPPermissionDefinitionProviderContext, AXPPermissionDefinitionService, AXPPermissionDirective, AXPPermissionEvaluatorScopeProvider, AXPPermissionGuard, AXPPlatformAuthRuntimeService, AXPSessionContext, AXPSessionService, AXPSessionStatus, AXPSignInMethod, AXPSignInMethodRegistry, AXPUnauthenticatedError, AXPUnauthorizedError, AXPUserProfileIdentityService, AXP_ACCESS_POLICY_CACHE_PORT, AXP_APPLICATION_LOADER, AXP_DEFAULT_PLATFORM_OAUTH_CLIENT_ID, AXP_DEFAULT_PLATFORM_OAUTH_SCOPE, AXP_FEATURE_CHECKER, AXP_FEATURE_LOADER, AXP_LOGIN_CHALLENGE_PROVIDER, AXP_OAUTH_PKCE_VERIFIER_KEY, AXP_PERMISSION_CHECKER, AXP_PERMISSION_DEFINITION_PROVIDER, AXP_PERMISSION_LOADER, AXP_PERMISSION_MODULES_RESOLVER, AXP_PLATFORM_OAUTH_CSRF_COOKIE, AXP_TENANT_LOADER, AXP_USER_AVATAR_CACHE_PORT, JwtUtil, MAX_ACTIVE_DEVICE_SESSIONS_CODE, PkceUtil, TimeUtil, asAuthListProvidersResponse, clearOAuthPendingStrategy, clearSessionLimitPendingTenant, commandMessageTextForError, exchangePlatformOAuthAuthorizationCode, fetchPlatformOAuthDiscoveryDocument, findIdentityProvider, getOAuthPendingStrategy, hasLegacyOAuthPkceVerifier, hasOAuthPendingFlow, initPlatformOAuthAuthorization, isMaxActiveDeviceSessionsError, isOAuthRedirectInFlight, platformOAuthSessionUpdateParams, readPlatformOAuthCsrfToken, readSessionLimitPendingTenant, resolvePlatformOAuthAuthorizeEndpoint, resolvePlatformOAuthClientConfig, resolvePlatformOAuthIssuer, resolvePlatformOAuthRedirectUri, resolvePlatformOAuthTokenEndpoint, setOAuthPendingStrategy, shouldPlatformOAuthBindSession, storeSessionLimitPendingTenant, toPlatformOAuthSignInResult, traceAuthAccessPolicy, traceAuthCatalog, traceAuthGuard, traceAuthLifecycle, traceAuthLifecycleError, traceAuthRestore, traceAuthWorkspace, warnAuthAccessPolicy, warnAuthLifecycle, warnAuthWorkspace };
2802
+ export { AUTH_SESSION_TRACE_NS, AUTH_SESSION_TRACE_TITLE, AXPAuthGuard, AXPAuthModule, AXPAuthStrategy, AXPAuthStrategyRegistryService, AXPFeatureDirective, AXPFeatureEvaluatorScopeProvider, AXPFeatureGuard, AXPLoginChallengeComponentBase, AXPLoginChallengeProvider, AXPPermissionDefinitionBuilder, AXPPermissionDefinitionGroupBuilder, AXPPermissionDefinitionProviderContext, AXPPermissionDefinitionService, AXPPermissionDirective, AXPPermissionEvaluatorScopeProvider, AXPPermissionGuard, AXPPlatformAuthRuntimeService, AXPSessionContext, AXPSessionService, AXPSessionStatus, AXPSignInMethod, AXPSignInMethodRegistry, AXPUnauthenticatedError, AXPUnauthorizedError, AXPUserProfileIdentityService, AXP_ACCESS_POLICY_CACHE_PORT, AXP_APPLICATION_LOADER, AXP_DEFAULT_PLATFORM_OAUTH_CLIENT_ID, AXP_DEFAULT_PLATFORM_OAUTH_SCOPE, AXP_FEATURE_CHECKER, AXP_FEATURE_LOADER, AXP_LOGIN_CHALLENGE_PROVIDER, AXP_OAUTH_PKCE_VERIFIER_KEY, AXP_PERMISSION_CHECKER, AXP_PERMISSION_DEFINITION_PROVIDER, AXP_PERMISSION_LOADER, AXP_PERMISSION_MODULES_RESOLVER, AXP_PLATFORM_OAUTH_CSRF_COOKIE, AXP_TENANT_LOADER, AXP_USER_AVATAR_CACHE_PORT, DEFINITIVE_AUTH_REFRESH_CODES, JwtUtil, MAX_ACTIVE_DEVICE_SESSIONS_CODE, PkceUtil, TimeUtil, asAuthListProvidersResponse, clearOAuthPendingStrategy, clearSessionLimitPendingTenant, commandMessageTextForError, exchangePlatformOAuthAuthorizationCode, fetchPlatformOAuthDiscoveryDocument, findIdentityProvider, getOAuthPendingStrategy, hasLegacyOAuthPkceVerifier, hasOAuthPendingFlow, initPlatformOAuthAuthorization, isDefinitiveAuthRefreshFailure, isMaxActiveDeviceSessionsError, isOAuthRedirectInFlight, platformOAuthSessionUpdateParams, readPlatformOAuthCsrfToken, readSessionLimitPendingTenant, resolvePlatformOAuthAuthorizeEndpoint, resolvePlatformOAuthClientConfig, resolvePlatformOAuthIssuer, resolvePlatformOAuthRedirectUri, resolvePlatformOAuthTokenEndpoint, resolveStatusAfterSoftRestore, setOAuthPendingStrategy, shouldPlatformOAuthBindSession, shouldRetryAccessPolicyAuthorization, storeSessionLimitPendingTenant, toPlatformOAuthSignInResult, traceAuthAccessPolicy, traceAuthCatalog, traceAuthGuard, traceAuthLifecycle, traceAuthLifecycleError, traceAuthRestore, traceAuthWorkspace, warnAuthAccessPolicy, warnAuthLifecycle, warnAuthWorkspace };
2499
2803
  //# sourceMappingURL=acorex-platform-framework-client-auth.mjs.map