@masterteam/delegations 0.0.56 → 0.0.57

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.
@@ -10,7 +10,7 @@ import { ModalService } from '@masterteam/components/modal';
10
10
  import { ToastService } from '@masterteam/components/toast';
11
11
  import { Icon } from '@masterteam/icons';
12
12
  import { Popover } from 'primeng/popover';
13
- import { EMPTY, switchMap, from, finalize, shareReplay, filter, catchError, throwError } from 'rxjs';
13
+ import { EMPTY, switchMap, catchError, from, finalize, shareReplay, filter, throwError } from 'rxjs';
14
14
  import { HttpContextToken, HttpContext, HttpClient, HttpParams, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
15
15
  import { handleApiRequest, REQUEST_CONTEXT, UserSearchFieldConfig, TextareaFieldConfig, DateFieldConfig, RadioButtonFieldConfig, MultiSelectFieldConfig, ToggleFieldConfig, ValidatorConfig } from '@masterteam/components';
16
16
  import { Button } from '@masterteam/components/button';
@@ -53,13 +53,21 @@ class StartDelegationSession {
53
53
  }
54
54
  }
55
55
  /**
56
- * Re-mint the session the user was in before they reloaded the page. Dispatched
57
- * by the state itself from the persisted delegation id — never a token.
56
+ * Re-mint the session the user was in before they reloaded the page, from the
57
+ * persisted delegation id — never a token.
58
+ *
59
+ * Pass `delegation` when the row is already in hand; otherwise the state loads
60
+ * the candidates itself, so a host can await this action **before** the
61
+ * authenticated shell mounts and boot straight into delegated mode instead of
62
+ * loading everything twice.
63
+ *
64
+ * Completes quietly when there is nothing to resume or the delegation is no
65
+ * longer startable — the user simply stays themselves.
58
66
  */
59
67
  class ResumeDelegationSession {
60
68
  delegation;
61
69
  static type = '[DelegationSession] Resume';
62
- constructor(delegation) {
70
+ constructor(delegation = null) {
63
71
  this.delegation = delegation;
64
72
  }
65
73
  }
@@ -178,12 +186,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
178
186
  args: [{ providedIn: 'root' }]
179
187
  }] });
180
188
 
181
- var DelegationSessionActionKey;
182
- (function (DelegationSessionActionKey) {
183
- DelegationSessionActionKey["LoadCandidates"] = "loadCandidates";
184
- DelegationSessionActionKey["StartSession"] = "startSession";
185
- })(DelegationSessionActionKey || (DelegationSessionActionKey = {}));
186
-
187
189
  const STORAGE_KEY = 'mt.delegation.session-resume.v1';
188
190
  /**
189
191
  * Lets a delegated session survive a page reload **without persisting the
@@ -271,6 +273,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
271
273
  args: [{ providedIn: 'root' }]
272
274
  }] });
273
275
 
276
+ var DelegationSessionActionKey;
277
+ (function (DelegationSessionActionKey) {
278
+ DelegationSessionActionKey["LoadCandidates"] = "loadCandidates";
279
+ DelegationSessionActionKey["StartSession"] = "startSession";
280
+ })(DelegationSessionActionKey || (DelegationSessionActionKey = {}));
281
+
274
282
  /** Package-private: raw delegated credentials never enter NGXS or public models. */
275
283
  class DelegationTokenVault {
276
284
  value = signal(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
@@ -437,37 +445,60 @@ let DelegationSessionState = class DelegationSessionState {
437
445
  });
438
446
  }
439
447
  /**
440
- * Re-mints the delegated session the user was in before they reloaded.
448
+ * Fallback resume for hosts that do not await {@link ResumeDelegationSession}
449
+ * before mounting their authenticated shell: pick the session back up as soon
450
+ * as the candidates arrive.
441
451
  *
442
- * The token itself was never persisted (doc 05); only the delegation id was,
443
- * so this is a full `POST .../session` and the backend re-authorizes from
444
- * scratch. Attempted at most once per page load — a delegation that has since
445
- * expired or been revoked must not retry on every candidate refresh.
452
+ * Prefer the awaited path resuming after the shell has already loaded means
453
+ * every actor-scoped read runs twice, once as the real user and again under
454
+ * delegated authority.
446
455
  */
447
456
  tryResume(candidates) {
448
- if (this.resumeAttempted) {
457
+ if (this.resumeAttempted || this.resumeStore.pending() == null) {
449
458
  return;
450
459
  }
451
460
  const delegationId = this.resumeStore.pending();
452
- if (delegationId == null) {
453
- return;
454
- }
455
461
  const row = candidates.find((candidate) => candidate.delegationId === delegationId);
456
- this.resumeAttempted = true;
457
462
  if (!row) {
458
463
  // No longer startable (expired, cancelled, scope revoked) — forget it.
464
+ this.resumeAttempted = true;
459
465
  this.resumeStore.clear();
460
466
  return;
461
467
  }
462
- queueMicrotask(() => this.store.dispatch(new ResumeDelegationSession(row)).subscribe({
463
- error: () => this.resumeStore.clear(),
464
- }));
468
+ queueMicrotask(() => this.store.dispatch(new ResumeDelegationSession(row)).subscribe());
465
469
  }
466
470
  resume(ctx, { delegation }) {
467
- if (ctx.getState().active) {
471
+ const delegationId = this.resumeStore.pending();
472
+ if (ctx.getState().active || delegationId == null) {
468
473
  return EMPTY;
469
474
  }
470
- return this.startSession(ctx, delegation, null, 'ResumeSession');
475
+ // Claim the attempt up front so the nested candidate load below does not
476
+ // also fire `tryResume` for the same delegation.
477
+ this.resumeAttempted = true;
478
+ const known = delegation?.delegationId === delegationId
479
+ ? delegation
480
+ : ctx
481
+ .getState()
482
+ .candidates.find((candidate) => candidate.delegationId === delegationId);
483
+ const start$ = known
484
+ ? this.startSession(ctx, known, null, 'ResumeSession')
485
+ : this.store.dispatch(new LoadDelegationCandidates()).pipe(switchMap(() => {
486
+ const row = ctx
487
+ .getState()
488
+ .candidates.find((candidate) => candidate.delegationId === delegationId);
489
+ if (!row) {
490
+ // Expired, cancelled, or scope revoked while the tab was closed.
491
+ this.resumeStore.clear();
492
+ return EMPTY;
493
+ }
494
+ return this.startSession(ctx, row, null, 'ResumeSession');
495
+ }));
496
+ // A failed resume is not the caller's problem: drop the marker and let the
497
+ // user carry on as themselves rather than blocking the boot on an error.
498
+ return start$.pipe(catchError(() => {
499
+ this.resumeStore.clear();
500
+ return EMPTY;
501
+ }));
471
502
  }
472
503
  start(ctx, { delegation }) {
473
504
  const previous = ctx.getState().active;
@@ -611,6 +642,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
611
642
  class DelegationSessionFacade {
612
643
  store = inject(Store);
613
644
  promptReceipts = inject(DelegationPromptReceiptService);
645
+ resumeStore = inject(DelegationSessionResumeStore);
614
646
  endInFlight = null;
615
647
  // ---------------------------------------------------------------------------
616
648
  // Data slices
@@ -654,6 +686,23 @@ class DelegationSessionFacade {
654
686
  claimPromptCandidates(candidates) {
655
687
  return this.promptReceipts.claimUnseen(candidates);
656
688
  }
689
+ /**
690
+ * True when a previous session is waiting to be re-minted after a reload.
691
+ * Synchronous (a `localStorage` read), so a route guard can check it without
692
+ * paying for a round-trip on the overwhelmingly common no-delegation boot.
693
+ */
694
+ hasPendingResume() {
695
+ return this.resumeStore.pending() != null;
696
+ }
697
+ /**
698
+ * Re-mints the delegated session the user reloaded out of, and completes when
699
+ * delegated mode is in force (or when there is nothing to resume). Await this
700
+ * **before** mounting the authenticated shell so its reads run once, already
701
+ * delegated.
702
+ */
703
+ resumeSession() {
704
+ return this.store.dispatch(new ResumeDelegationSession());
705
+ }
657
706
  startSession(delegation) {
658
707
  return this.store.dispatch(new StartDelegationSession(delegation));
659
708
  }