@securitydept/token-set-context-client-angular 0.3.0-beta.1 → 0.3.0-beta.11

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,468 +1,158 @@
1
- import { HTTP_INTERCEPTORS } from '@angular/common/http';
2
1
  import * as i0 from '@angular/core';
3
- import { signal, inject, DestroyRef, Injectable, InjectionToken, provideEnvironmentInitializer, EnvironmentInjector, runInInjectionContext, Component } from '@angular/core';
4
- import { getTokenFreshness, TokenFreshnessState, attachTokenSetResumeReconciliation, TokenSetAuthFlowSource } from '@securitydept/token-set-context-client/orchestration';
5
- import { from, switchMap, firstValueFrom, take } from 'rxjs';
6
- import { createTokenSetAuthRegistry } from '@securitydept/token-set-context-client/registry';
7
- export { ClientInitializationPriority } from '@securitydept/token-set-context-client/registry';
8
- import { bridgeToAngularSignal, signalToObservable, AUTH_PLANNER_HOST, extractFullRouteRequirements, withRouteRequirements } from '@securitydept/client-angular';
9
- import { Router } from '@angular/router';
2
+ import { InjectionToken, makeEnvironmentProviders, inject, Injectable, input, DestroyRef, afterNextRender, Component } from '@angular/core';
3
+ import { REQUIREMENT_PLANNER_HOST, SecuritydeptInjector, createSignal, createComputed, ResourceStatus, resourceFromSnapshots } from '@securitydept/client';
4
+ import { ENVIRONMENT, provideEnvironmentProvider, SECURITYDEPT_INJECTOR, createAngularCanActivate, createAngularCanActivateChild, secureRoute, secureRouteRoot, toNgSignal } from '@securitydept/client-angular';
5
+ import { provideTokenSetClientRegistry as provideTokenSetClientRegistry$1, TOKEN_SET_CLIENT_REGISTRY as TOKEN_SET_CLIENT_REGISTRY$1, TokenSetClientRegistryRequirementBehaviour, TokenSetClientRegistryPlannerHost, TokenSetCallbackClientSelectionKind, selectTokenSetBackendCallbackClientFromRegistry, selectTokenSetFrontendCallbackClientFromRegistry } from '@securitydept/token-set-context-client/registry';
6
+ import { HTTP_INTERCEPTORS } from '@angular/common/http';
7
+ import { from, switchMap } from 'rxjs';
8
+ import { OidcModeCallbackHandlingKind } from '@securitydept/token-set-context-client/orchestration';
9
+
10
+ const TOKEN_SET_CLIENT_REGISTRY = new InjectionToken("TOKEN_SET_CLIENT_REGISTRY");
11
+ function provideTokenSetClientRegistry(options = {}) {
12
+ return [
13
+ ...provideTokenSetClientRegistry$1(options).map(provideEnvironmentProvider),
14
+ {
15
+ provide: TOKEN_SET_CLIENT_REGISTRY,
16
+ useFactory: (environment) => environment.injector.get(TOKEN_SET_CLIENT_REGISTRY$1),
17
+ deps: [ENVIRONMENT],
18
+ },
19
+ ];
20
+ }
10
21
 
11
- // ============================================================================
12
- // 5. TokenSetAuthService — per-client service wrapper
13
- // ============================================================================
22
+ // Token-set requirement planner host — Angular DI adapter over core registry auth coordination
14
23
  /**
15
- * Per-client Angular service that bridges a single OIDC mode client to
16
- * Angular signals + RxJS.
24
+ * Provide registry-backed requirement behaviour for Angular guards.
17
25
  *
18
- * Iteration 110 change: lifecycle teardown is now explicit. Construct with
19
- * `new TokenSetAuthService(client, autoRestore)` and call `.dispose()` when
20
- * the service owner tears down. The shared registry core handles this
21
- * automatically — adopters that wire the service directly (rare) must call
22
- * `dispose()` themselves, typically bound to a `DestroyRef.onDestroy`.
26
+ * This is intentionally a thin adapter over the framework-neutral registry
27
+ * auth-coordination layer. Route requirements must carry
28
+ * `attributes.query: TokenSetClientQueryOptions`.
23
29
  */
24
- class TokenSetAuthService {
25
- client;
26
- /** Current auth snapshot as an Angular signal. */
27
- authState;
28
- /** Current auth snapshot as an RxJS Observable. */
29
- authState$;
30
- /** Promise that resolves when initial state restore completes (or null if skipped). */
31
- restorePromise;
32
- authEvents;
33
- cleanup;
34
- disposed = false;
35
- constructor(
36
- /** The underlying SDK client instance. */
37
- client, autoRestore) {
38
- this.client = client;
39
- this.authState = signal(null, ...(ngDevMode ? [{ debugName: "authState" }] : /* istanbul ignore next */ []));
40
- this.cleanup = bridgeToAngularSignal(client.state, this.authState);
41
- this.authState$ = signalToObservable(client.state);
42
- this.authEvents = client.authEvents;
43
- this.restorePromise = autoRestore ? client.restorePersistedState() : null;
44
- }
45
- /** Whether the user is currently authenticated. */
46
- isAuthenticated() {
47
- return this.accessToken() !== null;
48
- }
49
- /** Current access token, or null. */
50
- accessToken() {
51
- const snapshot = this.authState();
52
- if (!isFreshOrUsable(snapshot)) {
53
- return null;
54
- }
55
- return snapshot?.tokens.accessToken ?? null;
56
- }
57
- async ensureFreshAuthState(options) {
58
- return await this.client.ensureFreshAuthState(options);
59
- }
60
- async ensureAuthForResource(options) {
61
- return await this.client.ensureAuthForResource(options);
62
- }
63
- async ensureAccessToken(options) {
64
- return ((await this.ensureFreshAuthState(options))?.tokens.accessToken ?? null);
65
- }
66
- async ensureAuthorizationHeader(options) {
67
- return await this.client.ensureAuthorizationHeader(options);
68
- }
69
- /**
70
- * Release all resources owned by this service.
71
- *
72
- * - Unsubscribes the SDK state bridge
73
- * - Calls `client.dispose()`
74
- *
75
- * Idempotent: subsequent calls are no-ops. Called by the shared registry
76
- * at teardown; adopters invoking TokenSetAuthService directly must call
77
- * this themselves (typically bound to `DestroyRef.onDestroy`).
78
- */
79
- dispose() {
80
- if (this.disposed)
81
- return;
82
- this.disposed = true;
83
- try {
84
- this.cleanup();
85
- }
86
- catch {
87
- // Swallow — best effort.
88
- }
89
- try {
90
- this.client.dispose();
91
- }
92
- catch {
93
- // Swallow — best effort.
94
- }
95
- }
96
- }
97
- function isFreshOrUsable(snapshot) {
98
- const freshness = getTokenFreshness(snapshot, {
99
- now: Date.now(),
100
- clockSkewMs: 30_000,
101
- refreshWindowMs: 0,
102
- });
103
- return (freshness === TokenFreshnessState.Fresh ||
104
- freshness === TokenFreshnessState.RefreshDue ||
105
- freshness === TokenFreshnessState.NoExpiry);
106
- }
107
-
108
- class TokenSetAuthRegistry {
109
- /**
110
- * Shared framework-neutral registry core. Angular consumers rarely need
111
- * direct access, but this is exposed so adapter-internal helpers
112
- * (`CallbackResumeService`, `bearer-interceptor`) can query the core
113
- * directly when they don't need Angular-specific behaviour.
114
- */
115
- core;
116
- authEvents;
117
- constructor() {
118
- this.core = createTokenSetAuthRegistry({
119
- materialize: (client, entry) => {
120
- const angularEntry = entry;
121
- const managedClient = attachTokenSetResumeReconciliation(client, {
122
- resumeReconciliation: angularEntry.resumeReconciliation,
123
- resumeReconciliationOptions: angularEntry.resumeReconciliationOptions,
124
- });
125
- return new TokenSetAuthService(managedClient, entry.autoRestore ?? true);
30
+ function provideTokenSetRequirementPlannerHost(options = {}) {
31
+ return makeEnvironmentProviders([
32
+ {
33
+ provide: SECURITYDEPT_INJECTOR,
34
+ useFactory: () => {
35
+ const registry = inject(TOKEN_SET_CLIENT_REGISTRY);
36
+ const environment = inject(ENVIRONMENT);
37
+ const parentInjector = inject(SECURITYDEPT_INJECTOR, {
38
+ optional: true,
39
+ skipSelf: true,
40
+ }) ?? undefined;
41
+ const parent = (parentInjector?.get(REQUIREMENT_PLANNER_HOST, null) ??
42
+ undefined);
43
+ const behaviour = new TokenSetClientRegistryRequirementBehaviour(registry, options);
44
+ const host = new TokenSetClientRegistryPlannerHost(registry, behaviour, environment, parent);
45
+ const providers = [
46
+ {
47
+ provide: REQUIREMENT_PLANNER_HOST,
48
+ useValue: host,
49
+ },
50
+ ];
51
+ return parentInjector
52
+ ? SecuritydeptInjector.fromParentInjector(parentInjector, providers)
53
+ : SecuritydeptInjector.resolveAndCreate(providers);
126
54
  },
127
- dispose: (service) => service.dispose(),
128
- accessTokenOf: (service) => service.accessToken(),
129
- ensureAccessTokenOf: (service) => service.ensureAccessToken(),
130
- ensureAuthorizationHeaderOf: (service) => service.ensureAuthorizationHeader(),
131
- ensureAuthForResourceOf: (service, options) => service.ensureAuthForResource(options),
132
- authEventsOf: (service) => service.authEvents,
133
- });
134
- this.authEvents = this.core.authEvents;
135
- // Try to bind core.dispose() to the current injection context's
136
- // DestroyRef. Registry is typically constructed via DI (where this
137
- // always succeeds); pure unit tests instantiate directly and must
138
- // call `.dispose()` manually.
139
- try {
140
- const destroyRef = inject(DestroyRef);
141
- destroyRef.onDestroy(() => this.core.dispose());
142
- }
143
- catch {
144
- // Not in an injection context — adopter is expected to call
145
- // registry.dispose() manually (or never, for short-lived tests).
146
- }
147
- }
148
- register(entry) {
149
- return this.core.register(entry);
150
- }
151
- /**
152
- * Manually dispose the registry. Normally bound to the ambient
153
- * `DestroyRef` at construction; call this explicitly when instantiating
154
- * the registry outside of an Angular injection context (e.g. in unit
155
- * tests).
156
- */
157
- dispose() {
158
- this.core.dispose();
159
- }
160
- // --------------------------------------------------------------------------
161
- // Pass-through API (backwards-compatible with iteration 109 surface)
162
- // --------------------------------------------------------------------------
163
- isReady(key) {
164
- return this.core.isReady(key);
165
- }
166
- readinessState(key) {
167
- return this.core.readinessState(key);
168
- }
169
- async whenReady(key) {
170
- return this.core.whenReady(key);
171
- }
172
- /**
173
- * Preload a lazy client without throwing on rejection. Callers are
174
- * expected to attach `.catch` handlers for fire-and-forget usage.
175
- */
176
- preload(key) {
177
- return this.core.preload(key);
178
- }
179
- /**
180
- * Schedule preload for every lazy+not-initialized client using
181
- * `requestIdleCallback` / `setTimeout` fallback.
182
- */
183
- idleWarmup() {
184
- return this.core.idleWarmup();
185
- }
186
- reset(key) {
187
- this.core.reset(key);
188
- }
189
- metaFor(clientKey) {
190
- return this.core.metaFor(clientKey);
191
- }
192
- get(key) {
193
- return this.core.get(key);
194
- }
195
- require(key) {
196
- return this.core.require(key);
197
- }
198
- keys() {
199
- return this.core.keys();
200
- }
201
- entries() {
202
- return this.core.entriesSnapshot();
203
- }
204
- // ---- URL / callback / requirement / provider-family discrimination -----
205
- *clientKeyGenForUrl(url) {
206
- yield* this.core.clientKeyGenForUrl(url);
207
- }
208
- *clientKeyGenForCallback(url) {
209
- yield* this.core.clientKeyGenForCallback(url);
210
- }
211
- *clientKeyGenForRequirement(requirementKind) {
212
- yield* this.core.clientKeyGenForRequirement(requirementKind);
213
- }
214
- *clientKeyGenForProviderFamily(providerFamily) {
215
- yield* this.core.clientKeyGenForProviderFamily(providerFamily);
216
- }
217
- clientKeyListForUrl(url) {
218
- return this.core.clientKeyListForUrl(url);
219
- }
220
- clientKeyListForCallback(url) {
221
- return this.core.clientKeyListForCallback(url);
222
- }
223
- clientKeyListForRequirement(requirementKind) {
224
- return this.core.clientKeyListForRequirement(requirementKind);
225
- }
226
- clientKeyListForProviderFamily(providerFamily) {
227
- return this.core.clientKeyListForProviderFamily(providerFamily);
228
- }
229
- clientKeyForUrl(url, selector) {
230
- return this.core.clientKeyForUrl(url, selector);
231
- }
232
- clientKeyForCallback(url, selector) {
233
- return this.core.clientKeyForCallback(url, selector);
234
- }
235
- clientKeyForRequirement(requirementKind, selector) {
236
- return this.core.clientKeyForRequirement(requirementKind, selector);
237
- }
238
- clientKeyForProviderFamily(providerFamily, selector) {
239
- return this.core.clientKeyForProviderFamily(providerFamily, selector);
240
- }
241
- requireForRequirement(requirementKind, selector) {
242
- return this.core.requireForRequirement(requirementKind, selector);
243
- }
244
- requireForProviderFamily(providerFamily, selector) {
245
- return this.core.requireForProviderFamily(providerFamily, selector);
246
- }
247
- *clientKeyGenForFilter(filter) {
248
- yield* this.core.clientKeyGenForFilter(filter);
249
- }
250
- *clientKeyGenForOptions(options) {
251
- yield* this.core.clientKeyGenForOptions(options);
252
- }
253
- clientKeysForOptions(options) {
254
- return this.core.clientKeysForOptions(options);
255
- }
256
- accessToken(key) {
257
- return this.core.accessToken(key);
258
- }
259
- async ensureAccessToken(key) {
260
- return await this.core.ensureAccessToken(key);
261
- }
262
- async ensureAuthorizationHeader(key) {
263
- return await this.core.ensureAuthorizationHeader(key);
264
- }
265
- async ensureAuthForResource(options = {}) {
266
- return await this.core.ensureAuthForResource(options);
267
- }
268
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: TokenSetAuthRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
269
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: TokenSetAuthRegistry });
55
+ },
56
+ ]);
270
57
  }
271
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: TokenSetAuthRegistry, decorators: [{
272
- type: Injectable
273
- }], ctorParameters: () => [] });
274
58
 
59
+ // Token-set secure route helpers — query-first registry requirements
275
60
  /**
276
- * DI token for {@link BearerInterceptorOptions}. Adopters normally configure
277
- * options via `provideTokenSetBearerInterceptor({ ... })`; the token is
278
- * exported for advanced cases where the options need to be supplied or
279
- * overridden separately (for example, in tests).
61
+ * Create a token-set `CanActivateFn`. The behaviour host is resolved from DI
62
+ * (provided by {@link provideTokenSetRequirementPlannerHost}).
280
63
  */
281
- const TOKEN_SET_BEARER_INTERCEPTOR_OPTIONS = new InjectionToken("TOKEN_SET_BEARER_INTERCEPTOR_OPTIONS");
64
+ function createTokenSetCanActivate(options) {
65
+ return createAngularCanActivate(options);
66
+ }
282
67
  /**
283
- * Angular class-based bearer-token interceptor (`HTTP_INTERCEPTORS` style).
284
- *
285
- * Selects the correct access token from the multi-client registry by URL
286
- * pattern. Behaviour for unmatched URLs is governed by
287
- * {@link BearerInterceptorOptions.strictUrlMatch}.
288
- *
289
- * Prefer the functional version via `createTokenSetBearerInterceptor()` +
290
- * `withInterceptors()` when using `provideHttpClient`. Use this class-based
291
- * version when you need classic `HTTP_INTERCEPTORS` multi-provider style
292
- * (e.g. NgModule apps).
293
- *
294
- * @example
295
- * ```ts
296
- * // NgModule providers:
297
- * provideTokenSetBearerInterceptor({ strictUrlMatch: true })
298
- * ```
68
+ * Create a token-set `CanActivateChildFn`. The behaviour host is resolved from
69
+ * DI (provided by {@link provideTokenSetRequirementPlannerHost}).
299
70
  */
300
- class TokenSetBearerInterceptor {
301
- registry = inject(TokenSetAuthRegistry);
302
- options = inject(TOKEN_SET_BEARER_INTERCEPTOR_OPTIONS, { optional: true }) ?? {};
71
+ function createTokenSetCanActivateChild(options) {
72
+ return createAngularCanActivateChild(options);
73
+ }
74
+ /**
75
+ * Declare a secured child route. Writes token-set registry requirement
76
+ * metadata only; enforcement is owned by a guarded
77
+ * {@link secureTokenSetRouteRoot} ancestor.
78
+ */
79
+ function secureTokenSetRoute(path, security = {}, routeOptions) {
80
+ return secureRoute(path, {
81
+ requirements: security.requirements,
82
+ composition: security.composition,
83
+ }, routeOptions);
84
+ }
85
+ /**
86
+ * Declare a guarded token-set route root. Delegates guard assembly to the
87
+ * client-angular base helper and mounts registry-backed behaviour hooks.
88
+ */
89
+ function secureTokenSetRouteRoot(path, security = {}, routeOptions) {
90
+ const providers = [
91
+ ...(routeOptions?.providers ?? []),
92
+ provideTokenSetRequirementPlannerHost({
93
+ checkClientAuthenticated: security.checkClientAuthenticated,
94
+ onClientUnauthenticated: security.onClientUnauthenticated,
95
+ selectClientCandidate: security.selectClientCandidate,
96
+ }),
97
+ ];
98
+ return secureRouteRoot(path, {
99
+ requirements: security.requirements,
100
+ composition: security.composition,
101
+ }, {
102
+ ...routeOptions,
103
+ providers,
104
+ });
105
+ }
106
+
107
+ const TOKEN_SET_CLIENT_REGISTRY_AUTHORIZATION_FOR_REQUEST = new InjectionToken("TOKEN_SET_CLIENT_REGISTRY_AUTHORIZATION_FOR_REQUEST");
108
+ class TokenSetClientRegistryAuthorizationInterceptor {
109
+ registry = inject(TOKEN_SET_CLIENT_REGISTRY);
110
+ authorizationForRequest = inject(TOKEN_SET_CLIENT_REGISTRY_AUTHORIZATION_FOR_REQUEST, {
111
+ optional: true,
112
+ }) ?? defaultTokenSetClientRegistryAuthorizationForRequest;
303
113
  intercept(req, next) {
304
- const key = this.registry.clientKeyForUrl(req.url);
305
- // When a client key matches but the client is still initializing
306
- // (async clientFactory not yet resolved), registry.get() returns
307
- // undefined. This is an explicit design decision: interceptors do NOT
308
- // block the HTTP request waiting for client initialization.
309
- // Responsibility for "ensure client is ready before making requests"
310
- // belongs to route guards (which use whenReady() to await readiness).
311
- // A not-yet-ready client correctly produces no token → request proceeds
312
- // without Authorization header.
313
- return from(resolveAuthorizationForRequest(this.registry, req.url, key, {
314
- strictUrlMatch: this.options.strictUrlMatch,
315
- })).pipe(switchMap((authorization) => {
114
+ return from(Promise.resolve(this.authorizationForRequest(this.registry, req))).pipe(switchMap((authorization) => {
316
115
  if (!authorization) {
317
116
  return next.handle(req);
318
117
  }
319
118
  return next.handle(req.clone({ setHeaders: { Authorization: authorization } }));
320
119
  }));
321
120
  }
322
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: TokenSetBearerInterceptor, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
323
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: TokenSetBearerInterceptor });
121
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: TokenSetClientRegistryAuthorizationInterceptor, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
122
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: TokenSetClientRegistryAuthorizationInterceptor });
324
123
  }
325
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: TokenSetBearerInterceptor, decorators: [{
124
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: TokenSetClientRegistryAuthorizationInterceptor, decorators: [{
326
125
  type: Injectable
327
126
  }] });
328
- /**
329
- * Create an `HTTP_INTERCEPTORS` multi-provider entry for bearer token injection.
330
- *
331
- * Returns a single `Provider` object that registers {@link TokenSetBearerInterceptor}
332
- * as a class-based HTTP interceptor. Suitable for NgModule `providers` arrays.
333
- *
334
- * For functional-interceptor style (standalone apps using `provideHttpClient`),
335
- * use `createTokenSetBearerInterceptor()` with `withInterceptors()` instead.
336
- *
337
- * @example
338
- * ```ts
339
- * @NgModule({
340
- * providers: [
341
- * provideTokenSetAuth({ clients: [...] }),
342
- * provideTokenSetBearerInterceptor(), // <— replaces hand-written AuthInterceptor
343
- * provideHttpClient(withInterceptorsFromDi()),
344
- * ],
345
- * })
346
- * export class AppModule {}
347
- * ```
348
- */
349
- /**
350
- * Create an `HTTP_INTERCEPTORS` multi-provider entry for bearer token injection.
351
- *
352
- * Returns Angular providers that register {@link TokenSetBearerInterceptor}
353
- * as a class-based HTTP interceptor and (optionally) the
354
- * {@link BearerInterceptorOptions} that govern its URL-match behaviour.
355
- * Suitable for NgModule `providers` arrays.
356
- *
357
- * For functional-interceptor style (standalone apps using `provideHttpClient`),
358
- * use `createTokenSetBearerInterceptor()` with `withInterceptors()` instead.
359
- *
360
- * @example
361
- * ```ts
362
- * @NgModule({
363
- * providers: [
364
- * provideTokenSetAuth({ clients: [...] }),
365
- * // Adopters with multiple backends or any third-party HTTP traffic
366
- * // MUST opt into strictUrlMatch to prevent token leakage.
367
- * provideTokenSetBearerInterceptor({ strictUrlMatch: true }),
368
- * provideHttpClient(withInterceptorsFromDi()),
369
- * ],
370
- * })
371
- * export class AppModule {}
372
- * ```
373
- */
374
- function provideTokenSetBearerInterceptor(options) {
127
+ function provideTokenSetClientRegistryAuthorizationInterceptor(options) {
375
128
  const providers = [
376
129
  {
377
130
  provide: HTTP_INTERCEPTORS,
378
- useClass: TokenSetBearerInterceptor,
131
+ useClass: TokenSetClientRegistryAuthorizationInterceptor,
379
132
  multi: true,
380
133
  },
381
134
  ];
382
135
  if (options) {
383
- providers.push({
384
- provide: TOKEN_SET_BEARER_INTERCEPTOR_OPTIONS,
385
- useValue: options,
386
- });
136
+ if (options.authorizationForRequest) {
137
+ providers.push({
138
+ provide: TOKEN_SET_CLIENT_REGISTRY_AUTHORIZATION_FOR_REQUEST,
139
+ useValue: options.authorizationForRequest,
140
+ });
141
+ }
387
142
  }
388
143
  return providers;
389
144
  }
390
- /**
391
- * Create an Angular `HttpInterceptorFn` that injects bearer authorization
392
- * headers, selecting the correct token from the multi-client registry
393
- * based on URL pattern matching.
394
- *
395
- * If no URL pattern matches, falls back to the first available token
396
- * (single-client convenience).
397
- *
398
- * ## Async client readiness
399
- *
400
- * The interceptor does **not** block or delay the HTTP request waiting for
401
- * an async client to materialize. If a client is still initializing when the
402
- * request fires, the request proceeds without an Authorization header.
403
- *
404
- * This is intentional: blocking HTTP for client initialization would deadlock
405
- * apps that make HTTP requests during initialization itself. Route guards
406
- * (which use `registry.whenReady()`) are the correct place to enforce
407
- * "client must be ready before user reaches this route".
408
- *
409
- * For NgModule-style apps using `HTTP_INTERCEPTORS`, use
410
- * `provideTokenSetBearerInterceptor()` instead.
411
- *
412
- * @example
413
- * ```ts
414
- * import { provideHttpClient, withInterceptors } from "@angular/common/http";
415
- * import { createTokenSetBearerInterceptor } from "@securitydept/token-set-context-client-angular";
416
- *
417
- * provideHttpClient(withInterceptors([
418
- * createTokenSetBearerInterceptor(registry),
419
- * ]));
420
- * ```
421
- */
422
- /**
423
- * Create an Angular `HttpInterceptorFn` that injects bearer authorization
424
- * headers, selecting the correct token from the multi-client registry
425
- * based on URL pattern matching.
426
- *
427
- * If no URL pattern matches, the unmatched-URL behaviour is governed by
428
- * {@link BearerInterceptorOptions.strictUrlMatch}: the default falls back
429
- * to `registry.accessToken()` (single-client convenience), while
430
- * `strictUrlMatch: true` returns no token, ensuring bearer headers are
431
- * never injected for URLs outside the registered patterns.
432
- *
433
- * ## Async client readiness
434
- *
435
- * The interceptor does **not** block or delay the HTTP request waiting for
436
- * an async client to materialize. If a client is still initializing when the
437
- * request fires, the request proceeds without an Authorization header.
438
- *
439
- * This is intentional: blocking HTTP for client initialization would deadlock
440
- * apps that make HTTP requests during initialization itself. Route guards
441
- * (which use `registry.whenReady()`) are the correct place to enforce
442
- * "client must be ready before user reaches this route".
443
- *
444
- * For NgModule-style apps using `HTTP_INTERCEPTORS`, use
445
- * `provideTokenSetBearerInterceptor()` instead.
446
- *
447
- * @example
448
- * ```ts
449
- * import { provideHttpClient, withInterceptors } from "@angular/common/http";
450
- * import { createTokenSetBearerInterceptor } from "@securitydept/token-set-context-client-angular";
451
- *
452
- * provideHttpClient(withInterceptors([
453
- * createTokenSetBearerInterceptor(registry, { strictUrlMatch: true }),
454
- * ]));
455
- * ```
456
- */
457
- function createTokenSetBearerInterceptor(registry, options = {}) {
458
- return (req, next) => {
459
- // Try URL-pattern-based client selection first.
460
- const key = registry.clientKeyForUrl(req.url);
461
- // Explicit not-yet-ready semantics: if the client exists in metadata
462
- // (via clientKeyForUrl) but registry.get() returns undefined because the
463
- // async clientFactory has not resolved yet, the request proceeds without
464
- // a token. See class-based interceptor above for full rationale.
465
- return from(resolveAuthorizationForRequest(registry, req.url, key, options)).pipe(switchMap((authorization) => {
145
+ function createTokenSetClientRegistryAuthorizationInterceptor(options) {
146
+ return function tokenSetClientRegistryAuthorizationInterceptorFn(req, next) {
147
+ const registry = options?.registry ?? inject(TOKEN_SET_CLIENT_REGISTRY);
148
+ const authorizationForRequest = options?.authorizationForRequest ??
149
+ (options
150
+ ? null
151
+ : inject(TOKEN_SET_CLIENT_REGISTRY_AUTHORIZATION_FOR_REQUEST, {
152
+ optional: true,
153
+ })) ??
154
+ defaultTokenSetClientRegistryAuthorizationForRequest;
155
+ return from(Promise.resolve(authorizationForRequest(registry, req))).pipe(switchMap((authorization) => {
466
156
  if (!authorization) {
467
157
  return next(req);
468
158
  }
@@ -470,622 +160,136 @@ function createTokenSetBearerInterceptor(registry, options = {}) {
470
160
  }));
471
161
  };
472
162
  }
473
- async function resolveAuthorizationForRequest(registry, url, key, options) {
474
- if (!key && options.strictUrlMatch) {
163
+ async function defaultTokenSetClientRegistryAuthorizationForRequest(registry, request) {
164
+ const record = await registry.clientRecordForQuery({
165
+ url: request.url,
166
+ }, { initialize: true });
167
+ if (!record) {
475
168
  return null;
476
169
  }
477
- const result = await registry.ensureAuthForResource({
478
- key,
479
- waitForReady: false,
480
- source: TokenSetAuthFlowSource.HttpInterceptor,
481
- needsAuthorizationHeader: true,
482
- forceRefreshWhenDue: true,
483
- url,
484
- });
485
- return result?.authorizationHeader ?? null;
486
- }
487
-
488
- /**
489
- * Angular-native service for handling OIDC redirect callbacks,
490
- * with multi-client discrimination support.
491
- *
492
- * The post-authentication redirect URI is propagated automatically through
493
- * the core client's pending state (set via `loginWithRedirect({ postAuthRedirectUri })`
494
- * and recovered in `handleCallback()`). No manual URL storage is required.
495
- *
496
- * @example
497
- * ```ts
498
- * @Component({ ... })
499
- * export class AuthCallbackComponent {
500
- * private readonly callbackService = inject(CallbackResumeService);
501
- * private readonly router = inject(Router);
502
- *
503
- * async ngOnInit() {
504
- * const result = await this.callbackService.handleCallback(
505
- * window.location.href,
506
- * );
507
- * this.router.navigateByUrl(result.resumeUrl, { replaceUrl: true });
508
- * }
509
- * }
510
- * ```
511
- */
512
- class CallbackResumeService {
513
- registry = inject(TokenSetAuthRegistry);
514
- /**
515
- * Check whether a URL is an OIDC authorization callback for any registered
516
- * client in this registry.
517
- *
518
- * This is a convenience wrapper over `clientKeyForCallback()`. Use it for
519
- * programmatic early-exit guards (e.g. in a service constructor) before
520
- * calling `handleCallback()`.
521
- *
522
- * @param url - The full URL to check (e.g. `window.location.href`).
523
- * @returns `true` when the URL matches a registered callback path and
524
- * contains an `code` or `error` query parameter.
525
- */
526
- isCallback(url) {
527
- return this.registry.clientKeyForCallback(url) !== undefined;
528
- }
529
- /**
530
- * Handle the OIDC callback, auto-detecting which client the callback
531
- * belongs to via registered callback paths.
532
- *
533
- * The `resumeUrl` in the result is taken directly from the core client's
534
- * pending state (`postAuthRedirectUri` set at `loginWithRedirect()` time),
535
- * falling back to `"/"` when no redirect URI was recorded.
536
- *
537
- * @param callbackUrl - The full current URL (e.g. `window.location.href`).
538
- * @param explicitClientKey - Optional: force-select a client key instead of
539
- * auto-detecting from registered callback paths.
540
- */
541
- async handleCallback(callbackUrl, explicitClientKey) {
542
- // Determine which client this callback is for.
543
- const clientKey = explicitClientKey ?? this.registry.clientKeyForCallback(callbackUrl);
544
- if (!clientKey) {
545
- throw new Error(`[CallbackResumeService] Cannot determine which client this callback belongs to. ` +
546
- `URL: ${callbackUrl}. Register callbackPath in the client entry.`);
547
- }
548
- // Use whenReady() — not require() — so that if the client's async
549
- // clientFactory is still in-flight when the callback page first loads,
550
- // we wait for it to materialize rather than throwing.
551
- const service = await this.registry.whenReady(clientKey);
552
- const result = await service.client.handleCallback(callbackUrl);
553
- // postAuthRedirectUri is recovered from the core client's pending state,
554
- // which was stored by authorizeUrl() when loginWithRedirect() was called.
555
- return {
556
- clientKey,
557
- snapshot: result.snapshot,
558
- resumeUrl: result.postAuthRedirectUri ?? "/",
559
- };
560
- }
561
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: CallbackResumeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
562
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: CallbackResumeService });
563
- }
564
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: CallbackResumeService, decorators: [{
565
- type: Injectable
566
- }] });
567
-
568
- // ============================================================================
569
- // 8. Callback helper — OIDC redirect callback with multi-client discrimination
570
- // ============================================================================
571
- /**
572
- * Check whether the current URL is an OIDC authorization callback.
573
- */
574
- function isOidcCallback(options) {
575
- try {
576
- const url = new URL(options.currentUrl);
577
- const callbackUrl = new URL(options.callbackPath, url.origin);
578
- return (url.pathname === callbackUrl.pathname &&
579
- (url.searchParams.has("code") || url.searchParams.has("error")));
580
- }
581
- catch {
582
- return false;
583
- }
584
- }
585
-
586
- /** InjectionToken for the multi-client auth registry. */
587
- const TOKEN_SET_AUTH_REGISTRY = new InjectionToken("TOKEN_SET_AUTH_REGISTRY");
588
-
589
- /**
590
- * Create Angular providers for the multi-client token-set auth integration.
591
- *
592
- * Registers all services into Angular DI so adopters can directly inject
593
- * `TokenSetAuthRegistry`, `CallbackResumeService`, and look up individual
594
- * `TokenSetAuthService` instances by key.
595
- *
596
- * ## Async client initialization
597
- *
598
- * When a client's `clientFactory` returns a `Promise` (e.g. because it
599
- * needs to fetch a config projection from a backend endpoint), the
600
- * registry tracks its initialization state automatically. Guards and
601
- * interceptors should use `registry.whenReady(key)` to await
602
- * materialization before accessing the service.
603
- *
604
- * Adapter-managed browser clients also install token-set page-resume
605
- * reconciliation by default during materialization. Set
606
- * `resumeReconciliation: false` on an individual client entry only when the
607
- * host intentionally owns equivalent browser lifecycle wiring.
608
- *
609
- * @example
610
- * ```ts
611
- * import { provideTokenSetAuth } from "@securitydept/token-set-context-client-angular";
612
- * import { resolveConfigProjection, networkConfigSource, createFrontendOidcModeClient }
613
- * from "@securitydept/token-set-context-client/frontend-oidc-mode";
614
- *
615
- * export const appConfig = {
616
- * providers: [
617
- * provideTokenSetAuth({
618
- * clients: [
619
- * {
620
- * key: "main",
621
- * // Async clientFactory: fetches config projection from backend
622
- * clientFactory: async () => {
623
- * const resolved = await resolveConfigProjection([
624
- * networkConfigSource({
625
- * apiEndpoint: "https://api.example.com/api",
626
- * redirectUri: `${location.origin}/auth/callback`,
627
- * }),
628
- * ]);
629
- * return createFrontendOidcModeClient(resolved.config, runtime);
630
- * },
631
- * urlPatterns: ["/api/"],
632
- * callbackPath: "/auth/callback",
633
- * // Optional: opt out only if the host already wires equivalent
634
- * // resume reconciliation for this browser client.
635
- * // resumeReconciliation: false,
636
- * },
637
- * ],
638
- * }),
639
- * ],
640
- * };
641
- * ```
642
- */
643
- function provideTokenSetAuth(options) {
644
- return [
645
- // Registry is a singleton — all clients share one registry.
646
- {
647
- provide: TokenSetAuthRegistry,
648
- useFactory: () => new TokenSetAuthRegistry(),
649
- },
650
- // Eagerly register all client entries during environment initialization.
651
- // provideEnvironmentInitializer runs in injection context, so inject() works.
652
- // Note: async clientFactories create promises that the registry tracks;
653
- // the initializer does not block Angular bootstrap on them (guards/interceptors
654
- // use registry.whenReady() instead).
655
- provideEnvironmentInitializer(() => {
656
- const registry = inject(TokenSetAuthRegistry);
657
- for (const entry of options.clients) {
658
- registry.register(entry);
659
- }
660
- if (options.idleWarmup !== false) {
661
- // Kick off idle-time preload for any lazy clients. The cancel
662
- // handle is intentionally discarded — teardown happens through
663
- // the registry's own DestroyRef binding at dispose time.
664
- registry.idleWarmup();
665
- }
666
- }),
667
- // Token for direct registry access.
668
- {
669
- provide: TOKEN_SET_AUTH_REGISTRY,
670
- useExisting: TokenSetAuthRegistry,
671
- },
672
- // Callback resume service.
673
- CallbackResumeService,
674
- ];
170
+ const header = await record.client.authorizationHeaderValue.whenValue();
171
+ return header ?? null;
675
172
  }
676
173
 
677
- // ---------------------------------------------------------------------------
678
- // Guard factory
679
- // ---------------------------------------------------------------------------
680
- /**
681
- * Create an Angular `CanActivateFn` that performs full-route requirement
682
- * aggregation and single-pass planner evaluation.
683
- *
684
- * **Canonical Angular Router auth pattern** (route-metadata driven):
685
- * 1. Requirements are declared in route `data` using `withRouteRequirements()`.
686
- * 2. When the guard runs, it walks `route.pathFromRoot` to collect ALL
687
- * requirements from every ancestor segment in a single pass.
688
- * 3. Each requirement's `kind` is mapped to one or more registry clients,
689
- * unless overridden by a `requirementPolicies[id].selector`.
690
- * 4. The complete candidate set is handed to the `PlannerHost` for a
691
- * single `evaluate()` call — the planner selects the first unresolved
692
- * requirement and calls the matching handler.
693
- *
694
- * **Handler resolution order** (most specific wins):
695
- * 1. `requirementPolicies[requirement.id].onUnauthenticated`
696
- * 2. `requirementHandlers[requirement.kind]`
697
- * 3. `defaultOnUnauthenticated`
698
- * 4. Block navigation (`false`) if none of the above is provided.
699
- *
700
- * **Client resolution order** for a requirement:
701
- * 1. `requirementPolicies[requirement.id].selector.clientKey` — single explicit key
702
- * 2. `requirementPolicies[requirement.id].selector.query` — composite query
703
- * 3. `registry.clientKeyListForRequirement(requirement.kind)` — default mapping
704
- *
705
- * This model enables:
706
- * - One-pass requirement collection (no per-segment DI scope accumulation)
707
- * - One-call planner evaluation (chooser sees the full set at once)
708
- * - One redirect decision (no per-segment guard coordination needed)
709
- * - Fine-grained per-requirement policy via `requirementPolicies`
710
- *
711
- * @example
712
- * ```ts
713
- * // routes.ts — declare requirements in route data
714
- * import { withRouteRequirements } from "@securitydept/client-angular";
715
- * import {
716
- * createFrontendOidcLoginRedirectHandler,
717
- * createTokenSetRouteAggregationGuard,
718
- * } from "@securitydept/token-set-context-client-angular";
719
- *
720
- * // Simple — kind-level handler
721
- * const guard = createTokenSetRouteAggregationGuard({
722
- * requirementHandlers: {
723
- * frontend_oidc: createFrontendOidcLoginRedirectHandler({ clientKey: "confluence" }),
724
- * },
725
- * });
726
- *
727
- * // Fine-grained — per-requirement policy
728
- * const guard = createTokenSetRouteAggregationGuard({
729
- * requirementPolicies: {
730
- * "confluence-oidc": {
731
- * selector: { clientKey: "confluence" },
732
- * onUnauthenticated: () => "/auth/confluence",
733
- * },
734
- * "admin-oidc": {
735
- * selector: { query: { providerFamily: "internal-sso" } },
736
- * onUnauthenticated: (failing) => "/auth/admin",
737
- * },
738
- * },
739
- * });
740
- *
741
- * export const routes: Routes = [
742
- * {
743
- * path: "",
744
- * data: withRouteRequirements([{ id: "session", kind: "session" }]),
745
- * children: [
746
- * {
747
- * path: "confluence",
748
- * data: withRouteRequirements([{ id: "confluence-oidc", kind: "frontend_oidc" }]),
749
- * canActivate: [guard],
750
- * },
751
- * ],
752
- * },
753
- * ];
754
- * ```
755
- */
756
- function createTokenSetRouteAggregationGuard(options = {}) {
757
- return async (route, state) => {
758
- const injector = inject(EnvironmentInjector);
759
- const registry = inject(TokenSetAuthRegistry);
760
- const router = inject(Router);
761
- const handlerContext = {
762
- route,
763
- state,
764
- attemptedUrl: state.url,
765
- };
766
- // ── Resolve planner-host ────────────────────────────────────────────
767
- const plannerHost = options.plannerHostResolver?.(route, state) ??
768
- options.plannerHost ??
769
- inject(AUTH_PLANNER_HOST, { optional: true });
770
- if (!plannerHost) {
771
- throw new Error("[createTokenSetRouteAggregationGuard] No PlannerHost found. " +
772
- "Provide AUTH_PLANNER_HOST via provideAuthPlannerHost() " +
773
- "in your app/route providers, or pass plannerHost inline.");
774
- }
775
- // ── Step 1: Full-route requirement aggregation ───────────────────────
776
- const allRequirements = extractFullRouteRequirements(route, options.requirementsKey);
777
- if (allRequirements.length === 0) {
778
- return true;
779
- }
780
- const resolvedRequirements = [];
781
- for (const req of allRequirements) {
782
- const policy = options.requirementPolicies?.[req.id];
783
- let clientKeys;
784
- if (policy?.selector) {
785
- // Policy provides an explicit selector — use it instead of kind mapping.
786
- if (policy.selector.clientKey !== undefined) {
787
- clientKeys = [policy.selector.clientKey];
788
- }
789
- else {
790
- clientKeys = registry.clientKeysForOptions(policy.selector.query);
791
- }
792
- }
793
- else {
794
- // Default: resolve by requirement kind.
795
- clientKeys = registry.clientKeyListForRequirement(req.kind);
796
- }
797
- if (clientKeys.length === 0) {
798
- // No client for this requirement — skip (lets non-token-set requirements
799
- // coexist in the same route chain without breaking the guard).
800
- continue;
801
- }
802
- const entries = await Promise.all(clientKeys.map(async (key) => ({
803
- // Use whenReady() — not require() — so that if the client's
804
- // async clientFactory is still in-flight when the guard first
805
- // fires, we block here until it materializes rather than
806
- // crashing or silently bypassing auth.
807
- service: await registry.whenReady(key),
808
- clientKey: key,
809
- meta: registry.metaFor(key),
810
- })));
811
- resolvedRequirements.push({ requirement: req, entries });
812
- }
813
- if (resolvedRequirements.length === 0) {
814
- return true;
815
- }
816
- // ── Step 3: Build planner candidates ────────────────────────────────
817
- // De-duplicate by requirementId (last declaration for the same id wins).
818
- const seenIds = new Map();
819
- for (const r of resolvedRequirements) {
820
- seenIds.set(r.requirement.id, r);
174
+ class TokenSetBackendCallbackComponent {
175
+ clientQuery = input(/* @ts-ignore */
176
+ ...(ngDevMode ? [undefined, { debugName: "clientQuery" }] : /* istanbul ignore next */ []));
177
+ clientGuard = input(/* @ts-ignore */
178
+ ...(ngDevMode ? [undefined, { debugName: "clientGuard" }] : /* istanbul ignore next */ []));
179
+ autoInitialize = input(true, /* @ts-ignore */
180
+ ...(ngDevMode ? [{ debugName: "autoInitialize" }] : /* istanbul ignore next */ []));
181
+ environment = inject(ENVIRONMENT);
182
+ registry = inject(TOKEN_SET_CLIENT_REGISTRY);
183
+ destroyRef = inject(DestroyRef);
184
+ selectionSource = createSignal(null);
185
+ selectionSignal = createComputed(() => this.selectionSource.get()?.get() ?? {
186
+ status: ResourceStatus.Idle,
187
+ });
188
+ selection = toNgSignal(this.selectionSignal, { requireSync: true });
189
+ resource = resourceFromSnapshots(() => {
190
+ const selection = this.selectionSignal.get();
191
+ switch (selection.status) {
192
+ case ResourceStatus.Idle:
193
+ return { status: ResourceStatus.Idle };
194
+ case ResourceStatus.Loading:
195
+ return { status: ResourceStatus.Loading };
196
+ case ResourceStatus.LoadingError:
197
+ return selection;
198
+ case ResourceStatus.Resolved:
199
+ return selection.value.kind ===
200
+ TokenSetCallbackClientSelectionKind.NotApplicable
201
+ ? {
202
+ status: ResourceStatus.Resolved,
203
+ value: { kind: OidcModeCallbackHandlingKind.NotApplicable },
204
+ }
205
+ : selection.value.client.callback.state.get();
821
206
  }
822
- const deduped = [...seenIds.values()];
823
- const candidates = deduped.map((r) => ({
824
- requirementId: r.requirement.id,
825
- requirementKind: r.requirement.kind,
826
- label: r.requirement.label,
827
- attributes: r.requirement.attributes,
828
- checkAuthenticated: () => r.entries.every(({ service }) => service.isAuthenticated()),
829
- onUnauthenticated: async () => {
830
- const failing = r.entries.filter(({ service }) => !service.isAuthenticated());
831
- // Handler resolution: policy > kind handler > default
832
- const handler = options.requirementPolicies?.[r.requirement.id]?.onUnauthenticated ??
833
- options.requirementHandlers?.[r.requirement.kind] ??
834
- options.defaultOnUnauthenticated;
835
- if (!handler)
836
- return false;
837
- const result = runUnauthenticatedHandlerInContext(injector, handler, failing, r.requirement, handlerContext);
838
- const resolved = await result;
839
- if (typeof resolved === "string")
840
- return resolved;
841
- if (typeof resolved === "boolean")
842
- return resolved;
843
- return resolved.toString();
844
- },
845
- _resolved: r,
846
- }));
847
- // ── Step 4: Single planner evaluation ───────────────────────────────
848
- const decide = async () => {
849
- const result = await plannerHost.evaluate(candidates);
850
- if (result.allAuthenticated)
851
- return true;
852
- if (result.pendingCandidate) {
853
- const candidate = result.pendingCandidate;
854
- const failing = candidate._resolved.entries.filter(({ service }) => !service.isAuthenticated());
855
- // Handler resolution: policy > kind handler > default
856
- const handler = options.requirementPolicies?.[candidate._resolved.requirement.id]
857
- ?.onUnauthenticated ??
858
- options.requirementHandlers?.[candidate._resolved.requirement.kind] ??
859
- options.defaultOnUnauthenticated;
860
- if (!handler)
861
- return false;
862
- const action = await runUnauthenticatedHandlerInContext(injector, handler, failing, candidate._resolved.requirement, handlerContext);
863
- if (typeof action === "string")
864
- return router.parseUrl(action);
865
- if (typeof action === "boolean")
866
- return action;
867
- return action;
868
- }
869
- return false;
870
- };
871
- // ── Restore-aware: wait for all pending restores ─────────────────────
872
- const pendingRestores = deduped
873
- .flatMap((r) => r.entries)
874
- .map(({ service }) => service.restorePromise)
875
- .filter((p) => p !== null);
876
- async function awaitAuthMaterial() {
877
- await Promise.all(pendingRestores);
878
- const freshnessBarriers = deduped
879
- .flatMap((r) => r.entries.map((entry) => ({
880
- ...entry,
881
- requirement: r.requirement,
882
- })))
883
- .map(({ service, clientKey, meta, requirement }) => service.ensureAuthForResource({
884
- source: TokenSetAuthFlowSource.RouteGuard,
885
- clientKey,
886
- requirement: { id: requirement.id, kind: requirement.kind },
887
- providerFamily: meta.providerFamily,
888
- url: state.url,
889
- forceRefreshWhenDue: true,
207
+ });
208
+ state = toNgSignal(this.resource, { requireSync: true });
209
+ constructor() {
210
+ this.destroyRef.onDestroy(() => this.resource.dispose());
211
+ afterNextRender(() => {
212
+ this.selectionSource.set(selectTokenSetBackendCallbackClientFromRegistry({
213
+ registry: this.registry,
214
+ callbackUrl: this.environment.router?.currentUrl()?.toString() ?? "",
215
+ clientQuery: this.clientQuery(),
216
+ clientGuard: this.clientGuard(),
217
+ initialize: this.autoInitialize(),
890
218
  }));
891
- await Promise.all(freshnessBarriers);
892
- }
893
- if (deduped.every((r) => r.entries.length === 0)) {
894
- return decide();
895
- }
896
- // await the restore-phase Observable before running planner evaluation
897
- // so the async guard always returns a Promise, not an Observable.
898
- return firstValueFrom(from(awaitAuthMaterial()).pipe(switchMap(() => from(decide())), take(1)));
899
- };
900
- }
901
- function runUnauthenticatedHandlerInContext(injector, handler, unauthenticated, requirement, context) {
902
- return runInInjectionContext(injector, () => handler(unauthenticated, requirement, context));
903
- }
904
- /**
905
- * Build an Angular route-security handler that starts frontend-oidc login and
906
- * records the attempted navigation URL as `postAuthRedirectUri`.
907
- *
908
- * Use this instead of reading `Router.url` inside a guard handler. During guard
909
- * execution Angular has not committed the attempted navigation yet, so
910
- * `Router.url` still points at the previously active page.
911
- *
912
- * After the browser redirect is started, the returned guard result intentionally
913
- * never settles. That keeps Angular from finalizing the rejected in-app
914
- * navigation while the page is already leaving for an external IdP.
915
- */
916
- function createFrontendOidcLoginRedirectHandler(options) {
917
- return async (unauthenticated, _requirement, context) => {
918
- const clientKey = options?.clientKey ?? unauthenticated[0]?.clientKey;
919
- if (!clientKey)
920
- return false;
921
- const registry = inject(TokenSetAuthRegistry);
922
- const service = await registry.whenReady(clientKey);
923
- if (!isLoginWithRedirectClient(service.client))
924
- return false;
925
- await service.client.loginWithRedirect({
926
- postAuthRedirectUri: context.attemptedUrl || options?.fallbackPostAuthRedirectUri || "/",
927
219
  });
928
- return await neverSettlingRedirectGuardResult();
929
- };
930
- }
931
- function neverSettlingRedirectGuardResult() {
932
- return new Promise(() => {
933
- // Intentionally never resolves: a full-page browser redirect is in progress.
934
- });
935
- }
936
- function isLoginWithRedirectClient(client) {
937
- return (typeof client === "object" &&
938
- client !== null &&
939
- "loginWithRedirect" in client &&
940
- typeof client.loginWithRedirect === "function");
941
- }
942
-
943
- /**
944
- * Route data key for choosing a planner-host registered at the route root.
945
- */
946
- const TOKEN_SET_ROUTE_PLANNER_HOST_KEY_DATA_KEY = "tokenSetPlannerHostKey";
947
- /**
948
- * Build a secured child route declaration.
949
- *
950
- * The returned route contains only serializable metadata:
951
- * requirements, composition strategy, planner-host lookup key, and any extra
952
- * route data the caller provides.
953
- */
954
- function secureRoute(path, securityOptions = {}, routeOptions = {}) {
955
- const data = withRouteRequirements(securityOptions.requirements ?? [], {
956
- composition: securityOptions.composition,
957
- extra: {
958
- ...(routeOptions.data ?? {}),
959
- ...(securityOptions.plannerHostKey
960
- ? {
961
- [TOKEN_SET_ROUTE_PLANNER_HOST_KEY_DATA_KEY]: securityOptions.plannerHostKey,
962
- }
963
- : {}),
964
- },
965
- });
966
- return {
967
- ...routeOptions,
968
- path,
969
- data,
970
- };
971
- }
972
- /**
973
- * Build a secured route-tree root.
974
- *
975
- * This is the canonical Angular adopter entry for token-set route security:
976
- * root-level runtime policy stays here, child routes use {@link secureRoute}
977
- * for serializable declarations only.
978
- */
979
- function secureRouteRoot(path, securityOptions = {}, routeOptions = {}) {
980
- const canActivate = createTokenSetRouteAggregationGuard({
981
- requirementPolicies: securityOptions.requirementPolicies,
982
- requirementHandlers: securityOptions.requirementHandlers,
983
- defaultOnUnauthenticated: securityOptions.defaultOnUnauthenticated,
984
- requirementsKey: securityOptions.requirementsKey,
985
- plannerHostResolver: (route, state) => resolvePlannerHostForRoute(route, state, securityOptions),
986
- });
987
- const canActivateChild = createCanActivateChildAdapter(canActivate);
988
- const baseRoute = secureRoute(path, securityOptions, routeOptions);
989
- return {
990
- ...baseRoute,
991
- canActivate: [canActivate, ...(routeOptions.canActivate ?? [])],
992
- canActivateChild: [
993
- canActivateChild,
994
- ...(routeOptions.canActivateChild ?? []),
995
- ],
996
- };
997
- }
998
- /**
999
- * Alias with a token-set-specific name for callers who prefer explicit imports.
1000
- */
1001
- const secureTokenSetRoute = secureRoute;
1002
- /**
1003
- * Alias with a token-set-specific name for callers who prefer explicit imports.
1004
- */
1005
- const secureTokenSetRouteRoot = secureRouteRoot;
1006
- function createCanActivateChildAdapter(canActivate) {
1007
- return (childRoute, state) => canActivate(childRoute, state);
1008
- }
1009
- function resolvePlannerHostForRoute(route, _state, securityOptions) {
1010
- const plannerHostKey = findNearestPlannerHostKey(route) ?? securityOptions.defaultPlannerHostKey;
1011
- if (plannerHostKey) {
1012
- const resolved = securityOptions.plannerHosts?.[plannerHostKey];
1013
- if (!resolved) {
1014
- throw new Error(`[secureRouteRoot] No planner host registered for key "${plannerHostKey}"`);
1015
- }
1016
- return resolved;
1017
220
  }
1018
- return undefined;
1019
- }
1020
- function findNearestPlannerHostKey(route) {
1021
- const chain = route.pathFromRoot ?? [route];
1022
- for (let index = chain.length - 1; index >= 0; index -= 1) {
1023
- const segment = chain[index];
1024
- if (!segment)
1025
- continue;
1026
- const raw = segment.data[TOKEN_SET_ROUTE_PLANNER_HOST_KEY_DATA_KEY] ??
1027
- segment.routeConfig?.data?.[TOKEN_SET_ROUTE_PLANNER_HOST_KEY_DATA_KEY];
1028
- if (typeof raw === "string" && raw.length > 0) {
1029
- return raw;
1030
- }
1031
- }
1032
- return undefined;
221
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: TokenSetBackendCallbackComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
222
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.0", type: TokenSetBackendCallbackComponent, isStandalone: true, selector: "sd-token-set-backend-callback", inputs: { clientQuery: { classPropertyName: "clientQuery", publicName: "clientQuery", isSignal: true, isRequired: false, transformFunction: null }, clientGuard: { classPropertyName: "clientGuard", publicName: "clientGuard", isSignal: true, isRequired: false, transformFunction: null }, autoInitialize: { classPropertyName: "autoInitialize", publicName: "autoInitialize", isSignal: true, isRequired: false, transformFunction: null } }, exportAs: ["sdTokenSetBackendCallback"], ngImport: i0, template: "", isInline: true });
1033
223
  }
224
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: TokenSetBackendCallbackComponent, decorators: [{
225
+ type: Component,
226
+ args: [{
227
+ selector: "sd-token-set-backend-callback",
228
+ standalone: true,
229
+ template: "",
230
+ exportAs: "sdTokenSetBackendCallback",
231
+ }]
232
+ }], ctorParameters: () => [], propDecorators: { clientQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "clientQuery", required: false }] }], clientGuard: [{ type: i0.Input, args: [{ isSignal: true, alias: "clientGuard", required: false }] }], autoInitialize: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoInitialize", required: false }] }] } });
1034
233
 
1035
- // ============================================================================
1036
- // 9. TokenSetCallbackComponent — drop-in standalone callback route component
1037
- // ============================================================================
1038
- /**
1039
- * Drop-in standalone Angular component for OIDC redirect callback routes.
1040
- *
1041
- * Handles the full callback lifecycle on initialization:
1042
- * 1. Calls `CallbackResumeService.handleCallback()` with the current URL.
1043
- * 2. Navigates to `result.resumeUrl` (the post-auth redirect set at login time).
1044
- * 3. On error: logs and falls back to navigating to `"/"`.
1045
- *
1046
- * Register as a route component — no additional glue needed:
1047
- * ```ts
1048
- * import { TokenSetCallbackComponent } from "@securitydept/token-set-context-client-angular";
1049
- *
1050
- * export const routes: Routes = [
1051
- * { path: "auth/callback", component: TokenSetCallbackComponent },
1052
- * ];
1053
- * ```
1054
- *
1055
- * For custom error handling or UI, subclass this component or build your own
1056
- * using `CallbackResumeService` directly.
1057
- */
1058
- class TokenSetCallbackComponent {
1059
- callbackService = inject(CallbackResumeService);
1060
- router = inject(Router);
1061
- ngOnInit() {
1062
- const currentUrl = window.location.href;
1063
- if (!this.callbackService.isCallback(currentUrl)) {
1064
- // Not a callback URL — navigate home to avoid processing errors.
1065
- this.router.navigateByUrl("/", { replaceUrl: true });
1066
- return;
234
+ class TokenSetFrontendCallbackComponent {
235
+ clientQuery = input(/* @ts-ignore */
236
+ ...(ngDevMode ? [undefined, { debugName: "clientQuery" }] : /* istanbul ignore next */ []));
237
+ clientGuard = input(/* @ts-ignore */
238
+ ...(ngDevMode ? [undefined, { debugName: "clientGuard" }] : /* istanbul ignore next */ []));
239
+ autoInitialize = input(true, /* @ts-ignore */
240
+ ...(ngDevMode ? [{ debugName: "autoInitialize" }] : /* istanbul ignore next */ []));
241
+ environment = inject(ENVIRONMENT);
242
+ registry = inject(TOKEN_SET_CLIENT_REGISTRY);
243
+ destroyRef = inject(DestroyRef);
244
+ selectionSource = createSignal(null);
245
+ selectionSignal = createComputed(() => this.selectionSource.get()?.get() ?? {
246
+ status: ResourceStatus.Idle,
247
+ });
248
+ selection = toNgSignal(this.selectionSignal, { requireSync: true });
249
+ resource = resourceFromSnapshots(() => {
250
+ const selection = this.selectionSignal.get();
251
+ switch (selection.status) {
252
+ case ResourceStatus.Idle:
253
+ return { status: ResourceStatus.Idle };
254
+ case ResourceStatus.Loading:
255
+ return { status: ResourceStatus.Loading };
256
+ case ResourceStatus.LoadingError:
257
+ return selection;
258
+ case ResourceStatus.Resolved:
259
+ return selection.value.kind ===
260
+ TokenSetCallbackClientSelectionKind.NotApplicable
261
+ ? {
262
+ status: ResourceStatus.Resolved,
263
+ value: { kind: OidcModeCallbackHandlingKind.NotApplicable },
264
+ }
265
+ : selection.value.client.callback.state.get();
1067
266
  }
1068
- this.callbackService
1069
- .handleCallback(currentUrl)
1070
- .then((result) => {
1071
- this.router.navigateByUrl(result.resumeUrl, { replaceUrl: true });
1072
- })
1073
- .catch((error) => {
1074
- console.error("[TokenSetCallbackComponent] Callback handling failed:", error);
1075
- this.router.navigateByUrl("/", { replaceUrl: true });
267
+ });
268
+ state = toNgSignal(this.resource, { requireSync: true });
269
+ constructor() {
270
+ this.destroyRef.onDestroy(() => this.resource.dispose());
271
+ afterNextRender(() => {
272
+ this.selectionSource.set(selectTokenSetFrontendCallbackClientFromRegistry({
273
+ registry: this.registry,
274
+ callbackUrl: this.environment.router?.currentUrl()?.toString() ?? "",
275
+ clientQuery: this.clientQuery(),
276
+ clientGuard: this.clientGuard(),
277
+ initialize: this.autoInitialize(),
278
+ }));
1076
279
  });
1077
280
  }
1078
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: TokenSetCallbackComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1079
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.8", type: TokenSetCallbackComponent, isStandalone: true, selector: "token-set-callback", ngImport: i0, template: "", isInline: true });
281
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: TokenSetFrontendCallbackComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
282
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.0", type: TokenSetFrontendCallbackComponent, isStandalone: true, selector: "sd-token-set-frontend-callback", inputs: { clientQuery: { classPropertyName: "clientQuery", publicName: "clientQuery", isSignal: true, isRequired: false, transformFunction: null }, clientGuard: { classPropertyName: "clientGuard", publicName: "clientGuard", isSignal: true, isRequired: false, transformFunction: null }, autoInitialize: { classPropertyName: "autoInitialize", publicName: "autoInitialize", isSignal: true, isRequired: false, transformFunction: null } }, exportAs: ["sdTokenSetFrontendCallback"], ngImport: i0, template: "", isInline: true });
1080
283
  }
1081
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: TokenSetCallbackComponent, decorators: [{
284
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: TokenSetFrontendCallbackComponent, decorators: [{
1082
285
  type: Component,
1083
286
  args: [{
1084
- selector: "token-set-callback",
287
+ selector: "sd-token-set-frontend-callback",
1085
288
  standalone: true,
1086
289
  template: "",
290
+ exportAs: "sdTokenSetFrontendCallback",
1087
291
  }]
1088
- }] });
292
+ }], ctorParameters: () => [], propDecorators: { clientQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "clientQuery", required: false }] }], clientGuard: [{ type: i0.Input, args: [{ isSignal: true, alias: "clientGuard", required: false }] }], autoInitialize: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoInitialize", required: false }] }] } });
1089
293
 
1090
294
  // Angular integration family for @securitydept/token-set-context-client
1091
295
  //
@@ -1093,22 +297,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
1093
297
  // import { ... } from "@securitydept/token-set-context-client-angular"
1094
298
  //
1095
299
  // Multi-client-first Angular-native adapter surface:
1096
- // - Keyed client registration via provideTokenSetAuth()
1097
- // - Multi-client lookup via TokenSetAuthRegistry
1098
- // - Per-client TokenSetAuthService with signal + RxJS bridge
1099
- // - Multi-client bearer interceptor (functional + class-based)
1100
- // - provideTokenSetBearerInterceptor() for NgModule HTTP_INTERCEPTORS setup
1101
- // - OIDC callback resume with client key discrimination
1102
- // - CallbackResumeService.isCallback() for programmatic callback detection
1103
- // - TokenSetCallbackComponent standalone component (drop-in callback route)
300
+ // - Keyed client registration via provideTokenSetClientRegistry()
301
+ // - Multi-client lookup via the core TokenSetClientRegistry
302
+ // - Per-client mode client replay signals with RxJS bridge support
303
+ // - Client-registry authorization interceptor (functional + class-based)
304
+ // - provideTokenSetClientRegistryAuthorizationInterceptor() for NgModule HTTP_INTERCEPTORS setup
1104
305
  // - Requirement/provider-family → client key mapping
1105
- // - secureRouteRoot() / secureRoute() — canonical Angular Router route-security
1106
- // builders: keep non-serializable runtime policy at the root, let child
1107
- // routes declare serializable requirement metadata only, and wire both
1108
- // canActivate + canActivateChild to the full-route aggregation guard.
1109
- // - createTokenSetRouteAggregationGuard() — lower-level advanced guard for
1110
- // direct route-tree evaluation when adopters intentionally bypass the route
1111
- // builder helpers.
306
+ // - secureTokenSetRouteRoot() / secureTokenSetRoute() — token-set Angular
307
+ // Router route-security builders that delegate to the client-angular base
308
+ // helpers and mount the token-set behaviour host.
309
+ // - provideTokenSetRequirementPlannerHost() — provides the token-set
310
+ // RequirementPlannerHost behaviour to the client-angular guard pipeline.
311
+ // - createTokenSetCanActivate() / createTokenSetCanActivateChild() — token-set
312
+ // named guard factories over the client-angular base guards.
1112
313
  // - Signal/Observable bridge utilities live in @securitydept/client-angular
1113
314
  //
1114
315
  // Built by ng-packagr (APF / FESM2022). Decorators are fully supported.
@@ -1123,5 +324,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
1123
324
  * Generated bundle index. Do not edit.
1124
325
  */
1125
326
 
1126
- export { CallbackResumeService, TOKEN_SET_AUTH_REGISTRY, TOKEN_SET_BEARER_INTERCEPTOR_OPTIONS, TOKEN_SET_ROUTE_PLANNER_HOST_KEY_DATA_KEY, TokenSetAuthRegistry, TokenSetAuthService, TokenSetBearerInterceptor, TokenSetCallbackComponent, createFrontendOidcLoginRedirectHandler, createTokenSetBearerInterceptor, createTokenSetRouteAggregationGuard, isOidcCallback, provideTokenSetAuth, provideTokenSetBearerInterceptor, secureRoute, secureRouteRoot, secureTokenSetRoute, secureTokenSetRouteRoot };
327
+ export { TOKEN_SET_CLIENT_REGISTRY, TOKEN_SET_CLIENT_REGISTRY_AUTHORIZATION_FOR_REQUEST, TokenSetBackendCallbackComponent, TokenSetClientRegistryAuthorizationInterceptor, TokenSetFrontendCallbackComponent, createTokenSetCanActivate, createTokenSetCanActivateChild, createTokenSetClientRegistryAuthorizationInterceptor, defaultTokenSetClientRegistryAuthorizationForRequest, provideTokenSetClientRegistry, provideTokenSetClientRegistryAuthorizationInterceptor, provideTokenSetRequirementPlannerHost, secureTokenSetRoute, secureTokenSetRouteRoot };
1127
328
  //# sourceMappingURL=securitydept-token-set-context-client-angular.mjs.map