@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.
- package/fesm2022/arsedizioni-ars-utils-clipper.common.mjs +101 -26
- package/fesm2022/arsedizioni-ars-utils-clipper.common.mjs.map +1 -1
- package/fesm2022/arsedizioni-ars-utils-clipper.ui.mjs +5 -5
- package/fesm2022/arsedizioni-ars-utils-clipper.ui.mjs.map +1 -1
- package/fesm2022/arsedizioni-ars-utils-evolution.common.mjs +85 -25
- package/fesm2022/arsedizioni-ars-utils-evolution.common.mjs.map +1 -1
- package/package.json +1 -1
- package/types/arsedizioni-ars-utils-clipper.common.d.ts +54 -10
- package/types/arsedizioni-ars-utils-evolution.common.d.ts +39 -11
|
@@ -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 (
|
|
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.
|
|
@@ -2383,16 +2400,26 @@ class ClipperLoginService {
|
|
|
2383
2400
|
/** Whether {@link initialize} has already run. */
|
|
2384
2401
|
this.initialized = false;
|
|
2385
2402
|
}
|
|
2403
|
+
/**
|
|
2404
|
+
* Gets the signed-in account.
|
|
2405
|
+
* @returns The current {@link ClipperUserInfo}, or `undefined` when there is no session.
|
|
2406
|
+
*/
|
|
2407
|
+
get user() { return this.core.user; }
|
|
2386
2408
|
////
|
|
2387
2409
|
// BOOTSTRAP
|
|
2388
2410
|
////
|
|
2389
2411
|
/**
|
|
2390
|
-
*
|
|
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.
|
|
2391
2414
|
*
|
|
2392
2415
|
* Called by `ClipperService.initialize`, right after `ClipperCoreService.initialize`, so a
|
|
2393
|
-
* consumer gets the restore for free and never has to know this method exists.
|
|
2394
|
-
*
|
|
2395
|
-
*
|
|
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.
|
|
2396
2423
|
*
|
|
2397
2424
|
* A host application that drives its own return from an external provider, or that starts on its
|
|
2398
2425
|
* own sign-in page, should call `core.setLoggedIn(false)` before bootstrapping rather than let
|
|
@@ -2403,27 +2430,54 @@ class ClipperLoginService {
|
|
|
2403
2430
|
if (this.initialized)
|
|
2404
2431
|
return;
|
|
2405
2432
|
this.initialized = true;
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
this.restoreSession();
|
|
2409
|
-
}
|
|
2433
|
+
this.core.loggingIn.set(true);
|
|
2434
|
+
this.restoreSession();
|
|
2410
2435
|
}
|
|
2411
2436
|
/**
|
|
2412
|
-
* Asks `/session/
|
|
2413
|
-
*
|
|
2437
|
+
* Asks `/session/status` whether this browser holds a live session, and hydrates the context
|
|
2438
|
+
* through {@link me} when it does.
|
|
2439
|
+
*
|
|
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.
|
|
2414
2444
|
*
|
|
2415
|
-
* Nothing is cleared on failure, and that is deliberate:
|
|
2416
|
-
*
|
|
2417
|
-
*
|
|
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.
|
|
2418
2448
|
* @returns void
|
|
2419
2449
|
*/
|
|
2420
2450
|
restoreSession() {
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
if (
|
|
2425
|
-
|
|
2426
|
-
|
|
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
|
+
} }))
|
|
2457
|
+
.subscribe({
|
|
2458
|
+
next: r => {
|
|
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);
|
|
2477
|
+
}
|
|
2478
|
+
},
|
|
2479
|
+
// Transport failure: keep the stored hint, the next bootstrap will ask again.
|
|
2480
|
+
error: () => { }
|
|
2427
2481
|
});
|
|
2428
2482
|
}
|
|
2429
2483
|
////
|
|
@@ -2487,6 +2541,18 @@ class ClipperLoginService {
|
|
|
2487
2541
|
.post(this.core.serviceUri + '/session/logout', {})
|
|
2488
2542
|
.pipe(finalize(() => this.core.clear(true)), catchError(() => of({ success: false })));
|
|
2489
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
|
+
}
|
|
2490
2556
|
/**
|
|
2491
2557
|
* Reads the current session from the server, which resolves it from the auth cookie claims
|
|
2492
2558
|
* without touching the database. Used to restore the state after a page refresh instead of
|
|
@@ -2645,6 +2711,11 @@ class ClipperCoreService {
|
|
|
2645
2711
|
}
|
|
2646
2712
|
return this._loginInfo;
|
|
2647
2713
|
}
|
|
2714
|
+
/**
|
|
2715
|
+
* Gets the signed-in account.
|
|
2716
|
+
* @returns The current {@link ClipperUserInfo}, or `undefined` when there is no session.
|
|
2717
|
+
*/
|
|
2718
|
+
get user() { return this.loginInfo?.context; }
|
|
2648
2719
|
////
|
|
2649
2720
|
// SHARED MUTATORS
|
|
2650
2721
|
////
|
|
@@ -2682,7 +2753,7 @@ class ClipperCoreService {
|
|
|
2682
2753
|
// Through the getter, never the field: the context is loaded from storage lazily, so a caller
|
|
2683
2754
|
// that stores before anyone has read would otherwise find the field empty and DELETE a
|
|
2684
2755
|
// perfectly good stored context.
|
|
2685
|
-
if (!this.
|
|
2756
|
+
if (!this.user) {
|
|
2686
2757
|
// Nothing to store. Falling through would call `JSON.stringify(undefined)`, whose result is
|
|
2687
2758
|
// the value `undefined`, which local storage writes as the nine-letter STRING "undefined" --
|
|
2688
2759
|
// a key that exists, parses to nothing, and makes the next bootstrap believe a session was
|
|
@@ -2720,7 +2791,7 @@ class ClipperCoreService {
|
|
|
2720
2791
|
if (this.loginInfo) {
|
|
2721
2792
|
const channels = [];
|
|
2722
2793
|
this.loginInfo.channels?.forEach(n => {
|
|
2723
|
-
const channelSubscription = this.
|
|
2794
|
+
const channelSubscription = this.user?.channels?.find(x => x.channel === n.channelId);
|
|
2724
2795
|
n.isSuspended = channelSubscription?.isSuspended === true;
|
|
2725
2796
|
const channel = ClipperChannels.find(x => x.value === n.channelId);
|
|
2726
2797
|
if (channel) {
|
|
@@ -3260,6 +3331,8 @@ class ClipperService {
|
|
|
3260
3331
|
get flags() { return this.core.flags; }
|
|
3261
3332
|
/** @returns The current login context, or `undefined` if not authenticated. */
|
|
3262
3333
|
get loginInfo() { return this.core.loginInfo; }
|
|
3334
|
+
/** @returns The signed-in account, or `undefined` when there is no session. */
|
|
3335
|
+
get user() { return this.core.user; }
|
|
3263
3336
|
/** @returns A read-only signal indicating whether the user is logged in. */
|
|
3264
3337
|
get loggedIn() { return this.core.loggedIn; }
|
|
3265
3338
|
/** @returns A signal indicating whether a login is in progress. */
|
|
@@ -3297,10 +3370,12 @@ class ClipperService {
|
|
|
3297
3370
|
*/
|
|
3298
3371
|
initialize(serviceUri, appUri, flags = ClipperServiceFlags.None) {
|
|
3299
3372
|
this.core.initialize(serviceUri, appUri, flags);
|
|
3300
|
-
// Second, and in this order: the core sets the base URI that `/session/
|
|
3301
|
-
// it no longer declares a session from what local storage
|
|
3302
|
-
//
|
|
3303
|
-
//
|
|
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.
|
|
3304
3379
|
this.session.initialize();
|
|
3305
3380
|
}
|
|
3306
3381
|
/**
|