@arsedizioni/ars-utils 22.5.14 → 22.5.16

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.
@@ -516,6 +516,19 @@ function isAuthEndpoint(url) {
516
516
  || url.includes('/passkeys/request-options')
517
517
  || url.includes('/passkeys/login');
518
518
  }
519
+ /**
520
+ * Returns whether the URL is the silent link probe, whose failures must never reach the user.
521
+ *
522
+ * `/session/status` runs unconditionally at every bootstrap, including for users who never linked
523
+ * Evolution at all. When the API is unreachable, reporting that failure would show "Evolution non è
524
+ * disponibile" to somebody who never asked Evolution for anything: the error is still rethrown for
525
+ * the caller to observe, but nothing is broadcast.
526
+ * @param url - The request URL.
527
+ * @returns True when the URL is the status probe.
528
+ */
529
+ function isStatusProbe(url) {
530
+ return url.includes('/session/status');
531
+ }
519
532
  /**
520
533
  * Resolves the user-facing message for a failed request.
521
534
  * @param error - The raw HTTP error object.
@@ -641,7 +654,11 @@ function evolutionAuthInterceptor(serviceUri, flags = EvolutionServiceFlags.None
641
654
  // at all. Everything that gets here is an Evolution request anyway: the others returned
642
655
  // before the pipe was built.
643
656
  const errorStatus = parseInt(error?.status ?? '0');
644
- if (errorStatus === 401 && !isAuthEndpoint(authenticatedRequest.url)) {
657
+ if (isStatusProbe(authenticatedRequest.url)) {
658
+ // The silent probe stays silent: its caller reads the outcome from the rethrown error,
659
+ // nobody else needs to hear about it.
660
+ }
661
+ else if (errorStatus === 401 && !isAuthEndpoint(authenticatedRequest.url)) {
645
662
  // Expired or invalidated session on an authenticated request. Notified once: several
646
663
  // in-flight requests failing together is how a single expiry becomes a wall of dialogs.
647
664
  if (!sessionExpiredNotified) {
@@ -741,6 +758,11 @@ class CoreService {
741
758
  }
742
759
  return this._loginInfo;
743
760
  }
761
+ /**
762
+ * Gets the signed-in account.
763
+ * @returns The current {@link EvolutionUserInfo}, or `undefined` when there is no session.
764
+ */
765
+ get user() { return this.loginInfo?.context; }
744
766
  /**
745
767
  * Sets the logged-in state.
746
768
  * @param value - The new logged-in value.
@@ -895,7 +917,7 @@ class LoginService {
895
917
  * Gets the signed-in account with its licence context.
896
918
  * @returns The current {@link EvolutionUserInfo}, or undefined when there is no session.
897
919
  */
898
- get user() { return this.core.loginInfo?.context; }
920
+ get user() { return this.core.user; }
899
921
  /** `true` when this browser has a session the API has accepted. */
900
922
  get loggedIn() { return this.core.loggedIn; }
901
923
  /** `true` while an authentication is in flight. */
@@ -925,8 +947,8 @@ class LoginService {
925
947
  clear() { this.core.clear(); }
926
948
  // ── Lifecycle ───────────────────────────────────────────────────────────────
927
949
  /**
928
- * Initialises the service with the back-end URI and optional feature flags, and revalidates a
929
- * session that survived a page refresh instead of assuming it is still there.
950
+ * Initialises the service with the back-end URI and optional feature flags, and asks the API — the
951
+ * only party that can read the HttpOnly cookie — whether this browser still holds a session.
930
952
  *
931
953
  * Must be called once during application bootstrap, before any other method. What it used to do
932
954
  * on a refresh was announce `LOGIN_COMPLETED` on the strength of a value in local storage; the
@@ -950,12 +972,13 @@ class LoginService {
950
972
  this.handleBroadcastMessage(message);
951
973
  });
952
974
  }
953
- // A stored context means this browser signed in at some point, never that the session is still
954
- // alive: the API is the only one that knows, and it is asked here.
955
- if (this.core.loggedIn()) {
956
- this.core.loggingIn.set(true);
957
- this.restoreSession();
958
- }
975
+ // The probe runs UNCONDITIONALLY, where it used to run only when a stored context existed. The
976
+ // stored context is just a hint -- "this browser signed in at some point" -- and the cookie can
977
+ // outlive it: local storage cleared while the month-long cookie stayed put made this library
978
+ // declare "no link" with a perfectly valid session in the browser. The API is the only one that
979
+ // knows, and it is asked every time.
980
+ this.core.loggingIn.set(true);
981
+ this.restoreSession();
959
982
  }
960
983
  /**
961
984
  * Dispatches an incoming broadcast message to the appropriate handler.
@@ -1046,6 +1069,18 @@ class LoginService {
1046
1069
  return r;
1047
1070
  }));
1048
1071
  }
1072
+ /**
1073
+ * Asks the API whether this browser holds a live session, without ever failing with a 401: the
1074
+ * route is anonymous and answers `connected: false` to a caller with no session. This is the one
1075
+ * honest way to know the link state -- the auth cookie is HttpOnly and belongs to the API's host,
1076
+ * so it cannot be read from here, and its presence would prove nothing anyway, since the session
1077
+ * also lives server-side and may have been revoked or evicted.
1078
+ * @returns An observable that emits the `ApiResult<EvolutionSessionStatus>`.
1079
+ */
1080
+ status() {
1081
+ return this.httpClient
1082
+ .get(this.core.serviceUri + '/session/status');
1083
+ }
1049
1084
  /**
1050
1085
  * Reads the current session from the server, which resolves it from the auth cookie's claims.
1051
1086
  * Used to restore the state after a page refresh instead of trusting what local storage holds.
@@ -1068,25 +1103,49 @@ class LoginService {
1068
1103
  }));
1069
1104
  }
1070
1105
  /**
1071
- * Asks `/session/me` whether the restored session is still alive, and announces the login when it
1072
- * is.
1106
+ * Asks `/session/status` whether this browser holds a live session, and hydrates the context
1107
+ * through {@link me} when it does.
1108
+ *
1109
+ * The probe is silent by construction: `/session/status` never answers 401, so a visitor who
1110
+ * never signed in -- or a cookie whose session has died -- comes back as `connected: false` with
1111
+ * no error broadcast and no dialog. Only then is the stale stored context cleared, so the
1112
+ * `loggedIn` signal tells the truth at every bootstrap.
1073
1113
  *
1074
- * Nothing is cleared on failure, and that is deliberate: a failure here is not proof of a dead
1075
- * session -- the API may be unreachable, the proxy may have answered 502, the device may be
1076
- * offline -- and clearing on any of those would sign the user out with a perfectly valid cookie.
1077
- * A genuine 401 is a different matter and the interceptor has already reported it.
1114
+ * Nothing is cleared on a transport failure, and that is deliberate: an unreachable API is not
1115
+ * proof of a dead session -- the proxy may have answered 502, the device may be offline -- and
1116
+ * clearing on any of those would sign the user out with a perfectly valid cookie.
1078
1117
  * @returns void
1079
1118
  */
1080
1119
  restoreSession() {
1081
- this.me()
1082
- .pipe(finalize(() => this.core.loggingIn.set(false)))
1120
+ // Whether the probe handed over to `/session/me`, which then owns lowering `loggingIn`.
1121
+ let restoring = false;
1122
+ this.status()
1123
+ .pipe(finalize(() => { if (!restoring) {
1124
+ this.core.loggingIn.set(false);
1125
+ } }))
1083
1126
  .subscribe({
1084
1127
  next: r => {
1085
- if (r?.success) {
1086
- this.broadcastService.sendMessage(EvolutionMessages.LOGIN_COMPLETED);
1128
+ if (r?.success && r.value?.connected) {
1129
+ restoring = true;
1130
+ this.me()
1131
+ .pipe(finalize(() => this.core.loggingIn.set(false)))
1132
+ .subscribe({
1133
+ next: m => {
1134
+ if (m?.success) {
1135
+ this.broadcastService.sendMessage(EvolutionMessages.LOGIN_COMPLETED);
1136
+ }
1137
+ },
1138
+ // Consumed so the rethrown error is not reported a second time as an unhandled one.
1139
+ error: () => { }
1140
+ });
1141
+ }
1142
+ else if (this.core.loggedIn()) {
1143
+ // The stored context outlived its session: drop it, or the next bootstrap would keep
1144
+ // claiming a session that is gone.
1145
+ this.core.clear();
1087
1146
  }
1088
1147
  },
1089
- // Consumed so the rethrown error is not reported a second time as an unhandled one.
1148
+ // Transport failure: keep the stored hint, the next bootstrap will ask again.
1090
1149
  error: () => { }
1091
1150
  });
1092
1151
  }
@@ -1322,7 +1381,7 @@ class AccountService {
1322
1381
  * @returns An observable that emits `ApiResult<EvolutionUserInfo>`.
1323
1382
  */
1324
1383
  refreshLicence() {
1325
- return this.httpClient.get(this.core.serviceUri + '/account/licenses/refresh/' + (this.core.loginInfo?.context?.userId ?? -1));
1384
+ return this.httpClient.get(this.core.serviceUri + '/account/licenses/refresh/' + (this.core.user?.userId ?? -1));
1326
1385
  }
1327
1386
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: AccountService, deps: [], target: i0.ɵɵFactoryTarget.Service }); }
1328
1387
  static { this.ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.3", ngImport: i0, type: AccountService }); }
@@ -1376,11 +1435,12 @@ class EvolutionService {
1376
1435
  * Gets the signed-in account with its licence context.
1377
1436
  * @returns The current {@link EvolutionUserInfo}, or undefined when there is no session.
1378
1437
  */
1379
- get user() { return this.core.loginInfo?.context; }
1438
+ get user() { return this.core.user; }
1380
1439
  /**
1381
1440
  * `true` when this browser has a session the API has accepted. It starts from the stored context
1382
- * and is confirmed -- or left to the interceptor's 401 handling -- by the `/session/me` call
1383
- * {@link initialize} makes.
1441
+ * and is settled -- raised or cleared -- by the silent `/session/status` probe {@link initialize}
1442
+ * makes, which is the one honest way to read a state that lives in an HttpOnly cookie plus a
1443
+ * server-side lease. This signal IS the link state: bind it, do not poll it.
1384
1444
  */
1385
1445
  get loggedIn() { return this.core.loggedIn; }
1386
1446
  /** `true` while an authentication is in flight. */