@securitydept/client-angular 0.2.0-beta.1

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.
@@ -0,0 +1,529 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, makeEnvironmentProviders, inject, Injectable } from '@angular/core';
3
+ import { createPlannerHost, resolveEffectiveClientSet, RequirementsClientSetComposition } from '@securitydept/client/auth-coordination';
4
+ export { resolveEffectiveClientSet } from '@securitydept/client/auth-coordination';
5
+ import { Observable } from 'rxjs';
6
+
7
+ // Angular planner-host integration — DI-based planner provider / lookup
8
+ //
9
+ // Canonical import path:
10
+ // import { AUTH_PLANNER_HOST, provideAuthPlannerHost, ... } from "@securitydept/client-angular"
11
+ //
12
+ // Provides Angular DI glue for the shared planner-host contract:
13
+ // - InjectionToken for PlannerHost
14
+ // - Provider helpers for app / module / route scoped planners
15
+ // - InjectionToken + provider helpers for ScopedRequirementsClientSet
16
+ // - injectPlannerHost() convenience function with fail-fast
17
+ //
18
+ // Architecture boundary:
19
+ // - Does NOT own the planner-host contract (that lives in @securitydept/client)
20
+ // - Does NOT carry token-set-specific mapping
21
+ // - Provides the Angular DI wiring for the shared contract
22
+ //
23
+ // Stability: provisional
24
+ // ---------------------------------------------------------------------------
25
+ // PlannerHost injection token
26
+ // ---------------------------------------------------------------------------
27
+ /**
28
+ * DI token for the shared {@link PlannerHost}.
29
+ *
30
+ * Provide at the app level via {@link provideAuthPlannerHost}.
31
+ * Guards use {@link injectPlannerHost} to look up the nearest instance.
32
+ */
33
+ const AUTH_PLANNER_HOST = new InjectionToken("AUTH_PLANNER_HOST");
34
+ /**
35
+ * Provide an {@link AUTH_PLANNER_HOST} at the current injector level.
36
+ *
37
+ * Use in app config, route providers, or NgModule providers to establish
38
+ * a planner-host scope. Child routes inherit the nearest parent's planner.
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * // app.config.ts — app-level planner (default sequential strategy)
43
+ * export const appConfig: ApplicationConfig = {
44
+ * providers: [provideAuthPlannerHost()],
45
+ * };
46
+ *
47
+ * // Feature route — override with custom async chooser strategy
48
+ * {
49
+ * path: "admin",
50
+ * providers: [provideAuthPlannerHost({
51
+ * selectCandidate: async (candidates) => showAdminChooser(candidates),
52
+ * })],
53
+ * children: [...]
54
+ * }
55
+ * ```
56
+ */
57
+ function provideAuthPlannerHost(options) {
58
+ const plannerOptions = {};
59
+ if (options?.selectCandidate) {
60
+ plannerOptions.selectCandidate = options.selectCandidate;
61
+ }
62
+ return makeEnvironmentProviders([
63
+ { provide: AUTH_PLANNER_HOST, useValue: createPlannerHost(plannerOptions) },
64
+ ]);
65
+ }
66
+ /**
67
+ * Inject the nearest {@link PlannerHost} from the DI hierarchy.
68
+ *
69
+ * Throws an explicit error if no planner-host is provided, preventing
70
+ * silent fallback behavior.
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * const plannerHost = injectPlannerHost();
75
+ * const result = await plannerHost.evaluate(candidates);
76
+ * ```
77
+ */
78
+ function injectPlannerHost() {
79
+ const host = inject(AUTH_PLANNER_HOST, { optional: true });
80
+ if (!host) {
81
+ throw new Error("[injectPlannerHost] No AUTH_PLANNER_HOST found in the injector hierarchy. " +
82
+ "Use provideAuthPlannerHost() in your app config or route providers.");
83
+ }
84
+ return host;
85
+ }
86
+ // ---------------------------------------------------------------------------
87
+ // Requirements client set injection token — stores the already-resolved
88
+ // effective options for the current scope (parent + child composed).
89
+ // ---------------------------------------------------------------------------
90
+ /**
91
+ * DI token for the effective requirements client set at the current scope.
92
+ *
93
+ * The value is always the **resolved** options after composing all ancestor
94
+ * scopes — it is NOT the raw `ScopedRequirementsClientSet` declaration.
95
+ *
96
+ * Provide via {@link provideRouteScopedRequirements}.
97
+ * Guards inject this token to get the effective scope requirements and then
98
+ * merge their own declared candidates on top.
99
+ */
100
+ const AUTH_REQUIREMENTS_CLIENT_SET = new InjectionToken("AUTH_REQUIREMENTS_CLIENT_SET");
101
+ /**
102
+ * Provide a {@link ScopedRequirementsClientSet} at the current route/module scope.
103
+ *
104
+ * Internally, the provider resolves the effective client set by composing the
105
+ * declared `scopedSet` with the parent scope's already-resolved options
106
+ * (injected via `skipSelf: true`). The token value stored in DI is the
107
+ * resolved `readonly AuthGuardClientOption[]`, not the raw declaration.
108
+ *
109
+ * This mirrors the React `AuthRequirementsClientSetProvider` semantics:
110
+ * each level stores its resolved effective options, so children inherit
111
+ * the composed result without re-resolving the full ancestor chain.
112
+ *
113
+ * The `scopedSet` argument may also be a **factory function** — it will be
114
+ * called inside Angular's `useFactory` context, so `inject()` is valid.
115
+ * Use this when the requirement options need DI-resolved services (e.g. a
116
+ * token-set registry or a redirect service).
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * // routes.ts — static declaration (no DI needed)
121
+ * {
122
+ * path: "",
123
+ * providers: [
124
+ * provideRouteScopedRequirements({
125
+ * composition: RequirementsClientSetComposition.Replace,
126
+ * options: [sessionClientOption],
127
+ * }),
128
+ * ],
129
+ * }
130
+ *
131
+ * // Feature route merges OIDC on top of the parent's session requirement
132
+ * {
133
+ * path: "confluence",
134
+ * providers: [
135
+ * provideRouteScopedRequirements({
136
+ * composition: RequirementsClientSetComposition.Merge,
137
+ * options: [confluenceOidcClientOption],
138
+ * }),
139
+ * ],
140
+ * // Route-level DI scope only. Angular Router adopters should prefer
141
+ * // secureRouteRoot()/secureRoute() over direct guard wiring.
142
+ * }
143
+ *
144
+ * // Factory form — use when options depend on injected services
145
+ * {
146
+ * path: "protected",
147
+ * providers: [
148
+ * provideRouteScopedRequirements(() => {
149
+ * const registry = inject(TokenSetAuthRegistry);
150
+ * const authService = inject(AuthService);
151
+ * return {
152
+ * composition: RequirementsClientSetComposition.Replace,
153
+ * options: [{
154
+ * requirementId: "oidc",
155
+ * requirementKind: "frontend_oidc",
156
+ * checkAuthenticated: () => registry.require(clientKey).isAuthenticated(),
157
+ * onUnauthenticated: () => { authService.redirectToLogin(clientKey).subscribe(); return false; },
158
+ * }],
159
+ * };
160
+ * }),
161
+ * ],
162
+ * }
163
+ * ```
164
+ */
165
+ function provideRouteScopedRequirements(scopedSet) {
166
+ return makeEnvironmentProviders([
167
+ {
168
+ provide: AUTH_REQUIREMENTS_CLIENT_SET,
169
+ useFactory: () => {
170
+ // Walk up to the nearest parent scope's already-resolved effective set.
171
+ // skipSelf: true ensures we don't read our own (not-yet-set) value.
172
+ const parentOptions = inject(AUTH_REQUIREMENTS_CLIENT_SET, {
173
+ optional: true,
174
+ skipSelf: true,
175
+ }) ?? [];
176
+ // Support factory form so callers can use inject() to resolve
177
+ // DI services (e.g. auth registry, redirect service) inside their
178
+ // option callbacks without bypassing this helper.
179
+ const resolved = typeof scopedSet === "function" ? scopedSet() : scopedSet;
180
+ return resolveEffectiveClientSet(parentOptions, resolved);
181
+ },
182
+ },
183
+ ]);
184
+ }
185
+
186
+ // Angular Router route adapter for auth-coordination
187
+ //
188
+ // Canonical import path:
189
+ // import { ... } from "@securitydept/client-angular"
190
+ //
191
+ // Provides the projection layer between Angular Router's ActivatedRouteSnapshot
192
+ // tree and the SDK's framework-agnostic RouteMatchNode[] contract.
193
+ // Uses real @angular/router types (ActivatedRouteSnapshot) so consuming apps
194
+ // get full type-safety and IDE autocomplete without any casting.
195
+ //
196
+ // Architecture boundary:
197
+ // - Does NOT own the Angular router, guards, or navigation lifecycle.
198
+ // - Does NOT re-export core orchestration primitives.
199
+ // - Does NOT carry token-set-specific mapping or policy.
200
+ // - The adopter wires this into their guards / resolvers.
201
+ //
202
+ // Stability: provisional
203
+ /** Default key for auth requirements in Angular route `data`. */
204
+ const DEFAULT_ROUTE_REQUIREMENTS_KEY = "authRequirements";
205
+ // ---------------------------------------------------------------------------
206
+ // Service
207
+ // ---------------------------------------------------------------------------
208
+ /**
209
+ * Angular service that bridges Angular Router's `ActivatedRouteSnapshot` to
210
+ * the SDK's framework-agnostic `RouteMatchNode[]` contract.
211
+ *
212
+ * Inject this into `CanActivateFn` guards or class-based guards.
213
+ *
214
+ * @example
215
+ * ```ts
216
+ * export const authGuard: CanActivateFn = (route) => {
217
+ * const adapter = inject(AuthRouteAdapter);
218
+ * const result = adapter.check(route, orchestrator);
219
+ * if (!result.canActivate && result.pendingRequirement) {
220
+ * // Trigger auth flow for the pending requirement
221
+ * }
222
+ * return result.canActivate;
223
+ * };
224
+ * ```
225
+ */
226
+ class AuthRouteAdapter {
227
+ /**
228
+ * Project the `pathFromRoot` of an `ActivatedRouteSnapshot` into the SDK's
229
+ * `RouteMatchNode[]` chain.
230
+ *
231
+ * Route segments without a `routeConfig` (e.g. the root segment) are
232
+ * assigned routeId `"__root__"`. Segments with a config use the `path`
233
+ * as their routeId (or `"__index__"` for empty-path segments).
234
+ */
235
+ projectRouteMatch(leafRoute, options) {
236
+ const key = options?.requirementsKey ?? DEFAULT_ROUTE_REQUIREMENTS_KEY;
237
+ const chain = leafRoute.pathFromRoot ?? [leafRoute];
238
+ return chain.map((segment) => this.segmentToNode(segment, key));
239
+ }
240
+ /**
241
+ * Project an explicit array of `ActivatedRouteSnapshot` segments into the
242
+ * SDK's `RouteMatchNode[]` chain.
243
+ *
244
+ * Use when you already have a flattened chain (e.g. from
245
+ * `ActivatedRoute.pathFromRoot`).
246
+ */
247
+ projectRouteSegments(segments, options) {
248
+ const key = options?.requirementsKey ?? DEFAULT_ROUTE_REQUIREMENTS_KEY;
249
+ return segments.map((segment) => this.segmentToNode(segment, key));
250
+ }
251
+ /**
252
+ * Project the route snapshot and check whether all requirements are settled.
253
+ *
254
+ * @param leafRoute - The leaf `ActivatedRouteSnapshot` from the guard call.
255
+ * @param orchestrator - The SDK orchestrator managing the requirement plan.
256
+ * @param options - Optional projection options.
257
+ */
258
+ check(leafRoute, orchestrator, options) {
259
+ const chain = this.projectRouteMatch(leafRoute, options);
260
+ orchestrator.activateMatchedRoutes(chain);
261
+ const snap = orchestrator.snapshot();
262
+ return {
263
+ canActivate: snap.settled,
264
+ pendingRequirement: snap.pendingRequirement,
265
+ };
266
+ }
267
+ segmentToNode(segment, key) {
268
+ const routeId = this.resolveRouteId(segment);
269
+ const rawRequirements = segment.data[key] ?? segment.routeConfig?.data?.[key];
270
+ const requirements = Array.isArray(rawRequirements)
271
+ ? rawRequirements
272
+ : [];
273
+ return { routeId, requirements };
274
+ }
275
+ resolveRouteId(segment) {
276
+ if (!segment.routeConfig)
277
+ return "__root__";
278
+ const path = segment.routeConfig.path;
279
+ if (path === undefined || path === null || path === "")
280
+ return "__index__";
281
+ return path;
282
+ }
283
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: AuthRouteAdapter, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
284
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: AuthRouteAdapter, providedIn: "root" });
285
+ }
286
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: AuthRouteAdapter, decorators: [{
287
+ type: Injectable,
288
+ args: [{ providedIn: "root" }]
289
+ }] });
290
+
291
+ // Angular route metadata requirement helper
292
+ //
293
+ // Canonical import path:
294
+ // import { withRouteRequirements, extractFullRouteRequirements, ... } from "@securitydept/client-angular"
295
+ //
296
+ // Provides the canonical route-metadata layer for Angular Router auth requirement
297
+ // declaration. Requirements are stored in route `data` rather than in DI scope,
298
+ // enabling full-route aggregation guards to collect the complete requirement set
299
+ // from the entire path-from-root chain in a single pass.
300
+ //
301
+ // Architecture boundary:
302
+ // - Does NOT own auth enforcement or planner logic.
303
+ // - Does NOT carry token-set-specific mapping or policy.
304
+ // - Provides typed helpers for declaring and reading requirements from route data.
305
+ // - framework-specific (Angular Router types) but requirement-system-agnostic.
306
+ //
307
+ // Relationship to provideRouteScopedRequirements():
308
+ // - provideRouteScopedRequirements() is a lower-level DI-scope helper.
309
+ // - This module is the canonical path for Angular Router auth requirement
310
+ // declaration when full-route aggregation is needed.
311
+ //
312
+ // Key sentinel note:
313
+ // DEFAULT_ROUTE_REQUIREMENTS_KEY is defined in router.ts (AuthRouteAdapter's
314
+ // companion constant). ROUTE_REQUIREMENTS_DATA_KEY exported here is its
315
+ // semantic alias — both hold the same value ("authRequirements").
316
+ // Adopters may use either name; they are interchangeable.
317
+ //
318
+ // Stability: provisional
319
+ // ---------------------------------------------------------------------------
320
+ // Route data key
321
+ // ---------------------------------------------------------------------------
322
+ /**
323
+ * Default key for auth requirements in Angular route `data`.
324
+ *
325
+ * Semantic alias for {@link DEFAULT_ROUTE_REQUIREMENTS_KEY} (defined in
326
+ * `router.ts`). Both hold the same value (`"authRequirements"`).
327
+ * Use either name — they are interchangeable.
328
+ *
329
+ * Set this key in `Route.data` to declare auth requirements for a route segment:
330
+ * ```ts
331
+ * { path: "dashboard", data: withRouteRequirements([{ id: "session", kind: "session" }]) }
332
+ * ```
333
+ *
334
+ * The key is also read by {@link AuthRouteAdapter} when projecting snapshots.
335
+ *
336
+ * @see {@link withRouteRequirements}
337
+ * @see {@link extractFullRouteRequirements}
338
+ */
339
+ const ROUTE_REQUIREMENTS_DATA_KEY = DEFAULT_ROUTE_REQUIREMENTS_KEY;
340
+ /**
341
+ * Default key for the route-level composition strategy in Angular route `data`.
342
+ *
343
+ * Used together with {@link ROUTE_REQUIREMENTS_DATA_KEY}. The default
344
+ * composition is `merge` when the key is omitted.
345
+ */
346
+ const ROUTE_REQUIREMENTS_COMPOSITION_DATA_KEY = "authRequirementsComposition";
347
+ // ---------------------------------------------------------------------------
348
+ // Declaration helper
349
+ // ---------------------------------------------------------------------------
350
+ /**
351
+ * Build an Angular route `data` object declaring auth requirements for a
352
+ * single route segment.
353
+ *
354
+ * Use this as the `data` field in an Angular `Route` config. Nested routes
355
+ * can declare their own requirements; a full-route aggregation guard will
356
+ * walk the entire `pathFromRoot` chain to collect all requirements in one pass.
357
+ *
358
+ * @example
359
+ * ```ts
360
+ * // Parent route declares base requirement
361
+ * {
362
+ * path: "app",
363
+ * data: withRouteRequirements([{ id: "session", kind: "session" }]),
364
+ * children: [
365
+ * // Child route adds an additional requirement
366
+ * {
367
+ * path: "admin",
368
+ * data: withRouteRequirements([{ id: "admin-oidc", kind: "backend_oidc" }]),
369
+ * canActivate: [createTokenSetRouteAggregationGuard(...)],
370
+ * },
371
+ * ],
372
+ * }
373
+ * ```
374
+ *
375
+ * @param requirements - One or more auth requirements for this route segment.
376
+ * @param extra - Optional additional data properties to merge.
377
+ */
378
+ function withRouteRequirements(requirements, optionsOrExtra, maybeExtra) {
379
+ const { composition, extra } = normalizeRequirementOptions(optionsOrExtra, maybeExtra);
380
+ return {
381
+ ...extra,
382
+ [ROUTE_REQUIREMENTS_DATA_KEY]: requirements,
383
+ [ROUTE_REQUIREMENTS_COMPOSITION_DATA_KEY]: composition,
384
+ };
385
+ }
386
+ // ---------------------------------------------------------------------------
387
+ // Aggregation helper
388
+ // ---------------------------------------------------------------------------
389
+ /**
390
+ * Extract and flatten auth requirements from a complete Angular route chain.
391
+ *
392
+ * Walks all segments from root to leaf (from `leafRoute.pathFromRoot`)
393
+ * and concatenates the declared {@link AuthRequirement} arrays in order.
394
+ * Requirements from ancestor routes appear before requirements from
395
+ * descendant routes. Duplicate `id` values are NOT de-duplicated here —
396
+ * the caller (guard / enforcement layer) is responsible for deduplication
397
+ * or merge logic.
398
+ *
399
+ * This is the extraction counter-part to {@link withRouteRequirements}.
400
+ * A full-route aggregation guard calls this once and hands the complete
401
+ * flat array to the planner / enforcement layer.
402
+ *
403
+ * @param leafRoute - The leaf `ActivatedRouteSnapshot` from the guard call.
404
+ * @param requirementsKey - Override the route data key (default: `"authRequirements"`).
405
+ */
406
+ function extractFullRouteRequirements(leafRoute, requirementsKey = ROUTE_REQUIREMENTS_DATA_KEY, compositionKey = ROUTE_REQUIREMENTS_COMPOSITION_DATA_KEY) {
407
+ const chain = leafRoute.pathFromRoot ?? [leafRoute];
408
+ let result = [];
409
+ for (const segment of chain) {
410
+ const raw = segment.data[requirementsKey] ??
411
+ segment.routeConfig?.data?.[requirementsKey];
412
+ const composition = readRouteComposition(segment, compositionKey) ??
413
+ RequirementsClientSetComposition.Merge;
414
+ const requirements = Array.isArray(raw) ? raw : [];
415
+ result = resolveEffectiveRequirements(result, {
416
+ composition,
417
+ requirements,
418
+ });
419
+ }
420
+ return result;
421
+ }
422
+ /**
423
+ * Resolve the effective auth requirements by composing the parent chain with a
424
+ * child route declaration.
425
+ *
426
+ * Composition semantics mirror `resolveEffectiveClientSet()`:
427
+ * - `inherit` keeps the parent requirements unchanged
428
+ * - `merge` appends child requirements and replaces same-id parent entries
429
+ * - `replace` discards the parent chain and uses only the child's requirements
430
+ */
431
+ function resolveEffectiveRequirements(parentRequirements, child) {
432
+ switch (child.composition) {
433
+ case RequirementsClientSetComposition.Inherit:
434
+ return [...parentRequirements];
435
+ case RequirementsClientSetComposition.Merge: {
436
+ const merged = new Map();
437
+ for (const requirement of parentRequirements) {
438
+ merged.set(requirement.id, requirement);
439
+ }
440
+ for (const requirement of child.requirements) {
441
+ merged.set(requirement.id, requirement);
442
+ }
443
+ return [...merged.values()];
444
+ }
445
+ case RequirementsClientSetComposition.Replace:
446
+ return [...child.requirements];
447
+ default: {
448
+ const _exhaustive = child.composition;
449
+ throw new Error(`[resolveEffectiveRequirements] Unknown composition: ${_exhaustive}`);
450
+ }
451
+ }
452
+ }
453
+ function normalizeRequirementOptions(optionsOrExtra, maybeExtra) {
454
+ if (optionsOrExtra &&
455
+ ("composition" in optionsOrExtra || "extra" in optionsOrExtra)) {
456
+ const options = optionsOrExtra;
457
+ return {
458
+ composition: options.composition ?? RequirementsClientSetComposition.Merge,
459
+ extra: maybeExtra ?? options.extra,
460
+ };
461
+ }
462
+ return {
463
+ composition: RequirementsClientSetComposition.Merge,
464
+ extra: optionsOrExtra,
465
+ };
466
+ }
467
+ function readRouteComposition(segment, compositionKey) {
468
+ const raw = segment.data[compositionKey] ?? segment.routeConfig?.data?.[compositionKey];
469
+ if (raw === RequirementsClientSetComposition.Inherit ||
470
+ raw === RequirementsClientSetComposition.Merge ||
471
+ raw === RequirementsClientSetComposition.Replace) {
472
+ return raw;
473
+ }
474
+ return undefined;
475
+ }
476
+
477
+ // Angular signal / Observable bridge utilities
478
+ //
479
+ // Canonical import path:
480
+ // import { bridgeToAngularSignal, signalToObservable } from "@securitydept/client-angular"
481
+ //
482
+ // These utilities convert SDK-native ReadableSignalTrait values to Angular-native
483
+ // primitives (WritableSignal, Observable) so that framework adapter packages
484
+ // can surface reactive state using idiomatic Angular APIs.
485
+ //
486
+ // Owner: @securitydept/client-angular — these are generic framework bridges,
487
+ // not token-set-specific utilities. All Angular adapters (basic-auth, session,
488
+ // token-set, …) should import from here instead of implementing their own.
489
+ //
490
+ // Stability: provisional (framework adapter)
491
+ /**
492
+ * Bridge an SDK `ReadableSignalTrait` to an Angular writable signal.
493
+ *
494
+ * Immediately syncs the current value, then subscribes to future changes.
495
+ *
496
+ * @returns Cleanup function that unsubscribes from the SDK signal.
497
+ */
498
+ function bridgeToAngularSignal(source, target) {
499
+ target.set(source.get());
500
+ return source.subscribe(() => {
501
+ target.set(source.get());
502
+ });
503
+ }
504
+ /**
505
+ * Bridge an SDK `ReadableSignalTrait` to an RxJS Observable.
506
+ *
507
+ * The observable emits the current value immediately on subscribe, then
508
+ * emits whenever the SDK signal value changes. Completes on unsubscribe.
509
+ */
510
+ function signalToObservable(source) {
511
+ return new Observable((subscriber) => {
512
+ subscriber.next(source.get());
513
+ const unsubscribe = source.subscribe(() => {
514
+ subscriber.next(source.get());
515
+ });
516
+ return () => unsubscribe();
517
+ });
518
+ }
519
+
520
+ /*
521
+ * Public API Surface of @securitydept/client-angular
522
+ */
523
+
524
+ /**
525
+ * Generated bundle index. Do not edit.
526
+ */
527
+
528
+ export { AUTH_PLANNER_HOST, AUTH_REQUIREMENTS_CLIENT_SET, AuthRouteAdapter, DEFAULT_ROUTE_REQUIREMENTS_KEY, ROUTE_REQUIREMENTS_COMPOSITION_DATA_KEY, ROUTE_REQUIREMENTS_DATA_KEY, bridgeToAngularSignal, extractFullRouteRequirements, injectPlannerHost, provideAuthPlannerHost, provideRouteScopedRequirements, resolveEffectiveRequirements, signalToObservable, withRouteRequirements };
529
+ //# sourceMappingURL=securitydept-client-angular.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"securitydept-client-angular.mjs","sources":["../../src/lib/planner-host.ts","../../src/lib/router.ts","../../src/lib/route-requirements.ts","../../src/lib/signal-bridge.ts","../../src/public-api.ts","../../src/securitydept-client-angular.ts"],"sourcesContent":["// Angular planner-host integration — DI-based planner provider / lookup\n//\n// Canonical import path:\n// import { AUTH_PLANNER_HOST, provideAuthPlannerHost, ... } from \"@securitydept/client-angular\"\n//\n// Provides Angular DI glue for the shared planner-host contract:\n// - InjectionToken for PlannerHost\n// - Provider helpers for app / module / route scoped planners\n// - InjectionToken + provider helpers for ScopedRequirementsClientSet\n// - injectPlannerHost() convenience function with fail-fast\n//\n// Architecture boundary:\n// - Does NOT own the planner-host contract (that lives in @securitydept/client)\n// - Does NOT carry token-set-specific mapping\n// - Provides the Angular DI wiring for the shared contract\n//\n// Stability: provisional\n\nimport {\n\ttype EnvironmentProviders,\n\tInjectionToken,\n\tinject,\n\tmakeEnvironmentProviders,\n} from \"@angular/core\";\nimport type {\n\tAuthGuardClientOption,\n\tCandidateSelector,\n\tCreatePlannerHostOptions,\n\tPlannerHost,\n\tScopedRequirementsClientSet,\n} from \"@securitydept/client/auth-coordination\";\nimport {\n\tcreatePlannerHost,\n\tresolveEffectiveClientSet,\n} from \"@securitydept/client/auth-coordination\";\n\n// ---------------------------------------------------------------------------\n// PlannerHost injection token\n// ---------------------------------------------------------------------------\n\n/**\n * DI token for the shared {@link PlannerHost}.\n *\n * Provide at the app level via {@link provideAuthPlannerHost}.\n * Guards use {@link injectPlannerHost} to look up the nearest instance.\n */\nexport const AUTH_PLANNER_HOST = new InjectionToken<PlannerHost>(\n\t\"AUTH_PLANNER_HOST\",\n);\n\n/**\n * Options for {@link provideAuthPlannerHost}.\n */\nexport interface ProvideAuthPlannerHostOptions {\n\t/**\n\t * Custom candidate selection strategy.\n\t * @see {@link CandidateSelector}\n\t */\n\tselectCandidate?: CandidateSelector;\n}\n\n/**\n * Provide an {@link AUTH_PLANNER_HOST} at the current injector level.\n *\n * Use in app config, route providers, or NgModule providers to establish\n * a planner-host scope. Child routes inherit the nearest parent's planner.\n *\n * @example\n * ```ts\n * // app.config.ts — app-level planner (default sequential strategy)\n * export const appConfig: ApplicationConfig = {\n * providers: [provideAuthPlannerHost()],\n * };\n *\n * // Feature route — override with custom async chooser strategy\n * {\n * path: \"admin\",\n * providers: [provideAuthPlannerHost({\n * selectCandidate: async (candidates) => showAdminChooser(candidates),\n * })],\n * children: [...]\n * }\n * ```\n */\nexport function provideAuthPlannerHost(\n\toptions?: ProvideAuthPlannerHostOptions,\n): EnvironmentProviders {\n\tconst plannerOptions: CreatePlannerHostOptions = {};\n\tif (options?.selectCandidate) {\n\t\tplannerOptions.selectCandidate = options.selectCandidate;\n\t}\n\treturn makeEnvironmentProviders([\n\t\t{ provide: AUTH_PLANNER_HOST, useValue: createPlannerHost(plannerOptions) },\n\t]);\n}\n\n/**\n * Inject the nearest {@link PlannerHost} from the DI hierarchy.\n *\n * Throws an explicit error if no planner-host is provided, preventing\n * silent fallback behavior.\n *\n * @example\n * ```ts\n * const plannerHost = injectPlannerHost();\n * const result = await plannerHost.evaluate(candidates);\n * ```\n */\nexport function injectPlannerHost(): PlannerHost {\n\tconst host = inject(AUTH_PLANNER_HOST, { optional: true });\n\tif (!host) {\n\t\tthrow new Error(\n\t\t\t\"[injectPlannerHost] No AUTH_PLANNER_HOST found in the injector hierarchy. \" +\n\t\t\t\t\"Use provideAuthPlannerHost() in your app config or route providers.\",\n\t\t);\n\t}\n\treturn host;\n}\n\n// ---------------------------------------------------------------------------\n// Requirements client set injection token — stores the already-resolved\n// effective options for the current scope (parent + child composed).\n// ---------------------------------------------------------------------------\n\n/**\n * DI token for the effective requirements client set at the current scope.\n *\n * The value is always the **resolved** options after composing all ancestor\n * scopes — it is NOT the raw `ScopedRequirementsClientSet` declaration.\n *\n * Provide via {@link provideRouteScopedRequirements}.\n * Guards inject this token to get the effective scope requirements and then\n * merge their own declared candidates on top.\n */\nexport const AUTH_REQUIREMENTS_CLIENT_SET = new InjectionToken<\n\treadonly AuthGuardClientOption[]\n>(\"AUTH_REQUIREMENTS_CLIENT_SET\");\n\n/**\n * Provide a {@link ScopedRequirementsClientSet} at the current route/module scope.\n *\n * Internally, the provider resolves the effective client set by composing the\n * declared `scopedSet` with the parent scope's already-resolved options\n * (injected via `skipSelf: true`). The token value stored in DI is the\n * resolved `readonly AuthGuardClientOption[]`, not the raw declaration.\n *\n * This mirrors the React `AuthRequirementsClientSetProvider` semantics:\n * each level stores its resolved effective options, so children inherit\n * the composed result without re-resolving the full ancestor chain.\n *\n * The `scopedSet` argument may also be a **factory function** — it will be\n * called inside Angular's `useFactory` context, so `inject()` is valid.\n * Use this when the requirement options need DI-resolved services (e.g. a\n * token-set registry or a redirect service).\n *\n * @example\n * ```ts\n * // routes.ts — static declaration (no DI needed)\n * {\n * path: \"\",\n * providers: [\n * provideRouteScopedRequirements({\n * composition: RequirementsClientSetComposition.Replace,\n * options: [sessionClientOption],\n * }),\n * ],\n * }\n *\n * // Feature route merges OIDC on top of the parent's session requirement\n * {\n * path: \"confluence\",\n * providers: [\n * provideRouteScopedRequirements({\n * composition: RequirementsClientSetComposition.Merge,\n * options: [confluenceOidcClientOption],\n * }),\n * ],\n * // Route-level DI scope only. Angular Router adopters should prefer\n * // secureRouteRoot()/secureRoute() over direct guard wiring.\n * }\n *\n * // Factory form — use when options depend on injected services\n * {\n * path: \"protected\",\n * providers: [\n * provideRouteScopedRequirements(() => {\n * const registry = inject(TokenSetAuthRegistry);\n * const authService = inject(AuthService);\n * return {\n * composition: RequirementsClientSetComposition.Replace,\n * options: [{\n * requirementId: \"oidc\",\n * requirementKind: \"frontend_oidc\",\n * checkAuthenticated: () => registry.require(clientKey).isAuthenticated(),\n * onUnauthenticated: () => { authService.redirectToLogin(clientKey).subscribe(); return false; },\n * }],\n * };\n * }),\n * ],\n * }\n * ```\n */\nexport function provideRouteScopedRequirements(\n\tscopedSet: ScopedRequirementsClientSet | (() => ScopedRequirementsClientSet),\n): EnvironmentProviders {\n\treturn makeEnvironmentProviders([\n\t\t{\n\t\t\tprovide: AUTH_REQUIREMENTS_CLIENT_SET,\n\t\t\tuseFactory: (): readonly AuthGuardClientOption[] => {\n\t\t\t\t// Walk up to the nearest parent scope's already-resolved effective set.\n\t\t\t\t// skipSelf: true ensures we don't read our own (not-yet-set) value.\n\t\t\t\tconst parentOptions: readonly AuthGuardClientOption[] =\n\t\t\t\t\tinject(AUTH_REQUIREMENTS_CLIENT_SET, {\n\t\t\t\t\t\toptional: true,\n\t\t\t\t\t\tskipSelf: true,\n\t\t\t\t\t}) ?? [];\n\t\t\t\t// Support factory form so callers can use inject() to resolve\n\t\t\t\t// DI services (e.g. auth registry, redirect service) inside their\n\t\t\t\t// option callbacks without bypassing this helper.\n\t\t\t\tconst resolved =\n\t\t\t\t\ttypeof scopedSet === \"function\" ? scopedSet() : scopedSet;\n\t\t\t\treturn resolveEffectiveClientSet(parentOptions, resolved);\n\t\t\t},\n\t\t},\n\t]);\n}\n\n// Re-export composition helpers so consumers don't need a second import\nexport { resolveEffectiveClientSet };\n","// Angular Router route adapter for auth-coordination\n//\n// Canonical import path:\n// import { ... } from \"@securitydept/client-angular\"\n//\n// Provides the projection layer between Angular Router's ActivatedRouteSnapshot\n// tree and the SDK's framework-agnostic RouteMatchNode[] contract.\n// Uses real @angular/router types (ActivatedRouteSnapshot) so consuming apps\n// get full type-safety and IDE autocomplete without any casting.\n//\n// Architecture boundary:\n// - Does NOT own the Angular router, guards, or navigation lifecycle.\n// - Does NOT re-export core orchestration primitives.\n// - Does NOT carry token-set-specific mapping or policy.\n// - The adopter wires this into their guards / resolvers.\n//\n// Stability: provisional\n\nimport { Injectable } from \"@angular/core\";\nimport type { ActivatedRouteSnapshot } from \"@angular/router\";\nimport type {\n\tAuthRequirement,\n\tRouteMatchNode,\n} from \"@securitydept/client/auth-coordination\";\n\n// ---------------------------------------------------------------------------\n// Projection options\n// ---------------------------------------------------------------------------\n\n/** Options for {@link AuthRouteAdapter} projection methods. */\nexport interface AuthRouteAdapterOptions {\n\t/**\n\t * Key used to read auth requirements from route `data`.\n\t *\n\t * @default \"authRequirements\"\n\t */\n\trequirementsKey?: string;\n}\n\n/** Default key for auth requirements in Angular route `data`. */\nexport const DEFAULT_ROUTE_REQUIREMENTS_KEY = \"authRequirements\";\n\n// ---------------------------------------------------------------------------\n// Guard adapter result\n// ---------------------------------------------------------------------------\n\n/** Result from {@link AuthRouteAdapter.check}. */\nexport interface RouteGuardResult {\n\t/** Whether all requirements are settled (route can activate). */\n\tcanActivate: boolean;\n\t/** The pending requirement still awaiting resolution, if any. */\n\tpendingRequirement: AuthRequirement | null;\n}\n\n// ---------------------------------------------------------------------------\n// Service\n// ---------------------------------------------------------------------------\n\n/**\n * Angular service that bridges Angular Router's `ActivatedRouteSnapshot` to\n * the SDK's framework-agnostic `RouteMatchNode[]` contract.\n *\n * Inject this into `CanActivateFn` guards or class-based guards.\n *\n * @example\n * ```ts\n * export const authGuard: CanActivateFn = (route) => {\n * const adapter = inject(AuthRouteAdapter);\n * const result = adapter.check(route, orchestrator);\n * if (!result.canActivate && result.pendingRequirement) {\n * // Trigger auth flow for the pending requirement\n * }\n * return result.canActivate;\n * };\n * ```\n */\n@Injectable({ providedIn: \"root\" })\nexport class AuthRouteAdapter {\n\t/**\n\t * Project the `pathFromRoot` of an `ActivatedRouteSnapshot` into the SDK's\n\t * `RouteMatchNode[]` chain.\n\t *\n\t * Route segments without a `routeConfig` (e.g. the root segment) are\n\t * assigned routeId `\"__root__\"`. Segments with a config use the `path`\n\t * as their routeId (or `\"__index__\"` for empty-path segments).\n\t */\n\tprojectRouteMatch(\n\t\tleafRoute: ActivatedRouteSnapshot,\n\t\toptions?: AuthRouteAdapterOptions,\n\t): RouteMatchNode[] {\n\t\tconst key = options?.requirementsKey ?? DEFAULT_ROUTE_REQUIREMENTS_KEY;\n\t\tconst chain = leafRoute.pathFromRoot ?? [leafRoute];\n\t\treturn chain.map((segment) => this.segmentToNode(segment, key));\n\t}\n\n\t/**\n\t * Project an explicit array of `ActivatedRouteSnapshot` segments into the\n\t * SDK's `RouteMatchNode[]` chain.\n\t *\n\t * Use when you already have a flattened chain (e.g. from\n\t * `ActivatedRoute.pathFromRoot`).\n\t */\n\tprojectRouteSegments(\n\t\tsegments: readonly ActivatedRouteSnapshot[],\n\t\toptions?: AuthRouteAdapterOptions,\n\t): RouteMatchNode[] {\n\t\tconst key = options?.requirementsKey ?? DEFAULT_ROUTE_REQUIREMENTS_KEY;\n\t\treturn segments.map((segment) => this.segmentToNode(segment, key));\n\t}\n\n\t/**\n\t * Project the route snapshot and check whether all requirements are settled.\n\t *\n\t * @param leafRoute - The leaf `ActivatedRouteSnapshot` from the guard call.\n\t * @param orchestrator - The SDK orchestrator managing the requirement plan.\n\t * @param options - Optional projection options.\n\t */\n\tcheck(\n\t\tleafRoute: ActivatedRouteSnapshot,\n\t\torchestrator: {\n\t\t\tactivateMatchedRoutes(chain: readonly RouteMatchNode[]): void;\n\t\t\tsnapshot(): {\n\t\t\t\tsettled: boolean;\n\t\t\t\tpendingRequirement: AuthRequirement | null;\n\t\t\t};\n\t\t},\n\t\toptions?: AuthRouteAdapterOptions,\n\t): RouteGuardResult {\n\t\tconst chain = this.projectRouteMatch(leafRoute, options);\n\t\torchestrator.activateMatchedRoutes(chain);\n\t\tconst snap = orchestrator.snapshot();\n\t\treturn {\n\t\t\tcanActivate: snap.settled,\n\t\t\tpendingRequirement: snap.pendingRequirement,\n\t\t};\n\t}\n\n\tprivate segmentToNode(\n\t\tsegment: ActivatedRouteSnapshot,\n\t\tkey: string,\n\t): RouteMatchNode {\n\t\tconst routeId = this.resolveRouteId(segment);\n\t\tconst rawRequirements =\n\t\t\tsegment.data[key] ?? segment.routeConfig?.data?.[key];\n\t\tconst requirements: AuthRequirement[] = Array.isArray(rawRequirements)\n\t\t\t? rawRequirements\n\t\t\t: [];\n\t\treturn { routeId, requirements };\n\t}\n\n\tprivate resolveRouteId(segment: ActivatedRouteSnapshot): string {\n\t\tif (!segment.routeConfig) return \"__root__\";\n\t\tconst path = segment.routeConfig.path;\n\t\tif (path === undefined || path === null || path === \"\") return \"__index__\";\n\t\treturn path;\n\t}\n}\n","// Angular route metadata requirement helper\n//\n// Canonical import path:\n// import { withRouteRequirements, extractFullRouteRequirements, ... } from \"@securitydept/client-angular\"\n//\n// Provides the canonical route-metadata layer for Angular Router auth requirement\n// declaration. Requirements are stored in route `data` rather than in DI scope,\n// enabling full-route aggregation guards to collect the complete requirement set\n// from the entire path-from-root chain in a single pass.\n//\n// Architecture boundary:\n// - Does NOT own auth enforcement or planner logic.\n// - Does NOT carry token-set-specific mapping or policy.\n// - Provides typed helpers for declaring and reading requirements from route data.\n// - framework-specific (Angular Router types) but requirement-system-agnostic.\n//\n// Relationship to provideRouteScopedRequirements():\n// - provideRouteScopedRequirements() is a lower-level DI-scope helper.\n// - This module is the canonical path for Angular Router auth requirement\n// declaration when full-route aggregation is needed.\n//\n// Key sentinel note:\n// DEFAULT_ROUTE_REQUIREMENTS_KEY is defined in router.ts (AuthRouteAdapter's\n// companion constant). ROUTE_REQUIREMENTS_DATA_KEY exported here is its\n// semantic alias — both hold the same value (\"authRequirements\").\n// Adopters may use either name; they are interchangeable.\n//\n// Stability: provisional\n\nimport type { ActivatedRouteSnapshot } from \"@angular/router\";\nimport {\n\ttype AuthRequirement,\n\tRequirementsClientSetComposition,\n} from \"@securitydept/client/auth-coordination\";\nimport { DEFAULT_ROUTE_REQUIREMENTS_KEY } from \"./router\";\n\n// ---------------------------------------------------------------------------\n// Route data key\n// ---------------------------------------------------------------------------\n\n/**\n * Default key for auth requirements in Angular route `data`.\n *\n * Semantic alias for {@link DEFAULT_ROUTE_REQUIREMENTS_KEY} (defined in\n * `router.ts`). Both hold the same value (`\"authRequirements\"`).\n * Use either name — they are interchangeable.\n *\n * Set this key in `Route.data` to declare auth requirements for a route segment:\n * ```ts\n * { path: \"dashboard\", data: withRouteRequirements([{ id: \"session\", kind: \"session\" }]) }\n * ```\n *\n * The key is also read by {@link AuthRouteAdapter} when projecting snapshots.\n *\n * @see {@link withRouteRequirements}\n * @see {@link extractFullRouteRequirements}\n */\nexport const ROUTE_REQUIREMENTS_DATA_KEY = DEFAULT_ROUTE_REQUIREMENTS_KEY;\n\n/**\n * Default key for the route-level composition strategy in Angular route `data`.\n *\n * Used together with {@link ROUTE_REQUIREMENTS_DATA_KEY}. The default\n * composition is `merge` when the key is omitted.\n */\nexport const ROUTE_REQUIREMENTS_COMPOSITION_DATA_KEY =\n\t\"authRequirementsComposition\";\n\n/**\n * Route-level auth requirement declaration stored in Angular route `data`.\n */\nexport interface RouteRequirementsDeclaration {\n\trequirements: readonly AuthRequirement[];\n\tcomposition: RequirementsClientSetComposition;\n}\n\n/**\n * Options for {@link withRouteRequirements}.\n */\nexport interface WithRouteRequirementsOptions {\n\t/**\n\t * How this route segment composes with its parent route chain.\n\t * @default RequirementsClientSetComposition.Merge\n\t */\n\tcomposition?: RequirementsClientSetComposition;\n\n\t/**\n\t * Additional route data properties to merge into the returned object.\n\t */\n\textra?: Record<string, unknown>;\n}\n\n// ---------------------------------------------------------------------------\n// Declaration helper\n// ---------------------------------------------------------------------------\n\n/**\n * Build an Angular route `data` object declaring auth requirements for a\n * single route segment.\n *\n * Use this as the `data` field in an Angular `Route` config. Nested routes\n * can declare their own requirements; a full-route aggregation guard will\n * walk the entire `pathFromRoot` chain to collect all requirements in one pass.\n *\n * @example\n * ```ts\n * // Parent route declares base requirement\n * {\n * path: \"app\",\n * data: withRouteRequirements([{ id: \"session\", kind: \"session\" }]),\n * children: [\n * // Child route adds an additional requirement\n * {\n * path: \"admin\",\n * data: withRouteRequirements([{ id: \"admin-oidc\", kind: \"backend_oidc\" }]),\n * canActivate: [createTokenSetRouteAggregationGuard(...)],\n * },\n * ],\n * }\n * ```\n *\n * @param requirements - One or more auth requirements for this route segment.\n * @param extra - Optional additional data properties to merge.\n */\nexport function withRouteRequirements(\n\trequirements: AuthRequirement[],\n\toptionsOrExtra?: WithRouteRequirementsOptions | Record<string, unknown>,\n\tmaybeExtra?: Record<string, unknown>,\n): Record<string, unknown> {\n\tconst { composition, extra } = normalizeRequirementOptions(\n\t\toptionsOrExtra,\n\t\tmaybeExtra,\n\t);\n\treturn {\n\t\t...extra,\n\t\t[ROUTE_REQUIREMENTS_DATA_KEY]: requirements,\n\t\t[ROUTE_REQUIREMENTS_COMPOSITION_DATA_KEY]: composition,\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Aggregation helper\n// ---------------------------------------------------------------------------\n\n/**\n * Extract and flatten auth requirements from a complete Angular route chain.\n *\n * Walks all segments from root to leaf (from `leafRoute.pathFromRoot`)\n * and concatenates the declared {@link AuthRequirement} arrays in order.\n * Requirements from ancestor routes appear before requirements from\n * descendant routes. Duplicate `id` values are NOT de-duplicated here —\n * the caller (guard / enforcement layer) is responsible for deduplication\n * or merge logic.\n *\n * This is the extraction counter-part to {@link withRouteRequirements}.\n * A full-route aggregation guard calls this once and hands the complete\n * flat array to the planner / enforcement layer.\n *\n * @param leafRoute - The leaf `ActivatedRouteSnapshot` from the guard call.\n * @param requirementsKey - Override the route data key (default: `\"authRequirements\"`).\n */\nexport function extractFullRouteRequirements(\n\tleafRoute: ActivatedRouteSnapshot,\n\trequirementsKey = ROUTE_REQUIREMENTS_DATA_KEY,\n\tcompositionKey = ROUTE_REQUIREMENTS_COMPOSITION_DATA_KEY,\n): AuthRequirement[] {\n\tconst chain = leafRoute.pathFromRoot ?? [leafRoute];\n\tlet result: AuthRequirement[] = [];\n\tfor (const segment of chain) {\n\t\tconst raw =\n\t\t\tsegment.data[requirementsKey] ??\n\t\t\tsegment.routeConfig?.data?.[requirementsKey];\n\t\tconst composition =\n\t\t\treadRouteComposition(segment, compositionKey) ??\n\t\t\tRequirementsClientSetComposition.Merge;\n\t\tconst requirements = Array.isArray(raw) ? (raw as AuthRequirement[]) : [];\n\t\tresult = resolveEffectiveRequirements(result, {\n\t\t\tcomposition,\n\t\t\trequirements,\n\t\t});\n\t}\n\treturn result;\n}\n\n/**\n * Resolve the effective auth requirements by composing the parent chain with a\n * child route declaration.\n *\n * Composition semantics mirror `resolveEffectiveClientSet()`:\n * - `inherit` keeps the parent requirements unchanged\n * - `merge` appends child requirements and replaces same-id parent entries\n * - `replace` discards the parent chain and uses only the child's requirements\n */\nexport function resolveEffectiveRequirements(\n\tparentRequirements: readonly AuthRequirement[],\n\tchild: RouteRequirementsDeclaration,\n): AuthRequirement[] {\n\tswitch (child.composition) {\n\t\tcase RequirementsClientSetComposition.Inherit:\n\t\t\treturn [...parentRequirements];\n\n\t\tcase RequirementsClientSetComposition.Merge: {\n\t\t\tconst merged = new Map<string, AuthRequirement>();\n\t\t\tfor (const requirement of parentRequirements) {\n\t\t\t\tmerged.set(requirement.id, requirement);\n\t\t\t}\n\t\t\tfor (const requirement of child.requirements) {\n\t\t\t\tmerged.set(requirement.id, requirement);\n\t\t\t}\n\t\t\treturn [...merged.values()];\n\t\t}\n\n\t\tcase RequirementsClientSetComposition.Replace:\n\t\t\treturn [...child.requirements];\n\n\t\tdefault: {\n\t\t\tconst _exhaustive: never = child.composition;\n\t\t\tthrow new Error(\n\t\t\t\t`[resolveEffectiveRequirements] Unknown composition: ${_exhaustive}`,\n\t\t\t);\n\t\t}\n\t}\n}\n\nfunction normalizeRequirementOptions(\n\toptionsOrExtra?: WithRouteRequirementsOptions | Record<string, unknown>,\n\tmaybeExtra?: Record<string, unknown>,\n): {\n\tcomposition: RequirementsClientSetComposition;\n\textra?: Record<string, unknown>;\n} {\n\tif (\n\t\toptionsOrExtra &&\n\t\t(\"composition\" in optionsOrExtra || \"extra\" in optionsOrExtra)\n\t) {\n\t\tconst options = optionsOrExtra as WithRouteRequirementsOptions;\n\t\treturn {\n\t\t\tcomposition:\n\t\t\t\toptions.composition ?? RequirementsClientSetComposition.Merge,\n\t\t\textra: maybeExtra ?? options.extra,\n\t\t};\n\t}\n\n\treturn {\n\t\tcomposition: RequirementsClientSetComposition.Merge,\n\t\textra: optionsOrExtra as Record<string, unknown> | undefined,\n\t};\n}\n\nfunction readRouteComposition(\n\tsegment: ActivatedRouteSnapshot,\n\tcompositionKey: string,\n): RequirementsClientSetComposition | undefined {\n\tconst raw =\n\t\tsegment.data[compositionKey] ?? segment.routeConfig?.data?.[compositionKey];\n\tif (\n\t\traw === RequirementsClientSetComposition.Inherit ||\n\t\traw === RequirementsClientSetComposition.Merge ||\n\t\traw === RequirementsClientSetComposition.Replace\n\t) {\n\t\treturn raw;\n\t}\n\treturn undefined;\n}\n","// Angular signal / Observable bridge utilities\n//\n// Canonical import path:\n// import { bridgeToAngularSignal, signalToObservable } from \"@securitydept/client-angular\"\n//\n// These utilities convert SDK-native ReadableSignalTrait values to Angular-native\n// primitives (WritableSignal, Observable) so that framework adapter packages\n// can surface reactive state using idiomatic Angular APIs.\n//\n// Owner: @securitydept/client-angular — these are generic framework bridges,\n// not token-set-specific utilities. All Angular adapters (basic-auth, session,\n// token-set, …) should import from here instead of implementing their own.\n//\n// Stability: provisional (framework adapter)\n\nimport type { WritableSignal } from \"@angular/core\";\nimport type { ReadableSignalTrait } from \"@securitydept/client\";\nimport { Observable } from \"rxjs\";\n\n/**\n * Bridge an SDK `ReadableSignalTrait` to an Angular writable signal.\n *\n * Immediately syncs the current value, then subscribes to future changes.\n *\n * @returns Cleanup function that unsubscribes from the SDK signal.\n */\nexport function bridgeToAngularSignal<T>(\n\tsource: ReadableSignalTrait<T>,\n\ttarget: WritableSignal<T>,\n): () => void {\n\ttarget.set(source.get());\n\treturn source.subscribe(() => {\n\t\ttarget.set(source.get());\n\t});\n}\n\n/**\n * Bridge an SDK `ReadableSignalTrait` to an RxJS Observable.\n *\n * The observable emits the current value immediately on subscribe, then\n * emits whenever the SDK signal value changes. Completes on unsubscribe.\n */\nexport function signalToObservable<T>(\n\tsource: ReadableSignalTrait<T>,\n): Observable<T> {\n\treturn new Observable<T>((subscriber) => {\n\t\tsubscriber.next(source.get());\n\t\tconst unsubscribe = source.subscribe(() => {\n\t\t\tsubscriber.next(source.get());\n\t\t});\n\t\treturn () => unsubscribe();\n\t});\n}\n","/*\n * Public API Surface of @securitydept/client-angular\n */\n\nexport * from \"./lib/planner-host\";\nexport * from \"./lib/route-requirements\";\nexport * from \"./lib/router\";\nexport * from \"./lib/signal-bridge\";\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,336 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, EnvironmentProviders, WritableSignal } from '@angular/core';
3
+ import { PlannerHost, AuthGuardClientOption, CandidateSelector, ScopedRequirementsClientSet, AuthRequirement, RequirementsClientSetComposition, RouteMatchNode } from '@securitydept/client/auth-coordination';
4
+ export { resolveEffectiveClientSet } from '@securitydept/client/auth-coordination';
5
+ import { ActivatedRouteSnapshot } from '@angular/router';
6
+ import { ReadableSignalTrait } from '@securitydept/client';
7
+ import { Observable } from 'rxjs';
8
+
9
+ /**
10
+ * DI token for the shared {@link PlannerHost}.
11
+ *
12
+ * Provide at the app level via {@link provideAuthPlannerHost}.
13
+ * Guards use {@link injectPlannerHost} to look up the nearest instance.
14
+ */
15
+ declare const AUTH_PLANNER_HOST: InjectionToken<PlannerHost>;
16
+ /**
17
+ * Options for {@link provideAuthPlannerHost}.
18
+ */
19
+ interface ProvideAuthPlannerHostOptions {
20
+ /**
21
+ * Custom candidate selection strategy.
22
+ * @see {@link CandidateSelector}
23
+ */
24
+ selectCandidate?: CandidateSelector;
25
+ }
26
+ /**
27
+ * Provide an {@link AUTH_PLANNER_HOST} at the current injector level.
28
+ *
29
+ * Use in app config, route providers, or NgModule providers to establish
30
+ * a planner-host scope. Child routes inherit the nearest parent's planner.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * // app.config.ts — app-level planner (default sequential strategy)
35
+ * export const appConfig: ApplicationConfig = {
36
+ * providers: [provideAuthPlannerHost()],
37
+ * };
38
+ *
39
+ * // Feature route — override with custom async chooser strategy
40
+ * {
41
+ * path: "admin",
42
+ * providers: [provideAuthPlannerHost({
43
+ * selectCandidate: async (candidates) => showAdminChooser(candidates),
44
+ * })],
45
+ * children: [...]
46
+ * }
47
+ * ```
48
+ */
49
+ declare function provideAuthPlannerHost(options?: ProvideAuthPlannerHostOptions): EnvironmentProviders;
50
+ /**
51
+ * Inject the nearest {@link PlannerHost} from the DI hierarchy.
52
+ *
53
+ * Throws an explicit error if no planner-host is provided, preventing
54
+ * silent fallback behavior.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * const plannerHost = injectPlannerHost();
59
+ * const result = await plannerHost.evaluate(candidates);
60
+ * ```
61
+ */
62
+ declare function injectPlannerHost(): PlannerHost;
63
+ /**
64
+ * DI token for the effective requirements client set at the current scope.
65
+ *
66
+ * The value is always the **resolved** options after composing all ancestor
67
+ * scopes — it is NOT the raw `ScopedRequirementsClientSet` declaration.
68
+ *
69
+ * Provide via {@link provideRouteScopedRequirements}.
70
+ * Guards inject this token to get the effective scope requirements and then
71
+ * merge their own declared candidates on top.
72
+ */
73
+ declare const AUTH_REQUIREMENTS_CLIENT_SET: InjectionToken<readonly AuthGuardClientOption[]>;
74
+ /**
75
+ * Provide a {@link ScopedRequirementsClientSet} at the current route/module scope.
76
+ *
77
+ * Internally, the provider resolves the effective client set by composing the
78
+ * declared `scopedSet` with the parent scope's already-resolved options
79
+ * (injected via `skipSelf: true`). The token value stored in DI is the
80
+ * resolved `readonly AuthGuardClientOption[]`, not the raw declaration.
81
+ *
82
+ * This mirrors the React `AuthRequirementsClientSetProvider` semantics:
83
+ * each level stores its resolved effective options, so children inherit
84
+ * the composed result without re-resolving the full ancestor chain.
85
+ *
86
+ * The `scopedSet` argument may also be a **factory function** — it will be
87
+ * called inside Angular's `useFactory` context, so `inject()` is valid.
88
+ * Use this when the requirement options need DI-resolved services (e.g. a
89
+ * token-set registry or a redirect service).
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * // routes.ts — static declaration (no DI needed)
94
+ * {
95
+ * path: "",
96
+ * providers: [
97
+ * provideRouteScopedRequirements({
98
+ * composition: RequirementsClientSetComposition.Replace,
99
+ * options: [sessionClientOption],
100
+ * }),
101
+ * ],
102
+ * }
103
+ *
104
+ * // Feature route merges OIDC on top of the parent's session requirement
105
+ * {
106
+ * path: "confluence",
107
+ * providers: [
108
+ * provideRouteScopedRequirements({
109
+ * composition: RequirementsClientSetComposition.Merge,
110
+ * options: [confluenceOidcClientOption],
111
+ * }),
112
+ * ],
113
+ * // Route-level DI scope only. Angular Router adopters should prefer
114
+ * // secureRouteRoot()/secureRoute() over direct guard wiring.
115
+ * }
116
+ *
117
+ * // Factory form — use when options depend on injected services
118
+ * {
119
+ * path: "protected",
120
+ * providers: [
121
+ * provideRouteScopedRequirements(() => {
122
+ * const registry = inject(TokenSetAuthRegistry);
123
+ * const authService = inject(AuthService);
124
+ * return {
125
+ * composition: RequirementsClientSetComposition.Replace,
126
+ * options: [{
127
+ * requirementId: "oidc",
128
+ * requirementKind: "frontend_oidc",
129
+ * checkAuthenticated: () => registry.require(clientKey).isAuthenticated(),
130
+ * onUnauthenticated: () => { authService.redirectToLogin(clientKey).subscribe(); return false; },
131
+ * }],
132
+ * };
133
+ * }),
134
+ * ],
135
+ * }
136
+ * ```
137
+ */
138
+ declare function provideRouteScopedRequirements(scopedSet: ScopedRequirementsClientSet | (() => ScopedRequirementsClientSet)): EnvironmentProviders;
139
+
140
+ /**
141
+ * Default key for auth requirements in Angular route `data`.
142
+ *
143
+ * Semantic alias for {@link DEFAULT_ROUTE_REQUIREMENTS_KEY} (defined in
144
+ * `router.ts`). Both hold the same value (`"authRequirements"`).
145
+ * Use either name — they are interchangeable.
146
+ *
147
+ * Set this key in `Route.data` to declare auth requirements for a route segment:
148
+ * ```ts
149
+ * { path: "dashboard", data: withRouteRequirements([{ id: "session", kind: "session" }]) }
150
+ * ```
151
+ *
152
+ * The key is also read by {@link AuthRouteAdapter} when projecting snapshots.
153
+ *
154
+ * @see {@link withRouteRequirements}
155
+ * @see {@link extractFullRouteRequirements}
156
+ */
157
+ declare const ROUTE_REQUIREMENTS_DATA_KEY = "authRequirements";
158
+ /**
159
+ * Default key for the route-level composition strategy in Angular route `data`.
160
+ *
161
+ * Used together with {@link ROUTE_REQUIREMENTS_DATA_KEY}. The default
162
+ * composition is `merge` when the key is omitted.
163
+ */
164
+ declare const ROUTE_REQUIREMENTS_COMPOSITION_DATA_KEY = "authRequirementsComposition";
165
+ /**
166
+ * Route-level auth requirement declaration stored in Angular route `data`.
167
+ */
168
+ interface RouteRequirementsDeclaration {
169
+ requirements: readonly AuthRequirement[];
170
+ composition: RequirementsClientSetComposition;
171
+ }
172
+ /**
173
+ * Options for {@link withRouteRequirements}.
174
+ */
175
+ interface WithRouteRequirementsOptions {
176
+ /**
177
+ * How this route segment composes with its parent route chain.
178
+ * @default RequirementsClientSetComposition.Merge
179
+ */
180
+ composition?: RequirementsClientSetComposition;
181
+ /**
182
+ * Additional route data properties to merge into the returned object.
183
+ */
184
+ extra?: Record<string, unknown>;
185
+ }
186
+ /**
187
+ * Build an Angular route `data` object declaring auth requirements for a
188
+ * single route segment.
189
+ *
190
+ * Use this as the `data` field in an Angular `Route` config. Nested routes
191
+ * can declare their own requirements; a full-route aggregation guard will
192
+ * walk the entire `pathFromRoot` chain to collect all requirements in one pass.
193
+ *
194
+ * @example
195
+ * ```ts
196
+ * // Parent route declares base requirement
197
+ * {
198
+ * path: "app",
199
+ * data: withRouteRequirements([{ id: "session", kind: "session" }]),
200
+ * children: [
201
+ * // Child route adds an additional requirement
202
+ * {
203
+ * path: "admin",
204
+ * data: withRouteRequirements([{ id: "admin-oidc", kind: "backend_oidc" }]),
205
+ * canActivate: [createTokenSetRouteAggregationGuard(...)],
206
+ * },
207
+ * ],
208
+ * }
209
+ * ```
210
+ *
211
+ * @param requirements - One or more auth requirements for this route segment.
212
+ * @param extra - Optional additional data properties to merge.
213
+ */
214
+ declare function withRouteRequirements(requirements: AuthRequirement[], optionsOrExtra?: WithRouteRequirementsOptions | Record<string, unknown>, maybeExtra?: Record<string, unknown>): Record<string, unknown>;
215
+ /**
216
+ * Extract and flatten auth requirements from a complete Angular route chain.
217
+ *
218
+ * Walks all segments from root to leaf (from `leafRoute.pathFromRoot`)
219
+ * and concatenates the declared {@link AuthRequirement} arrays in order.
220
+ * Requirements from ancestor routes appear before requirements from
221
+ * descendant routes. Duplicate `id` values are NOT de-duplicated here —
222
+ * the caller (guard / enforcement layer) is responsible for deduplication
223
+ * or merge logic.
224
+ *
225
+ * This is the extraction counter-part to {@link withRouteRequirements}.
226
+ * A full-route aggregation guard calls this once and hands the complete
227
+ * flat array to the planner / enforcement layer.
228
+ *
229
+ * @param leafRoute - The leaf `ActivatedRouteSnapshot` from the guard call.
230
+ * @param requirementsKey - Override the route data key (default: `"authRequirements"`).
231
+ */
232
+ declare function extractFullRouteRequirements(leafRoute: ActivatedRouteSnapshot, requirementsKey?: string, compositionKey?: string): AuthRequirement[];
233
+ /**
234
+ * Resolve the effective auth requirements by composing the parent chain with a
235
+ * child route declaration.
236
+ *
237
+ * Composition semantics mirror `resolveEffectiveClientSet()`:
238
+ * - `inherit` keeps the parent requirements unchanged
239
+ * - `merge` appends child requirements and replaces same-id parent entries
240
+ * - `replace` discards the parent chain and uses only the child's requirements
241
+ */
242
+ declare function resolveEffectiveRequirements(parentRequirements: readonly AuthRequirement[], child: RouteRequirementsDeclaration): AuthRequirement[];
243
+
244
+ /** Options for {@link AuthRouteAdapter} projection methods. */
245
+ interface AuthRouteAdapterOptions {
246
+ /**
247
+ * Key used to read auth requirements from route `data`.
248
+ *
249
+ * @default "authRequirements"
250
+ */
251
+ requirementsKey?: string;
252
+ }
253
+ /** Default key for auth requirements in Angular route `data`. */
254
+ declare const DEFAULT_ROUTE_REQUIREMENTS_KEY = "authRequirements";
255
+ /** Result from {@link AuthRouteAdapter.check}. */
256
+ interface RouteGuardResult {
257
+ /** Whether all requirements are settled (route can activate). */
258
+ canActivate: boolean;
259
+ /** The pending requirement still awaiting resolution, if any. */
260
+ pendingRequirement: AuthRequirement | null;
261
+ }
262
+ /**
263
+ * Angular service that bridges Angular Router's `ActivatedRouteSnapshot` to
264
+ * the SDK's framework-agnostic `RouteMatchNode[]` contract.
265
+ *
266
+ * Inject this into `CanActivateFn` guards or class-based guards.
267
+ *
268
+ * @example
269
+ * ```ts
270
+ * export const authGuard: CanActivateFn = (route) => {
271
+ * const adapter = inject(AuthRouteAdapter);
272
+ * const result = adapter.check(route, orchestrator);
273
+ * if (!result.canActivate && result.pendingRequirement) {
274
+ * // Trigger auth flow for the pending requirement
275
+ * }
276
+ * return result.canActivate;
277
+ * };
278
+ * ```
279
+ */
280
+ declare class AuthRouteAdapter {
281
+ /**
282
+ * Project the `pathFromRoot` of an `ActivatedRouteSnapshot` into the SDK's
283
+ * `RouteMatchNode[]` chain.
284
+ *
285
+ * Route segments without a `routeConfig` (e.g. the root segment) are
286
+ * assigned routeId `"__root__"`. Segments with a config use the `path`
287
+ * as their routeId (or `"__index__"` for empty-path segments).
288
+ */
289
+ projectRouteMatch(leafRoute: ActivatedRouteSnapshot, options?: AuthRouteAdapterOptions): RouteMatchNode[];
290
+ /**
291
+ * Project an explicit array of `ActivatedRouteSnapshot` segments into the
292
+ * SDK's `RouteMatchNode[]` chain.
293
+ *
294
+ * Use when you already have a flattened chain (e.g. from
295
+ * `ActivatedRoute.pathFromRoot`).
296
+ */
297
+ projectRouteSegments(segments: readonly ActivatedRouteSnapshot[], options?: AuthRouteAdapterOptions): RouteMatchNode[];
298
+ /**
299
+ * Project the route snapshot and check whether all requirements are settled.
300
+ *
301
+ * @param leafRoute - The leaf `ActivatedRouteSnapshot` from the guard call.
302
+ * @param orchestrator - The SDK orchestrator managing the requirement plan.
303
+ * @param options - Optional projection options.
304
+ */
305
+ check(leafRoute: ActivatedRouteSnapshot, orchestrator: {
306
+ activateMatchedRoutes(chain: readonly RouteMatchNode[]): void;
307
+ snapshot(): {
308
+ settled: boolean;
309
+ pendingRequirement: AuthRequirement | null;
310
+ };
311
+ }, options?: AuthRouteAdapterOptions): RouteGuardResult;
312
+ private segmentToNode;
313
+ private resolveRouteId;
314
+ static ɵfac: i0.ɵɵFactoryDeclaration<AuthRouteAdapter, never>;
315
+ static ɵprov: i0.ɵɵInjectableDeclaration<AuthRouteAdapter>;
316
+ }
317
+
318
+ /**
319
+ * Bridge an SDK `ReadableSignalTrait` to an Angular writable signal.
320
+ *
321
+ * Immediately syncs the current value, then subscribes to future changes.
322
+ *
323
+ * @returns Cleanup function that unsubscribes from the SDK signal.
324
+ */
325
+ declare function bridgeToAngularSignal<T>(source: ReadableSignalTrait<T>, target: WritableSignal<T>): () => void;
326
+ /**
327
+ * Bridge an SDK `ReadableSignalTrait` to an RxJS Observable.
328
+ *
329
+ * The observable emits the current value immediately on subscribe, then
330
+ * emits whenever the SDK signal value changes. Completes on unsubscribe.
331
+ */
332
+ declare function signalToObservable<T>(source: ReadableSignalTrait<T>): Observable<T>;
333
+
334
+ export { AUTH_PLANNER_HOST, AUTH_REQUIREMENTS_CLIENT_SET, AuthRouteAdapter, DEFAULT_ROUTE_REQUIREMENTS_KEY, ROUTE_REQUIREMENTS_COMPOSITION_DATA_KEY, ROUTE_REQUIREMENTS_DATA_KEY, bridgeToAngularSignal, extractFullRouteRequirements, injectPlannerHost, provideAuthPlannerHost, provideRouteScopedRequirements, resolveEffectiveRequirements, signalToObservable, withRouteRequirements };
335
+ export type { AuthRouteAdapterOptions, ProvideAuthPlannerHostOptions, RouteGuardResult, RouteRequirementsDeclaration, WithRouteRequirementsOptions };
336
+ //# sourceMappingURL=securitydept-client-angular.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"securitydept-client-angular.d.ts","sources":["../../src/lib/planner-host.ts","../../src/lib/route-requirements.ts","../../src/lib/router.ts","../../src/lib/signal-bridge.ts"],"mappings":";;;;;;;;AAwCA;;;;;AAKG;AACH,cAAa,iBAAiB,EAAA,cAAA,CAAA,WAAA;AAI9B;;AAEG;UACc,6BAA6B;AAC7C;;;AAGG;sBACe,iBAAiB;AACnC;AAED;;;;;;;;;;;;;;;;;;;;;;AAsBG;AACH,iBAAgB,sBAAsB,WAC3B,6BAA6B,GACrC,oBAAoB;AAUvB;;;;;;;;;;;AAWG;AACH,iBAAgB,iBAAiB,IAAI,WAAW;AAgBhD;;;;;;;;;AASG;AACH,cAAa,4BAA4B,EAAA,cAAA,UAAA,qBAAA;AAIzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+DG;AACH,iBAAgB,8BAA8B,YAClC,2BAA2B,UAAU,2BAA2B,IACzE,oBAAoB;;ACpKvB;;;;;;;;;;;;;;;;AAgBG;AACH,cAAa,2BAA2B;AAExC;;;;;AAKG;AACH,cAAa,uCAAuC;AAGpD;;AAEG;UACc,4BAA4B;AAC5C,2BAAuB,eAAe;iBACzB,gCAAgC;AAC7C;AAED;;AAEG;UACc,4BAA4B;AAC5C;;;AAGG;kBACW,gCAAgC;AAE9C;;AAEG;YACK,MAAM;AACd;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BG;AACH,iBAAgB,qBAAqB,eACtB,eAAe,qBACZ,4BAA4B,GAAG,MAAM,gCACzC,MAAM,oBACjB,MAAM;AAgBT;;;;;;;;;;;;;;;;AAgBG;AACH,iBAAgB,4BAA4B,YAChC,sBAAsB,sDAG/B,eAAe;AAmBlB;;;;;;;;AAQG;AACH,iBAAgB,4BAA4B,8BACd,eAAe,WACrC,4BAA4B,GACjC,eAAe;;ACvKlB;UACiB,uBAAuB;AACvC;;;;AAIG;;AAEH;AAED;AACA,cAAa,8BAA8B;AAM3C;UACiB,gBAAgB;;;;AAIhC,wBAAoB,eAAe;AACnC;AAMD;;;;;;;;;;;;;;;;;AAiBG;AACH,cACa,gBAAgB;AAC5B;;;;;;;AAOG;iCAES,sBAAsB,YACvB,uBAAuB,GAC/B,cAAc;AAMjB;;;;;;AAMG;AACH,4CACoB,sBAAsB,cAC/B,uBAAuB,GAC/B,cAAc;AAKjB;;;;;;AAMG;AACH,qBACY,sBAAsB;8CAEM,cAAc;AACpD;;AAEC,gCAAoB,eAAe;;AAEpC,iBACS,uBAAuB,GAC/B,gBAAgB;AAUnB;AAaA;yCAzEY,gBAAgB;6CAAhB,gBAAgB;AA+E5B;;ACzID;;;;;;AAMG;AACH,iBAAgB,qBAAqB,YAC5B,mBAAmB,aACnB,cAAc;AAQvB;;;;;AAKG;AACH,iBAAgB,kBAAkB,YACzB,mBAAmB,MACzB,UAAU;;;;","names":[]}
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@securitydept/client-angular",
3
+ "version": "0.2.0-beta.1",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "exports": {
10
+ ".": {
11
+ "monorepo-tsc": "./src/public-api.ts",
12
+ "types": "./dist/types/securitydept-client-angular.d.ts",
13
+ "default": "./dist/fesm2022/securitydept-client-angular.mjs"
14
+ }
15
+ },
16
+ "scripts": {
17
+ "build": "ng-packagr -p ng-package.json -c tsconfig.lib.json"
18
+ },
19
+ "peerDependencies": {
20
+ "@securitydept/client": "workspace:*",
21
+ "@angular/core": ">=19.0.0",
22
+ "@angular/router": ">=19.0.0",
23
+ "rxjs": ">=7.0.0"
24
+ },
25
+ "devDependencies": {
26
+ "@securitydept/client": "workspace:*"
27
+ }
28
+ }