@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.
@@ -1,12 +1,12 @@
1
1
  import * as i0 from "@angular/core";
2
- import { ChangeDetectionStrategy, Component, DestroyRef, Injectable, InjectionToken, computed, effect, inject, input, makeEnvironmentProviders, provideAppInitializer, signal, untracked } from "@angular/core";
2
+ import { ChangeDetectionStrategy, Component, DestroyRef, Injectable, InjectionToken, Injector, computed, effect, inject, input, makeEnvironmentProviders, provideAppInitializer, signal, untracked } from "@angular/core";
3
3
  import { HttpClient, HttpContext, HttpContextToken, HttpErrorResponse, HttpParams } from "@angular/common/http";
4
- import { Action, Selector, State, Store, provideStore } from "@ngxs/store";
4
+ import { Action, Actions, Selector, State, Store, ofActionDispatched, provideStore } from "@ngxs/store";
5
5
  import { __decorate } from "tslib";
6
6
  import { ReplaySubject, Subject, catchError, debounceTime, defer, finalize, firstValueFrom, from, map, of, shareReplay, switchMap, take, tap, throwError } from "rxjs";
7
7
  import { catchError as catchError$1, map as map$1, shareReplay as shareReplay$1, switchMap as switchMap$1, tap as tap$1 } from "rxjs/operators";
8
8
  import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
9
- import { Router } from "@angular/router";
9
+ import { ActivatedRoute, Router } from "@angular/router";
10
10
  import { Location, NgIf } from "@angular/common";
11
11
  import * as i1 from "@angular/forms";
12
12
  import { FormsModule } from "@angular/forms";
@@ -27,6 +27,9 @@ let AppConfigState = class AppConfigState {
27
27
  loaded: true
28
28
  });
29
29
  }
30
+ static config(state) {
31
+ return state.config;
32
+ }
30
33
  static manifest(state) {
31
34
  return state.config?.manifest ?? null;
32
35
  }
@@ -38,7 +41,7 @@ let AppConfigState = class AppConfigState {
38
41
  }
39
42
  static ɵfac = i0.ɵɵngDeclareFactory({
40
43
  minVersion: "12.0.0",
41
- version: "22.2.0",
44
+ version: "22.2.3",
42
45
  ngImport: i0,
43
46
  type: AppConfigState,
44
47
  deps: [],
@@ -46,12 +49,13 @@ let AppConfigState = class AppConfigState {
46
49
  });
47
50
  static ɵprov = i0.ɵɵngDeclareInjectable({
48
51
  minVersion: "12.0.0",
49
- version: "22.2.0",
52
+ version: "22.2.3",
50
53
  ngImport: i0,
51
54
  type: AppConfigState
52
55
  });
53
56
  };
54
57
  __decorate([Action(SetAppConfig)], AppConfigState.prototype, "set", null);
58
+ __decorate([Selector()], AppConfigState, "config", null);
55
59
  __decorate([Selector()], AppConfigState, "manifest", null);
56
60
  __decorate([Selector()], AppConfigState, "viewers", null);
57
61
  __decorate([Selector()], AppConfigState, "loaded", null);
@@ -64,7 +68,7 @@ AppConfigState = __decorate([State({
64
68
  })], AppConfigState);
65
69
  i0.ɵɵngDeclareClassMetadata({
66
70
  minVersion: "12.0.0",
67
- version: "22.2.0",
71
+ version: "22.2.3",
68
72
  ngImport: i0,
69
73
  type: AppConfigState,
70
74
  decorators: [{ type: Injectable }],
@@ -98,7 +102,7 @@ var IdentityApiClient = class IdentityApiClient {
98
102
  }
99
103
  static ɵfac = i0.ɵɵngDeclareFactory({
100
104
  minVersion: "12.0.0",
101
- version: "22.2.0",
105
+ version: "22.2.3",
102
106
  ngImport: i0,
103
107
  type: IdentityApiClient,
104
108
  deps: [],
@@ -106,7 +110,7 @@ var IdentityApiClient = class IdentityApiClient {
106
110
  });
107
111
  static ɵprov = i0.ɵɵngDeclareInjectable({
108
112
  minVersion: "12.0.0",
109
- version: "22.2.0",
113
+ version: "22.2.3",
110
114
  ngImport: i0,
111
115
  type: IdentityApiClient,
112
116
  providedIn: "root"
@@ -114,7 +118,7 @@ var IdentityApiClient = class IdentityApiClient {
114
118
  };
115
119
  i0.ɵɵngDeclareClassMetadata({
116
120
  minVersion: "12.0.0",
117
- version: "22.2.0",
121
+ version: "22.2.3",
118
122
  ngImport: i0,
119
123
  type: IdentityApiClient,
120
124
  decorators: [{
@@ -122,46 +126,6 @@ i0.ɵɵngDeclareClassMetadata({
122
126
  args: [{ providedIn: "root" }]
123
127
  }]
124
128
  });
125
- var RealtimeTokenClient = class RealtimeTokenClient {
126
- http = inject(HttpClient);
127
- store = inject(Store);
128
- get manifest() {
129
- const m = this.store.selectSnapshot(AppConfigState.manifest);
130
- if (!m) throw new Error("ApiManifest not loaded — call AppInitService.load() first");
131
- return m;
132
- }
133
- connectionToken() {
134
- return this.http.post(this.manifest.apiBase + "/centrifugo/connection-token", {});
135
- }
136
- subscriptionToken(channel) {
137
- return this.http.post(this.manifest.apiBase + "/centrifugo/subscription-token", { channel });
138
- }
139
- static ɵfac = i0.ɵɵngDeclareFactory({
140
- minVersion: "12.0.0",
141
- version: "22.2.0",
142
- ngImport: i0,
143
- type: RealtimeTokenClient,
144
- deps: [],
145
- target: i0.ɵɵFactoryTarget.Injectable
146
- });
147
- static ɵprov = i0.ɵɵngDeclareInjectable({
148
- minVersion: "12.0.0",
149
- version: "22.2.0",
150
- ngImport: i0,
151
- type: RealtimeTokenClient,
152
- providedIn: "root"
153
- });
154
- };
155
- i0.ɵɵngDeclareClassMetadata({
156
- minVersion: "12.0.0",
157
- version: "22.2.0",
158
- ngImport: i0,
159
- type: RealtimeTokenClient,
160
- decorators: [{
161
- type: Injectable,
162
- args: [{ providedIn: "root" }]
163
- }]
164
- });
165
129
  var Login = class {
166
130
  identifier;
167
131
  password;
@@ -191,6 +155,255 @@ var PatchCurrentUser = class {
191
155
  this.patch = patch;
192
156
  }
193
157
  };
158
+ const ELEVATION_PROMPT = new InjectionToken("coolms.elevation-prompt");
159
+ const ELEVATION_ACTION = new HttpContextToken(() => null);
160
+ const ELEVATION_NOTICE = new InjectionToken("coolms.elevation-notice");
161
+ const BYPASS_ELEVATION = new HttpContextToken(() => false);
162
+ const CROSS_TAB_KEY = "coolms.elevation.changed";
163
+ const RECONCILE_MIN_INTERVAL_MS = 2e3;
164
+ const ELEVATION_REQUIRED_HEADER = "X-Elevation-Required";
165
+ const BACKGROUND_REQUEST = new HttpContextToken(() => false);
166
+ function isElevationRefusal(err) {
167
+ return err instanceof HttpErrorResponse && err.status === 403 && !!err.headers.get("X-Elevation-Required");
168
+ }
169
+ var ElevationService = class ElevationService {
170
+ http = inject(HttpClient);
171
+ store = inject(Store);
172
+ prompt = inject(ELEVATION_PROMPT, { optional: true });
173
+ destroyRef = inject(DestroyRef);
174
+ served = signal(null, ...ngDevMode ? [{ debugName: "served" }] : /* istanbul ignore next */ []);
175
+ state = () => {
176
+ const served = this.served();
177
+ if (served === null || !isPast(served)) return served;
178
+ const expired = expiredView(served);
179
+ untracked(() => this.served.set(expired));
180
+ this.announceExpiryLater();
181
+ return expired;
182
+ };
183
+ elevated = () => this.state()?.elevated ?? false;
184
+ mayElevate = () => this.state()?.mayElevate ?? false;
185
+ expiresAt = () => {
186
+ const iso = this.state()?.expiresAt;
187
+ return iso ? new Date(iso) : null;
188
+ };
189
+ changes$ = new Subject();
190
+ expiryTimer = null;
191
+ pendingPrompt = null;
192
+ expiryAnnouncePending = false;
193
+ lastReconcileAt = 0;
194
+ constructor() {
195
+ if (typeof window !== "undefined") {
196
+ window.addEventListener("storage", this.onStorage);
197
+ window.addEventListener("focus", this.onFocus);
198
+ if (typeof document !== "undefined") document.addEventListener("visibilitychange", this.onVisibilityChange);
199
+ this.destroyRef.onDestroy(() => {
200
+ window.removeEventListener("storage", this.onStorage);
201
+ window.removeEventListener("focus", this.onFocus);
202
+ if (typeof document !== "undefined") document.removeEventListener("visibilitychange", this.onVisibilityChange);
203
+ this.clearTimer();
204
+ });
205
+ }
206
+ }
207
+ get canPrompt() {
208
+ return this.available && this.prompt !== null;
209
+ }
210
+ get apiBase() {
211
+ return this.store.selectSnapshot(AppConfigState.manifest)?.apiBase ?? "/api/v1";
212
+ }
213
+ get available() {
214
+ return this.url !== null;
215
+ }
216
+ get url() {
217
+ return this.store.selectSnapshot(AppConfigState.manifest)?.identity?.elevationUrl ?? null;
218
+ }
219
+ isThisApi(url) {
220
+ const apiBase = this.store.selectSnapshot(AppConfigState.manifest)?.apiBase ?? "/api/v1";
221
+ let path = url.split("?")[0];
222
+ if (/^https?:\/\//.test(path)) try {
223
+ const parsed = new URL(path);
224
+ if (typeof window !== "undefined" && parsed.host !== window.location.host) return false;
225
+ path = parsed.pathname;
226
+ } catch {
227
+ return false;
228
+ }
229
+ return path === apiBase || path.startsWith(`${apiBase}/`);
230
+ }
231
+ refresh() {
232
+ const url = this.url;
233
+ if (!url) return of(this.inert());
234
+ return this.http.get(url, this.options()).pipe(tap((s) => this.apply(s)));
235
+ }
236
+ elevate(password, code) {
237
+ const url = this.url;
238
+ if (!url) return of(this.inert());
239
+ const body = { password };
240
+ if (code) body.code = code;
241
+ return this.http.post(url, body, this.options()).pipe(tap((s) => this.apply(s, "granted")));
242
+ }
243
+ drop(reason = "dropped_by_user") {
244
+ const url = this.url;
245
+ if (!url) return of(this.inert());
246
+ return this.http.delete(`${url}?reason=${reason}`, this.options()).pipe(switchMap(() => this.refresh()));
247
+ }
248
+ offerFor(refusal) {
249
+ if (!this.available || !this.prompt) return of(false);
250
+ return this.refresh().pipe(switchMap((state) => state.elevated ? of(false) : this.requestElevation({
251
+ state,
252
+ refusal
253
+ })));
254
+ }
255
+ requestElevation(request) {
256
+ if (!this.prompt) return of(false);
257
+ if (this.pendingPrompt) return this.pendingPrompt;
258
+ const prompt = this.prompt;
259
+ this.pendingPrompt = defer(() => prompt.open(request)).pipe(finalize(() => {
260
+ this.pendingPrompt = null;
261
+ }), shareReplay(1));
262
+ return this.pendingPrompt;
263
+ }
264
+ options() {
265
+ return { context: new HttpContext().set(BYPASS_ELEVATION, true) };
266
+ }
267
+ inert() {
268
+ return {
269
+ elevated: false,
270
+ ended: {
271
+ reason: "never",
272
+ at: null
273
+ },
274
+ lifetimeSeconds: 0,
275
+ lifetimeCeilingSeconds: 0,
276
+ mfaRequired: false,
277
+ warnings: []
278
+ };
279
+ }
280
+ apply(next, explicit) {
281
+ const prev = untracked(() => this.served());
282
+ this.served.set(next);
283
+ this.schedule(next);
284
+ const kind = explicit ?? this.kindOf(prev, next);
285
+ if (kind === null) return;
286
+ this.expiryAnnouncePending = false;
287
+ this.changes$.next({
288
+ kind,
289
+ state: next
290
+ });
291
+ this.announceToOtherTabs();
292
+ }
293
+ kindOf(prev, next) {
294
+ const was = prev?.elevated ?? false;
295
+ if (was && !next.elevated) return next.ended.reason === "expired" ? "expired" : "dropped";
296
+ if (!was && next.elevated) return "granted";
297
+ if (was && next.elevated && (prev?.expiresAt ?? null) !== (next.expiresAt ?? null)) return "observed";
298
+ return null;
299
+ }
300
+ announceExpiryLater() {
301
+ if (this.expiryAnnouncePending) return;
302
+ this.expiryAnnouncePending = true;
303
+ queueMicrotask(() => {
304
+ if (!this.expiryAnnouncePending) return;
305
+ this.expiryAnnouncePending = false;
306
+ const state = untracked(() => this.served());
307
+ if (state !== null && !state.elevated) this.changes$.next({
308
+ kind: "expired",
309
+ state
310
+ });
311
+ });
312
+ }
313
+ schedule(state) {
314
+ this.clearTimer();
315
+ if (!state.elevated || !state.expiresAt) return;
316
+ const wanted = Date.parse(state.expiresAt) - Date.now() + 250;
317
+ const ms = Math.min(Math.max(1e3, wanted), 2147483647);
318
+ this.expiryTimer = setTimeout(() => {
319
+ this.expiryTimer = null;
320
+ this.refresh().subscribe({
321
+ next: (s) => {
322
+ if (s.elevated && s.expiresAt && Date.parse(s.expiresAt) <= Date.now()) this.scheduleRetry();
323
+ },
324
+ error: () => this.scheduleRetry()
325
+ });
326
+ }, ms);
327
+ }
328
+ scheduleRetry() {
329
+ this.clearTimer();
330
+ this.expiryTimer = setTimeout(() => {
331
+ this.expiryTimer = null;
332
+ this.refresh().subscribe({ error: () => void 0 });
333
+ }, 5e3);
334
+ }
335
+ clearTimer() {
336
+ if (this.expiryTimer !== null) {
337
+ clearTimeout(this.expiryTimer);
338
+ this.expiryTimer = null;
339
+ }
340
+ }
341
+ announceToOtherTabs() {
342
+ try {
343
+ localStorage.setItem(CROSS_TAB_KEY, String(Date.now()));
344
+ } catch {}
345
+ }
346
+ onStorage = (event) => {
347
+ if (event.key !== CROSS_TAB_KEY || !this.available) return;
348
+ this.refresh().subscribe({ error: () => void 0 });
349
+ };
350
+ onFocus = () => this.reconcile();
351
+ onVisibilityChange = () => {
352
+ if (document.visibilityState === "visible") this.reconcile();
353
+ };
354
+ reconcile() {
355
+ this.state();
356
+ if (!this.available) return;
357
+ const now = Date.now();
358
+ if (now - this.lastReconcileAt < RECONCILE_MIN_INTERVAL_MS) return;
359
+ this.lastReconcileAt = now;
360
+ this.refresh().subscribe({ error: () => void 0 });
361
+ }
362
+ static ɵfac = i0.ɵɵngDeclareFactory({
363
+ minVersion: "12.0.0",
364
+ version: "22.2.3",
365
+ ngImport: i0,
366
+ type: ElevationService,
367
+ deps: [],
368
+ target: i0.ɵɵFactoryTarget.Injectable
369
+ });
370
+ static ɵprov = i0.ɵɵngDeclareInjectable({
371
+ minVersion: "12.0.0",
372
+ version: "22.2.3",
373
+ ngImport: i0,
374
+ type: ElevationService,
375
+ providedIn: "root"
376
+ });
377
+ };
378
+ i0.ɵɵngDeclareClassMetadata({
379
+ minVersion: "12.0.0",
380
+ version: "22.2.3",
381
+ ngImport: i0,
382
+ type: ElevationService,
383
+ decorators: [{
384
+ type: Injectable,
385
+ args: [{ providedIn: "root" }]
386
+ }],
387
+ ctorParameters: () => []
388
+ });
389
+ function isPast(state) {
390
+ if (!state.elevated || !state.expiresAt) return false;
391
+ const at = Date.parse(state.expiresAt);
392
+ return Number.isFinite(at) && at <= Date.now();
393
+ }
394
+ function expiredView(state) {
395
+ return {
396
+ ...state,
397
+ elevated: false,
398
+ expiresAt: null,
399
+ grantedAt: null,
400
+ factorConfirmed: null,
401
+ ended: {
402
+ reason: "expired",
403
+ at: state.expiresAt ?? null
404
+ }
405
+ };
406
+ }
194
407
  var UserPreferencesService = class UserPreferencesService {
195
408
  http = inject(HttpClient);
196
409
  store = inject(Store);
@@ -287,7 +500,10 @@ var UserPreferencesService = class UserPreferencesService {
287
500
  syncToServer(prefs) {
288
501
  const url = this.store.selectSnapshot(AppConfigState.manifest)?.auth?.preferencesUrl;
289
502
  if (!url) return;
290
- this.http.patch(url, { uiPrefs: prefs }, { headers: { "Content-Type": "application/merge-patch+json" } }).subscribe({ error: (err) => console.warn("Preferences sync failed:", err) });
503
+ this.http.patch(url, { uiPrefs: prefs }, {
504
+ headers: { "Content-Type": "application/merge-patch+json" },
505
+ context: new HttpContext().set(BACKGROUND_REQUEST, true)
506
+ }).subscribe({ error: (err) => console.warn("Preferences sync failed:", err) });
291
507
  }
292
508
  getGridPref(gridId) {
293
509
  try {
@@ -397,7 +613,7 @@ var UserPreferencesService = class UserPreferencesService {
397
613
  }
398
614
  static ɵfac = i0.ɵɵngDeclareFactory({
399
615
  minVersion: "12.0.0",
400
- version: "22.2.0",
616
+ version: "22.2.3",
401
617
  ngImport: i0,
402
618
  type: UserPreferencesService,
403
619
  deps: [],
@@ -405,7 +621,7 @@ var UserPreferencesService = class UserPreferencesService {
405
621
  });
406
622
  static ɵprov = i0.ɵɵngDeclareInjectable({
407
623
  minVersion: "12.0.0",
408
- version: "22.2.0",
624
+ version: "22.2.3",
409
625
  ngImport: i0,
410
626
  type: UserPreferencesService,
411
627
  providedIn: "root"
@@ -413,7 +629,7 @@ var UserPreferencesService = class UserPreferencesService {
413
629
  };
414
630
  i0.ɵɵngDeclareClassMetadata({
415
631
  minVersion: "12.0.0",
416
- version: "22.2.0",
632
+ version: "22.2.3",
417
633
  ngImport: i0,
418
634
  type: UserPreferencesService,
419
635
  decorators: [{
@@ -509,7 +725,7 @@ let AuthState = class AuthState {
509
725
  }
510
726
  static ɵfac = i0.ɵɵngDeclareFactory({
511
727
  minVersion: "12.0.0",
512
- version: "22.2.0",
728
+ version: "22.2.3",
513
729
  ngImport: i0,
514
730
  type: AuthState,
515
731
  deps: [],
@@ -517,7 +733,7 @@ let AuthState = class AuthState {
517
733
  });
518
734
  static ɵprov = i0.ɵɵngDeclareInjectable({
519
735
  minVersion: "12.0.0",
520
- version: "22.2.0",
736
+ version: "22.2.3",
521
737
  ngImport: i0,
522
738
  type: AuthState
523
739
  });
@@ -543,7 +759,7 @@ AuthState = __decorate([State({
543
759
  })], AuthState);
544
760
  i0.ɵɵngDeclareClassMetadata({
545
761
  minVersion: "12.0.0",
546
- version: "22.2.0",
762
+ version: "22.2.3",
547
763
  ngImport: i0,
548
764
  type: AuthState,
549
765
  decorators: [{ type: Injectable }],
@@ -555,6 +771,127 @@ i0.ɵɵngDeclareClassMetadata({
555
771
  patchCurrentUser: []
556
772
  }
557
773
  });
774
+ var ConsoleAccessService = class ConsoleAccessService {
775
+ static URL = "/api/v1/admin/manifest";
776
+ static LEGACY_URL = "/api/v1/console/manifest";
777
+ static REFUSED_REASON = "no-console-access";
778
+ http = inject(HttpClient);
779
+ store = inject(Store);
780
+ answer = "unknown";
781
+ pending = null;
782
+ generation = 0;
783
+ constructor() {
784
+ inject(Actions).pipe(ofActionDispatched(Login, Logout, RestoreSession)).subscribe(() => {
785
+ this.generation++;
786
+ this.answer = "unknown";
787
+ this.pending = null;
788
+ });
789
+ }
790
+ ensure() {
791
+ if (!this.store.selectSnapshot(AuthState.isAuthenticated)) return Promise.resolve("unknown");
792
+ if ("unknown" !== this.answer) return Promise.resolve(this.answer);
793
+ if (null !== this.pending) return this.pending;
794
+ const askedFor = this.generation;
795
+ const pending = firstValueFrom(this.http.get(ConsoleAccessService.URL).pipe(catchError((error) => error instanceof HttpErrorResponse && 404 === error.status ? this.http.get(ConsoleAccessService.LEGACY_URL) : throwError(() => error)), map((response) => ({
796
+ access: "granted",
797
+ response
798
+ })), catchError((error) => of({
799
+ access: error instanceof HttpErrorResponse && 403 === error.status ? "refused" : "unknown",
800
+ response: null
801
+ })))).then(({ access, response }) => {
802
+ if (askedFor !== this.generation) return this.ensure();
803
+ if (null !== response) this.merge(response);
804
+ this.pending = null;
805
+ this.answer = access;
806
+ return access;
807
+ });
808
+ this.pending = pending;
809
+ return pending;
810
+ }
811
+ signOut() {
812
+ const logout = this.store.selectSnapshot(AppConfigState.manifest)?.auth?.logout ?? "/api/v1/auth/logout";
813
+ return this.http.post(logout, null).pipe(catchError(() => of(null)), switchMap(() => this.store.dispatch(new Logout())), map(() => void 0));
814
+ }
815
+ merge(response) {
816
+ const config = this.store.selectSnapshot(AppConfigState.config);
817
+ this.store.dispatch(new SetAppConfig({
818
+ ...config ?? {},
819
+ manifest: {
820
+ ...config?.manifest ?? {},
821
+ ...response.manifest
822
+ }
823
+ }));
824
+ }
825
+ static ɵfac = i0.ɵɵngDeclareFactory({
826
+ minVersion: "12.0.0",
827
+ version: "22.2.3",
828
+ ngImport: i0,
829
+ type: ConsoleAccessService,
830
+ deps: [],
831
+ target: i0.ɵɵFactoryTarget.Injectable
832
+ });
833
+ static ɵprov = i0.ɵɵngDeclareInjectable({
834
+ minVersion: "12.0.0",
835
+ version: "22.2.3",
836
+ ngImport: i0,
837
+ type: ConsoleAccessService,
838
+ providedIn: "root"
839
+ });
840
+ };
841
+ i0.ɵɵngDeclareClassMetadata({
842
+ minVersion: "12.0.0",
843
+ version: "22.2.3",
844
+ ngImport: i0,
845
+ type: ConsoleAccessService,
846
+ decorators: [{
847
+ type: Injectable,
848
+ args: [{ providedIn: "root" }]
849
+ }],
850
+ ctorParameters: () => []
851
+ });
852
+ const BACKGROUND = { context: new HttpContext().set(BACKGROUND_REQUEST, true) };
853
+ var RealtimeTokenClient = class RealtimeTokenClient {
854
+ static REFUSED = "no console access: no realtime connection";
855
+ http = inject(HttpClient);
856
+ store = inject(Store);
857
+ injector = inject(Injector);
858
+ get manifest() {
859
+ const m = this.store.selectSnapshot(AppConfigState.manifest);
860
+ if (!m) throw new Error("ApiManifest not loaded — call AppInitService.load() first");
861
+ return m;
862
+ }
863
+ connectionToken() {
864
+ return defer(() => this.injector.get(ConsoleAccessService).ensure()).pipe(switchMap((access) => "refused" === access ? throwError(() => new Error(RealtimeTokenClient.REFUSED)) : this.http.post(this.manifest.apiBase + "/centrifugo/connection-token", {}, BACKGROUND)));
865
+ }
866
+ subscriptionToken(channel) {
867
+ return this.http.post(this.manifest.apiBase + "/centrifugo/subscription-token", { channel }, BACKGROUND);
868
+ }
869
+ static ɵfac = i0.ɵɵngDeclareFactory({
870
+ minVersion: "12.0.0",
871
+ version: "22.2.3",
872
+ ngImport: i0,
873
+ type: RealtimeTokenClient,
874
+ deps: [],
875
+ target: i0.ɵɵFactoryTarget.Injectable
876
+ });
877
+ static ɵprov = i0.ɵɵngDeclareInjectable({
878
+ minVersion: "12.0.0",
879
+ version: "22.2.3",
880
+ ngImport: i0,
881
+ type: RealtimeTokenClient,
882
+ providedIn: "root"
883
+ });
884
+ };
885
+ i0.ɵɵngDeclareClassMetadata({
886
+ minVersion: "12.0.0",
887
+ version: "22.2.3",
888
+ ngImport: i0,
889
+ type: RealtimeTokenClient,
890
+ decorators: [{
891
+ type: Injectable,
892
+ args: [{ providedIn: "root" }]
893
+ }]
894
+ });
558
895
  const LOCK_NAME = "coolms_auth_refresh";
559
896
  const FRESH_TOKEN_GRACE_MS = 3e4;
560
897
  const STORAGE_KEY$1 = "coolms_token";
@@ -632,7 +969,7 @@ var AuthRefreshCoordinator = class AuthRefreshCoordinator {
632
969
  }
633
970
  static ɵfac = i0.ɵɵngDeclareFactory({
634
971
  minVersion: "12.0.0",
635
- version: "22.2.0",
972
+ version: "22.2.3",
636
973
  ngImport: i0,
637
974
  type: AuthRefreshCoordinator,
638
975
  deps: [],
@@ -640,7 +977,7 @@ var AuthRefreshCoordinator = class AuthRefreshCoordinator {
640
977
  });
641
978
  static ɵprov = i0.ɵɵngDeclareInjectable({
642
979
  minVersion: "12.0.0",
643
- version: "22.2.0",
980
+ version: "22.2.3",
644
981
  ngImport: i0,
645
982
  type: AuthRefreshCoordinator,
646
983
  providedIn: "root"
@@ -648,7 +985,7 @@ var AuthRefreshCoordinator = class AuthRefreshCoordinator {
648
985
  };
649
986
  i0.ɵɵngDeclareClassMetadata({
650
987
  minVersion: "12.0.0",
651
- version: "22.2.0",
988
+ version: "22.2.3",
652
989
  ngImport: i0,
653
990
  type: AuthRefreshCoordinator,
654
991
  decorators: [{
@@ -711,7 +1048,7 @@ var CrossTabAuthSyncService = class CrossTabAuthSyncService {
711
1048
  };
712
1049
  static ɵfac = i0.ɵɵngDeclareFactory({
713
1050
  minVersion: "12.0.0",
714
- version: "22.2.0",
1051
+ version: "22.2.3",
715
1052
  ngImport: i0,
716
1053
  type: CrossTabAuthSyncService,
717
1054
  deps: [],
@@ -719,7 +1056,7 @@ var CrossTabAuthSyncService = class CrossTabAuthSyncService {
719
1056
  });
720
1057
  static ɵprov = i0.ɵɵngDeclareInjectable({
721
1058
  minVersion: "12.0.0",
722
- version: "22.2.0",
1059
+ version: "22.2.3",
723
1060
  ngImport: i0,
724
1061
  type: CrossTabAuthSyncService,
725
1062
  providedIn: "root"
@@ -727,7 +1064,7 @@ var CrossTabAuthSyncService = class CrossTabAuthSyncService {
727
1064
  };
728
1065
  i0.ɵɵngDeclareClassMetadata({
729
1066
  minVersion: "12.0.0",
730
- version: "22.2.0",
1067
+ version: "22.2.3",
731
1068
  ngImport: i0,
732
1069
  type: CrossTabAuthSyncService,
733
1070
  decorators: [{
@@ -788,7 +1125,7 @@ var AppInitService = class AppInitService {
788
1125
  }
789
1126
  static ɵfac = i0.ɵɵngDeclareFactory({
790
1127
  minVersion: "12.0.0",
791
- version: "22.2.0",
1128
+ version: "22.2.3",
792
1129
  ngImport: i0,
793
1130
  type: AppInitService,
794
1131
  deps: [],
@@ -796,7 +1133,7 @@ var AppInitService = class AppInitService {
796
1133
  });
797
1134
  static ɵprov = i0.ɵɵngDeclareInjectable({
798
1135
  minVersion: "12.0.0",
799
- version: "22.2.0",
1136
+ version: "22.2.3",
800
1137
  ngImport: i0,
801
1138
  type: AppInitService,
802
1139
  providedIn: "root"
@@ -804,7 +1141,7 @@ var AppInitService = class AppInitService {
804
1141
  };
805
1142
  i0.ɵɵngDeclareClassMetadata({
806
1143
  minVersion: "12.0.0",
807
- version: "22.2.0",
1144
+ version: "22.2.3",
808
1145
  ngImport: i0,
809
1146
  type: AppInitService,
810
1147
  decorators: [{
@@ -816,9 +1153,13 @@ const authGuard = () => {
816
1153
  const init = inject(AppInitService);
817
1154
  const store = inject(Store);
818
1155
  const router = inject(Router);
819
- return init.ready$.pipe(take(1), map(() => {
820
- if (store.selectSnapshot(AuthState.isAuthenticated)) return true;
821
- return router.createUrlTree(["/login"]);
1156
+ const consoleAccess = inject(ConsoleAccessService);
1157
+ return init.ready$.pipe(take(1), switchMap(() => {
1158
+ if (!store.selectSnapshot(AuthState.isAuthenticated)) return of(router.createUrlTree(["/login"]));
1159
+ return from(consoleAccess.ensure()).pipe(switchMap((access) => {
1160
+ if (!store.selectSnapshot(AuthState.isAuthenticated)) return of(router.createUrlTree(["/login"]));
1161
+ return "refused" !== access ? of(true) : consoleAccess.signOut().pipe(map(() => router.createUrlTree(["/login"], { queryParams: { reason: ConsoleAccessService.REFUSED_REASON } })));
1162
+ }));
822
1163
  }));
823
1164
  };
824
1165
  const loginPageGuard = () => {
@@ -833,262 +1174,28 @@ const loginPageGuard = () => {
833
1174
  const ELEVATION_WARNING_PASSWORD_NOT_SET = "elevation.password_not_set";
834
1175
  const ELEVATION_WARNING_FACTOR_MISSING = "elevation.factor_missing";
835
1176
  const ELEVATION_WARNING_MFA_OFF = "elevation.mfa_off";
836
- const ELEVATION_PROMPT = new InjectionToken("coolms.elevation-prompt");
837
- const BYPASS_ELEVATION = new HttpContextToken(() => false);
838
- const CROSS_TAB_KEY = "coolms.elevation.changed";
839
- const RECONCILE_MIN_INTERVAL_MS = 2e3;
840
- const ELEVATION_REQUIRED_HEADER = "X-Elevation-Required";
841
- var ElevationService = class ElevationService {
842
- http = inject(HttpClient);
843
- store = inject(Store);
844
- prompt = inject(ELEVATION_PROMPT, { optional: true });
845
- destroyRef = inject(DestroyRef);
846
- served = signal(null, ...ngDevMode ? [{ debugName: "served" }] : /* istanbul ignore next */ []);
847
- state = () => {
848
- const served = this.served();
849
- if (served === null || !isPast(served)) return served;
850
- const expired = expiredView(served);
851
- untracked(() => this.served.set(expired));
852
- this.announceExpiryLater();
853
- return expired;
854
- };
855
- elevated = () => this.state()?.elevated ?? false;
856
- expiresAt = () => {
857
- const iso = this.state()?.expiresAt;
858
- return iso ? new Date(iso) : null;
859
- };
860
- changes$ = new Subject();
861
- expiryTimer = null;
862
- pendingPrompt = null;
863
- expiryAnnouncePending = false;
864
- lastReconcileAt = 0;
865
- constructor() {
866
- if (typeof window !== "undefined") {
867
- window.addEventListener("storage", this.onStorage);
868
- window.addEventListener("focus", this.onFocus);
869
- if (typeof document !== "undefined") document.addEventListener("visibilitychange", this.onVisibilityChange);
870
- this.destroyRef.onDestroy(() => {
871
- window.removeEventListener("storage", this.onStorage);
872
- window.removeEventListener("focus", this.onFocus);
873
- if (typeof document !== "undefined") document.removeEventListener("visibilitychange", this.onVisibilityChange);
874
- this.clearTimer();
875
- });
876
- }
877
- }
878
- get available() {
879
- return this.url !== null;
880
- }
881
- get url() {
882
- return this.store.selectSnapshot(AppConfigState.manifest)?.identity?.elevationUrl ?? null;
883
- }
884
- isThisApi(url) {
885
- const apiBase = this.store.selectSnapshot(AppConfigState.manifest)?.apiBase ?? "/api/v1";
886
- let path = url.split("?")[0];
887
- if (/^https?:\/\//.test(path)) try {
888
- const parsed = new URL(path);
889
- if (typeof window !== "undefined" && parsed.host !== window.location.host) return false;
890
- path = parsed.pathname;
891
- } catch {
892
- return false;
893
- }
894
- return path === apiBase || path.startsWith(`${apiBase}/`);
895
- }
896
- refresh() {
897
- const url = this.url;
898
- if (!url) return of(this.inert());
899
- return this.http.get(url, this.options()).pipe(tap((s) => this.apply(s)));
900
- }
901
- elevate(password, code) {
902
- const url = this.url;
903
- if (!url) return of(this.inert());
904
- const body = { password };
905
- if (code) body.code = code;
906
- return this.http.post(url, body, this.options()).pipe(tap((s) => this.apply(s, "granted")));
907
- }
908
- drop(reason = "dropped_by_user") {
909
- const url = this.url;
910
- if (!url) return of(this.inert());
911
- return this.http.delete(`${url}?reason=${reason}`, this.options()).pipe(switchMap(() => this.refresh()));
912
- }
913
- offerFor(refusal) {
914
- if (!this.available || !this.prompt) return of(false);
915
- return this.refresh().pipe(switchMap((state) => state.elevated ? of(false) : this.requestElevation({
916
- state,
917
- refusal
918
- })));
919
- }
920
- requestElevation(request) {
921
- if (!this.prompt) return of(false);
922
- if (this.pendingPrompt) return this.pendingPrompt;
923
- const prompt = this.prompt;
924
- this.pendingPrompt = defer(() => prompt.open(request)).pipe(finalize(() => {
925
- this.pendingPrompt = null;
926
- }), shareReplay(1));
927
- return this.pendingPrompt;
928
- }
929
- options() {
930
- return { context: new HttpContext().set(BYPASS_ELEVATION, true) };
931
- }
932
- inert() {
933
- return {
934
- elevated: false,
935
- ended: {
936
- reason: "never",
937
- at: null
938
- },
939
- lifetimeSeconds: 0,
940
- lifetimeCeilingSeconds: 0,
941
- mfaRequired: false,
942
- warnings: []
943
- };
944
- }
945
- apply(next, explicit) {
946
- const prev = untracked(() => this.served());
947
- this.served.set(next);
948
- this.schedule(next);
949
- const kind = explicit ?? this.kindOf(prev, next);
950
- if (kind === null) return;
951
- this.expiryAnnouncePending = false;
952
- this.changes$.next({
953
- kind,
954
- state: next
955
- });
956
- this.announceToOtherTabs();
957
- }
958
- kindOf(prev, next) {
959
- const was = prev?.elevated ?? false;
960
- if (was && !next.elevated) return next.ended.reason === "expired" ? "expired" : "dropped";
961
- if (!was && next.elevated) return "granted";
962
- if (was && next.elevated && (prev?.expiresAt ?? null) !== (next.expiresAt ?? null)) return "observed";
963
- return null;
964
- }
965
- announceExpiryLater() {
966
- if (this.expiryAnnouncePending) return;
967
- this.expiryAnnouncePending = true;
968
- queueMicrotask(() => {
969
- if (!this.expiryAnnouncePending) return;
970
- this.expiryAnnouncePending = false;
971
- const state = untracked(() => this.served());
972
- if (state !== null && !state.elevated) this.changes$.next({
973
- kind: "expired",
974
- state
975
- });
976
- });
977
- }
978
- schedule(state) {
979
- this.clearTimer();
980
- if (!state.elevated || !state.expiresAt) return;
981
- const wanted = Date.parse(state.expiresAt) - Date.now() + 250;
982
- const ms = Math.min(Math.max(1e3, wanted), 2147483647);
983
- this.expiryTimer = setTimeout(() => {
984
- this.expiryTimer = null;
985
- this.refresh().subscribe({
986
- next: (s) => {
987
- if (s.elevated && s.expiresAt && Date.parse(s.expiresAt) <= Date.now()) this.scheduleRetry();
988
- },
989
- error: () => this.scheduleRetry()
990
- });
991
- }, ms);
992
- }
993
- scheduleRetry() {
994
- this.clearTimer();
995
- this.expiryTimer = setTimeout(() => {
996
- this.expiryTimer = null;
997
- this.refresh().subscribe({ error: () => void 0 });
998
- }, 5e3);
999
- }
1000
- clearTimer() {
1001
- if (this.expiryTimer !== null) {
1002
- clearTimeout(this.expiryTimer);
1003
- this.expiryTimer = null;
1004
- }
1005
- }
1006
- announceToOtherTabs() {
1007
- try {
1008
- localStorage.setItem(CROSS_TAB_KEY, String(Date.now()));
1009
- } catch {}
1010
- }
1011
- onStorage = (event) => {
1012
- if (event.key !== CROSS_TAB_KEY || !this.available) return;
1013
- this.refresh().subscribe({ error: () => void 0 });
1014
- };
1015
- onFocus = () => this.reconcile();
1016
- onVisibilityChange = () => {
1017
- if (document.visibilityState === "visible") this.reconcile();
1018
- };
1019
- reconcile() {
1020
- this.state();
1021
- if (!this.available) return;
1022
- const now = Date.now();
1023
- if (now - this.lastReconcileAt < RECONCILE_MIN_INTERVAL_MS) return;
1024
- this.lastReconcileAt = now;
1025
- this.refresh().subscribe({ error: () => void 0 });
1026
- }
1027
- static ɵfac = i0.ɵɵngDeclareFactory({
1028
- minVersion: "12.0.0",
1029
- version: "22.2.0",
1030
- ngImport: i0,
1031
- type: ElevationService,
1032
- deps: [],
1033
- target: i0.ɵɵFactoryTarget.Injectable
1034
- });
1035
- static ɵprov = i0.ɵɵngDeclareInjectable({
1036
- minVersion: "12.0.0",
1037
- version: "22.2.0",
1038
- ngImport: i0,
1039
- type: ElevationService,
1040
- providedIn: "root"
1041
- });
1042
- };
1043
- i0.ɵɵngDeclareClassMetadata({
1044
- minVersion: "12.0.0",
1045
- version: "22.2.0",
1046
- ngImport: i0,
1047
- type: ElevationService,
1048
- decorators: [{
1049
- type: Injectable,
1050
- args: [{ providedIn: "root" }]
1051
- }],
1052
- ctorParameters: () => []
1053
- });
1054
- function isPast(state) {
1055
- if (!state.elevated || !state.expiresAt) return false;
1056
- const at = Date.parse(state.expiresAt);
1057
- return Number.isFinite(at) && at <= Date.now();
1058
- }
1059
- function expiredView(state) {
1060
- return {
1061
- ...state,
1062
- elevated: false,
1063
- expiresAt: null,
1064
- grantedAt: null,
1065
- factorConfirmed: null,
1066
- ended: {
1067
- reason: "expired",
1068
- at: state.expiresAt ?? null
1069
- }
1070
- };
1071
- }
1177
+ const FRAMEWORK_REFUSAL = /^\s*access denied\.?\s*$/i;
1072
1178
  var ErrorHandlerService = class ErrorHandlerService {
1073
1179
  humanize(err) {
1074
1180
  if (!(err instanceof HttpErrorResponse)) return err instanceof Error ? err.message : "An unexpected error occurred.";
1181
+ const elevationRefusal = isElevationRefusal(err);
1075
1182
  const body = err.error;
1076
1183
  if (body && typeof body === "object") {
1077
1184
  const detail = body["detail"] ?? body["hydra:description"] ?? body["message"];
1078
- if (typeof detail === "string" && detail.trim()) return detail.trim();
1185
+ if (typeof detail === "string" && detail.trim() && !FRAMEWORK_REFUSAL.test(detail)) return detail.trim();
1079
1186
  if (Array.isArray(body["violations"])) {
1080
1187
  const msgs = body["violations"].map((v) => v.message).filter(Boolean);
1081
1188
  if (msgs.length) return msgs.join(" ");
1082
1189
  }
1083
1190
  }
1084
- return this.statusMessage(err.status);
1191
+ return elevationRefusal ? "This needs an elevated session." : this.statusMessage(err.status);
1085
1192
  }
1086
1193
  statusMessage(status) {
1087
1194
  switch (status) {
1088
1195
  case 0: return "Network error. Check your connection.";
1089
1196
  case 400: return "Invalid request.";
1090
1197
  case 401: return "Session expired. Please sign in again.";
1091
- case 403: return "Access denied.";
1198
+ case 403: return "You don't have access to this.";
1092
1199
  case 404: return "Resource not found.";
1093
1200
  case 409: return "Conflict — resource already exists.";
1094
1201
  case 422: return "Validation failed.";
@@ -1100,7 +1207,7 @@ var ErrorHandlerService = class ErrorHandlerService {
1100
1207
  }
1101
1208
  static ɵfac = i0.ɵɵngDeclareFactory({
1102
1209
  minVersion: "12.0.0",
1103
- version: "22.2.0",
1210
+ version: "22.2.3",
1104
1211
  ngImport: i0,
1105
1212
  type: ErrorHandlerService,
1106
1213
  deps: [],
@@ -1108,7 +1215,7 @@ var ErrorHandlerService = class ErrorHandlerService {
1108
1215
  });
1109
1216
  static ɵprov = i0.ɵɵngDeclareInjectable({
1110
1217
  minVersion: "12.0.0",
1111
- version: "22.2.0",
1218
+ version: "22.2.3",
1112
1219
  ngImport: i0,
1113
1220
  type: ErrorHandlerService,
1114
1221
  providedIn: "root"
@@ -1116,7 +1223,7 @@ var ErrorHandlerService = class ErrorHandlerService {
1116
1223
  };
1117
1224
  i0.ɵɵngDeclareClassMetadata({
1118
1225
  minVersion: "12.0.0",
1119
- version: "22.2.0",
1226
+ version: "22.2.3",
1120
1227
  ngImport: i0,
1121
1228
  type: ErrorHandlerService,
1122
1229
  decorators: [{
@@ -1124,42 +1231,85 @@ i0.ɵɵngDeclareClassMetadata({
1124
1231
  args: [{ providedIn: "root" }]
1125
1232
  }]
1126
1233
  });
1234
+ const READS = /* @__PURE__ */ new Set([
1235
+ "GET",
1236
+ "HEAD",
1237
+ "OPTIONS"
1238
+ ]);
1127
1239
  const elevationInterceptor = (req, next) => {
1128
1240
  const elevation = inject(ElevationService);
1241
+ if (req.context.get(BYPASS_ELEVATION) || req.context.get(BACKGROUND_REQUEST) || READS.has(req.method.toUpperCase()) || !elevation.isThisApi(req.url)) return next(req);
1242
+ const notice = inject(ELEVATION_NOTICE, { optional: true });
1129
1243
  const errors = inject(ErrorHandlerService);
1130
- if (req.context.get(BYPASS_ELEVATION) || !elevation.isThisApi(req.url)) return next(req);
1131
1244
  return next(req).pipe(catchError((err) => {
1132
- if (!(err instanceof HttpErrorResponse) || err.status !== 403 || !err.headers.get("X-Elevation-Required")) return throwError(() => err);
1133
- return elevation.offerFor(errors.humanize(err)).pipe(switchMap((granted) => granted ? next(req) : throwError(() => err)));
1245
+ if (!isElevationRefusal(err)) return throwError(() => err);
1246
+ const refusal = errors.humanize(err);
1247
+ const action = actionOf(req, elevation.apiBase);
1248
+ const decline = () => {
1249
+ notice?.refused(refusal, action);
1250
+ return throwError(() => err);
1251
+ };
1252
+ if (!elevation.canPrompt) return decline();
1253
+ return elevation.refresh().pipe(switchMap((state) => state.elevated ? of(true) : elevation.requestElevation({
1254
+ state,
1255
+ refusal,
1256
+ action
1257
+ })), catchError(() => of(false)), switchMap((granted) => granted ? next(req) : decline()));
1134
1258
  }));
1135
1259
  };
1260
+ function actionOf(req, apiBase = "/api/v1") {
1261
+ const named = req.context.get(ELEVATION_ACTION);
1262
+ if (named) return named;
1263
+ const verb = {
1264
+ POST: "Creating",
1265
+ PUT: "Saving",
1266
+ PATCH: "Saving",
1267
+ DELETE: "Deleting"
1268
+ }[req.method.toUpperCase()] ?? "This change";
1269
+ let path = req.url.split("?")[0];
1270
+ try {
1271
+ path = new URL(path, "http://x").pathname;
1272
+ } catch {}
1273
+ if (path.startsWith(apiBase)) path = path.slice(apiBase.length);
1274
+ const words = path.split("/").filter((s) => s !== "" && !/^[0-9a-f-]{8,}$/i.test(s) && !/^\d+$/.test(s)).map((s) => s.replace(/[-_]/g, " "));
1275
+ return words.length === 0 ? verb : `${verb} ${words.join(" ")}`;
1276
+ }
1136
1277
  var LoginComponent = class LoginComponent {
1137
1278
  identifier = "";
1138
1279
  password = "";
1139
- loading = false;
1140
- error = null;
1280
+ loading = signal(false, ...ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []);
1281
+ error = signal(null, ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
1141
1282
  store = inject(Store);
1142
1283
  router = inject(Router);
1284
+ static NO_CONSOLE_ACCESS = "This account has no access to the console.";
1285
+ constructor() {
1286
+ inject(ActivatedRoute).queryParamMap.pipe(takeUntilDestroyed()).subscribe((params) => {
1287
+ if (ConsoleAccessService.REFUSED_REASON === params.get("reason")) {
1288
+ this.loading.set(false);
1289
+ this.error.set(LoginComponent.NO_CONSOLE_ACCESS);
1290
+ }
1291
+ });
1292
+ }
1143
1293
  submit() {
1144
1294
  if (!this.identifier || !this.password) return;
1145
- this.loading = true;
1146
- this.error = null;
1295
+ this.loading.set(true);
1296
+ this.error.set(null);
1147
1297
  this.store.dispatch(new Login(this.identifier, this.password)).subscribe({
1148
1298
  next: () => {
1149
1299
  this.router.navigate(["/"]).catch(() => {
1150
- this.loading = false;
1151
- this.error = "Signed in, but the admin could not be opened. Please reload.";
1300
+ this.loading.set(false);
1301
+ this.error.set("Signed in, but the admin could not be opened. Please reload.");
1152
1302
  });
1153
1303
  },
1154
1304
  error: (err) => {
1155
- this.loading = false;
1156
- this.error = err?.error?.detail ?? err?.error?.message ?? err?.message ?? "Invalid credentials. Please try again.";
1305
+ this.loading.set(false);
1306
+ this.error.set(err?.error?.detail ?? err?.error?.message ?? err?.message ?? "Invalid credentials. Please try again.");
1157
1307
  }
1158
1308
  });
1159
1309
  }
1160
1310
  static ɵfac = i0.ɵɵngDeclareFactory({
1161
1311
  minVersion: "12.0.0",
1162
- version: "22.2.0",
1312
+ version: "22.2.3",
1163
1313
  ngImport: i0,
1164
1314
  type: LoginComponent,
1165
1315
  deps: [],
@@ -1167,12 +1317,12 @@ var LoginComponent = class LoginComponent {
1167
1317
  });
1168
1318
  static ɵcmp = i0.ɵɵngDeclareComponent({
1169
1319
  minVersion: "14.0.0",
1170
- version: "22.2.0",
1320
+ version: "22.2.3",
1171
1321
  type: LoginComponent,
1172
1322
  isStandalone: true,
1173
1323
  selector: "coolms-admin-login",
1174
1324
  ngImport: i0,
1175
- template: "<div class=\"d-flex align-items-center justify-content-center min-vh-100 bg-light\">\n <div class=\"card shadow-sm\" style=\"width: 360px\">\n <div class=\"card-body p-4\">\n <h1 class=\"h5 mb-1\">CoolMS Admin</h1>\n <p class=\"text-muted small mb-4\">Sign in to continue</p>\n\n <div *ngIf=\"error\" class=\"alert alert-danger py-2 small\">{{ error }}</div>\n\n <form (ngSubmit)=\"submit()\" #loginForm=\"ngForm\">\n <div class=\"mb-3\">\n <label class=\"form-label\">Email or username</label>\n <input\n type=\"text\"\n class=\"form-control\"\n [(ngModel)]=\"identifier\"\n name=\"identifier\"\n required\n autocomplete=\"username\"\n />\n </div>\n <div class=\"mb-3\">\n <label class=\"form-label\">Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n [(ngModel)]=\"password\"\n name=\"password\"\n required\n autocomplete=\"current-password\"\n />\n </div>\n <button\n type=\"submit\"\n class=\"cms-btn cms-btn-primary w-100 justify-content-center\"\n [disabled]=\"loading || loginForm.invalid\"\n >\n {{ loading ? 'Signing in…' : 'Sign in' }}\n </button>\n </form>\n </div>\n </div>\n</div>\n",
1325
+ template: "<div class=\"d-flex align-items-center justify-content-center min-vh-100 bg-light\">\n <div class=\"card shadow-sm\" style=\"width: 360px\">\n <div class=\"card-body p-4\">\n <h1 class=\"h5 mb-1\">CoolMS Admin</h1>\n <p class=\"text-muted small mb-4\">Sign in to continue</p>\n\n <div *ngIf=\"error()\" class=\"alert alert-danger py-2 small\">{{ error() }}</div>\n\n <form (ngSubmit)=\"submit()\" #loginForm=\"ngForm\">\n <div class=\"mb-3\">\n <label class=\"form-label\">Email or username</label>\n <input\n type=\"text\"\n class=\"form-control\"\n [(ngModel)]=\"identifier\"\n name=\"identifier\"\n required\n autocomplete=\"username\"\n />\n </div>\n <div class=\"mb-3\">\n <label class=\"form-label\">Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n [(ngModel)]=\"password\"\n name=\"password\"\n required\n autocomplete=\"current-password\"\n />\n </div>\n <button\n type=\"submit\"\n class=\"cms-btn cms-btn-primary w-100 justify-content-center\"\n [disabled]=\"loading() || loginForm.invalid\"\n >\n {{ loading() ? 'Signing in…' : 'Sign in' }}\n </button>\n </form>\n </div>\n </div>\n</div>\n",
1176
1326
  dependencies: [
1177
1327
  {
1178
1328
  kind: "ngmodule",
@@ -1240,7 +1390,7 @@ var LoginComponent = class LoginComponent {
1240
1390
  };
1241
1391
  i0.ɵɵngDeclareClassMetadata({
1242
1392
  minVersion: "12.0.0",
1243
- version: "22.2.0",
1393
+ version: "22.2.3",
1244
1394
  ngImport: i0,
1245
1395
  type: LoginComponent,
1246
1396
  decorators: [{
@@ -1249,9 +1399,10 @@ i0.ɵɵngDeclareClassMetadata({
1249
1399
  selector: "coolms-admin-login",
1250
1400
  standalone: true,
1251
1401
  imports: [FormsModule, NgIf],
1252
- template: "<div class=\"d-flex align-items-center justify-content-center min-vh-100 bg-light\">\n <div class=\"card shadow-sm\" style=\"width: 360px\">\n <div class=\"card-body p-4\">\n <h1 class=\"h5 mb-1\">CoolMS Admin</h1>\n <p class=\"text-muted small mb-4\">Sign in to continue</p>\n\n <div *ngIf=\"error\" class=\"alert alert-danger py-2 small\">{{ error }}</div>\n\n <form (ngSubmit)=\"submit()\" #loginForm=\"ngForm\">\n <div class=\"mb-3\">\n <label class=\"form-label\">Email or username</label>\n <input\n type=\"text\"\n class=\"form-control\"\n [(ngModel)]=\"identifier\"\n name=\"identifier\"\n required\n autocomplete=\"username\"\n />\n </div>\n <div class=\"mb-3\">\n <label class=\"form-label\">Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n [(ngModel)]=\"password\"\n name=\"password\"\n required\n autocomplete=\"current-password\"\n />\n </div>\n <button\n type=\"submit\"\n class=\"cms-btn cms-btn-primary w-100 justify-content-center\"\n [disabled]=\"loading || loginForm.invalid\"\n >\n {{ loading ? 'Signing in…' : 'Sign in' }}\n </button>\n </form>\n </div>\n </div>\n</div>\n"
1402
+ template: "<div class=\"d-flex align-items-center justify-content-center min-vh-100 bg-light\">\n <div class=\"card shadow-sm\" style=\"width: 360px\">\n <div class=\"card-body p-4\">\n <h1 class=\"h5 mb-1\">CoolMS Admin</h1>\n <p class=\"text-muted small mb-4\">Sign in to continue</p>\n\n <div *ngIf=\"error()\" class=\"alert alert-danger py-2 small\">{{ error() }}</div>\n\n <form (ngSubmit)=\"submit()\" #loginForm=\"ngForm\">\n <div class=\"mb-3\">\n <label class=\"form-label\">Email or username</label>\n <input\n type=\"text\"\n class=\"form-control\"\n [(ngModel)]=\"identifier\"\n name=\"identifier\"\n required\n autocomplete=\"username\"\n />\n </div>\n <div class=\"mb-3\">\n <label class=\"form-label\">Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n [(ngModel)]=\"password\"\n name=\"password\"\n required\n autocomplete=\"current-password\"\n />\n </div>\n <button\n type=\"submit\"\n class=\"cms-btn cms-btn-primary w-100 justify-content-center\"\n [disabled]=\"loading() || loginForm.invalid\"\n >\n {{ loading() ? 'Signing in…' : 'Sign in' }}\n </button>\n </form>\n </div>\n </div>\n</div>\n"
1253
1403
  }]
1254
- }]
1404
+ }],
1405
+ ctorParameters: () => []
1255
1406
  });
1256
1407
  var ConfigService = class ConfigService {
1257
1408
  http = inject(HttpClient);
@@ -1276,14 +1427,17 @@ var ConfigService = class ConfigService {
1276
1427
  const key = `${type}/${id}`;
1277
1428
  if (!this.cache.has(key)) {
1278
1429
  const base = this.store.selectSnapshot(AppConfigState.manifest)?.configBase ?? "/api/v1/config";
1279
- const obs = this.http.get(`${base}/${type}/${id}`).pipe(map((r) => r.data ?? r), shareReplay(1));
1430
+ const obs = this.http.get(`${base}/${type}/${id}`).pipe(map((r) => r.data ?? r), catchError((err) => {
1431
+ this.cache.delete(key);
1432
+ return throwError(() => err);
1433
+ }), shareReplay(1));
1280
1434
  this.cache.set(key, obs);
1281
1435
  }
1282
1436
  return this.cache.get(key);
1283
1437
  }
1284
1438
  static ɵfac = i0.ɵɵngDeclareFactory({
1285
1439
  minVersion: "12.0.0",
1286
- version: "22.2.0",
1440
+ version: "22.2.3",
1287
1441
  ngImport: i0,
1288
1442
  type: ConfigService,
1289
1443
  deps: [],
@@ -1291,7 +1445,7 @@ var ConfigService = class ConfigService {
1291
1445
  });
1292
1446
  static ɵprov = i0.ɵɵngDeclareInjectable({
1293
1447
  minVersion: "12.0.0",
1294
- version: "22.2.0",
1448
+ version: "22.2.3",
1295
1449
  ngImport: i0,
1296
1450
  type: ConfigService,
1297
1451
  providedIn: "root"
@@ -1299,7 +1453,7 @@ var ConfigService = class ConfigService {
1299
1453
  };
1300
1454
  i0.ɵɵngDeclareClassMetadata({
1301
1455
  minVersion: "12.0.0",
1302
- version: "22.2.0",
1456
+ version: "22.2.3",
1303
1457
  ngImport: i0,
1304
1458
  type: ConfigService,
1305
1459
  decorators: [{
@@ -1409,7 +1563,7 @@ var ThemeService = class ThemeService {
1409
1563
  }
1410
1564
  static ɵfac = i0.ɵɵngDeclareFactory({
1411
1565
  minVersion: "12.0.0",
1412
- version: "22.2.0",
1566
+ version: "22.2.3",
1413
1567
  ngImport: i0,
1414
1568
  type: ThemeService,
1415
1569
  deps: [],
@@ -1417,7 +1571,7 @@ var ThemeService = class ThemeService {
1417
1571
  });
1418
1572
  static ɵprov = i0.ɵɵngDeclareInjectable({
1419
1573
  minVersion: "12.0.0",
1420
- version: "22.2.0",
1574
+ version: "22.2.3",
1421
1575
  ngImport: i0,
1422
1576
  type: ThemeService,
1423
1577
  providedIn: "root"
@@ -1425,7 +1579,7 @@ var ThemeService = class ThemeService {
1425
1579
  };
1426
1580
  i0.ɵɵngDeclareClassMetadata({
1427
1581
  minVersion: "12.0.0",
1428
- version: "22.2.0",
1582
+ version: "22.2.3",
1429
1583
  ngImport: i0,
1430
1584
  type: ThemeService,
1431
1585
  decorators: [{
@@ -1441,7 +1595,7 @@ var CmsLoaderComponent = class CmsLoaderComponent {
1441
1595
  clipId = "cms-loader-" + Math.random().toString(36).slice(2, 9);
1442
1596
  static ɵfac = i0.ɵɵngDeclareFactory({
1443
1597
  minVersion: "12.0.0",
1444
- version: "22.2.0",
1598
+ version: "22.2.3",
1445
1599
  ngImport: i0,
1446
1600
  type: CmsLoaderComponent,
1447
1601
  deps: [],
@@ -1449,7 +1603,7 @@ var CmsLoaderComponent = class CmsLoaderComponent {
1449
1603
  });
1450
1604
  static ɵcmp = i0.ɵɵngDeclareComponent({
1451
1605
  minVersion: "17.0.0",
1452
- version: "22.2.0",
1606
+ version: "22.2.3",
1453
1607
  type: CmsLoaderComponent,
1454
1608
  isStandalone: true,
1455
1609
  selector: "cms-loader",
@@ -1510,7 +1664,7 @@ var CmsLoaderComponent = class CmsLoaderComponent {
1510
1664
  };
1511
1665
  i0.ɵɵngDeclareClassMetadata({
1512
1666
  minVersion: "12.0.0",
1513
- version: "22.2.0",
1667
+ version: "22.2.3",
1514
1668
  ngImport: i0,
1515
1669
  type: CmsLoaderComponent,
1516
1670
  decorators: [{
@@ -1660,7 +1814,7 @@ var SidebarStateService = class SidebarStateService {
1660
1814
  }
1661
1815
  static ɵfac = i0.ɵɵngDeclareFactory({
1662
1816
  minVersion: "12.0.0",
1663
- version: "22.2.0",
1817
+ version: "22.2.3",
1664
1818
  ngImport: i0,
1665
1819
  type: SidebarStateService,
1666
1820
  deps: [],
@@ -1668,7 +1822,7 @@ var SidebarStateService = class SidebarStateService {
1668
1822
  });
1669
1823
  static ɵprov = i0.ɵɵngDeclareInjectable({
1670
1824
  minVersion: "12.0.0",
1671
- version: "22.2.0",
1825
+ version: "22.2.3",
1672
1826
  ngImport: i0,
1673
1827
  type: SidebarStateService,
1674
1828
  providedIn: "root"
@@ -1676,7 +1830,7 @@ var SidebarStateService = class SidebarStateService {
1676
1830
  };
1677
1831
  i0.ɵɵngDeclareClassMetadata({
1678
1832
  minVersion: "12.0.0",
1679
- version: "22.2.0",
1833
+ version: "22.2.3",
1680
1834
  ngImport: i0,
1681
1835
  type: SidebarStateService,
1682
1836
  decorators: [{
@@ -1692,7 +1846,7 @@ var EntitySearchService = class EntitySearchService {
1692
1846
  }
1693
1847
  static ɵfac = i0.ɵɵngDeclareFactory({
1694
1848
  minVersion: "12.0.0",
1695
- version: "22.2.0",
1849
+ version: "22.2.3",
1696
1850
  ngImport: i0,
1697
1851
  type: EntitySearchService,
1698
1852
  deps: [],
@@ -1700,7 +1854,7 @@ var EntitySearchService = class EntitySearchService {
1700
1854
  });
1701
1855
  static ɵprov = i0.ɵɵngDeclareInjectable({
1702
1856
  minVersion: "12.0.0",
1703
- version: "22.2.0",
1857
+ version: "22.2.3",
1704
1858
  ngImport: i0,
1705
1859
  type: EntitySearchService,
1706
1860
  providedIn: "root"
@@ -1708,7 +1862,7 @@ var EntitySearchService = class EntitySearchService {
1708
1862
  };
1709
1863
  i0.ɵɵngDeclareClassMetadata({
1710
1864
  minVersion: "12.0.0",
1711
- version: "22.2.0",
1865
+ version: "22.2.3",
1712
1866
  ngImport: i0,
1713
1867
  type: EntitySearchService,
1714
1868
  decorators: [{
@@ -1724,8 +1878,8 @@ var NaviGraphService = class NaviGraphService {
1724
1878
  adminNav = signal([], ...ngDevMode ? [{ debugName: "adminNav" }] : /* istanbul ignore next */ []);
1725
1879
  topbarNav = signal([], ...ngDevMode ? [{ debugName: "topbarNav" }] : /* istanbul ignore next */ []);
1726
1880
  cache = /* @__PURE__ */ new Map();
1727
- loadAdminNav(url) {
1728
- return this.loadTree(url).pipe(map((tree) => {
1881
+ loadAdminNav(url, options = {}) {
1882
+ return this.loadTree(url, options.fresh ?? false).pipe(map((tree) => {
1729
1883
  this.adminNav.set(tree);
1730
1884
  return tree;
1731
1885
  }));
@@ -1775,8 +1929,8 @@ var NaviGraphService = class NaviGraphService {
1775
1929
  default: return false;
1776
1930
  }
1777
1931
  }
1778
- loadTree(url) {
1779
- if (this.cache.has(url)) return of(this.cache.get(url));
1932
+ loadTree(url, fresh = false) {
1933
+ if (!fresh && this.cache.has(url)) return of(this.cache.get(url));
1780
1934
  return this.http.get(url, { headers: { Accept: "application/ld+json" } }).pipe(map((r) => {
1781
1935
  const raw = r["member"] ?? r["hydra:member"] ?? [];
1782
1936
  const flat = Array.isArray(raw) ? raw : [];
@@ -1785,7 +1939,7 @@ var NaviGraphService = class NaviGraphService {
1785
1939
  return tree;
1786
1940
  }), catchError((err) => {
1787
1941
  console.error("[NaviGraph] loadTree failed for", url, this.errors.humanize(err));
1788
- return of([]);
1942
+ return of(this.cache.get(url) ?? []);
1789
1943
  }));
1790
1944
  }
1791
1945
  buildTree(flat) {
@@ -1806,7 +1960,7 @@ var NaviGraphService = class NaviGraphService {
1806
1960
  }
1807
1961
  static ɵfac = i0.ɵɵngDeclareFactory({
1808
1962
  minVersion: "12.0.0",
1809
- version: "22.2.0",
1963
+ version: "22.2.3",
1810
1964
  ngImport: i0,
1811
1965
  type: NaviGraphService,
1812
1966
  deps: [],
@@ -1814,7 +1968,7 @@ var NaviGraphService = class NaviGraphService {
1814
1968
  });
1815
1969
  static ɵprov = i0.ɵɵngDeclareInjectable({
1816
1970
  minVersion: "12.0.0",
1817
- version: "22.2.0",
1971
+ version: "22.2.3",
1818
1972
  ngImport: i0,
1819
1973
  type: NaviGraphService,
1820
1974
  providedIn: "root"
@@ -1822,7 +1976,7 @@ var NaviGraphService = class NaviGraphService {
1822
1976
  };
1823
1977
  i0.ɵɵngDeclareClassMetadata({
1824
1978
  minVersion: "12.0.0",
1825
- version: "22.2.0",
1979
+ version: "22.2.3",
1826
1980
  ngImport: i0,
1827
1981
  type: NaviGraphService,
1828
1982
  decorators: [{
@@ -1830,6 +1984,9 @@ i0.ɵɵngDeclareClassMetadata({
1830
1984
  args: [{ providedIn: "root" }]
1831
1985
  }]
1832
1986
  });
1987
+ function naviAccessOf(node) {
1988
+ return node.access === "elevate" ? "elevate" : "allowed";
1989
+ }
1833
1990
  var ComponentRegistry = class ComponentRegistry {
1834
1991
  static map = /* @__PURE__ */ new Map();
1835
1992
  static register(key, component) {
@@ -1858,7 +2015,7 @@ var ComponentRegistry = class ComponentRegistry {
1858
2015
  }
1859
2016
  static ɵfac = i0.ɵɵngDeclareFactory({
1860
2017
  minVersion: "12.0.0",
1861
- version: "22.2.0",
2018
+ version: "22.2.3",
1862
2019
  ngImport: i0,
1863
2020
  type: ComponentRegistry,
1864
2021
  deps: [],
@@ -1866,7 +2023,7 @@ var ComponentRegistry = class ComponentRegistry {
1866
2023
  });
1867
2024
  static ɵprov = i0.ɵɵngDeclareInjectable({
1868
2025
  minVersion: "12.0.0",
1869
- version: "22.2.0",
2026
+ version: "22.2.3",
1870
2027
  ngImport: i0,
1871
2028
  type: ComponentRegistry,
1872
2029
  providedIn: "root"
@@ -1874,7 +2031,7 @@ var ComponentRegistry = class ComponentRegistry {
1874
2031
  };
1875
2032
  i0.ɵɵngDeclareClassMetadata({
1876
2033
  minVersion: "12.0.0",
1877
- version: "22.2.0",
2034
+ version: "22.2.3",
1878
2035
  ngImport: i0,
1879
2036
  type: ComponentRegistry,
1880
2037
  decorators: [{
@@ -1957,7 +2114,7 @@ var ConsoleActivation = class ConsoleActivation {
1957
2114
  }
1958
2115
  static ɵfac = i0.ɵɵngDeclareFactory({
1959
2116
  minVersion: "12.0.0",
1960
- version: "22.2.0",
2117
+ version: "22.2.3",
1961
2118
  ngImport: i0,
1962
2119
  type: ConsoleActivation,
1963
2120
  deps: [],
@@ -1965,7 +2122,7 @@ var ConsoleActivation = class ConsoleActivation {
1965
2122
  });
1966
2123
  static ɵprov = i0.ɵɵngDeclareInjectable({
1967
2124
  minVersion: "12.0.0",
1968
- version: "22.2.0",
2125
+ version: "22.2.3",
1969
2126
  ngImport: i0,
1970
2127
  type: ConsoleActivation,
1971
2128
  providedIn: "root"
@@ -1973,7 +2130,7 @@ var ConsoleActivation = class ConsoleActivation {
1973
2130
  };
1974
2131
  i0.ɵɵngDeclareClassMetadata({
1975
2132
  minVersion: "12.0.0",
1976
- version: "22.2.0",
2133
+ version: "22.2.3",
1977
2134
  ngImport: i0,
1978
2135
  type: ConsoleActivation,
1979
2136
  decorators: [{
@@ -1984,7 +2141,8 @@ i0.ɵɵngDeclareClassMetadata({
1984
2141
  function consoleModuleInstalled(module) {
1985
2142
  return () => {
1986
2143
  const activation = inject(ConsoleActivation);
1987
- return inject(AppInitService).ready$.pipe(take(1), map(() => activation.isInstalled(module)));
2144
+ const consoleAccess = inject(ConsoleAccessService);
2145
+ return inject(AppInitService).ready$.pipe(take(1), switchMap(() => from(consoleAccess.ensure())), map(() => activation.isInstalled(module)));
1988
2146
  };
1989
2147
  }
1990
2148
  function consoleChildren(entries) {
@@ -2043,6 +2201,6 @@ function shouldStamp(url) {
2043
2201
  if (path.startsWith("/api/v1/auth/")) return false;
2044
2202
  return true;
2045
2203
  }
2046
- export { AppConfigState, AppInitService, AuthRefreshCoordinator, AuthState, BYPASS_AUTH, BYPASS_ELEVATION, CONSOLE_CONTRACT, CONSOLE_ENTRIES, CURRENT_SECTION, CmsLoaderComponent, ComponentRegistry, ConfigService, ConsoleActivation, ConsoleAssemblyError, ConsolePanelHost, CrossTabAuthSyncService, 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, isPortProvision, loginPageGuard, provideConsole, resolvePattern, satisfiesRange, sectionInterceptor };
2204
+ 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_ACTION, 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, actionOf, assembleConsole, authGuard, authInterceptor, consoleChildren, consoleEntry, consoleModuleInstalled, elevationInterceptor, installedConsoleModules, isElevationRefusal, isPortProvision, loginPageGuard, naviAccessOf, provideConsole, resolvePattern, satisfiesRange, sectionInterceptor };
2047
2205
 
2048
2206
  //# sourceMappingURL=coolms-core-angular.mjs.map