@coolms/core-angular 2.0.0-alpha.3 → 2.0.0-alpha.5
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/CHANGELOG.md +134 -0
- package/README.md +16 -15
- package/fesm2022/coolms-core-angular.mjs +1254 -249
- package/fesm2022/coolms-core-angular.mjs.map +1 -1
- package/package.json +4 -1
- package/types/coolms-core-angular.d.ts +831 -69
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as rxjs from 'rxjs';
|
|
2
|
-
import { Observable } from 'rxjs';
|
|
2
|
+
import { Observable, Subject } from 'rxjs';
|
|
3
3
|
import * as i0 from '@angular/core';
|
|
4
|
-
import { Type,
|
|
4
|
+
import { InjectionToken, Type, Provider, EnvironmentProviders } from '@angular/core';
|
|
5
5
|
import { StateContext } from '@ngxs/store';
|
|
6
|
-
import { CanActivateFn } from '@angular/router';
|
|
6
|
+
import { CanActivateFn, Routes, CanMatchFn } from '@angular/router';
|
|
7
7
|
import { HttpContextToken, HttpInterceptorFn } from '@angular/common/http';
|
|
8
8
|
|
|
9
9
|
interface AuthApiManifest {
|
|
@@ -79,6 +79,44 @@ interface VfsApiManifest {
|
|
|
79
79
|
*/
|
|
80
80
|
interface DocumentApiManifest {
|
|
81
81
|
readonly spacesUrl?: string;
|
|
82
|
+
/**
|
|
83
|
+
* !! Carried here rather than derived from `spacesUrl`. Concatenating
|
|
84
|
+
* `/available` onto that would couple the client to a URL shape the router
|
|
85
|
+
* owns, and it would keep working until the route moved -- then fail as a
|
|
86
|
+
* 404 the UI reports as "no sites available", which is indistinguishable
|
|
87
|
+
* from the true empty answer.
|
|
88
|
+
*
|
|
89
|
+
* !! ABSENT ARRIVES AS `''`, NOT `undefined`. The producer declares these
|
|
90
|
+
* as non-nullable strings defaulting to empty, so an installation that has
|
|
91
|
+
* not set them sends an empty string and the `?` here never fires.
|
|
92
|
+
*
|
|
93
|
+
* Guard on FALSINESS -- `!url`, `Boolean(url)`. The dangerous rewrite is
|
|
94
|
+
* `url === undefined` (or `!== undefined`), which reads as more precise and
|
|
95
|
+
* treats `''` as present, handing a caller a request to the empty string.
|
|
96
|
+
*
|
|
97
|
+
* !! A `?? fallback` is NOT the hazard, though it looks like the obvious
|
|
98
|
+
* one: `'' ?? x` is `''`, so a fallback never fires on the value this
|
|
99
|
+
* producer actually sends. Measured by mutation -- adding one changed
|
|
100
|
+
* nothing, changing the guard broke three assertions.
|
|
101
|
+
*/
|
|
102
|
+
readonly spacesAvailableUrl?: string;
|
|
103
|
+
readonly spaceEnablementUrl?: string;
|
|
104
|
+
/**
|
|
105
|
+
* FQCN of the entity a filter-mode audience can be built from.
|
|
106
|
+
*
|
|
107
|
+
* !! The SERVER owns this and the client must not know it. The generation
|
|
108
|
+
* wizard offers Filter mode only when a template's context schema
|
|
109
|
+
* references this type, and it used to compare against a constant compiled
|
|
110
|
+
* into this bundle -- which shipped the consuming application's class name
|
|
111
|
+
* inside a published package, and tied that package to an installation
|
|
112
|
+
* having that exact class.
|
|
113
|
+
*
|
|
114
|
+
* !! Absent arrives as `''`, like the urls above. Treat empty as **filter
|
|
115
|
+
* mode unavailable**, never as a reason to fall back to a remembered
|
|
116
|
+
* value: the server is the thing that rejects every other type, so a guess
|
|
117
|
+
* here offers a mode the server will refuse.
|
|
118
|
+
*/
|
|
119
|
+
readonly filterAudienceEntity?: string;
|
|
82
120
|
}
|
|
83
121
|
interface DynamicEntityApiManifest {
|
|
84
122
|
readonly typesUrl: string;
|
|
@@ -116,6 +154,13 @@ interface IdentityApiManifest {
|
|
|
116
154
|
readonly settingsSectionUrl: string;
|
|
117
155
|
readonly colorUrl: string;
|
|
118
156
|
readonly rolesUrl?: string;
|
|
157
|
+
readonly elevationUrl?: string;
|
|
158
|
+
readonly deletionsUrl?: string;
|
|
159
|
+
readonly footprintsUrl?: string;
|
|
160
|
+
readonly userDeletionUrl?: string;
|
|
161
|
+
readonly userLegalHoldsUrl?: string;
|
|
162
|
+
readonly holdsSettingsBlock?: string;
|
|
163
|
+
readonly legalHoldFormId?: string | null;
|
|
119
164
|
}
|
|
120
165
|
/**
|
|
121
166
|
* Editor sub-manifest emitted by the backend's Editor module. The
|
|
@@ -153,7 +198,7 @@ interface EditorApiManifest {
|
|
|
153
198
|
* Viewer manifest emitted by the backend's Document module.
|
|
154
199
|
* Each viewer declares the MIME types / extensions it handles, the
|
|
155
200
|
* Angular component selector to dispatch into, and one config blob per
|
|
156
|
-
* profile. Profile config is open-ended
|
|
201
|
+
* profile. Profile config is open-ended -- every viewer interprets the
|
|
157
202
|
* keys it understands and ignores the rest.
|
|
158
203
|
*/
|
|
159
204
|
interface ViewerProfileApiManifest {
|
|
@@ -190,8 +235,29 @@ interface PlatformDefaults {
|
|
|
190
235
|
*/
|
|
191
236
|
readonly accentColor?: string | null;
|
|
192
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* The host contracts in force here (the platform rule: hosts implement
|
|
240
|
+
* contracts, modules offer entries): for each contract, what the one
|
|
241
|
+
* installed theme implementing it declares, and the installed modules whose
|
|
242
|
+
* entries the installer matched against it. A host mounts a module only when
|
|
243
|
+
* it is listed.
|
|
244
|
+
*/
|
|
245
|
+
interface UiApiManifest {
|
|
246
|
+
/** The contracts in force, name -> MAJOR.MINOR (`{ console: '1.0' }`): one installed theme implements each. */
|
|
247
|
+
readonly contracts: Readonly<Record<string, string>>;
|
|
248
|
+
/** Which installed theme implements each contract, name -> theme slug. */
|
|
249
|
+
readonly hosts?: Readonly<Record<string, string>>;
|
|
250
|
+
readonly modules: ReadonlyArray<UiModuleEntryManifest>;
|
|
251
|
+
}
|
|
252
|
+
interface UiModuleEntryManifest {
|
|
253
|
+
readonly module: string;
|
|
254
|
+
readonly contract: string;
|
|
255
|
+
readonly range: string;
|
|
256
|
+
readonly framework: string;
|
|
257
|
+
}
|
|
193
258
|
interface ApiManifest {
|
|
194
259
|
readonly apiBase: string;
|
|
260
|
+
readonly ui?: UiApiManifest;
|
|
195
261
|
readonly configBase?: string;
|
|
196
262
|
readonly auth?: AuthApiManifest;
|
|
197
263
|
readonly identity?: IdentityApiManifest;
|
|
@@ -213,6 +279,15 @@ interface ApiManifest {
|
|
|
213
279
|
}>;
|
|
214
280
|
readonly platformDefaults?: PlatformDefaults;
|
|
215
281
|
}
|
|
282
|
+
/**
|
|
283
|
+
* GET /api/v1/admin/manifest -- the admin's endpoint map, answered to every signed-in account
|
|
284
|
+
* since 2026-10-06 (no host groups: each call it names is decided by its own route). A server
|
|
285
|
+
* from before that answers at /api/v1/console/manifest, to the console group only. Read once
|
|
286
|
+
* signed in.
|
|
287
|
+
*/
|
|
288
|
+
interface ConsoleManifestResponse {
|
|
289
|
+
readonly manifest: ApiManifest;
|
|
290
|
+
}
|
|
216
291
|
interface ThemeConfigResponse {
|
|
217
292
|
readonly slug?: string | null;
|
|
218
293
|
readonly feStack?: string | null;
|
|
@@ -357,7 +432,7 @@ interface LayoutNode {
|
|
|
357
432
|
}
|
|
358
433
|
type FormActionType = 'submit' | 'button' | 'reset';
|
|
359
434
|
type FormActionVariant = 'primary' | 'secondary' | 'danger' | 'link';
|
|
360
|
-
/** Terminal form button (Submit / Cancel /
|
|
435
|
+
/** Terminal form button (Submit / Cancel / ...) -- mirrors PHP FormAction.
|
|
361
436
|
* Resolved by the backend (declared `formOptions.actions`, else lifted from
|
|
362
437
|
* legacy submit fields, else a default Submit). `label` is already translated. */
|
|
363
438
|
interface FormAction {
|
|
@@ -434,7 +509,7 @@ declare class IdentityApiClient {
|
|
|
434
509
|
me(): Observable<UserDto>;
|
|
435
510
|
getSettings(): Observable<Record<string, Record<string, unknown>>>;
|
|
436
511
|
static ɵfac: i0.ɵɵFactoryDeclaration<IdentityApiClient, never>;
|
|
437
|
-
static ɵprov: i0.ɵɵInjectableDeclaration<
|
|
512
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
438
513
|
}
|
|
439
514
|
|
|
440
515
|
/**
|
|
@@ -446,6 +521,13 @@ interface CentrifugoConnectionTokenDto {
|
|
|
446
521
|
expiresAt: number;
|
|
447
522
|
ttl: number;
|
|
448
523
|
wsUrl: string;
|
|
524
|
+
/**
|
|
525
|
+
* The code the server closes this connection with when the caller's session
|
|
526
|
+
* ended (a sign-out everywhere, a password change, a deactivation). On it the
|
|
527
|
+
* client signs out and ends its calls at once, without reconnecting or waiting
|
|
528
|
+
* for a 401 (2026-09-26). Absent from a server older than that.
|
|
529
|
+
*/
|
|
530
|
+
sessionEndedCode?: number;
|
|
449
531
|
}
|
|
450
532
|
/**
|
|
451
533
|
* Response of `POST /centrifugo/subscription-token`: a per-channel JWT. The
|
|
@@ -471,15 +553,33 @@ interface CentrifugoSubscriptionTokenDto {
|
|
|
471
553
|
*
|
|
472
554
|
* `ApiService` keeps its identical signatures and delegates here, so nothing
|
|
473
555
|
* calling it had to change.
|
|
556
|
+
*
|
|
557
|
+
* !! Access first, then realtime (Dmitry, 2026-09-28): "The console opens a realtime
|
|
558
|
+
* connection before the access check. Reverse the order ... A refused account opens no
|
|
559
|
+
* socket." The socket cannot open without the connection token, and anything may ask for
|
|
560
|
+
* one early -- the ring subscription is built at app init and asks as soon as a user is
|
|
561
|
+
* set, before the guard's answer is in. So the token waits for the console's answer (the
|
|
562
|
+
* guard's own request, not a second one) and a refused sign-in gets none: no request, no
|
|
563
|
+
* socket. A failure to ask is not a refusal, as at the guard.
|
|
564
|
+
*
|
|
565
|
+
* Since 2026-10-06 the connection token is every signed-in account's (Dmitry: "The connection
|
|
566
|
+
* token only opens a socket; each channel is authorized when it is subscribed to"), and a
|
|
567
|
+
* current server answers the admin manifest to every signed-in account, so no
|
|
568
|
+
* sign-in is refused and every one gets its token; each subscription is then decided by its
|
|
569
|
+
* channel. The wait stays for a server from before that, where a refused sign-in still opens
|
|
570
|
+
* no socket.
|
|
474
571
|
*/
|
|
475
572
|
declare class RealtimeTokenClient {
|
|
573
|
+
/** The error a refused sign-in's connection-token request ends with. */
|
|
574
|
+
static readonly REFUSED = "no console access: no realtime connection";
|
|
476
575
|
private readonly http;
|
|
477
576
|
private readonly store;
|
|
577
|
+
private readonly injector;
|
|
478
578
|
private get manifest();
|
|
479
579
|
connectionToken(): Observable<CentrifugoConnectionTokenDto>;
|
|
480
580
|
subscriptionToken(channel: string): Observable<CentrifugoSubscriptionTokenDto>;
|
|
481
581
|
static ɵfac: i0.ɵɵFactoryDeclaration<RealtimeTokenClient, never>;
|
|
482
|
-
static ɵprov: i0.ɵɵInjectableDeclaration<
|
|
582
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
483
583
|
}
|
|
484
584
|
|
|
485
585
|
declare class Login {
|
|
@@ -533,7 +633,7 @@ declare class AuthState {
|
|
|
533
633
|
static expiresAt(state: AuthStateModel): string | null;
|
|
534
634
|
static currentUser(state: AuthStateModel): UserDto | null;
|
|
535
635
|
static ɵfac: i0.ɵɵFactoryDeclaration<AuthState, never>;
|
|
536
|
-
static ɵprov: i0.ɵɵInjectableDeclaration<
|
|
636
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
537
637
|
}
|
|
538
638
|
|
|
539
639
|
/**
|
|
@@ -541,10 +641,16 @@ declare class AuthState {
|
|
|
541
641
|
*
|
|
542
642
|
* The guard is intentionally ASYNC: it waits for AppInitService.ready$ before
|
|
543
643
|
* evaluating auth state. Without this, Angular's router can evaluate
|
|
544
|
-
* isAuthenticated while APP_INITIALIZER is still in-flight
|
|
644
|
+
* isAuthenticated while APP_INITIALIZER is still in-flight -- RestoreSession
|
|
545
645
|
* will have placed a non-null (but expired) token in state, causing the guard
|
|
546
646
|
* to pass even though the subsequent config/refresh requests will fail and
|
|
547
647
|
* dispatch Logout. Waiting for ready$ guarantees the auth state is settled.
|
|
648
|
+
*
|
|
649
|
+
* Signed in is not enough: the console is granted, and the server says to whom
|
|
650
|
+
* (ConsoleAccessService). An account it refuses -- a customer who registered on the
|
|
651
|
+
* public site -- is signed out of this sign-in, on the server as well as here, and
|
|
652
|
+
* sent to the login page, which says why. A failure to ask is not a refusal: the console opens, and every request it
|
|
653
|
+
* makes is still decided by the server.
|
|
548
654
|
*/
|
|
549
655
|
declare const authGuard: CanActivateFn;
|
|
550
656
|
|
|
@@ -591,6 +697,26 @@ declare const authInterceptor: HttpInterceptorFn;
|
|
|
591
697
|
* (real auth failure). Transient errors (network blips, 5xx,
|
|
592
698
|
* CORS, timeouts) propagate without clearing the session so a
|
|
593
699
|
* momentary refresh failure does not kill a valid login.
|
|
700
|
+
*
|
|
701
|
+
* A token the server REFUSED (2026-09-26). A caller answering a 401
|
|
702
|
+
* passes the access token that was refused. Until then the lock body
|
|
703
|
+
* handed back the cached token whenever its expiry looked fresh --
|
|
704
|
+
* which a revoked token's does: a session ended on the server (a
|
|
705
|
+
* sign-out everywhere from another device, a password change) was
|
|
706
|
+
* retried with the same dead token and never refreshed, so the client
|
|
707
|
+
* stayed signed in, and in its calls, until the token neared expiry
|
|
708
|
+
* (measured: a 1:1 call kept for the whole 30 s window, every request
|
|
709
|
+
* answered 401). Now the lock body re-reads the SHARED copy
|
|
710
|
+
* (`localStorage`, which every tab's `SetTokens` writes synchronously;
|
|
711
|
+
* this tab's state only follows a sibling's write when its `storage`
|
|
712
|
+
* event arrives):
|
|
713
|
+
* - a different access token there -> another tab (or an earlier
|
|
714
|
+
* refresh here) already rotated past the refused one: use it, no
|
|
715
|
+
* request;
|
|
716
|
+
* - the same one -> the server refused this token: refresh, with the
|
|
717
|
+
* newest refresh token -- the shared one, so a rotation a sibling
|
|
718
|
+
* made is never replayed with the token it consumed. A 401 there
|
|
719
|
+
* is the session's end: `Logout`.
|
|
594
720
|
*/
|
|
595
721
|
declare class AuthRefreshCoordinator {
|
|
596
722
|
private readonly api;
|
|
@@ -601,10 +727,16 @@ declare class AuthRefreshCoordinator {
|
|
|
601
727
|
* Return the in-flight refresh Observable, or spawn a new one
|
|
602
728
|
* with the supplied refresh-token value.
|
|
603
729
|
*
|
|
730
|
+
* `refusedAccessToken`: the access token the server just answered
|
|
731
|
+
* 401 to, when the caller is answering one. With it, the lock body
|
|
732
|
+
* refreshes unless the shared copy already holds another token;
|
|
733
|
+
* without it (a preemptive refresh), a fresh-looking cached token
|
|
734
|
+
* is handed back as before.
|
|
735
|
+
*
|
|
604
736
|
* Callers subscribe via `switchMap` / `firstValueFrom` to retry
|
|
605
737
|
* their original request with the resolved access token.
|
|
606
738
|
*/
|
|
607
|
-
refresh(refreshToken: string): Observable<string>;
|
|
739
|
+
refresh(refreshToken: string, refusedAccessToken?: string | null): Observable<string>;
|
|
608
740
|
/**
|
|
609
741
|
* Run the refresh body inside `navigator.locks` when the API is
|
|
610
742
|
* available; degrade to direct invocation otherwise. The lock
|
|
@@ -613,22 +745,34 @@ declare class AuthRefreshCoordinator {
|
|
|
613
745
|
*/
|
|
614
746
|
private refreshWithCrossTabLock;
|
|
615
747
|
/**
|
|
616
|
-
* Lock body. Re-reads
|
|
617
|
-
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
620
|
-
*
|
|
748
|
+
* Lock body. Re-reads the shared copy first (see the class note).
|
|
749
|
+
*
|
|
750
|
+
* Answering a refusal: a shared access token other than the refused
|
|
751
|
+
* one is a rotation already made -- taken, and mirrored into this
|
|
752
|
+
* tab's state if its `storage` event has not arrived yet. Otherwise,
|
|
753
|
+
* or for a preemptive caller whose cached token no longer looks
|
|
754
|
+
* fresh, the HTTP refresh, with the newest refresh token: shared,
|
|
755
|
+
* then state, then the caller's.
|
|
621
756
|
*/
|
|
622
757
|
private executeRefresh;
|
|
623
758
|
/**
|
|
624
|
-
*
|
|
625
|
-
*
|
|
626
|
-
*
|
|
627
|
-
|
|
759
|
+
* The session as the tabs share it, or null when there is none to
|
|
760
|
+
* read: no storage, nothing stored (signed out), or a value that is
|
|
761
|
+
* not a complete token set.
|
|
762
|
+
*/
|
|
763
|
+
private readShared;
|
|
764
|
+
/**
|
|
765
|
+
* For a preemptive caller only: return the current access token when
|
|
766
|
+
* its expiry is more than `FRESH_TOKEN_GRACE_MS` milliseconds away --
|
|
767
|
+
* signalling that some other tab refreshed for us. The shared copy
|
|
768
|
+
* first, which a sibling's rotation reaches before this tab's state
|
|
769
|
+
* does (mirrored here when it is newer); this tab's state when there
|
|
770
|
+
* is none. Null otherwise. Never asked for a token the server
|
|
771
|
+
* refused: a refused token's expiry looks just as fresh.
|
|
628
772
|
*/
|
|
629
773
|
private tryConsumeFreshState;
|
|
630
774
|
static ɵfac: i0.ɵɵFactoryDeclaration<AuthRefreshCoordinator, never>;
|
|
631
|
-
static ɵprov: i0.ɵɵInjectableDeclaration<
|
|
775
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
632
776
|
}
|
|
633
777
|
|
|
634
778
|
/**
|
|
@@ -661,16 +805,366 @@ declare class CrossTabAuthSyncService {
|
|
|
661
805
|
start(): void;
|
|
662
806
|
private readonly handleStorageEvent;
|
|
663
807
|
static ɵfac: i0.ɵɵFactoryDeclaration<CrossTabAuthSyncService, never>;
|
|
664
|
-
static ɵprov: i0.ɵɵInjectableDeclaration<
|
|
808
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* The elevation state of the signed-in session: the wire shape of
|
|
813
|
+
* `GET /auth/elevation`, returned again by `POST` on a grant.
|
|
814
|
+
*
|
|
815
|
+
* Elevation is state, not a role. It lasts a fixed time from the grant, ends
|
|
816
|
+
* when the admin panel is closed or reloaded, and the client derives nothing
|
|
817
|
+
* about it from mode bits or membership: this record and the node resource's
|
|
818
|
+
* capability flags are the two readings of ONE server-side decision, and the
|
|
819
|
+
* client keeps no third.
|
|
820
|
+
*/
|
|
821
|
+
interface ElevationState {
|
|
822
|
+
/** Whether this session is elevated right now. */
|
|
823
|
+
elevated: boolean;
|
|
824
|
+
/** When the current elevation ends (ISO 8601); absent or null when not elevated. */
|
|
825
|
+
expiresAt?: string | null;
|
|
826
|
+
/** When the current elevation was granted (ISO 8601); absent or null when not elevated. */
|
|
827
|
+
grantedAt?: string | null;
|
|
828
|
+
/** How the LAST elevation of this session ended. Rendered by the prompt before it asks. */
|
|
829
|
+
ended: ElevationEnded;
|
|
830
|
+
/** The lifetime a grant made now would get, in seconds (the setting, clamped to the ceiling). */
|
|
831
|
+
lifetimeSeconds: number;
|
|
832
|
+
/** The most any elevation can last, fixed in code. */
|
|
833
|
+
lifetimeCeilingSeconds: number;
|
|
834
|
+
/** Whether the CURRENT elevation was confirmed by a second factor; absent or null when not elevated. */
|
|
835
|
+
factorConfirmed?: boolean | null;
|
|
836
|
+
/** Whether a grant made now must carry a code from the person's own authenticator. */
|
|
837
|
+
mfaRequired: boolean;
|
|
838
|
+
/** What the prompt shows before asking; two of the codes mean this account cannot elevate yet. */
|
|
839
|
+
warnings: ElevationWarning[];
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* `closed` is the beacon (the panel was closed or reloaded), `dropped_by_user`
|
|
843
|
+
* the explicit drop, `refresh_from_other_pair` the server's refresh tripwire,
|
|
844
|
+
* `never` a session that has not elevated yet.
|
|
845
|
+
*/
|
|
846
|
+
type ElevationEndedReason = 'expired' | 'closed' | 'dropped_by_user' | 'refresh_from_other_pair' | 'never';
|
|
847
|
+
interface ElevationEnded {
|
|
848
|
+
reason: ElevationEndedReason;
|
|
849
|
+
/** ISO 8601, null for `never`. */
|
|
850
|
+
at: string | null;
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* `elevation.password_not_set` and `elevation.factor_missing` mean this
|
|
854
|
+
* account cannot elevate (it has no password, or no confirmed factor, of its
|
|
855
|
+
* own) and the message says which;
|
|
856
|
+
* `elevation.mfa_off` is the recommendation carried while the switch is off.
|
|
857
|
+
*/
|
|
858
|
+
interface ElevationWarning {
|
|
859
|
+
/** The three the server documents today; the client renders any it does not know as text. */
|
|
860
|
+
code: string;
|
|
861
|
+
message: string;
|
|
862
|
+
}
|
|
863
|
+
declare const ELEVATION_WARNING_PASSWORD_NOT_SET = "elevation.password_not_set";
|
|
864
|
+
declare const ELEVATION_WARNING_FACTOR_MISSING = "elevation.factor_missing";
|
|
865
|
+
declare const ELEVATION_WARNING_MFA_OFF = "elevation.mfa_off";
|
|
866
|
+
/** Why the elevation of a session changed, for whoever refetches on it. */
|
|
867
|
+
type ElevationChangeKind = 'granted' | 'dropped' | 'expired' | 'observed';
|
|
868
|
+
interface ElevationChange {
|
|
869
|
+
kind: ElevationChangeKind;
|
|
870
|
+
state: ElevationState;
|
|
871
|
+
}
|
|
872
|
+
/** The two reasons the client sends when it drops an elevation itself. */
|
|
873
|
+
type ElevationDropReason = 'dropped_by_user' | 'closed';
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* What the elevation prompt is told when it opens.
|
|
877
|
+
*
|
|
878
|
+
* `state` is the server's answer at the moment of asking -- `ended.reason` is
|
|
879
|
+
* rendered first (a person asked for a password again after F5
|
|
880
|
+
* with no explanation reads it as a fault), `warnings` may say this
|
|
881
|
+
* account cannot elevate yet, `mfaRequired` decides whether a code field
|
|
882
|
+
* shows from the start.
|
|
883
|
+
*
|
|
884
|
+
* `refusal` is the server's own sentence for the action that was refused
|
|
885
|
+
* ("Permission denied: cannot write '/docs/...'"), when a 403 opened the
|
|
886
|
+
* prompt; absent when a disabled control offered elevation instead.
|
|
887
|
+
*/
|
|
888
|
+
interface ElevationPromptRequest {
|
|
889
|
+
state: ElevationState;
|
|
890
|
+
refusal?: string;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* The prompt itself is a dialog, and dialogs are the kit's -- a layer above
|
|
894
|
+
* this one. Core owns the state, the interceptor and the decision to ask; the
|
|
895
|
+
* application binds this port to whatever renders the question. Same shape as
|
|
896
|
+
* `CURRENT_SECTION`: core declares the little it needs, the composition root
|
|
897
|
+
* provides it.
|
|
898
|
+
*
|
|
899
|
+
* Resolves to true when the session is elevated by the time the dialog closes,
|
|
900
|
+
* false when the person declined or could not. The port does not retry the
|
|
901
|
+
* refused action; the interceptor does, on true.
|
|
902
|
+
*/
|
|
903
|
+
interface ElevationPromptPort {
|
|
904
|
+
open(request: ElevationPromptRequest): Observable<boolean>;
|
|
665
905
|
}
|
|
906
|
+
/**
|
|
907
|
+
* Optional by design: with nothing bound, a 403 stays a 403 and the state
|
|
908
|
+
* service still answers -- an application without an admin panel has no
|
|
909
|
+
* prompt to show and loses nothing.
|
|
910
|
+
*/
|
|
911
|
+
declare const ELEVATION_PROMPT: InjectionToken<ElevationPromptPort>;
|
|
912
|
+
/**
|
|
913
|
+
* Told when an action a person took was refused for want of elevation: a
|
|
914
|
+
* write (not a read, not a background request) answered by a stamped 403.
|
|
915
|
+
*
|
|
916
|
+
* It must not open the prompt. The prompt opens only when the person asks for
|
|
917
|
+
* it -- the application shows the refusal and offers elevation as something to
|
|
918
|
+
* click (a notice with an Elevate button), and the click is what calls
|
|
919
|
+
* {@link ElevationService.offerFor}. A refused action is not repeated by
|
|
920
|
+
* itself after a grant: the person repeats it.
|
|
921
|
+
*
|
|
922
|
+
* `refusal` is the server's sentence for the refused action, already made
|
|
923
|
+
* readable by the error service.
|
|
924
|
+
*/
|
|
925
|
+
interface ElevationNoticePort {
|
|
926
|
+
refused(refusal: string): void;
|
|
927
|
+
}
|
|
928
|
+
/** Optional, like {@link ELEVATION_PROMPT}: with nothing bound, a refusal is only a 403. */
|
|
929
|
+
declare const ELEVATION_NOTICE: InjectionToken<ElevationNoticePort>;
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Set on a request that must never open the elevation prompt: the elevation
|
|
933
|
+
* endpoint's own calls (a wrong password is a 403 and is not a refused action),
|
|
934
|
+
* and any caller that would rather receive its 403 and decide for itself.
|
|
935
|
+
*/
|
|
936
|
+
declare const BYPASS_ELEVATION: HttpContextToken<boolean>;
|
|
937
|
+
/**
|
|
938
|
+
* The elevation state of this session, read from ONE place: the server.
|
|
939
|
+
*
|
|
940
|
+
* The elevation client requirement, in one service. The state endpoint is the only
|
|
941
|
+
* source -- `elevated`, `expiresAt`, how the last elevation ended -- and every
|
|
942
|
+
* change of it is announced on `changes$` so the Explorer listing can refetch
|
|
943
|
+
* (its capability flags are the server's reading of the same state, computed
|
|
944
|
+
* when the listing was fetched, and a `write: true` fetched while elevated
|
|
945
|
+
* must not outlive the elevation).
|
|
946
|
+
*
|
|
947
|
+
* **Expiry is driven by the READ, not by a timer.** An elevation ends because
|
|
948
|
+
* time passed, and time is not an event; a client that waits to be told has
|
|
949
|
+
* nothing to wait for when the machine was asleep at the moment. So `state()`
|
|
950
|
+
* does the arithmetic every time it is asked, and an answer whose `expiresAt`
|
|
951
|
+
* has gone by reports expired without a request. The timer below still fires
|
|
952
|
+
* at `expiresAt` and the tab still re-asks when it comes back into view -- both
|
|
953
|
+
* are how the SERVER's answer is refreshed, which is a different thing from
|
|
954
|
+
* knowing this one has run out.
|
|
955
|
+
*
|
|
956
|
+
* This is not a second decider. The server said "elevated until T"; after T,
|
|
957
|
+
* continuing to report `elevated: true` asserts something it never said.
|
|
958
|
+
*
|
|
959
|
+
* The prompt is not here: dialogs are the kit's, a layer above core. The
|
|
960
|
+
* service asks the `ELEVATION_PROMPT` port and shares ONE in-flight prompt, so
|
|
961
|
+
* a burst of refused requests (a multi-select delete is one 403 per node)
|
|
962
|
+
* opens one dialog and every caller waits on the same answer.
|
|
963
|
+
*/
|
|
964
|
+
/**
|
|
965
|
+
* The header a 403 refused for want of elevation carries, with the gate's name
|
|
966
|
+
* as its value. The server stamps it where the gate refused; the interceptor
|
|
967
|
+
* prompts on it and on nothing else. There is no list of gated URLs on this
|
|
968
|
+
* side any more: the one that stood here lagged the server by every route
|
|
969
|
+
* whose refusal came from the VFS permission service under another module's
|
|
970
|
+
* path (media, document and word templates, content distribution).
|
|
971
|
+
*/
|
|
972
|
+
declare const ELEVATION_REQUIRED_HEADER = "X-Elevation-Required";
|
|
973
|
+
/**
|
|
974
|
+
* Set on a request no person asked for: a realtime token, a preferences sync, a
|
|
975
|
+
* poll. Its refusal is never announced -- not by a dialog and not by a notice --
|
|
976
|
+
* because nobody is waiting on it and nobody can act on it. The caller still
|
|
977
|
+
* receives the 403 and decides what its own state shows.
|
|
978
|
+
*/
|
|
979
|
+
declare const BACKGROUND_REQUEST: HttpContextToken<boolean>;
|
|
980
|
+
/**
|
|
981
|
+
* Whether a failed request was refused for want of elevation: a 403 carrying
|
|
982
|
+
* the server's stamp. The one test every reader of a refusal uses -- the
|
|
983
|
+
* interceptor, a page's inline state, the error text -- so they cannot disagree.
|
|
984
|
+
*/
|
|
985
|
+
declare function isElevationRefusal(err: unknown): boolean;
|
|
986
|
+
declare class ElevationService {
|
|
987
|
+
private readonly http;
|
|
988
|
+
private readonly store;
|
|
989
|
+
private readonly prompt;
|
|
990
|
+
private readonly destroyRef;
|
|
991
|
+
/** The last answer the server gave, exactly as it gave it. */
|
|
992
|
+
private readonly served;
|
|
993
|
+
/**
|
|
994
|
+
* The state as it stands NOW; null until the first read from the server.
|
|
995
|
+
*
|
|
996
|
+
* Deliberately NOT a `computed`. A cached derivation is only recomputed
|
|
997
|
+
* when one of its dependencies changes, and nothing changes when an
|
|
998
|
+
* elevation simply runs out -- so it would keep answering `elevated: true`
|
|
999
|
+
* past the moment the server itself named, for as long as the machine
|
|
1000
|
+
* happened to be asleep.
|
|
1001
|
+
*
|
|
1002
|
+
* Reading past `expiresAt` also demotes the STORED answer. The value
|
|
1003
|
+
* returned here is already correct without that write; the write is only
|
|
1004
|
+
* how cached readers (a `computed`, an OnPush template) find out.
|
|
1005
|
+
*/
|
|
1006
|
+
readonly state: () => ElevationState | null;
|
|
1007
|
+
/** Convenience over `state`: false until the server has said otherwise. */
|
|
1008
|
+
readonly elevated: () => boolean;
|
|
1009
|
+
/** `expiresAt` as a Date, null when not elevated. */
|
|
1010
|
+
readonly expiresAt: () => Date | null;
|
|
1011
|
+
/**
|
|
1012
|
+
* Every change of elevation state: a grant, a drop (by this person, by the
|
|
1013
|
+
* beacon, by the tripwire), an expiry, or an observed change of a live
|
|
1014
|
+
* grant's `expiresAt`. A refresh that reports the same state emits nothing,
|
|
1015
|
+
* so subscribers that refetch a listing on it never refetch for no reason.
|
|
1016
|
+
*/
|
|
1017
|
+
readonly changes$: Subject<ElevationChange>;
|
|
1018
|
+
private expiryTimer;
|
|
1019
|
+
private pendingPrompt;
|
|
1020
|
+
private expiryAnnouncePending;
|
|
1021
|
+
private lastReconcileAt;
|
|
1022
|
+
constructor();
|
|
1023
|
+
/** The manifest carries the URL; without it the service is inert, not broken. */
|
|
1024
|
+
get available(): boolean;
|
|
1025
|
+
private get url();
|
|
1026
|
+
/**
|
|
1027
|
+
* Whether a URL belongs to THIS API: a relative one under the manifest's
|
|
1028
|
+
* `apiBase`, or an absolute one on this origin under it. The interceptor
|
|
1029
|
+
* acts on a stamped refusal from here and from nowhere else -- a third
|
|
1030
|
+
* party answering 403 with a header of the same name is not an invitation
|
|
1031
|
+
* to elevate this session.
|
|
1032
|
+
*/
|
|
1033
|
+
isThisApi(url: string): boolean;
|
|
1034
|
+
/** Ask the server. Applies the answer and announces it when it differs. */
|
|
1035
|
+
refresh(): Observable<ElevationState>;
|
|
1036
|
+
/**
|
|
1037
|
+
* Elevate with the person's own password and, when the switch is on, a
|
|
1038
|
+
* code from their own authenticator. Errors are the dialog's to branch on:
|
|
1039
|
+
* 403 not a member of the admin group, wrong password, or wrong / replayed
|
|
1040
|
+
* code, 428 a code is required, 409 this account cannot elevate yet, 429
|
|
1041
|
+
* throttled. They pass through
|
|
1042
|
+
* untouched, and never open the prompt (BYPASS_ELEVATION).
|
|
1043
|
+
*/
|
|
1044
|
+
elevate(password: string, code?: string): Observable<ElevationState>;
|
|
1045
|
+
/** Drop the elevation now. Idempotent on the server: nothing live is still a 204. */
|
|
1046
|
+
drop(reason?: ElevationDropReason): Observable<ElevationState>;
|
|
1047
|
+
/**
|
|
1048
|
+
* What a person's explicit request for elevation calls -- an Elevate button,
|
|
1049
|
+
* or a control that already knows its action needs elevation: ask the
|
|
1050
|
+
* server whether this session is elevated, and only when it is not, open
|
|
1051
|
+
* the prompt. Nothing calls it on a response by itself (the interceptor
|
|
1052
|
+
* only tells {@link ELEVATION_NOTICE}). Elevated already is false, no
|
|
1053
|
+
* prompt: elevation would change nothing.
|
|
1054
|
+
*/
|
|
1055
|
+
offerFor(refusal?: string): Observable<boolean>;
|
|
1056
|
+
/**
|
|
1057
|
+
* Open the prompt, or join the one already open. Resolves to whether the
|
|
1058
|
+
* session is elevated once the dialog closes.
|
|
1059
|
+
*/
|
|
1060
|
+
requestElevation(request: ElevationPromptRequest): Observable<boolean>;
|
|
1061
|
+
private options;
|
|
1062
|
+
private inert;
|
|
1063
|
+
/**
|
|
1064
|
+
* Store the answer, schedule the expiry read, announce a change.
|
|
1065
|
+
*
|
|
1066
|
+
* `explicit` names the change when the caller knows it (a grant). For a
|
|
1067
|
+
* plain refresh the kind is derived from the two states: elevated -> not
|
|
1068
|
+
* is `expired` when the server says so and `dropped` otherwise (the person,
|
|
1069
|
+
* another tab, the beacon, the tripwire); not -> elevated is a grant seen
|
|
1070
|
+
* from another tab; a live grant whose `expiresAt` moved is `observed`.
|
|
1071
|
+
*
|
|
1072
|
+
* `prev` is read RAW rather than through `state()`: the demotion is not
|
|
1073
|
+
* wanted here (this answer supersedes the stored one either way), and a
|
|
1074
|
+
* read that demoted would queue an expiry announcement the server's own
|
|
1075
|
+
* answer is about to make.
|
|
1076
|
+
*/
|
|
1077
|
+
private apply;
|
|
1078
|
+
private kindOf;
|
|
1079
|
+
/**
|
|
1080
|
+
* Tell the listings that this session's elevation ran out, once, and NOT
|
|
1081
|
+
* from inside the read that noticed: a subscriber refetches on this, and a
|
|
1082
|
+
* refetch started in the middle of a change detection pass is a write
|
|
1083
|
+
* during a read. The microtask runs at the end of the current task, before
|
|
1084
|
+
* anything can paint.
|
|
1085
|
+
*
|
|
1086
|
+
* A server answer arriving first clears the flag, so the announcement is
|
|
1087
|
+
* made by whichever of the two got there first, never by both. Other tabs
|
|
1088
|
+
* are NOT told: each one reaches the same conclusion from the same
|
|
1089
|
+
* `expiresAt` on its own read, so the storage write would only buy a
|
|
1090
|
+
* thundering herd of GETs.
|
|
1091
|
+
*/
|
|
1092
|
+
private announceExpiryLater;
|
|
1093
|
+
/**
|
|
1094
|
+
* Ask the server again at `expiresAt` -- an OPTIMISATION, not the
|
|
1095
|
+
* mechanism. `state()` already reports an elapsed grant as expired without
|
|
1096
|
+
* being told, so a timer that a sleeping machine swallows, or that a
|
|
1097
|
+
* background tab throttles to once a minute, now costs freshness rather
|
|
1098
|
+
* than correctness. What it buys is the listing refetch happening at the
|
|
1099
|
+
* moment of expiry instead of at the next read.
|
|
1100
|
+
*
|
|
1101
|
+
* Do not flip a local flag here: the lifetime counts from the grant on the
|
|
1102
|
+
* server's clock, and if the server still says elevated at that moment
|
|
1103
|
+
* (skew), the honest thing is to ask again shortly.
|
|
1104
|
+
*/
|
|
1105
|
+
private schedule;
|
|
1106
|
+
/**
|
|
1107
|
+
* One more attempt, then stop. Giving up is affordable now and was not
|
|
1108
|
+
* before: the state a failed read leaves behind is still read through the
|
|
1109
|
+
* arithmetic above, so an unreachable server cannot leave this tab
|
|
1110
|
+
* believing it is elevated. What is lost is only news from elsewhere -- a
|
|
1111
|
+
* grant or a drop in another session -- and that is what the tab picks up
|
|
1112
|
+
* when it next comes into view.
|
|
1113
|
+
*/
|
|
1114
|
+
private scheduleRetry;
|
|
1115
|
+
private clearTimer;
|
|
1116
|
+
private announceToOtherTabs;
|
|
1117
|
+
private readonly onStorage;
|
|
1118
|
+
private readonly onFocus;
|
|
1119
|
+
private readonly onVisibilityChange;
|
|
1120
|
+
/**
|
|
1121
|
+
* The tab is back. Read first -- that alone ends an elevation that ran out
|
|
1122
|
+
* while it was away, with no request and no network to depend on -- then
|
|
1123
|
+
* ask the server for what the read cannot know: a grant or a drop made
|
|
1124
|
+
* elsewhere, or a lifetime the server moved.
|
|
1125
|
+
*/
|
|
1126
|
+
private reconcile;
|
|
1127
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<ElevationService, never>;
|
|
1128
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* A refusal for want of elevation is a 403 the server stamps `X-Elevation-Required`
|
|
1133
|
+
* where its gate refused a member who has not elevated ({@link isElevationRefusal}).
|
|
1134
|
+
* This interceptor never opens the elevation prompt. The prompt opens only when a
|
|
1135
|
+
* person asks for it: an Elevate button, or an action whose control already knows it
|
|
1136
|
+
* needs elevation and offers it ({@link ElevationService.offerFor}).
|
|
1137
|
+
*
|
|
1138
|
+
* What it does with a stamped 403:
|
|
1139
|
+
* - On a write a person made (POST, PUT, PATCH, DELETE without
|
|
1140
|
+
* {@link BACKGROUND_REQUEST}): tells {@link ELEVATION_NOTICE}, which shows the
|
|
1141
|
+
* refusal with an Elevate button. The request is not held and not repeated; the
|
|
1142
|
+
* caller receives the 403 at once, and after elevating the person repeats the action.
|
|
1143
|
+
* - On a read: nothing. Reads are what a page makes as it loads and what polls make in
|
|
1144
|
+
* the background, and the page shows its own state for a refused one -- an inline
|
|
1145
|
+
* "needs an elevated session" with an Elevate button, or the feature hidden.
|
|
1146
|
+
* - On a background request: nothing. Nobody is waiting on it.
|
|
1147
|
+
*
|
|
1148
|
+
* Until 2026-10-08 any stamped 403 opened the prompt, so the setup reads of almost every
|
|
1149
|
+
* console page, and a realtime token at every load, opened it for an administrator who
|
|
1150
|
+
* had not elevated: 13 of 15 pages on a menu click, 15 of 15 on a reload (measured).
|
|
1151
|
+
*
|
|
1152
|
+
* The elevation endpoint's own calls carry {@link BYPASS_ELEVATION}: a wrong password is a
|
|
1153
|
+
* 403 and is not a refused action. A stamped 403 from anywhere but this API is ignored:
|
|
1154
|
+
* the header is the server's word, and only this server's counts.
|
|
1155
|
+
*/
|
|
1156
|
+
declare const elevationInterceptor: HttpInterceptorFn;
|
|
666
1157
|
|
|
667
1158
|
declare class LoginComponent {
|
|
668
1159
|
identifier: string;
|
|
669
1160
|
password: string;
|
|
670
|
-
loading: boolean
|
|
671
|
-
error: string | null
|
|
1161
|
+
readonly loading: i0.WritableSignal<boolean>;
|
|
1162
|
+
readonly error: i0.WritableSignal<string | null>;
|
|
672
1163
|
private readonly store;
|
|
673
1164
|
private readonly router;
|
|
1165
|
+
/** Shown when the guard signed an account out because the console is not granted to it. */
|
|
1166
|
+
static readonly NO_CONSOLE_ACCESS = "This account has no access to the console.";
|
|
1167
|
+
constructor();
|
|
674
1168
|
submit(): void;
|
|
675
1169
|
static ɵfac: i0.ɵɵFactoryDeclaration<LoginComponent, never>;
|
|
676
1170
|
static ɵcmp: i0.ɵɵComponentDeclaration<LoginComponent, "coolms-admin-login", never, {}, {}, never, never, true, never>;
|
|
@@ -684,10 +1178,10 @@ declare class AppInitService {
|
|
|
684
1178
|
private readonly prefs;
|
|
685
1179
|
private readonly crossTabSync;
|
|
686
1180
|
private readonly refreshCoordinator;
|
|
687
|
-
/** The only hardcoded URL
|
|
1181
|
+
/** The only hardcoded URL -- everything else comes from the manifest. */
|
|
688
1182
|
private readonly configUrl;
|
|
689
1183
|
/**
|
|
690
|
-
* Emits (once) when load() has fully completed
|
|
1184
|
+
* Emits (once) when load() has fully completed -- tokens settled, manifest
|
|
691
1185
|
* set (or swallowed on error). authGuard subscribes to this so it never
|
|
692
1186
|
* evaluates isAuthenticated against a partially-restored stale token.
|
|
693
1187
|
*/
|
|
@@ -711,7 +1205,64 @@ declare class AppInitService {
|
|
|
711
1205
|
/** GET an authed endpoint with an explicit Bearer header, bypassing the interceptor. */
|
|
712
1206
|
private probe;
|
|
713
1207
|
static ɵfac: i0.ɵɵFactoryDeclaration<AppInitService, never>;
|
|
714
|
-
static ɵprov: i0.ɵɵInjectableDeclaration<
|
|
1208
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/** What the server said about this sign-in and the console. */
|
|
1212
|
+
type ConsoleAccess = 'granted' | 'refused' | 'unknown';
|
|
1213
|
+
/**
|
|
1214
|
+
* Whether the signed-in account may use the console, and the console's endpoint
|
|
1215
|
+
* map when it may.
|
|
1216
|
+
*
|
|
1217
|
+
* The server decides: `GET /api/v1/admin/manifest` answers the map. Since 2026-10-06
|
|
1218
|
+
* (Dmitry: "no host groups"; access is decided by module groups alone) it answers every
|
|
1219
|
+
* signed-in account: the map grants nothing, each call it names is decided by its own
|
|
1220
|
+
* route, and an account in no group sees the admin with an empty menu. A server from
|
|
1221
|
+
* before that answers only at `/api/v1/console/manifest`, to the `console` group, and
|
|
1222
|
+
* 403 to anyone else -- there a refused sign-in is still signed out; this asks the new
|
|
1223
|
+
* address first and the old one only on its 404. The public theme config
|
|
1224
|
+
* read before sign-in carries only what the sign-in page needs; the rest arrives
|
|
1225
|
+
* here, once per sign-in, and is merged into the app config key by key: a key the
|
|
1226
|
+
* console's map carries replaces the public one whole, so the console's copy of a
|
|
1227
|
+
* section is always the complete one.
|
|
1228
|
+
*
|
|
1229
|
+
* `unknown` is a failure to ask (the network, a 5xx), not an answer: it is not kept,
|
|
1230
|
+
* so while the endpoint fails every navigation asks again.
|
|
1231
|
+
*
|
|
1232
|
+
* !! An answer belongs to the sign-in it was asked for. A sign-in, a sign-out or a
|
|
1233
|
+
* restored session starts a new generation; an answer that arrives for a generation
|
|
1234
|
+
* that is gone is kept for nobody -- A's late 403 must not sign B out -- and whoever
|
|
1235
|
+
* is waiting on it is answered for the sign-in there is now.
|
|
1236
|
+
*
|
|
1237
|
+
* !! With nobody signed in there is nothing to ask, and nothing is asked. Without
|
|
1238
|
+
* that, the re-ask after a sign-out went out with no token, its 401 made the
|
|
1239
|
+
* interceptor dispatch Logout, which started another generation, which re-asked:
|
|
1240
|
+
* a loop for as long as the tab lived (found in review).
|
|
1241
|
+
*/
|
|
1242
|
+
declare class ConsoleAccessService {
|
|
1243
|
+
/** The admin manifest's URL: fixed, like the theme config's, because nothing is known before it. */
|
|
1244
|
+
static readonly URL = "/api/v1/admin/manifest";
|
|
1245
|
+
/** The address before 2026-10-06, asked only when a server older than that answers 404 at {@link URL}. */
|
|
1246
|
+
static readonly LEGACY_URL = "/api/v1/console/manifest";
|
|
1247
|
+
/** The login page's query parameter value for "signed out: no console access". */
|
|
1248
|
+
static readonly REFUSED_REASON = "no-console-access";
|
|
1249
|
+
private readonly http;
|
|
1250
|
+
private readonly store;
|
|
1251
|
+
private answer;
|
|
1252
|
+
private pending;
|
|
1253
|
+
private generation;
|
|
1254
|
+
constructor();
|
|
1255
|
+
/** Ask once per sign-in; the manifest is merged into the app config when granted. */
|
|
1256
|
+
ensure(): Promise<ConsoleAccess>;
|
|
1257
|
+
/**
|
|
1258
|
+
* Sign a refused account out: this sign-in ends on the server (its refresh token
|
|
1259
|
+
* with it), as the account menu's sign-out does, and then in this browser --
|
|
1260
|
+
* whatever the server answered, so a refused request cannot leave it signed in.
|
|
1261
|
+
*/
|
|
1262
|
+
signOut(): Observable<void>;
|
|
1263
|
+
private merge;
|
|
1264
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<ConsoleAccessService, never>;
|
|
1265
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
715
1266
|
}
|
|
716
1267
|
|
|
717
1268
|
/**
|
|
@@ -749,7 +1300,7 @@ interface LayoutHeaderAction {
|
|
|
749
1300
|
disabled?: boolean;
|
|
750
1301
|
/**
|
|
751
1302
|
* Optional capability gate evaluated FE-side against runtime state
|
|
752
|
-
* (e.g. `administer` / `delete`)
|
|
1303
|
+
* (e.g. `administer` / `delete`) -- the config declares WHICH actions
|
|
753
1304
|
* exist + their gate; the FE decides if THIS user/row qualifies. Absent
|
|
754
1305
|
* = always shown. Used by detail pages whose actions are config-driven
|
|
755
1306
|
* (see `web:section-detail`).
|
|
@@ -764,11 +1315,11 @@ interface LayoutHeaderAction {
|
|
|
764
1315
|
* evaluator as a NaviGraph toolbar node, applied
|
|
765
1316
|
* by {@link LayoutActionsService}:
|
|
766
1317
|
*
|
|
767
|
-
* showWhen
|
|
768
|
-
* disabledWhen
|
|
769
|
-
* busyWhen
|
|
770
|
-
* busyLabel
|
|
771
|
-
* activeWhen
|
|
1318
|
+
* showWhen -- the action does not APPLY, so it is absent.
|
|
1319
|
+
* disabledWhen -- temporarily UNAVAILABLE: stays put and greys out.
|
|
1320
|
+
* busyWhen -- the action is RUNNING: greys out too, and may relabel.
|
|
1321
|
+
* busyLabel -- label shown while `busyWhen` holds ("Saving...").
|
|
1322
|
+
* activeWhen -- pressed/active styling.
|
|
772
1323
|
*
|
|
773
1324
|
* Two carriers (a layout config and a NaviGraph tree) but ONE vocabulary:
|
|
774
1325
|
* a page that moves between them should not have to relearn how to say
|
|
@@ -808,7 +1359,7 @@ interface LayoutConfig {
|
|
|
808
1359
|
headerActions?: LayoutHeaderAction[];
|
|
809
1360
|
/**
|
|
810
1361
|
* Actions rendered in the page's fixed bottom bar (`<cms-detail-footer>`)
|
|
811
|
-
*
|
|
1362
|
+
* -- primary / destructive actions kept out of the cramped header.
|
|
812
1363
|
* Same shape as `headerActions`; gated per-item via `requires`.
|
|
813
1364
|
*/
|
|
814
1365
|
footerActions?: LayoutHeaderAction[];
|
|
@@ -831,7 +1382,8 @@ interface DatagridConfig {
|
|
|
831
1382
|
* Fetches config YAMLs (served as JSON) from GET /api/v1/config/{type}/{id}.
|
|
832
1383
|
*
|
|
833
1384
|
* Responses are cached for the lifetime of the service (Map + shareReplay(1))
|
|
834
|
-
* so repeated calls never re-fetch the same resource.
|
|
1385
|
+
* so repeated calls never re-fetch the same resource. A failed read is dropped
|
|
1386
|
+
* from the cache, so the next call asks the server again.
|
|
835
1387
|
*
|
|
836
1388
|
* configBase is read from the API manifest (manifest.configBase) and falls
|
|
837
1389
|
* back to '/api/v1/config' when the manifest is not yet loaded.
|
|
@@ -847,7 +1399,7 @@ declare class ConfigService {
|
|
|
847
1399
|
datagrid(id: string): Observable<DatagridConfig>;
|
|
848
1400
|
private fetch;
|
|
849
1401
|
static ɵfac: i0.ɵɵFactoryDeclaration<ConfigService, never>;
|
|
850
|
-
static ɵprov: i0.ɵɵInjectableDeclaration<
|
|
1402
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
851
1403
|
}
|
|
852
1404
|
|
|
853
1405
|
declare class SetAppConfig {
|
|
@@ -861,16 +1413,18 @@ interface AppConfigStateModel {
|
|
|
861
1413
|
}
|
|
862
1414
|
declare class AppConfigState {
|
|
863
1415
|
set(ctx: StateContext<AppConfigStateModel>, { config }: SetAppConfig): void;
|
|
1416
|
+
/** The whole config as last set: the public theme config, with the console's manifest once granted. */
|
|
1417
|
+
static config(state: AppConfigStateModel): ThemeConfigResponse | null;
|
|
864
1418
|
static manifest(state: AppConfigStateModel): ApiManifest | null;
|
|
865
1419
|
/**
|
|
866
|
-
*
|
|
1420
|
+
* The viewer manifest. Null when the backend didn't ship a viewers
|
|
867
1421
|
* section (no tagged providers); consumers should fall back to a
|
|
868
1422
|
* "no preview" surface.
|
|
869
1423
|
*/
|
|
870
1424
|
static viewers(state: AppConfigStateModel): ViewerApiManifest | null;
|
|
871
1425
|
static loaded(state: AppConfigStateModel): boolean;
|
|
872
1426
|
static ɵfac: i0.ɵɵFactoryDeclaration<AppConfigState, never>;
|
|
873
|
-
static ɵprov: i0.ɵɵInjectableDeclaration<
|
|
1427
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
874
1428
|
}
|
|
875
1429
|
|
|
876
1430
|
/**
|
|
@@ -887,19 +1441,19 @@ declare class ErrorHandlerService {
|
|
|
887
1441
|
humanize(err: unknown): string;
|
|
888
1442
|
private statusMessage;
|
|
889
1443
|
static ɵfac: i0.ɵɵFactoryDeclaration<ErrorHandlerService, never>;
|
|
890
|
-
static ɵprov: i0.ɵɵInjectableDeclaration<
|
|
1444
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
891
1445
|
}
|
|
892
1446
|
|
|
893
1447
|
/** What the user PICKED. `system` defers to the OS. */
|
|
894
1448
|
type ThemeChoice = 'light' | 'dark' | 'system';
|
|
895
|
-
/** What that resolves to right now
|
|
1449
|
+
/** What that resolves to right now -- the only two the stylesheet knows. */
|
|
896
1450
|
type ResolvedTheme = 'light' | 'dark';
|
|
897
1451
|
/**
|
|
898
1452
|
* Applies the user's theme by setting `data-theme` on the document element.
|
|
899
1453
|
*
|
|
900
1454
|
* The stylesheet does the rest: `:root[data-theme='dark']` re-points the
|
|
901
1455
|
* `--cms-*` palette and every colour in the admin arrives through those names.
|
|
902
|
-
* Nothing here knows a single colour, which is the point
|
|
1456
|
+
* Nothing here knows a single colour, which is the point -- a second
|
|
903
1457
|
* theme is a block of tokens, not a code change.
|
|
904
1458
|
*
|
|
905
1459
|
* Follows the shape of UserCalendarPreferencesService and
|
|
@@ -923,7 +1477,7 @@ declare class ThemeService {
|
|
|
923
1477
|
readonly platformAccent: i0.Signal<string | null>;
|
|
924
1478
|
/**
|
|
925
1479
|
* The colour actually painted: personal override, else the deployment's,
|
|
926
|
-
* else null
|
|
1480
|
+
* else null -- which leaves the stylesheet's own accent in place.
|
|
927
1481
|
*
|
|
928
1482
|
* Three rungs and each is a real state. A user who has chosen nothing is
|
|
929
1483
|
* NOT the same as one who chose the deployment's colour: clear the
|
|
@@ -935,13 +1489,13 @@ declare class ThemeService {
|
|
|
935
1489
|
constructor();
|
|
936
1490
|
/**
|
|
937
1491
|
* One-shot load of the stored preference. Safe to call more than once and
|
|
938
|
-
* safe for anonymous users
|
|
1492
|
+
* safe for anonymous users -- a failure leaves the cached/system value in
|
|
939
1493
|
* place rather than forcing light, so the login screen still matches the OS.
|
|
940
1494
|
*/
|
|
941
1495
|
ensureLoaded(): Observable<ThemeChoice>;
|
|
942
1496
|
/**
|
|
943
1497
|
* Called by the Profile page after the Preferences tab saves, so the admin
|
|
944
|
-
* re-themes on the spot. Ignores anything that is not a known choice
|
|
1498
|
+
* re-themes on the spot. Ignores anything that is not a known choice --
|
|
945
1499
|
* the section PATCH returns the whole merged bag, not just this field.
|
|
946
1500
|
*/
|
|
947
1501
|
update(value: unknown): void;
|
|
@@ -955,14 +1509,14 @@ declare class ThemeService {
|
|
|
955
1509
|
/**
|
|
956
1510
|
* The manifest is loaded at bootstrap, well before the authenticated shell
|
|
957
1511
|
* calls this, so a snapshot read is enough and no subscription is needed.
|
|
958
|
-
* A malformed value is ignored rather than applied
|
|
1512
|
+
* A malformed value is ignored rather than applied -- the container refuses
|
|
959
1513
|
* to build with one, but the manifest crosses the network all the same.
|
|
960
1514
|
*/
|
|
961
1515
|
private readPlatformAccent;
|
|
962
1516
|
private setAccent;
|
|
963
1517
|
/**
|
|
964
1518
|
* Writes the override onto the document element, where an inline style
|
|
965
|
-
* outranks every stylesheet rule
|
|
1519
|
+
* outranks every stylesheet rule -- including the dark block -- so one
|
|
966
1520
|
* declaration re-points the accent in both themes.
|
|
967
1521
|
*
|
|
968
1522
|
* Only --cms-accent is set, not the -hover/-light/-text members of the
|
|
@@ -976,7 +1530,7 @@ declare class ThemeService {
|
|
|
976
1530
|
private static readCache;
|
|
977
1531
|
private static readAccentCache;
|
|
978
1532
|
static ɵfac: i0.ɵɵFactoryDeclaration<ThemeService, never>;
|
|
979
|
-
static ɵprov: i0.ɵɵInjectableDeclaration<
|
|
1533
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
980
1534
|
}
|
|
981
1535
|
|
|
982
1536
|
/**
|
|
@@ -988,7 +1542,7 @@ declare class ThemeService {
|
|
|
988
1542
|
* A spinner says "something is happening" and nothing else; every product has
|
|
989
1543
|
* one and none of them are yours. The bang is already the one glyph in the
|
|
990
1544
|
* wordmark that carries the brand, so a loader built from it says whose
|
|
991
|
-
* software is thinking
|
|
1545
|
+
* software is thinking -- for free, in the place a user is already looking.
|
|
992
1546
|
*
|
|
993
1547
|
* ## Why one component rather than a snippet per surface
|
|
994
1548
|
*
|
|
@@ -999,7 +1553,7 @@ declare class ThemeService {
|
|
|
999
1553
|
*
|
|
1000
1554
|
* ## The animation
|
|
1001
1555
|
*
|
|
1002
|
-
* The stem wipes upward and the dot lands after it, on a loop
|
|
1556
|
+
* The stem wipes upward and the dot lands after it, on a loop -- the shape being
|
|
1003
1557
|
* WRITTEN rather than spun. It is deliberately calm: a loader is background
|
|
1004
1558
|
* furniture, and anything with a hard beat gets irritating on the third viewing.
|
|
1005
1559
|
* Under `prefers-reduced-motion` it holds still and only breathes, because
|
|
@@ -1017,7 +1571,7 @@ declare class CmsLoaderComponent {
|
|
|
1017
1571
|
readonly inline: i0.InputSignal<boolean>;
|
|
1018
1572
|
/**
|
|
1019
1573
|
* A clip-path needs a document-unique id, and this component is used more
|
|
1020
|
-
* than once per page
|
|
1574
|
+
* than once per page -- two loaders sharing an id makes the second one clip
|
|
1021
1575
|
* against the first one's rectangle and stop animating.
|
|
1022
1576
|
*/
|
|
1023
1577
|
protected readonly clipId: string;
|
|
@@ -1026,7 +1580,7 @@ declare class CmsLoaderComponent {
|
|
|
1026
1580
|
}
|
|
1027
1581
|
|
|
1028
1582
|
/**
|
|
1029
|
-
* Unified user preferences service
|
|
1583
|
+
* Unified user preferences service -- all preference types share one
|
|
1030
1584
|
* localStorage key (`coolms_ui_prefs`) and one debounced server-sync pipeline.
|
|
1031
1585
|
*
|
|
1032
1586
|
* Key naming convention (stored as nested objects, referenced via dot-notation):
|
|
@@ -1147,7 +1701,7 @@ declare class UserPreferencesService {
|
|
|
1147
1701
|
/** @deprecated Use setPanelState('media_panel_left', ...) instead */
|
|
1148
1702
|
setMediaCollectionsPanel(width: number, collapsed: boolean): void;
|
|
1149
1703
|
static ɵfac: i0.ɵɵFactoryDeclaration<UserPreferencesService, never>;
|
|
1150
|
-
static ɵprov: i0.ɵɵInjectableDeclaration<
|
|
1704
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
1151
1705
|
}
|
|
1152
1706
|
|
|
1153
1707
|
/**
|
|
@@ -1195,7 +1749,7 @@ declare class SidebarStateService {
|
|
|
1195
1749
|
private readItemsPersisted;
|
|
1196
1750
|
private persistItems;
|
|
1197
1751
|
static ɵfac: i0.ɵɵFactoryDeclaration<SidebarStateService, never>;
|
|
1198
|
-
static ɵprov: i0.ɵɵInjectableDeclaration<
|
|
1752
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
1199
1753
|
}
|
|
1200
1754
|
|
|
1201
1755
|
/**
|
|
@@ -1203,7 +1757,7 @@ declare class SidebarStateService {
|
|
|
1203
1757
|
* what the picker emits via its `valueChange` output (and what the
|
|
1204
1758
|
* template's persisted context carries). `label` is the primary
|
|
1205
1759
|
* display line. `secondary` is an optional second line for
|
|
1206
|
-
* disambiguation (email, slug, etc.)
|
|
1760
|
+
* disambiguation (email, slug, etc.) -- the backend omits the key
|
|
1207
1761
|
* when there is nothing to show; receiving `null` from a custom
|
|
1208
1762
|
* resolver is normalized to `undefined` here.
|
|
1209
1763
|
*/
|
|
@@ -1215,7 +1769,7 @@ interface EntitySearchResult {
|
|
|
1215
1769
|
/**
|
|
1216
1770
|
* HTTP client for the Phase 2a `/api/entity-search` endpoint.
|
|
1217
1771
|
* Returns a stream of search results for the given entity FQCN.
|
|
1218
|
-
* Permission filtering happens at the backend resolver layer
|
|
1772
|
+
* Permission filtering happens at the backend resolver layer --
|
|
1219
1773
|
* this service is a thin RPC.
|
|
1220
1774
|
*/
|
|
1221
1775
|
declare class EntitySearchService {
|
|
@@ -1224,21 +1778,21 @@ declare class EntitySearchService {
|
|
|
1224
1778
|
* @param entityType Fully-qualified PHP class name of the entity
|
|
1225
1779
|
* being searched (e.g. the backend's User entity).
|
|
1226
1780
|
* @param query Free-text search input. Empty string is
|
|
1227
|
-
* valid
|
|
1781
|
+
* valid -- the backend returns the default
|
|
1228
1782
|
* listing for that entity type.
|
|
1229
1783
|
* @param limit Max rows to return; clamped server-side to
|
|
1230
1784
|
* [1, 100].
|
|
1231
1785
|
*/
|
|
1232
1786
|
search(entityType: string, query: string, limit?: number): Observable<EntitySearchResult[]>;
|
|
1233
1787
|
static ɵfac: i0.ɵɵFactoryDeclaration<EntitySearchService, never>;
|
|
1234
|
-
static ɵprov: i0.ɵɵInjectableDeclaration<
|
|
1788
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
1235
1789
|
}
|
|
1236
1790
|
|
|
1237
1791
|
/**
|
|
1238
1792
|
* Well-known click target values embedded in meta.target.
|
|
1239
|
-
* 'action.logout'
|
|
1240
|
-
* 'route'
|
|
1241
|
-
* '_blank'
|
|
1793
|
+
* 'action.logout' -- dispatch Logout action
|
|
1794
|
+
* 'route' -- navigate via routerLink (default when target absent)
|
|
1795
|
+
* '_blank' -- open in a new tab (hrefOverride must be set)
|
|
1242
1796
|
*/
|
|
1243
1797
|
type NaviGraphTarget = 'action.logout' | 'route' | '_blank' | (string & {});
|
|
1244
1798
|
interface NaviGraphMeta {
|
|
@@ -1291,9 +1845,9 @@ declare class NaviGraphService {
|
|
|
1291
1845
|
* Handle a nav-node click.
|
|
1292
1846
|
*
|
|
1293
1847
|
* Routing logic based on meta.target:
|
|
1294
|
-
* 'action.logout'
|
|
1295
|
-
* '_blank'
|
|
1296
|
-
* default
|
|
1848
|
+
* 'action.logout' -- dispatch Logout
|
|
1849
|
+
* '_blank' -- window.open (uses node.path as href)
|
|
1850
|
+
* default -- router.navigate with meta.routerLink or node.path
|
|
1297
1851
|
*/
|
|
1298
1852
|
handleClick(node: NaviGraphNode): void;
|
|
1299
1853
|
/**
|
|
@@ -1304,7 +1858,7 @@ declare class NaviGraphService {
|
|
|
1304
1858
|
isVisible(node: NaviGraphNode, record: Record<string, unknown>): boolean;
|
|
1305
1859
|
/**
|
|
1306
1860
|
* Standalone `showWhen` evaluator for callers that don't have a
|
|
1307
|
-
* full `NaviGraphNode` (e.g. a grid's row context menu
|
|
1861
|
+
* full `NaviGraphNode` (e.g. a grid's row context menu -- a
|
|
1308
1862
|
* `rowAction.showWhen` predicate is evaluated against the row's
|
|
1309
1863
|
* own projection). `null`/`undefined` predicates default to
|
|
1310
1864
|
* visible, mirroring `isVisible`'s "no rule = always shown"
|
|
@@ -1325,7 +1879,7 @@ declare class NaviGraphService {
|
|
|
1325
1879
|
*/
|
|
1326
1880
|
private buildTree;
|
|
1327
1881
|
static ɵfac: i0.ɵɵFactoryDeclaration<NaviGraphService, never>;
|
|
1328
|
-
static ɵprov: i0.ɵɵInjectableDeclaration<
|
|
1882
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
1329
1883
|
}
|
|
1330
1884
|
|
|
1331
1885
|
/**
|
|
@@ -1334,10 +1888,10 @@ declare class NaviGraphService {
|
|
|
1334
1888
|
* Backed by a static Map so components registered at module load time
|
|
1335
1889
|
* (e.g. in app.config.ts) are visible to all injected instances.
|
|
1336
1890
|
*
|
|
1337
|
-
* Usage
|
|
1891
|
+
* Usage -- static (app.config.ts / bootstrap time):
|
|
1338
1892
|
* ComponentRegistry.register('MediaLibraryPage', MediaLibraryPage);
|
|
1339
1893
|
*
|
|
1340
|
-
* Usage
|
|
1894
|
+
* Usage -- injected (SlotComponent / runtime):
|
|
1341
1895
|
* private readonly registry = inject(ComponentRegistry);
|
|
1342
1896
|
* const cls = this.registry.get('MediaLibraryPage');
|
|
1343
1897
|
*/
|
|
@@ -1352,9 +1906,217 @@ declare class ComponentRegistry {
|
|
|
1352
1906
|
has(key: string): boolean;
|
|
1353
1907
|
keys(): string[];
|
|
1354
1908
|
static ɵfac: i0.ɵɵFactoryDeclaration<ComponentRegistry, never>;
|
|
1355
|
-
static ɵprov: i0.ɵɵInjectableDeclaration<
|
|
1909
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
1356
1910
|
}
|
|
1357
1911
|
|
|
1912
|
+
/**
|
|
1913
|
+
* `console` -- the administration host contract (the platform rule: hosts implement contracts, modules offer entries).
|
|
1914
|
+
*
|
|
1915
|
+
* A module contributes to the console with ONE object, its entry: data the
|
|
1916
|
+
* host reads, never a call the module makes into the host. Six lists, each
|
|
1917
|
+
* optional; an absent list means "nothing here", never an error. The host
|
|
1918
|
+
* assembles the entries (build time today, install time under federation),
|
|
1919
|
+
* refuses collisions by name, and activates only the modules the backend's
|
|
1920
|
+
* manifest says are installed.
|
|
1921
|
+
*
|
|
1922
|
+
* The version is MAJOR.MINOR: a minor adds an optional list or field, a major
|
|
1923
|
+
* changes or removes one. An entry declares the range it was written for
|
|
1924
|
+
* (`^1.2` -- major 1, minor at least 2), checked at build against this
|
|
1925
|
+
* constant and at install against the theme's declaration.
|
|
1926
|
+
*/
|
|
1927
|
+
declare const CONSOLE_CONTRACT: {
|
|
1928
|
+
readonly name: "console";
|
|
1929
|
+
readonly version: "1.0";
|
|
1930
|
+
};
|
|
1931
|
+
interface ConsoleEntry {
|
|
1932
|
+
/** The backend module id, as `config/modules/<id>`. Activation is by this id. */
|
|
1933
|
+
readonly module: string;
|
|
1934
|
+
readonly contract: 'console';
|
|
1935
|
+
/** The console versions this entry was written for: a caret range over MAJOR.MINOR, e.g. '^1.0'. */
|
|
1936
|
+
readonly range: string;
|
|
1937
|
+
readonly routes?: readonly ConsoleRouteMount[];
|
|
1938
|
+
readonly bindings?: readonly ConsoleBinding[];
|
|
1939
|
+
readonly topbar?: readonly ConsoleTopbarItem[];
|
|
1940
|
+
readonly overlays?: readonly ConsoleOverlay[];
|
|
1941
|
+
readonly panels?: readonly ConsolePanel[];
|
|
1942
|
+
readonly states?: readonly Type<unknown>[];
|
|
1943
|
+
readonly providers?: readonly ConsoleProvision[];
|
|
1944
|
+
}
|
|
1945
|
+
/** What a route's `data` carries for the shell: the sidebar item to light, and whether the page owns its height. */
|
|
1946
|
+
interface ConsoleNavHint {
|
|
1947
|
+
readonly activeNav?: string;
|
|
1948
|
+
readonly fullHeight?: boolean;
|
|
1949
|
+
/** An intermediate crumb the top bar renders between Home and the page. */
|
|
1950
|
+
readonly breadcrumb?: {
|
|
1951
|
+
readonly label: string;
|
|
1952
|
+
readonly routerLink: string;
|
|
1953
|
+
};
|
|
1954
|
+
/** Which server layout the page renders, for the generic layout pages. */
|
|
1955
|
+
readonly layoutId?: string;
|
|
1956
|
+
}
|
|
1957
|
+
/**
|
|
1958
|
+
* A route mounted under the console's authenticated layout. The host adds
|
|
1959
|
+
* what a module may not decide: the layout component, `canActivate: [authGuard]`,
|
|
1960
|
+
* and a `canMatch` that answers whether the module is installed here. Routes
|
|
1961
|
+
* are lazy: a static component here would put every module's pages in `main`.
|
|
1962
|
+
*/
|
|
1963
|
+
type ConsoleRouteMount = {
|
|
1964
|
+
readonly path: string;
|
|
1965
|
+
readonly children: () => Promise<Routes>;
|
|
1966
|
+
readonly nav?: ConsoleNavHint;
|
|
1967
|
+
readonly providers?: readonly Provider[];
|
|
1968
|
+
} | {
|
|
1969
|
+
readonly path: string;
|
|
1970
|
+
readonly component: () => Promise<Type<unknown>>;
|
|
1971
|
+
readonly nav?: ConsoleNavHint;
|
|
1972
|
+
readonly providers?: readonly Provider[];
|
|
1973
|
+
} | {
|
|
1974
|
+
readonly path: string;
|
|
1975
|
+
readonly redirectTo: string;
|
|
1976
|
+
};
|
|
1977
|
+
/** A `ComponentRegistry` name -> class: names a module OWNS (its server layouts name them) and names it fills in another module's slot (`profile.tab:call`). */
|
|
1978
|
+
interface ConsoleBinding {
|
|
1979
|
+
readonly name: string;
|
|
1980
|
+
readonly component: Type<unknown>;
|
|
1981
|
+
}
|
|
1982
|
+
/** A tile in the top bar's quick-access strip, rendered in ascending `order`. */
|
|
1983
|
+
interface ConsoleTopbarItem {
|
|
1984
|
+
readonly id: string;
|
|
1985
|
+
readonly order: number;
|
|
1986
|
+
readonly component: Type<unknown>;
|
|
1987
|
+
}
|
|
1988
|
+
/** Mounted once, inside the authenticated layout, outside the router outlet. */
|
|
1989
|
+
interface ConsoleOverlay {
|
|
1990
|
+
readonly id: string;
|
|
1991
|
+
readonly component: Type<unknown>;
|
|
1992
|
+
}
|
|
1993
|
+
/**
|
|
1994
|
+
* A dock panel the layout opens and closes: the host renders its toggle in
|
|
1995
|
+
* the top bar and the panel in the dock, and the panel talks back through
|
|
1996
|
+
* {@link ConsolePanelHost} -- not through outputs, which a slot cannot carry.
|
|
1997
|
+
*/
|
|
1998
|
+
interface ConsolePanel {
|
|
1999
|
+
readonly id: string;
|
|
2000
|
+
readonly dock: 'bottom';
|
|
2001
|
+
/** The top-bar toggle: a label (the title), an icon name or a short text, and a key the host binds with Ctrl. */
|
|
2002
|
+
readonly toggle: {
|
|
2003
|
+
readonly label: string;
|
|
2004
|
+
readonly icon?: string;
|
|
2005
|
+
readonly text?: string;
|
|
2006
|
+
readonly key?: string;
|
|
2007
|
+
};
|
|
2008
|
+
readonly component: Type<unknown>;
|
|
2009
|
+
}
|
|
2010
|
+
/**
|
|
2011
|
+
* What a `provide*` line is: environment providers from a library's registry
|
|
2012
|
+
* (editor extensions, viewers, field widgets, file editors), or the
|
|
2013
|
+
* implementation of a single-implementation port, listed as such so the
|
|
2014
|
+
* assembly can refuse two modules implementing one port by name.
|
|
2015
|
+
*/
|
|
2016
|
+
type ConsoleProvision = EnvironmentProviders | Provider | ConsolePortProvision;
|
|
2017
|
+
interface ConsolePortProvision {
|
|
2018
|
+
readonly port: InjectionToken<unknown>;
|
|
2019
|
+
readonly useExisting: Type<unknown>;
|
|
2020
|
+
}
|
|
2021
|
+
declare function isPortProvision(p: ConsoleProvision): p is ConsolePortProvision;
|
|
2022
|
+
/** The identity function with the type: an entry file exports data, nothing runs. */
|
|
2023
|
+
declare function consoleEntry(entry: ConsoleEntry): ConsoleEntry;
|
|
2024
|
+
/**
|
|
2025
|
+
* Whether a caret range over MAJOR.MINOR admits a version: same major, minor
|
|
2026
|
+
* at least the range's. Malformed input admits nothing -- a range that cannot
|
|
2027
|
+
* be read must refuse, never pass.
|
|
2028
|
+
*/
|
|
2029
|
+
declare function satisfiesRange(range: string, version: string): boolean;
|
|
2030
|
+
/** The refusal an assembly raises: one message per collision, all of them, by name. */
|
|
2031
|
+
declare class ConsoleAssemblyError extends Error {
|
|
2032
|
+
readonly problems: readonly string[];
|
|
2033
|
+
constructor(problems: readonly string[]);
|
|
2034
|
+
}
|
|
2035
|
+
/**
|
|
2036
|
+
* The checks every assembly runs, in one place so the build-time spec, the
|
|
2037
|
+
* bootstrap and a future install-time loader agree: entry ranges against
|
|
2038
|
+
* this contract, and the collision rules of the six lists. Returns the
|
|
2039
|
+
* entries unchanged when nothing is wrong.
|
|
2040
|
+
*/
|
|
2041
|
+
declare function assembleConsole(entries: readonly ConsoleEntry[]): readonly ConsoleEntry[];
|
|
2042
|
+
|
|
2043
|
+
/**
|
|
2044
|
+
* The host side of `console@1`: what a theme calls to turn the assembled
|
|
2045
|
+
* entries into routes, providers and the lists its shell renders.
|
|
2046
|
+
*
|
|
2047
|
+
* Activation is the backend's: `GET /app-config` carries `ui.modules`, the
|
|
2048
|
+
* modules whose console entry the installer matched against the theme's
|
|
2049
|
+
* declaration. A module in the bundle but not in that list is dormant -- no
|
|
2050
|
+
* route matches, no tile, no overlay; a module in the list but not in the
|
|
2051
|
+
* bundle is reported by name ({@link ConsoleActivation.missing}), never
|
|
2052
|
+
* silently absent.
|
|
2053
|
+
*/
|
|
2054
|
+
/** Every entry this build assembled, after {@link assembleConsole}'s checks. */
|
|
2055
|
+
declare const CONSOLE_ENTRIES: InjectionToken<readonly ConsoleEntry[]>;
|
|
2056
|
+
/**
|
|
2057
|
+
* The port a dock panel talks back through: the layout provides one per
|
|
2058
|
+
* panel it mounts, so the panel never needs an output the slot cannot carry.
|
|
2059
|
+
*/
|
|
2060
|
+
declare abstract class ConsolePanelHost {
|
|
2061
|
+
abstract close(): void;
|
|
2062
|
+
abstract maximize(on: boolean): void;
|
|
2063
|
+
}
|
|
2064
|
+
/** The console modules the manifest says are installed here. */
|
|
2065
|
+
declare function installedConsoleModules(manifest: ApiManifest | null): ReadonlySet<string>;
|
|
2066
|
+
declare class ConsoleActivation {
|
|
2067
|
+
private readonly entries;
|
|
2068
|
+
private readonly manifest;
|
|
2069
|
+
readonly installed: i0.Signal<ReadonlySet<string>>;
|
|
2070
|
+
/** The entries of installed modules, in assembly order. */
|
|
2071
|
+
readonly active: i0.Signal<ConsoleEntry[]>;
|
|
2072
|
+
readonly topbar: i0.Signal<readonly ConsoleTopbarItem[]>;
|
|
2073
|
+
readonly overlays: i0.Signal<readonly ConsoleOverlay[]>;
|
|
2074
|
+
readonly panels: i0.Signal<readonly ConsolePanel[]>;
|
|
2075
|
+
/** Modules the manifest names that this build has no entry for: what a diagnostics page shows by name. */
|
|
2076
|
+
readonly missing: i0.Signal<string[]>;
|
|
2077
|
+
isInstalled(module: string): boolean;
|
|
2078
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<ConsoleActivation, never>;
|
|
2079
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
2080
|
+
}
|
|
2081
|
+
/**
|
|
2082
|
+
* `canMatch` for a module's mount: the route exists only where the module is
|
|
2083
|
+
* installed. Answered once the manifest is in: on a cold deep link the router
|
|
2084
|
+
* recognises the URL while the initializer is still fetching `/theme/config`
|
|
2085
|
+
* (the two run as parallel initializers), and a synchronous read then found
|
|
2086
|
+
* no module installed, so every pasted module URL fell to the wildcard and
|
|
2087
|
+
* landed on the dashboard (measured 2026-09-22: no chunk requested for the
|
|
2088
|
+
* mount, the dashboard's instead). `ready$` is what the auth guard waits on
|
|
2089
|
+
* too, and it fires whether or not the config fetch succeeded.
|
|
2090
|
+
*
|
|
2091
|
+
* !! Then the console's own manifest, when someone is signed in: `ui.modules` is
|
|
2092
|
+
* the console's, not the public config's, and arrives with it
|
|
2093
|
+
* ({@link ConsoleAccessService}). canMatch runs before the layout's auth guard,
|
|
2094
|
+
* which asks the same question, so answering from the public config alone said
|
|
2095
|
+
* "no" to every module once that config stopped carrying them. Nobody signed in:
|
|
2096
|
+
* nothing is asked, the answer is the public config's, and the layout's guard
|
|
2097
|
+
* sends the person to sign in. A refusal merges nothing, so the answer is no.
|
|
2098
|
+
*/
|
|
2099
|
+
declare function consoleModuleInstalled(module: string): CanMatchFn;
|
|
2100
|
+
/**
|
|
2101
|
+
* The children a theme mounts under its authenticated layout for the
|
|
2102
|
+
* assembled entries: one `Route` per mount, lazy as written, `data` from the
|
|
2103
|
+
* entry's nav hint, `canMatch` from the manifest. The theme keeps its own
|
|
2104
|
+
* shape around them (login, the layout, `canActivate: [authGuard]`, the
|
|
2105
|
+
* wildcard).
|
|
2106
|
+
*/
|
|
2107
|
+
declare function consoleChildren(entries: readonly ConsoleEntry[]): Routes;
|
|
2108
|
+
/**
|
|
2109
|
+
* The providers a theme adds for the assembled entries: the store with the
|
|
2110
|
+
* host's states and every module state, the registry bindings (an
|
|
2111
|
+
* initializer, so they exist before the first render), every module
|
|
2112
|
+
* provision, and {@link CONSOLE_ENTRIES} for the shell. Place it AFTER the
|
|
2113
|
+
* library providers a module's provisions extend (the editor bridge, the
|
|
2114
|
+
* PDF viewer), as the hand-written lines were.
|
|
2115
|
+
*/
|
|
2116
|
+
declare function provideConsole(entries: readonly ConsoleEntry[], options: {
|
|
2117
|
+
readonly hostStates: readonly Type<unknown>[];
|
|
2118
|
+
}): EnvironmentProviders;
|
|
2119
|
+
|
|
1358
2120
|
/**
|
|
1359
2121
|
* Phase H7 -- admin Site Selector.
|
|
1360
2122
|
*
|
|
@@ -1411,5 +2173,5 @@ interface CurrentSectionPort {
|
|
|
1411
2173
|
*/
|
|
1412
2174
|
declare const CURRENT_SECTION: InjectionToken<CurrentSectionPort>;
|
|
1413
2175
|
|
|
1414
|
-
export { AppConfigState, AppInitService, AuthRefreshCoordinator, AuthState, BYPASS_AUTH, CURRENT_SECTION, CmsLoaderComponent, ComponentRegistry, ConfigService, CrossTabAuthSyncService, EntitySearchService, ErrorHandlerService, IdentityApiClient, Login, LoginComponent, Logout, NaviGraphService, PatchCurrentUser, RealtimeTokenClient, RestoreSession, SetAppConfig, SetTokens, SidebarStateService, ThemeService, UserPreferencesService, authGuard, authInterceptor, loginPageGuard, resolvePattern, sectionInterceptor };
|
|
1415
|
-
export type { ApiManifest, AppConfigStateModel, AuthApiManifest, AuthStateModel, CentrifugoConnectionTokenDto, CentrifugoSubscriptionTokenDto, ContentApiManifest, CurrentSectionPort, DataGridApiManifest, DataGridPreference, DataGridPreferences, DataSourceDefinition, DataSourceLoading, DataSourceOption, DataSourceType, DataSourceWidget, DatagridConfig, DialogConfig, DocumentApiManifest, DomainExplorerApiManifest, DynamicEntityApiManifest, EditorApiManifest, EditorApiManifestEntry, EditorApiManifestProfile, EntitySearchResult, FieldItem, FieldSecurityPolicy, FieldType, FormAction, FormActionType, FormActionVariant, FormConfig, FormRenderDefinition, HydraCollection, HydraView, IdentityApiManifest, LayoutConfig, LayoutHeaderAction, LayoutNode, LayoutSection, MediaApiManifest, NaviApiManifest, NaviGraphMeta, NaviGraphNode, NaviGraphNodeFlat, NaviGraphTarget, NavigraphConfig, PlatformDefaults, RelationDefinition, ResolvedTheme, SectionApiManifest, SubFormDefinition, TerminalApiManifest, ThemeChoice, ThemeConfigResponse, TokenResponse, UserDto, ValidatorDefinition, VfsApiManifest, ViewerApiManifest, ViewerDefinitionApiManifest, ViewerProfileApiManifest, VisibilityCondition, VisibilityOperator };
|
|
2176
|
+
export { AppConfigState, AppInitService, AuthRefreshCoordinator, AuthState, BACKGROUND_REQUEST, BYPASS_AUTH, BYPASS_ELEVATION, CONSOLE_CONTRACT, CONSOLE_ENTRIES, CURRENT_SECTION, CmsLoaderComponent, ComponentRegistry, ConfigService, ConsoleAccessService, ConsoleActivation, ConsoleAssemblyError, ConsolePanelHost, CrossTabAuthSyncService, ELEVATION_NOTICE, ELEVATION_PROMPT, ELEVATION_REQUIRED_HEADER, ELEVATION_WARNING_FACTOR_MISSING, ELEVATION_WARNING_MFA_OFF, ELEVATION_WARNING_PASSWORD_NOT_SET, ElevationService, EntitySearchService, ErrorHandlerService, IdentityApiClient, Login, LoginComponent, Logout, NaviGraphService, PatchCurrentUser, RealtimeTokenClient, RestoreSession, SetAppConfig, SetTokens, SidebarStateService, ThemeService, UserPreferencesService, assembleConsole, authGuard, authInterceptor, consoleChildren, consoleEntry, consoleModuleInstalled, elevationInterceptor, installedConsoleModules, isElevationRefusal, isPortProvision, loginPageGuard, provideConsole, resolvePattern, satisfiesRange, sectionInterceptor };
|
|
2177
|
+
export type { ApiManifest, AppConfigStateModel, AuthApiManifest, AuthStateModel, CentrifugoConnectionTokenDto, CentrifugoSubscriptionTokenDto, ConsoleAccess, ConsoleBinding, ConsoleEntry, ConsoleManifestResponse, ConsoleNavHint, ConsoleOverlay, ConsolePanel, ConsolePortProvision, ConsoleProvision, ConsoleRouteMount, ConsoleTopbarItem, ContentApiManifest, CurrentSectionPort, DataGridApiManifest, DataGridPreference, DataGridPreferences, DataSourceDefinition, DataSourceLoading, DataSourceOption, DataSourceType, DataSourceWidget, DatagridConfig, DialogConfig, DocumentApiManifest, DomainExplorerApiManifest, DynamicEntityApiManifest, EditorApiManifest, EditorApiManifestEntry, EditorApiManifestProfile, ElevationChange, ElevationChangeKind, ElevationDropReason, ElevationEnded, ElevationEndedReason, ElevationNoticePort, ElevationPromptPort, ElevationPromptRequest, ElevationState, ElevationWarning, EntitySearchResult, FieldItem, FieldSecurityPolicy, FieldType, FormAction, FormActionType, FormActionVariant, FormConfig, FormRenderDefinition, HydraCollection, HydraView, IdentityApiManifest, LayoutConfig, LayoutHeaderAction, LayoutNode, LayoutSection, MediaApiManifest, NaviApiManifest, NaviGraphMeta, NaviGraphNode, NaviGraphNodeFlat, NaviGraphTarget, NavigraphConfig, PlatformDefaults, RelationDefinition, ResolvedTheme, SectionApiManifest, SubFormDefinition, TerminalApiManifest, ThemeChoice, ThemeConfigResponse, TokenResponse, UiApiManifest, UiModuleEntryManifest, UserDto, ValidatorDefinition, VfsApiManifest, ViewerApiManifest, ViewerDefinitionApiManifest, ViewerProfileApiManifest, VisibilityCondition, VisibilityOperator };
|