@arsedizioni/ars-utils 22.5.15 → 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) {
@@ -930,8 +947,8 @@ class LoginService {
930
947
  clear() { this.core.clear(); }
931
948
  // ── Lifecycle ───────────────────────────────────────────────────────────────
932
949
  /**
933
- * Initialises the service with the back-end URI and optional feature flags, and revalidates a
934
- * 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.
935
952
  *
936
953
  * Must be called once during application bootstrap, before any other method. What it used to do
937
954
  * on a refresh was announce `LOGIN_COMPLETED` on the strength of a value in local storage; the
@@ -955,12 +972,13 @@ class LoginService {
955
972
  this.handleBroadcastMessage(message);
956
973
  });
957
974
  }
958
- // A stored context means this browser signed in at some point, never that the session is still
959
- // alive: the API is the only one that knows, and it is asked here.
960
- if (this.core.loggedIn()) {
961
- this.core.loggingIn.set(true);
962
- this.restoreSession();
963
- }
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();
964
982
  }
965
983
  /**
966
984
  * Dispatches an incoming broadcast message to the appropriate handler.
@@ -1051,6 +1069,18 @@ class LoginService {
1051
1069
  return r;
1052
1070
  }));
1053
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
+ }
1054
1084
  /**
1055
1085
  * Reads the current session from the server, which resolves it from the auth cookie's claims.
1056
1086
  * Used to restore the state after a page refresh instead of trusting what local storage holds.
@@ -1073,25 +1103,49 @@ class LoginService {
1073
1103
  }));
1074
1104
  }
1075
1105
  /**
1076
- * Asks `/session/me` whether the restored session is still alive, and announces the login when it
1077
- * is.
1106
+ * Asks `/session/status` whether this browser holds a live session, and hydrates the context
1107
+ * through {@link me} when it does.
1078
1108
  *
1079
- * Nothing is cleared on failure, and that is deliberate: a failure here is not proof of a dead
1080
- * session -- the API may be unreachable, the proxy may have answered 502, the device may be
1081
- * offline -- and clearing on any of those would sign the user out with a perfectly valid cookie.
1082
- * A genuine 401 is a different matter and the interceptor has already reported it.
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.
1113
+ *
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.
1083
1117
  * @returns void
1084
1118
  */
1085
1119
  restoreSession() {
1086
- this.me()
1087
- .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
+ } }))
1088
1126
  .subscribe({
1089
1127
  next: r => {
1090
- if (r?.success) {
1091
- 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();
1092
1146
  }
1093
1147
  },
1094
- // 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.
1095
1149
  error: () => { }
1096
1150
  });
1097
1151
  }
@@ -1384,8 +1438,9 @@ class EvolutionService {
1384
1438
  get user() { return this.core.user; }
1385
1439
  /**
1386
1440
  * `true` when this browser has a session the API has accepted. It starts from the stored context
1387
- * and is confirmed -- or left to the interceptor's 401 handling -- by the `/session/me` call
1388
- * {@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.
1389
1444
  */
1390
1445
  get loggedIn() { return this.core.loggedIn; }
1391
1446
  /** `true` while an authentication is in flight. */