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