@coolms/core-angular 2.0.0-alpha.4 → 2.0.0-alpha.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coolms/core-angular",
3
- "version": "2.0.0-alpha.4",
3
+ "version": "2.0.0-alpha.6",
4
4
  "description": "The CoolMS client runtime for Angular: session and token refresh, boot manifest and app configuration, theme, user preferences, error handling, HTTP interceptors, and the wire types the CoolMS API emits.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -17,7 +17,7 @@
17
17
  "rxjs": "^7.8.0"
18
18
  },
19
19
  "engines": {
20
- "node": "^22.22.3 || ^24.15.0 || >=26"
20
+ "node": "^22.22.3 || ^24.15.0 || ^26.0.0"
21
21
  },
22
22
  "keywords": [
23
23
  "coolms",
@@ -3,7 +3,7 @@ import * as i0 from "@angular/core";
3
3
  import { EnvironmentProviders, InjectionToken, Provider, Type } from "@angular/core";
4
4
  import { StateContext } from "@ngxs/store";
5
5
  import { CanActivateFn, CanMatchFn, Routes } from "@angular/router";
6
- import { HttpContextToken, HttpInterceptorFn } from "@angular/common/http";
6
+ import { HttpContextToken, HttpInterceptorFn, HttpRequest } from "@angular/common/http";
7
7
  export interface AuthApiManifest {
8
8
  readonly login: string;
9
9
  readonly logout: string;
@@ -277,6 +277,15 @@ export interface ApiManifest {
277
277
  }>;
278
278
  readonly platformDefaults?: PlatformDefaults;
279
279
  }
280
+ /**
281
+ * GET /api/v1/admin/manifest -- the admin's endpoint map, answered to every signed-in account
282
+ * since 2026-10-06 (no host groups: each call it names is decided by its own route). A server
283
+ * from before that answers at /api/v1/console/manifest, to the console group only. Read once
284
+ * signed in.
285
+ */
286
+ export interface ConsoleManifestResponse {
287
+ readonly manifest: ApiManifest;
288
+ }
280
289
  export interface ThemeConfigResponse {
281
290
  readonly slug?: string | null;
282
291
  readonly feStack?: string | null;
@@ -536,10 +545,28 @@ export interface CentrifugoSubscriptionTokenDto {
536
545
  *
537
546
  * `ApiService` keeps its identical signatures and delegates here, so nothing
538
547
  * calling it had to change.
548
+ *
549
+ * !! Access first, then realtime (Dmitry, 2026-09-28): "The console opens a realtime
550
+ * connection before the access check. Reverse the order ... A refused account opens no
551
+ * socket." The socket cannot open without the connection token, and anything may ask for
552
+ * one early -- the ring subscription is built at app init and asks as soon as a user is
553
+ * set, before the guard's answer is in. So the token waits for the console's answer (the
554
+ * guard's own request, not a second one) and a refused sign-in gets none: no request, no
555
+ * socket. A failure to ask is not a refusal, as at the guard.
556
+ *
557
+ * Since 2026-10-06 the connection token is every signed-in account's (Dmitry: "The connection
558
+ * token only opens a socket; each channel is authorized when it is subscribed to"), and a
559
+ * current server answers the admin manifest to every signed-in account, so no
560
+ * sign-in is refused and every one gets its token; each subscription is then decided by its
561
+ * channel. The wait stays for a server from before that, where a refused sign-in still opens
562
+ * no socket.
539
563
  */
540
564
  export declare class RealtimeTokenClient {
565
+ /** The error a refused sign-in's connection-token request ends with. */
566
+ static readonly REFUSED = "no console access: no realtime connection";
541
567
  private readonly http;
542
568
  private readonly store;
569
+ private readonly injector;
543
570
  private get manifest();
544
571
  connectionToken(): Observable<CentrifugoConnectionTokenDto>;
545
572
  subscriptionToken(channel: string): Observable<CentrifugoSubscriptionTokenDto>;
@@ -607,6 +634,12 @@ export declare class AuthState {
607
634
  * will have placed a non-null (but expired) token in state, causing the guard
608
635
  * to pass even though the subsequent config/refresh requests will fail and
609
636
  * dispatch Logout. Waiting for ready$ guarantees the auth state is settled.
637
+ *
638
+ * Signed in is not enough: the console is granted, and the server says to whom
639
+ * (ConsoleAccessService). An account it refuses -- a customer who registered on the
640
+ * public site -- is signed out of this sign-in, on the server as well as here, and
641
+ * sent to the login page, which says why. A failure to ask is not a refusal: the console opens, and every request it
642
+ * makes is still decided by the server.
610
643
  */
611
644
  export declare const authGuard: CanActivateFn;
612
645
  /**
@@ -784,10 +817,15 @@ export interface ElevationState {
784
817
  lifetimeCeilingSeconds: number;
785
818
  /** Whether the CURRENT elevation was confirmed by a second factor; absent or null when not elevated. */
786
819
  factorConfirmed?: boolean | null;
787
- /** Whether a grant made now must carry a code from the admin authenticator. */
820
+ /** Whether a grant made now must carry a code from the person's own authenticator. */
788
821
  mfaRequired: boolean;
789
- /** What the prompt shows before asking; two of the codes mean the installation cannot elevate yet. */
822
+ /** What the prompt shows before asking; two of the codes mean this account cannot elevate yet. */
790
823
  warnings: ElevationWarning[];
824
+ /**
825
+ * Whether this account may elevate at all -- a member of the admin group, elevated or not. The one source a
826
+ * client offers "Elevate session" from; absent (an older server) reads as false.
827
+ */
828
+ mayElevate?: boolean;
791
829
  }
792
830
  /**
793
831
  * `closed` is the beacon (the panel was closed or reloaded), `dropped_by_user`
@@ -801,8 +839,9 @@ export interface ElevationEnded {
801
839
  at: string | null;
802
840
  }
803
841
  /**
804
- * `elevation.password_not_set` and `elevation.factor_missing` mean the
805
- * installation cannot elevate and the message names the server command;
842
+ * `elevation.password_not_set` and `elevation.factor_missing` mean this
843
+ * account cannot elevate (it has no password, or no confirmed factor, of its
844
+ * own) and the message says which;
806
845
  * `elevation.mfa_off` is the recommendation carried while the switch is off.
807
846
  */
808
847
  export interface ElevationWarning {
@@ -826,8 +865,8 @@ export type ElevationDropReason = 'dropped_by_user' | 'closed';
826
865
  *
827
866
  * `state` is the server's answer at the moment of asking -- `ended.reason` is
828
867
  * rendered first (a person asked for a password again after F5
829
- * with no explanation reads it as a fault), `warnings` may say the
830
- * installation cannot elevate yet, `mfaRequired` decides whether a code field
868
+ * with no explanation reads it as a fault), `warnings` may say this
869
+ * account cannot elevate yet, `mfaRequired` decides whether a code field
831
870
  * shows from the start.
832
871
  *
833
872
  * `refusal` is the server's own sentence for the action that was refused
@@ -837,6 +876,8 @@ export type ElevationDropReason = 'dropped_by_user' | 'closed';
837
876
  export interface ElevationPromptRequest {
838
877
  state: ElevationState;
839
878
  refusal?: string;
879
+ /** The refused action in words ("Saving navi nodes"), when a refused request opened the prompt. */
880
+ action?: string;
840
881
  }
841
882
  /**
842
883
  * The prompt itself is a dialog, and dialogs are the kit's -- a layer above
@@ -858,6 +899,31 @@ export interface ElevationPromptPort {
858
899
  * prompt to show and loses nothing.
859
900
  */
860
901
  export declare const ELEVATION_PROMPT: InjectionToken<ElevationPromptPort>;
902
+ /**
903
+ * Told when an action a person took was refused for want of elevation: a
904
+ * write (not a read, not a background request) answered by a stamped 403.
905
+ *
906
+ * It must not open the prompt. The prompt opens only when the person asks for
907
+ * it -- the application shows the refusal and offers elevation as something to
908
+ * click (a notice with an Elevate button), and the click is what calls
909
+ * {@link ElevationService.offerFor}. A refused action is not repeated by
910
+ * itself after a grant: the person repeats it.
911
+ *
912
+ * `refusal` is the server's sentence for the refused action, already made
913
+ * readable by the error service.
914
+ */
915
+ export interface ElevationNoticePort {
916
+ /** `action` names what the person did ("Saving navi nodes"); absent from older callers. */
917
+ refused(refusal: string, action?: string): void;
918
+ }
919
+ /**
920
+ * What a write IS, in the person's words, for the elevation prompt and notice when the server refuses it -- set by a
921
+ * caller that knows better than the URL ("Saving the menu entry"). Without it the interceptor names the method's verb
922
+ * and the resource the URL names.
923
+ */
924
+ export declare const ELEVATION_ACTION: HttpContextToken<string | null>;
925
+ /** Optional, like {@link ELEVATION_PROMPT}: with nothing bound, a refusal is only a 403. */
926
+ export declare const ELEVATION_NOTICE: InjectionToken<ElevationNoticePort>;
861
927
  /**
862
928
  * Set on a request that must never open the elevation prompt: the elevation
863
929
  * endpoint's own calls (a wrong password is a 403 and is not a refused action),
@@ -900,6 +966,19 @@ export declare const BYPASS_ELEVATION: HttpContextToken<boolean>;
900
966
  * path (media, document and word templates, content distribution).
901
967
  */
902
968
  export declare const ELEVATION_REQUIRED_HEADER = "X-Elevation-Required";
969
+ /**
970
+ * Set on a request no person asked for: a realtime token, a preferences sync, a
971
+ * poll. Its refusal is never announced -- not by a dialog and not by a notice --
972
+ * because nobody is waiting on it and nobody can act on it. The caller still
973
+ * receives the 403 and decides what its own state shows.
974
+ */
975
+ export declare const BACKGROUND_REQUEST: HttpContextToken<boolean>;
976
+ /**
977
+ * Whether a failed request was refused for want of elevation: a 403 carrying
978
+ * the server's stamp. The one test every reader of a refusal uses -- the
979
+ * interceptor, a page's inline state, the error text -- so they cannot disagree.
980
+ */
981
+ export declare function isElevationRefusal(err: unknown): boolean;
903
982
  export declare class ElevationService {
904
983
  private readonly http;
905
984
  private readonly store;
@@ -923,6 +1002,8 @@ export declare class ElevationService {
923
1002
  readonly state: () => ElevationState | null;
924
1003
  /** Convenience over `state`: false until the server has said otherwise. */
925
1004
  readonly elevated: () => boolean;
1005
+ /** Whether this account may elevate at all, as the server answered it: what "Elevate session" is offered from. */
1006
+ readonly mayElevate: () => boolean;
926
1007
  /** `expiresAt` as a Date, null when not elevated. */
927
1008
  readonly expiresAt: () => Date | null;
928
1009
  /**
@@ -937,6 +1018,10 @@ export declare class ElevationService {
937
1018
  private expiryAnnouncePending;
938
1019
  private lastReconcileAt;
939
1020
  constructor();
1021
+ /** Whether a prompt can be opened at all: this install offers elevation and a prompt is bound. */
1022
+ get canPrompt(): boolean;
1023
+ /** The manifest's API base, as {@link isThisApi} reads it. */
1024
+ get apiBase(): string;
940
1025
  /** The manifest carries the URL; without it the service is inert, not broken. */
941
1026
  get available(): boolean;
942
1027
  private get url();
@@ -951,20 +1036,23 @@ export declare class ElevationService {
951
1036
  /** Ask the server. Applies the answer and announces it when it differs. */
952
1037
  refresh(): Observable<ElevationState>;
953
1038
  /**
954
- * Elevate with the admin password and, when the switch is on, a code from
955
- * the admin authenticator. Errors are the dialog's to branch on: 403 wrong
956
- * password or wrong / replayed code, 428 a code is required, 409 the
957
- * installation cannot elevate yet, 429 throttled. They pass through
1039
+ * Elevate with the person's own password and, when the switch is on, a
1040
+ * code from their own authenticator. Errors are the dialog's to branch on:
1041
+ * 403 not a member of the admin group, wrong password, or wrong / replayed
1042
+ * code, 428 a code is required, 409 this account cannot elevate yet, 429
1043
+ * throttled. They pass through
958
1044
  * untouched, and never open the prompt (BYPASS_ELEVATION).
959
1045
  */
960
1046
  elevate(password: string, code?: string): Observable<ElevationState>;
961
1047
  /** Drop the elevation now. Idempotent on the server: nothing live is still a 204. */
962
1048
  drop(reason?: ElevationDropReason): Observable<ElevationState>;
963
1049
  /**
964
- * What the interceptor calls on a 403: ask the server whether this session
965
- * is elevated, and only when it is not, offer the prompt. A 403 while
966
- * elevated is a real refusal -- elevation would not change it -- and is
967
- * handed back as such (false, no prompt).
1050
+ * What a person's explicit request for elevation calls -- an Elevate button,
1051
+ * or a control that already knows its action needs elevation: ask the
1052
+ * server whether this session is elevated, and only when it is not, open
1053
+ * the prompt. Nothing calls it on a response by itself (the interceptor
1054
+ * only tells {@link ELEVATION_NOTICE}). Elevated already is false, no
1055
+ * prompt: elevation would change nothing.
968
1056
  */
969
1057
  offerFor(refusal?: string): Observable<boolean>;
970
1058
  /**
@@ -1042,36 +1130,54 @@ export declare class ElevationService {
1042
1130
  static ɵprov: i0.ɵɵInjectableDeclaration<any>;
1043
1131
  }
1044
1132
  /**
1045
- * The elevation prompt hangs off the 403, not off the listing's flags
1046
- * (the elevation client requirement, point 1) -- and off the 403 that SAYS it
1047
- * was refused for want of elevation: the server stamps `X-Elevation-Required`
1048
- * with the gate's name where its gate refused a member who has not elevated.
1049
- * Any route of this API can carry it -- the VFS, a media asset's
1050
- * permissions, a document template, an account's deletion -- so nothing here
1051
- * lists URLs; the list that stood here lagged the server by every route whose
1052
- * refusal came from the VFS permission service under another module's path.
1053
- *
1054
- * On the stamp: ask the server whether this session is elevated, and only when
1055
- * it is NOT open the prompt; on a grant send the refused request again, once.
1056
- * A 403 without the stamp is a refusal for another reason and is handed back
1057
- * as it came; so are a 403 while elevated, a declined prompt, and an
1058
- * installation that cannot elevate -- nothing here swallows a refusal. A
1059
- * stamped 403 from anywhere but this API is ignored: the header is the
1060
- * server's word, and only this server's counts.
1061
- *
1062
- * Order in the chain: BEFORE the auth interceptor, so the retry re-enters it
1063
- * and carries the token current at that moment (the prompt may have outlived
1064
- * a refresh). The elevation endpoint's own calls carry BYPASS_ELEVATION: a
1065
- * wrong password is a 403 and is not a refused action.
1133
+ * A refusal for want of elevation is a 403 the server stamps `X-Elevation-Required`
1134
+ * where its gate refused a member who has not elevated ({@link isElevationRefusal}).
1135
+ *
1136
+ * What it does with a stamped 403 (decided 2026-10-20 for a person's write):
1137
+ * - On a write a person made (POST, PUT, PATCH, DELETE without {@link BACKGROUND_REQUEST}):
1138
+ * the prompt opens THERE, above whatever the person is in -- an edit dialog stays open
1139
+ * beneath it with its input -- and the request is HELD. If the person elevates, it is
1140
+ * sent once more and the caller receives that answer, as if the first had been accepted;
1141
+ * if the session turns out to be elevated already (another tab), it is sent once more
1142
+ * without asking. If the person declines, {@link ELEVATION_NOTICE} is told -- "Elevation
1143
+ * required", naming the action, with an Elevate button -- and the caller receives the 403.
1144
+ * The repeat is sent past this interceptor, so a second refusal is a 403, never a second
1145
+ * prompt.
1146
+ * - On a read: nothing. Reads are what a page makes as it loads and what polls make in
1147
+ * the background, and the page shows its own state for a refused one -- an inline
1148
+ * "needs an elevated session" with an Elevate button, or the feature hidden.
1149
+ * - On a background request: nothing. Nobody is waiting on it.
1150
+ *
1151
+ * !! Why reads and background requests still open nothing. Until 2026-10-08 any stamped 403
1152
+ * opened the prompt, so the setup reads of almost every console page, and a realtime token at
1153
+ * every load, opened it for an administrator who had not elevated: 13 of 15 pages on a menu
1154
+ * click, 15 of 15 on a reload (measured). From then until 2026-10-20 a person's write opened
1155
+ * nothing either, only a toast -- which, with a dialog open, rendered under it, so the edit had
1156
+ * to be thrown away to elevate (measured on the served admin, Edit Node). A person's write is
1157
+ * the one refusal somebody is waiting on, which is why it is the one that asks.
1158
+ *
1159
+ * The elevation endpoint's own calls carry {@link BYPASS_ELEVATION}: a wrong password is a
1160
+ * 403 and is not a refused action. A stamped 403 from anywhere but this API is ignored:
1161
+ * the header is the server's word, and only this server's counts.
1066
1162
  */
1067
1163
  export declare const elevationInterceptor: HttpInterceptorFn;
1164
+ /**
1165
+ * The action a refused write was, in words for the person: what its caller said it is
1166
+ * ({@link ELEVATION_ACTION}), or else the method's verb and the resource the URL names --
1167
+ * "Saving navi nodes" for `PATCH /api/v1/navi/nodes/{id}`. Identifiers and the API base
1168
+ * are left out; they say nothing to the person who pressed the button.
1169
+ */
1170
+ export declare function actionOf(req: HttpRequest<unknown>, apiBase?: string): string;
1068
1171
  export declare class LoginComponent {
1069
1172
  identifier: string;
1070
1173
  password: string;
1071
- loading: boolean;
1072
- error: string | null;
1174
+ readonly loading: import("@angular/core").WritableSignal<boolean>;
1175
+ readonly error: import("@angular/core").WritableSignal<string | null>;
1073
1176
  private readonly store;
1074
1177
  private readonly router;
1178
+ /** Shown when the guard signed an account out because the console is not granted to it. */
1179
+ static readonly NO_CONSOLE_ACCESS = "This account has no access to the console.";
1180
+ constructor();
1075
1181
  submit(): void;
1076
1182
  static ɵfac: i0.ɵɵFactoryDeclaration<LoginComponent, never>;
1077
1183
  static ɵcmp: i0.ɵɵComponentDeclaration<LoginComponent, "coolms-admin-login", never, {}, {}, never, never, true, never>;
@@ -1113,6 +1219,62 @@ export declare class AppInitService {
1113
1219
  static ɵfac: i0.ɵɵFactoryDeclaration<AppInitService, never>;
1114
1220
  static ɵprov: i0.ɵɵInjectableDeclaration<any>;
1115
1221
  }
1222
+ /** What the server said about this sign-in and the console. */
1223
+ export type ConsoleAccess = 'granted' | 'refused' | 'unknown';
1224
+ /**
1225
+ * Whether the signed-in account may use the console, and the console's endpoint
1226
+ * map when it may.
1227
+ *
1228
+ * The server decides: `GET /api/v1/admin/manifest` answers the map. Since 2026-10-06
1229
+ * (Dmitry: "no host groups"; access is decided by module groups alone) it answers every
1230
+ * signed-in account: the map grants nothing, each call it names is decided by its own
1231
+ * route, and an account in no group sees the admin with an empty menu. A server from
1232
+ * before that answers only at `/api/v1/console/manifest`, to the `console` group, and
1233
+ * 403 to anyone else -- there a refused sign-in is still signed out; this asks the new
1234
+ * address first and the old one only on its 404. The public theme config
1235
+ * read before sign-in carries only what the sign-in page needs; the rest arrives
1236
+ * here, once per sign-in, and is merged into the app config key by key: a key the
1237
+ * console's map carries replaces the public one whole, so the console's copy of a
1238
+ * section is always the complete one.
1239
+ *
1240
+ * `unknown` is a failure to ask (the network, a 5xx), not an answer: it is not kept,
1241
+ * so while the endpoint fails every navigation asks again.
1242
+ *
1243
+ * !! An answer belongs to the sign-in it was asked for. A sign-in, a sign-out or a
1244
+ * restored session starts a new generation; an answer that arrives for a generation
1245
+ * that is gone is kept for nobody -- A's late 403 must not sign B out -- and whoever
1246
+ * is waiting on it is answered for the sign-in there is now.
1247
+ *
1248
+ * !! With nobody signed in there is nothing to ask, and nothing is asked. Without
1249
+ * that, the re-ask after a sign-out went out with no token, its 401 made the
1250
+ * interceptor dispatch Logout, which started another generation, which re-asked:
1251
+ * a loop for as long as the tab lived (found in review).
1252
+ */
1253
+ export declare class ConsoleAccessService {
1254
+ /** The admin manifest's URL: fixed, like the theme config's, because nothing is known before it. */
1255
+ static readonly URL = "/api/v1/admin/manifest";
1256
+ /** The address before 2026-10-06, asked only when a server older than that answers 404 at {@link URL}. */
1257
+ static readonly LEGACY_URL = "/api/v1/console/manifest";
1258
+ /** The login page's query parameter value for "signed out: no console access". */
1259
+ static readonly REFUSED_REASON = "no-console-access";
1260
+ private readonly http;
1261
+ private readonly store;
1262
+ private answer;
1263
+ private pending;
1264
+ private generation;
1265
+ constructor();
1266
+ /** Ask once per sign-in; the manifest is merged into the app config when granted. */
1267
+ ensure(): Promise<ConsoleAccess>;
1268
+ /**
1269
+ * Sign a refused account out: this sign-in ends on the server (its refresh token
1270
+ * with it), as the account menu's sign-out does, and then in this browser --
1271
+ * whatever the server answered, so a refused request cannot leave it signed in.
1272
+ */
1273
+ signOut(): Observable<void>;
1274
+ private merge;
1275
+ static ɵfac: i0.ɵɵFactoryDeclaration<ConsoleAccessService, never>;
1276
+ static ɵprov: i0.ɵɵInjectableDeclaration<any>;
1277
+ }
1116
1278
  /**
1117
1279
  * Layout config DTO -- the shape served from
1118
1280
  * `GET /api/v1/config/layout/{id}`. Mirrors the YAML at
@@ -1230,7 +1392,8 @@ export interface DatagridConfig {
1230
1392
  * Fetches config YAMLs (served as JSON) from GET /api/v1/config/{type}/{id}.
1231
1393
  *
1232
1394
  * Responses are cached for the lifetime of the service (Map + shareReplay(1))
1233
- * so repeated calls never re-fetch the same resource.
1395
+ * so repeated calls never re-fetch the same resource. A failed read is dropped
1396
+ * from the cache, so the next call asks the server again.
1234
1397
  *
1235
1398
  * configBase is read from the API manifest (manifest.configBase) and falls
1236
1399
  * back to '/api/v1/config' when the manifest is not yet loaded.
@@ -1259,6 +1422,8 @@ export interface AppConfigStateModel {
1259
1422
  }
1260
1423
  export declare class AppConfigState {
1261
1424
  set(ctx: StateContext<AppConfigStateModel>, { config }: SetAppConfig): void;
1425
+ /** The whole config as last set: the public theme config, with the console's manifest once granted. */
1426
+ static config(state: AppConfigStateModel): ThemeConfigResponse | null;
1262
1427
  static manifest(state: AppConfigStateModel): ApiManifest | null;
1263
1428
  /**
1264
1429
  * The viewer manifest. Null when the backend didn't ship a viewers
@@ -1658,6 +1823,14 @@ export interface NaviGraphMeta {
1658
1823
  readonly contributor?: string;
1659
1824
  readonly [key: string]: unknown;
1660
1825
  }
1826
+ /**
1827
+ * What the server says this account may do with a node, as it answered the graph:
1828
+ * 'allowed' -- its own rights open it; shown as usual;
1829
+ * 'elevate' -- a member of admin, not elevated, whose own rights refuse it and whom elevation would let in;
1830
+ * shown with a lock, and opening it asks for elevation first.
1831
+ * A node anyone else may not open is not in the answer at all. Absent (an older server) reads as 'allowed'.
1832
+ */
1833
+ export type NaviAccess = 'allowed' | 'elevate';
1661
1834
  /** Flat node as returned from GET /navi/trees/{slug}/graph */
1662
1835
  export interface NaviGraphNodeFlat {
1663
1836
  readonly id: string;
@@ -1668,7 +1841,10 @@ export interface NaviGraphNodeFlat {
1668
1841
  readonly isActive: boolean;
1669
1842
  readonly isVisible: boolean;
1670
1843
  readonly meta: NaviGraphMeta;
1844
+ readonly access?: NaviAccess;
1671
1845
  }
1846
+ /** The node's access as the server answered it; 'allowed' when the server said nothing (an older server). */
1847
+ export declare function naviAccessOf(node: Pick<NaviGraphNodeFlat, 'access'>): NaviAccess;
1672
1848
  /** Same node enriched with resolved children after client-side tree build */
1673
1849
  export interface NaviGraphNode extends NaviGraphNodeFlat {
1674
1850
  readonly children: NaviGraphNode[];
@@ -1691,7 +1867,14 @@ export declare class NaviGraphService {
1691
1867
  /** Resolved root nodes for navi.admin.topbar. */
1692
1868
  readonly topbarNav: import("@angular/core").WritableSignal<NaviGraphNode[]>;
1693
1869
  private readonly cache;
1694
- loadAdminNav(url: string): Observable<NaviGraphNode[]>;
1870
+ /**
1871
+ * `fresh` asks the server again instead of answering from this service's cache: the answer is per account AND
1872
+ * per elevation state (a node is 'elevate' before a grant and 'allowed' after it), so a change of elevation must
1873
+ * re-ask, or a lock would outlive the grant -- or come back only after a reload.
1874
+ */
1875
+ loadAdminNav(url: string, options?: {
1876
+ fresh?: boolean;
1877
+ }): Observable<NaviGraphNode[]>;
1695
1878
  loadTopbarNav(url: string): Observable<NaviGraphNode[]>;
1696
1879
  /**
1697
1880
  * Handle a nav-node click.
@@ -1724,7 +1907,7 @@ export declare class NaviGraphService {
1724
1907
  * the legacy Hydra key (`hydra:member`).
1725
1908
  * Results are cached by URL for the lifetime of the service instance.
1726
1909
  */
1727
- loadTree(url: string): Observable<NaviGraphNode[]>;
1910
+ loadTree(url: string, fresh?: boolean): Observable<NaviGraphNode[]>;
1728
1911
  /**
1729
1912
  * Build a nested tree from a flat array of nodes.
1730
1913
  * Nodes are sorted by sortOrder at each level.
@@ -1936,6 +2119,14 @@ export declare class ConsoleActivation {
1936
2119
  * landed on the dashboard (measured 2026-09-22: no chunk requested for the
1937
2120
  * mount, the dashboard's instead). `ready$` is what the auth guard waits on
1938
2121
  * too, and it fires whether or not the config fetch succeeded.
2122
+ *
2123
+ * !! Then the console's own manifest, when someone is signed in: `ui.modules` is
2124
+ * the console's, not the public config's, and arrives with it
2125
+ * ({@link ConsoleAccessService}). canMatch runs before the layout's auth guard,
2126
+ * which asks the same question, so answering from the public config alone said
2127
+ * "no" to every module once that config stopped carrying them. Nobody signed in:
2128
+ * nothing is asked, the answer is the public config's, and the layout's guard
2129
+ * sends the person to sign in. A refusal merges nothing, so the answer is no.
1939
2130
  */
1940
2131
  export declare function consoleModuleInstalled(module: string): CanMatchFn;
1941
2132
  /**