@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.
@@ -2185,6 +2185,19 @@ function isAuthEndpoint(url) {
2185
2185
  function isSessionExpired(errorStatus, url) {
2186
2186
  return errorStatus === 401 && !isAuthEndpoint(url);
2187
2187
  }
2188
+ /**
2189
+ * Returns whether the URL is the silent link probe, whose failures must never reach the user.
2190
+ *
2191
+ * `/session/status` runs unconditionally at every bootstrap, including for users who never linked
2192
+ * Clipper at all. When the API is unreachable, reporting that failure would show "Clipper non
2193
+ * disponibile" to somebody who never asked Clipper for anything: the probe simply stays unanswered
2194
+ * and the link state keeps whatever the stored hint says.
2195
+ * @param url - The request URL.
2196
+ * @returns True when the URL is the status probe.
2197
+ */
2198
+ function isStatusProbe(url) {
2199
+ return url.includes('/session/status');
2200
+ }
2188
2201
  /**
2189
2202
  * Resolves a user-facing error message based on the HTTP status code.
2190
2203
  *
@@ -2331,7 +2344,11 @@ function clipperAuthInterceptor(serviceUri, flags = ClipperServiceFlags.None) {
2331
2344
  // at all. Everything that gets here is a Clipper request anyway: the others returned
2332
2345
  // before the pipe was built.
2333
2346
  const errorStatus = parseInt(error?.status ?? '0');
2334
- if (isServiceUnavailable(error)) {
2347
+ if (isStatusProbe(authenticatedRequest.url)) {
2348
+ // The silent probe stays silent: its caller reads the outcome from the stream, nobody
2349
+ // else needs to hear about it.
2350
+ }
2351
+ else if (isServiceUnavailable(error)) {
2335
2352
  // Three attempts over ten seconds and the API is still not answering. There is nothing
2336
2353
  // left to retry, so the host application is told once, however many requests were in
2337
2354
  // flight.
@@ -2392,12 +2409,17 @@ class ClipperLoginService {
2392
2409
  // BOOTSTRAP
2393
2410
  ////
2394
2411
  /**
2395
- * Validates a context that survived a page refresh before trusting it.
2412
+ * Establishes the session state at bootstrap by asking the API — the only party that can read the
2413
+ * HttpOnly cookie — whether this browser still holds one.
2396
2414
  *
2397
2415
  * Called by `ClipperService.initialize`, right after `ClipperCoreService.initialize`, so a
2398
- * consumer gets the restore for free and never has to know this method exists. The auth cookie is
2399
- * HttpOnly and may have expired while the stored context lived on, so a stored context only ever
2400
- * means "there WAS a session" — this is what turns it into "there is one".
2416
+ * consumer gets the restore for free and never has to know this method exists.
2417
+ *
2418
+ * The probe now runs UNCONDITIONALLY, where it used to run only when a stored context existed.
2419
+ * The stored context is just a hint — "this browser signed in at some point" — and the cookie can
2420
+ * outlive it: local storage cleared while the month-long cookie stayed put made this library
2421
+ * declare "no link" with a perfectly valid session in the browser. The API is the only source of
2422
+ * truth on the matter, so it is asked every time.
2401
2423
  *
2402
2424
  * A host application that drives its own return from an external provider, or that starts on its
2403
2425
  * own sign-in page, should call `core.setLoggedIn(false)` before bootstrapping rather than let
@@ -2408,30 +2430,53 @@ class ClipperLoginService {
2408
2430
  if (this.initialized)
2409
2431
  return;
2410
2432
  this.initialized = true;
2411
- if (this.core.loggedIn()) {
2412
- this.core.loggingIn.set(true);
2413
- this.restoreSession();
2414
- }
2433
+ this.core.loggingIn.set(true);
2434
+ this.restoreSession();
2415
2435
  }
2416
2436
  /**
2417
- * Asks `/session/me` whether the restored session is still alive. On success the login is
2418
- * finalised from the current claims; on a dead session the interceptor's 401 handling takes over.
2437
+ * Asks `/session/status` whether this browser holds a live session, and hydrates the context
2438
+ * through {@link me} when it does.
2419
2439
  *
2420
- * Nothing is cleared on failure, and that is deliberate: a failure here is not proof of a dead
2421
- * session — the API may be unreachable, the proxy may have answered 502, the device may be
2422
- * offline. Clearing on any of those would sign the user out with a perfectly valid cookie.
2440
+ * The probe is silent by construction: `/session/status` never answers 401, so a visitor who
2441
+ * never signed in — or a cookie whose session has died — comes back as `connected: false` with no
2442
+ * error broadcast and no dialog. Only then is the stale stored context cleared, so the `loggedIn`
2443
+ * signal tells the truth at every bootstrap.
2444
+ *
2445
+ * Nothing is cleared on a transport failure, and that is deliberate: an unreachable API is not
2446
+ * proof of a dead session — the proxy may have answered 502, the device may be offline — and
2447
+ * clearing on any of those would sign the user out with a perfectly valid cookie.
2423
2448
  * @returns void
2424
2449
  */
2425
2450
  restoreSession() {
2426
- this.me()
2427
- .pipe(finalize(() => this.core.loggingIn.set(false)))
2451
+ // Whether the probe handed over to `/session/me`, which then owns lowering `loggingIn`.
2452
+ let restoring = false;
2453
+ this.status()
2454
+ .pipe(finalize(() => { if (!restoring) {
2455
+ this.core.loggingIn.set(false);
2456
+ } }))
2428
2457
  .subscribe({
2429
2458
  next: r => {
2430
- if (r?.success) {
2431
- this.broadcastService.sendMessage(ClipperMessages.LOGIN_COMPLETED);
2459
+ if (r?.success && r.value?.connected) {
2460
+ restoring = true;
2461
+ this.me()
2462
+ .pipe(finalize(() => this.core.loggingIn.set(false)))
2463
+ .subscribe({
2464
+ next: m => {
2465
+ if (m?.success) {
2466
+ this.broadcastService.sendMessage(ClipperMessages.LOGIN_COMPLETED);
2467
+ }
2468
+ },
2469
+ // Consumed so the rethrown error is not reported a second time as an unhandled one.
2470
+ error: () => { }
2471
+ });
2472
+ }
2473
+ else if (this.core.loggedIn()) {
2474
+ // The stored context outlived its session: drop it, or the next bootstrap would keep
2475
+ // claiming a session that is gone.
2476
+ this.core.clear(true);
2432
2477
  }
2433
2478
  },
2434
- // Consumed so the rethrown error is not reported a second time as an unhandled one.
2479
+ // Transport failure: keep the stored hint, the next bootstrap will ask again.
2435
2480
  error: () => { }
2436
2481
  });
2437
2482
  }
@@ -2496,6 +2541,18 @@ class ClipperLoginService {
2496
2541
  .post(this.core.serviceUri + '/session/logout', {})
2497
2542
  .pipe(finalize(() => this.core.clear(true)), catchError(() => of({ success: false })));
2498
2543
  }
2544
+ /**
2545
+ * Asks the API whether this browser holds a live session, without ever failing with a 401: the
2546
+ * route is anonymous and answers `connected: false` to a caller with no session. This is the one
2547
+ * honest way to know the link state — the auth cookie is HttpOnly and belongs to the API's host,
2548
+ * so it cannot be read from here, and its presence would prove nothing anyway, since the session
2549
+ * also lives server-side and may have been revoked or evicted.
2550
+ * @returns An observable emitting the API result wrapping the session status.
2551
+ */
2552
+ status() {
2553
+ return this.httpClient
2554
+ .get(this.core.serviceUri + '/session/status');
2555
+ }
2499
2556
  /**
2500
2557
  * Reads the current session from the server, which resolves it from the auth cookie claims
2501
2558
  * without touching the database. Used to restore the state after a page refresh instead of
@@ -3313,10 +3370,12 @@ class ClipperService {
3313
3370
  */
3314
3371
  initialize(serviceUri, appUri, flags = ClipperServiceFlags.None) {
3315
3372
  this.core.initialize(serviceUri, appUri, flags);
3316
- // Second, and in this order: the core sets the base URI that `/session/me` is built from, and
3317
- // it no longer declares a session from what local storage holds. Validating that context is
3318
- // what `ClipperLoginService.initialize` does, and doing it from here means a consumer keeps
3319
- // calling one method to bootstrap, exactly as before.
3373
+ // Second, and in this order: the core sets the base URI that `/session/status` and
3374
+ // `/session/me` are built from, and it no longer declares a session from what local storage
3375
+ // holds. Asking the API whether a session actually exists is what
3376
+ // `ClipperLoginService.initialize` does — unconditionally, since the HttpOnly cookie can
3377
+ // outlive any local hint — and doing it from here means a consumer keeps calling one method to
3378
+ // bootstrap, exactly as before.
3320
3379
  this.session.initialize();
3321
3380
  }
3322
3381
  /**