@securitydept/client-react 0.2.0-beta.3 → 0.2.0-beta.5
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.
|
@@ -59,6 +59,7 @@ interface TanStackRouteUnauthenticatedContext {
|
|
|
59
59
|
}
|
|
60
60
|
type TanStackRouteUnauthenticatedAction = boolean | string;
|
|
61
61
|
type TanStackRouteUnauthenticatedHandler = (requirement: AuthRequirement, context: TanStackRouteUnauthenticatedContext) => TanStackRouteUnauthenticatedAction | Promise<TanStackRouteUnauthenticatedAction>;
|
|
62
|
+
type TanStackRouteAuthenticatedCheck = (requirement: AuthRequirement) => boolean | Promise<boolean>;
|
|
62
63
|
/**
|
|
63
64
|
* Options for {@link withTanStackRouteRequirements}.
|
|
64
65
|
*/
|
|
@@ -289,6 +290,11 @@ declare function createTanStackRouteSecurityPolicy(options?: TanStackRouteSecuri
|
|
|
289
290
|
* @param checkAuthenticated - Returns true if a requirement is met.
|
|
290
291
|
*/
|
|
291
292
|
evaluate(matches: readonly TanStackRouteMatch[], checkAuthenticated: (requirement: AuthRequirement) => boolean, context?: TanStackRouteUnauthenticatedContext): TanStackRouteSecurityResult;
|
|
293
|
+
/**
|
|
294
|
+
* Async-capable evaluation for router guards that must wait for auth
|
|
295
|
+
* freshness checks before deciding whether navigation is allowed.
|
|
296
|
+
*/
|
|
297
|
+
evaluateAsync(matches: readonly TanStackRouteMatch[], checkAuthenticated: TanStackRouteAuthenticatedCheck, context?: TanStackRouteUnauthenticatedContext): Promise<TanStackRouteSecurityResult>;
|
|
292
298
|
};
|
|
293
299
|
/**
|
|
294
300
|
* Navigation-blocked error thrown by {@link createSecureBeforeLoad} when a
|
|
@@ -328,7 +334,7 @@ interface CreateSecureBeforeLoadOptions extends TanStackRouteSecurityPolicyOptio
|
|
|
328
334
|
* This is the auth-state bridge: the adopter supplies a function that
|
|
329
335
|
* checks their auth store / context.
|
|
330
336
|
*/
|
|
331
|
-
checkAuthenticated:
|
|
337
|
+
checkAuthenticated: TanStackRouteAuthenticatedCheck;
|
|
332
338
|
/**
|
|
333
339
|
* TanStack Router's `redirect()` function.
|
|
334
340
|
*
|
|
@@ -418,5 +424,5 @@ declare function createSecureBeforeLoad(options: CreateSecureBeforeLoadOptions):
|
|
|
418
424
|
*/
|
|
419
425
|
declare function createExternalRedirectBeforeLoadHandler(startRedirect: (requirement: AuthRequirement, context: TanStackRouteUnauthenticatedContext) => void | Promise<void>): TanStackRouteUnauthenticatedHandler;
|
|
420
426
|
//#endregion
|
|
421
|
-
export { type AnyRoute, type AuthRequirement, CreateSecureBeforeLoadOptions, DEFAULT_COMPOSITION_KEY, DEFAULT_REQUIREMENTS_KEY, type RegisteredRouter, RequirementsClientSetComposition, type RouteMatchNode, RouteRequirementsDeclaration, RouteSecurityBlockedError, SecureBeforeLoadContext, TanStackRouteLocation, TanStackRouteMatch, TanStackRouteSecurityPolicyOptions, TanStackRouteSecurityResult, TanStackRouteUnauthenticatedAction, TanStackRouteUnauthenticatedContext, TanStackRouteUnauthenticatedHandler, TanStackRouterAdapterOptions, WithTanStackRouteRequirementsOptions, createExternalRedirectBeforeLoadHandler, createSecureBeforeLoad, createTanStackRouteActivator, createTanStackRouteSecurityPolicy, extractTanStackRouteRequirements, projectTanStackRouteMatches, resolveEffectiveRequirements, withTanStackRouteRequirements };
|
|
427
|
+
export { type AnyRoute, type AuthRequirement, CreateSecureBeforeLoadOptions, DEFAULT_COMPOSITION_KEY, DEFAULT_REQUIREMENTS_KEY, type RegisteredRouter, RequirementsClientSetComposition, type RouteMatchNode, RouteRequirementsDeclaration, RouteSecurityBlockedError, SecureBeforeLoadContext, TanStackRouteAuthenticatedCheck, TanStackRouteLocation, TanStackRouteMatch, TanStackRouteSecurityPolicyOptions, TanStackRouteSecurityResult, TanStackRouteUnauthenticatedAction, TanStackRouteUnauthenticatedContext, TanStackRouteUnauthenticatedHandler, TanStackRouterAdapterOptions, WithTanStackRouteRequirementsOptions, createExternalRedirectBeforeLoadHandler, createSecureBeforeLoad, createTanStackRouteActivator, createTanStackRouteSecurityPolicy, extractTanStackRouteRequirements, projectTanStackRouteMatches, resolveEffectiveRequirements, withTanStackRouteRequirements };
|
|
422
428
|
//# sourceMappingURL=index.d.mts.map
|
|
@@ -197,25 +197,30 @@ function createTanStackRouteActivator(orchestrator, options) {
|
|
|
197
197
|
* ```
|
|
198
198
|
*/
|
|
199
199
|
function createTanStackRouteSecurityPolicy(options = {}) {
|
|
200
|
-
return {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const
|
|
200
|
+
return {
|
|
201
|
+
evaluate(matches, checkAuthenticated, context) {
|
|
202
|
+
const effectiveRequirements = extractTanStackRouteRequirements(matches, { requirementsKey: options.requirementsKey });
|
|
203
|
+
const handlerContext = context ?? createFallbackUnauthenticatedContext(matches);
|
|
204
|
+
for (const req of effectiveRequirements) if (!checkAuthenticated(req)) return createTanStackUnauthenticatedResult(req, effectiveRequirements, handlerContext, options);
|
|
205
|
+
return {
|
|
206
|
+
allMet: true,
|
|
207
|
+
pendingRequirement: void 0,
|
|
208
|
+
action: void 0,
|
|
209
|
+
effectiveRequirements
|
|
210
|
+
};
|
|
211
|
+
},
|
|
212
|
+
async evaluateAsync(matches, checkAuthenticated, context) {
|
|
213
|
+
const effectiveRequirements = extractTanStackRouteRequirements(matches, { requirementsKey: options.requirementsKey });
|
|
214
|
+
const handlerContext = context ?? createFallbackUnauthenticatedContext(matches);
|
|
215
|
+
for (const req of effectiveRequirements) if (!await checkAuthenticated(req)) return createTanStackUnauthenticatedResult(req, effectiveRequirements, handlerContext, options);
|
|
205
216
|
return {
|
|
206
|
-
allMet:
|
|
207
|
-
pendingRequirement:
|
|
208
|
-
action:
|
|
217
|
+
allMet: true,
|
|
218
|
+
pendingRequirement: void 0,
|
|
219
|
+
action: void 0,
|
|
209
220
|
effectiveRequirements
|
|
210
221
|
};
|
|
211
222
|
}
|
|
212
|
-
|
|
213
|
-
allMet: true,
|
|
214
|
-
pendingRequirement: void 0,
|
|
215
|
-
action: void 0,
|
|
216
|
-
effectiveRequirements
|
|
217
|
-
};
|
|
218
|
-
} };
|
|
223
|
+
};
|
|
219
224
|
}
|
|
220
225
|
/**
|
|
221
226
|
* Navigation-blocked error thrown by {@link createSecureBeforeLoad} when a
|
|
@@ -287,17 +292,20 @@ var RouteSecurityBlockedError = class extends Error {
|
|
|
287
292
|
* ```
|
|
288
293
|
*/
|
|
289
294
|
function createSecureBeforeLoad(options) {
|
|
290
|
-
const policy = createTanStackRouteSecurityPolicy(options);
|
|
291
295
|
return function secureBeforeLoad(ctx) {
|
|
292
296
|
const handlerContext = createUnauthenticatedContext(ctx);
|
|
293
|
-
const result =
|
|
294
|
-
if (result.
|
|
295
|
-
|
|
296
|
-
if (!pendingRequirement) return;
|
|
297
|
-
if (isPromiseLike(action)) return action.then((resolved) => handleSecureBeforeLoadAction(resolved, pendingRequirement, result, options));
|
|
298
|
-
return handleSecureBeforeLoadAction(action, pendingRequirement, result, options);
|
|
297
|
+
const result = evaluateRouteSecurityMaybeAsync(ctx.matches, options.checkAuthenticated, handlerContext, options);
|
|
298
|
+
if (isPromiseLike(result)) return result.then((resolved) => handleSecureBeforeLoadResult(resolved, options));
|
|
299
|
+
return handleSecureBeforeLoadResult(result, options);
|
|
299
300
|
};
|
|
300
301
|
}
|
|
302
|
+
function handleSecureBeforeLoadResult(result, options) {
|
|
303
|
+
if (result.allMet) return;
|
|
304
|
+
const { action, pendingRequirement } = result;
|
|
305
|
+
if (!pendingRequirement) return;
|
|
306
|
+
if (isPromiseLike(action)) return action.then((resolved) => handleSecureBeforeLoadAction(resolved, pendingRequirement, result, options));
|
|
307
|
+
return handleSecureBeforeLoadAction(action, pendingRequirement, result, options);
|
|
308
|
+
}
|
|
301
309
|
/**
|
|
302
310
|
* Build a route-security handler for full-page external redirects.
|
|
303
311
|
*
|
|
@@ -332,6 +340,46 @@ function createFallbackUnauthenticatedContext(matches) {
|
|
|
332
340
|
cause: void 0
|
|
333
341
|
};
|
|
334
342
|
}
|
|
343
|
+
function createTanStackUnauthenticatedResult(requirement, effectiveRequirements, context, options) {
|
|
344
|
+
const handler = options.requirementHandlers?.[requirement.kind] ?? options.defaultOnUnauthenticated;
|
|
345
|
+
return {
|
|
346
|
+
allMet: false,
|
|
347
|
+
pendingRequirement: requirement,
|
|
348
|
+
action: handler ? handler(requirement, context) : false,
|
|
349
|
+
effectiveRequirements
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function evaluateRouteSecurityMaybeAsync(matches, checkAuthenticated, context, options) {
|
|
353
|
+
const effectiveRequirements = extractTanStackRouteRequirements(matches, { requirementsKey: options.requirementsKey });
|
|
354
|
+
for (let index = 0; index < effectiveRequirements.length; index += 1) {
|
|
355
|
+
const requirement = effectiveRequirements[index];
|
|
356
|
+
if (!requirement) continue;
|
|
357
|
+
const authenticated = checkAuthenticated(requirement);
|
|
358
|
+
if (isPromiseLike(authenticated)) return evaluateRouteSecurityFromAsyncCheck(authenticated, index, effectiveRequirements, checkAuthenticated, context, options);
|
|
359
|
+
if (!authenticated) return createTanStackUnauthenticatedResult(requirement, effectiveRequirements, context, options);
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
allMet: true,
|
|
363
|
+
pendingRequirement: void 0,
|
|
364
|
+
action: void 0,
|
|
365
|
+
effectiveRequirements
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
async function evaluateRouteSecurityFromAsyncCheck(firstAuthenticated, firstIndex, effectiveRequirements, checkAuthenticated, context, options) {
|
|
369
|
+
const firstRequirement = effectiveRequirements[firstIndex];
|
|
370
|
+
if (firstRequirement && !await firstAuthenticated) return createTanStackUnauthenticatedResult(firstRequirement, effectiveRequirements, context, options);
|
|
371
|
+
for (let index = firstIndex + 1; index < effectiveRequirements.length; index += 1) {
|
|
372
|
+
const requirement = effectiveRequirements[index];
|
|
373
|
+
if (!requirement) continue;
|
|
374
|
+
if (!await checkAuthenticated(requirement)) return createTanStackUnauthenticatedResult(requirement, effectiveRequirements, context, options);
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
allMet: true,
|
|
378
|
+
pendingRequirement: void 0,
|
|
379
|
+
action: void 0,
|
|
380
|
+
effectiveRequirements
|
|
381
|
+
};
|
|
382
|
+
}
|
|
335
383
|
function handleSecureBeforeLoadAction(action, pendingRequirement, result, options) {
|
|
336
384
|
if (typeof action === "string") {
|
|
337
385
|
if (options.redirect) throw options.redirect({ to: action });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/tanstack-router/index.ts"],"sourcesContent":["// TanStack React Router route-security contract for auth-coordination\n//\n// Canonical import path:\n// import { ... } from \"@securitydept/client-react/tanstack-router\"\n//\n// Provides the complete route-security contract for TanStack React Router,\n// aligned with the Angular Router contract in @securitydept/client-angular:\n//\n// Adopter-facing canonical path (highest level):\n// - createSecureBeforeLoad() — root-level beforeLoad factory that wires\n// non-serializable runtime policy (handlers, redirect logic) into\n// TanStack Router's execution semantics. This is the TanStack equivalent\n// of Angular's secureRouteRoot().\n// - withTanStackRouteRequirements() — child-route serializable metadata\n// declaration. This is the TanStack equivalent of Angular's secureRoute().\n//\n// Lower-level building blocks:\n// - extractTanStackRouteRequirements() — full-route aggregation with\n// merge/replace/inherit composition\n// - createTanStackRouteSecurityPolicy() — headless evaluator (no router\n// execution glue; for custom integrations)\n// - projectTanStackRouteMatches() / createTanStackRouteActivator() —\n// RouteRequirementOrchestrator projection helpers\n//\n// Architecture boundary:\n// - Does NOT own the router, lifecycle, or render UI.\n// - Does NOT re-export core orchestration primitives.\n// - Does NOT carry token-set-specific mapping, policy, or registry logic.\n// - The adopter wires this into their router setup.\n//\n// Relationship to Angular sibling:\n// - createSecureBeforeLoad() ↔ secureRouteRoot()\n// - withTanStackRouteRequirements() ↔ secureRoute() / withRouteRequirements()\n// - extractTanStackRouteRequirements() ↔ extractFullRouteRequirements()\n// - createTanStackRouteSecurityPolicy() ↔ createTokenSetRouteAggregationGuard()\n// - Both share RequirementsClientSetComposition from @securitydept/client/auth-coordination\n//\n// Stability: provisional\n\nimport type {\n\tAuthRequirement,\n\tRouteMatchNode,\n} from \"@securitydept/client/auth-coordination\";\nimport { RequirementsClientSetComposition } from \"@securitydept/client/auth-coordination\";\nimport type { AnyRoute, RegisteredRouter } from \"@tanstack/react-router\";\n\nexport type { AuthRequirement, RouteMatchNode };\n\n// ---------------------------------------------------------------------------\n// Route match shape\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal shape of a TanStack Router route match entry.\n *\n * Structurally compatible with the matches returned by `useMatches()`.\n * The adapter reads `staticData` (set via `beforeLoad` or `staticData` option)\n * to find auth requirements.\n */\nexport interface TanStackRouteMatch {\n\t/** Route identifier (the route's `id` field). */\n\trouteId: string;\n\t/**\n\t * Static route context, typically set via `beforeLoad` or `staticData`.\n\t * Auth requirements should be declared here under a well-known key.\n\t *\n\t * Typed as `object` (not `Record<string, unknown>`) to remain structurally\n\t * compatible with TanStack Router's open `StaticDataRouteOption` interface,\n\t * which does not carry an index signature.\n\t */\n\tstaticData?: object;\n\t/** Route context merged from parent. */\n\tcontext?: object;\n\t/** Route loader data, if available. */\n\tloaderData?: object;\n}\n\n// ---------------------------------------------------------------------------\n// Route data keys\n// ---------------------------------------------------------------------------\n\n/** Default key for auth requirements in TanStack Router `staticData`. */\nexport const DEFAULT_REQUIREMENTS_KEY = \"authRequirements\";\n\n/** Default key for the composition strategy in TanStack Router `staticData`. */\nexport const DEFAULT_COMPOSITION_KEY = \"authRequirementsComposition\";\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\n/** Options for {@link projectTanStackRouteMatches} and {@link createTanStackRouteActivator}. */\nexport interface TanStackRouterAdapterOptions {\n\t/**\n\t * Key used to read auth requirements from `staticData`.\n\t *\n\t * @default \"authRequirements\"\n\t */\n\trequirementsKey?: string;\n}\n\nexport interface TanStackRouteLocation {\n\tpathname: string;\n\thref: string;\n}\n\n/**\n * Runtime context passed to route unauthenticated handlers.\n *\n * `attemptedUrl` is the TanStack Router target URL for the navigation being\n * guarded. Use it as `postAuthRedirectUri` when a handler starts a full-page\n * auth redirect; do not infer this from `window.location`, because the current\n * document URL can still point at the previously committed route.\n */\nexport interface TanStackRouteUnauthenticatedContext {\n\treadonly location: TanStackRouteLocation;\n\treadonly attemptedUrl: string;\n\treadonly matches: readonly TanStackRouteMatch[];\n\treadonly cause: string | undefined;\n}\n\nexport type TanStackRouteUnauthenticatedAction = boolean | string;\n\nexport type TanStackRouteUnauthenticatedHandler = (\n\trequirement: AuthRequirement,\n\tcontext: TanStackRouteUnauthenticatedContext,\n) =>\n\t| TanStackRouteUnauthenticatedAction\n\t| Promise<TanStackRouteUnauthenticatedAction>;\n\n// ---------------------------------------------------------------------------\n// Route metadata declaration helper\n// ---------------------------------------------------------------------------\n\n/**\n * Options for {@link withTanStackRouteRequirements}.\n */\nexport interface WithTanStackRouteRequirementsOptions {\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 staticData properties to merge into the returned object.\n\t */\n\textra?: Record<string, unknown>;\n}\n\n/**\n * Build a TanStack Router `staticData` object declaring auth requirements for\n * a single route segment.\n *\n * This is the TanStack Router equivalent of Angular's `withRouteRequirements()`.\n *\n * @example\n * ```ts\n * import { createRoute } from \"@tanstack/react-router\";\n * import { withTanStackRouteRequirements } from \"@securitydept/client-react/tanstack-router\";\n *\n * const dashboardRoute = createRoute({\n * getParentRoute: () => rootRoute,\n * path: \"dashboard\",\n * staticData: withTanStackRouteRequirements([\n * { id: \"session\", kind: \"session\" },\n * ]),\n * });\n *\n * // Child route replaces parent requirements:\n * const publicRoute = createRoute({\n * getParentRoute: () => dashboardRoute,\n * path: \"public\",\n * staticData: withTanStackRouteRequirements([], {\n * composition: RequirementsClientSetComposition.Replace,\n * }),\n * });\n * ```\n *\n * @param requirements - Auth requirements for this route segment.\n * @param options - Optional composition strategy and extra staticData.\n */\nexport function withTanStackRouteRequirements(\n\trequirements: AuthRequirement[],\n\toptions?: WithTanStackRouteRequirementsOptions,\n): Record<string, unknown> {\n\treturn {\n\t\t...(options?.extra ?? {}),\n\t\t[DEFAULT_REQUIREMENTS_KEY]: requirements,\n\t\t[DEFAULT_COMPOSITION_KEY]:\n\t\t\toptions?.composition ?? RequirementsClientSetComposition.Merge,\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Full-route aggregation with composition semantics\n// ---------------------------------------------------------------------------\n\n/**\n * Extract and compose auth requirements from a TanStack Router matched route\n * chain, respecting merge/replace/inherit composition at each segment.\n *\n * This is the TanStack Router equivalent of Angular's\n * `extractFullRouteRequirements()`. It walks the matched route chain from root\n * to leaf and applies the composition strategy declared at each segment:\n *\n * - `inherit` — keep parent requirements unchanged\n * - `merge` — append child requirements, replacing same-id parent entries\n * - `replace` — discard parent chain, use only child's requirements\n *\n * @param matches - Matched route chain from root to leaf (as from `useMatches()`).\n * @param options - Optional key override.\n */\nexport function extractTanStackRouteRequirements(\n\tmatches: readonly TanStackRouteMatch[],\n\toptions?: TanStackRouterAdapterOptions,\n): AuthRequirement[] {\n\tconst key = options?.requirementsKey ?? DEFAULT_REQUIREMENTS_KEY;\n\tlet result: AuthRequirement[] = [];\n\n\tfor (const match of matches) {\n\t\tconst raw = (match.staticData as Record<string, unknown>)?.[key];\n\t\tconst compositionRaw = (match.staticData as Record<string, unknown>)?.[\n\t\t\tDEFAULT_COMPOSITION_KEY\n\t\t];\n\t\tconst composition = isValidComposition(compositionRaw)\n\t\t\t? compositionRaw\n\t\t\t: RequirementsClientSetComposition.Merge;\n\t\tconst requirements: AuthRequirement[] = Array.isArray(raw) ? raw : [];\n\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// Composition resolution (shared logic with Angular sibling)\n// ---------------------------------------------------------------------------\n\n/**\n * Route-level auth requirement declaration for composition resolution.\n */\nexport interface RouteRequirementsDeclaration {\n\trequirements: readonly AuthRequirement[];\n\tcomposition: RequirementsClientSetComposition;\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 Angular's `resolveEffectiveRequirements()`:\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\n// ---------------------------------------------------------------------------\n// Projection (lower-level — preserved for backward compatibility)\n// ---------------------------------------------------------------------------\n\n/**\n * Project TanStack Router matched route entries into the SDK's\n * `RouteMatchNode[]` contract.\n *\n * Each match entry is mapped to a `RouteMatchNode`:\n * - `routeId` is taken from `match.routeId`\n * - `requirements` is read from `match.staticData[requirementsKey]`\n *\n * Entries with no requirements produce nodes with an empty requirements array,\n * preserving the full chain for shared-prefix diffing.\n *\n * @example\n * ```ts\n * import { useMatches } from \"@tanstack/react-router\";\n * import { projectTanStackRouteMatches } from \"@securitydept/client-react/tanstack-router\";\n *\n * const matches = useMatches();\n * const routeChain = projectTanStackRouteMatches(matches);\n * orchestrator.activateMatchedRoutes(routeChain);\n * ```\n */\nexport function projectTanStackRouteMatches(\n\tmatches: readonly TanStackRouteMatch[],\n\toptions?: TanStackRouterAdapterOptions,\n): RouteMatchNode[] {\n\tconst key = options?.requirementsKey ?? DEFAULT_REQUIREMENTS_KEY;\n\treturn matches.map((match) => {\n\t\tconst rawRequirements = (match.staticData as Record<string, unknown>)?.[\n\t\t\tkey\n\t\t];\n\t\tconst requirements: AuthRequirement[] = Array.isArray(rawRequirements)\n\t\t\t? rawRequirements\n\t\t\t: [];\n\t\treturn {\n\t\t\trouteId: match.routeId,\n\t\t\trequirements,\n\t\t};\n\t});\n}\n\n// ---------------------------------------------------------------------------\n// Activator factory (lower-level — preserved for backward compatibility)\n// ---------------------------------------------------------------------------\n\n/**\n * Create a route-change activator that projects TanStack Router matches and\n * activates them on the orchestrator.\n *\n * This is framework-agnostic (not a React hook) and can be called from a\n * `useEffect`, route subscriber, or any mechanism that notifies match changes.\n *\n * @example\n * ```ts\n * import { useMatches } from \"@@tanstack/react-router\";\n * import { createTanStackRouteActivator } from \"@securitydept/client-react/tanstack-router\";\n *\n * const activator = createTanStackRouteActivator(orchestrator);\n *\n * function RouteOrchestrationBridge() {\n * const matches = useMatches();\n * useEffect(() => activator.activate(matches), [matches]);\n * return null;\n * }\n * ```\n */\nexport function createTanStackRouteActivator(\n\torchestrator: {\n\t\tactivateMatchedRoutes(chain: readonly RouteMatchNode[]): void;\n\t\tdeactivateRoute(): void;\n\t},\n\toptions?: TanStackRouterAdapterOptions,\n) {\n\treturn {\n\t\t/** Project matches and activate the route chain on the orchestrator. */\n\t\tactivate(matches: readonly TanStackRouteMatch[]): void {\n\t\t\tconst chain = projectTanStackRouteMatches(matches, options);\n\t\t\torchestrator.activateMatchedRoutes(chain);\n\t\t},\n\t\t/** Deactivate the current route (e.g. on unmount or navigation away). */\n\t\tdeactivate(): void {\n\t\t\torchestrator.deactivateRoute();\n\t\t},\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Root-level route security runtime policy\n// ---------------------------------------------------------------------------\n\n/**\n * Root-level runtime policy for a secured TanStack Router route tree.\n *\n * This is the TanStack Router equivalent of Angular's\n * `SecureRouteRootSecurityOptions`. Non-serializable runtime callbacks\n * (planner host, onUnauthenticated handlers) live here, NOT in individual\n * route `staticData`.\n *\n * @example\n * ```ts\n * const routeSecurityPolicy = createTanStackRouteSecurityPolicy({\n * requirementHandlers: {\n * frontend_oidc: (failingRequirement) => {\n * redirectToLogin(failingRequirement);\n * return false;\n * },\n * },\n * });\n *\n * // Use in beforeLoad:\n * const protectedRoute = createRoute({\n * beforeLoad: ({ matches }) => {\n * routeSecurityPolicy.enforce(matches);\n * },\n * });\n * ```\n */\nexport interface TanStackRouteSecurityPolicyOptions {\n\t/**\n\t * Per-requirement-kind handlers.\n\t * Keys are `AuthRequirement.kind` strings.\n\t */\n\trequirementHandlers?: Record<string, TanStackRouteUnauthenticatedHandler>;\n\n\t/**\n\t * Fallback handler when no kind-specific handler matches.\n\t */\n\tdefaultOnUnauthenticated?: TanStackRouteUnauthenticatedHandler;\n\n\t/**\n\t * Custom key for reading requirements from staticData.\n\t * @default \"authRequirements\"\n\t */\n\trequirementsKey?: string;\n}\n\n/**\n * Route security evaluation result from\n * {@link createTanStackRouteSecurityPolicy}.\n */\nexport interface TanStackRouteSecurityResult {\n\t/** All requirements are met — allow navigation. */\n\treadonly allMet: boolean;\n\t/** The first unmet requirement, if any. */\n\treadonly pendingRequirement: AuthRequirement | undefined;\n\t/** Handler action for the pending requirement (from policy). */\n\treadonly action:\n\t\t| TanStackRouteUnauthenticatedAction\n\t\t| Promise<TanStackRouteUnauthenticatedAction>\n\t\t| undefined;\n\t/** The full effective requirements after composition. */\n\treadonly effectiveRequirements: readonly AuthRequirement[];\n}\n\n/**\n * Create a root-level route security policy that evaluates a TanStack Router\n * matched route chain and returns the enforcement decision.\n *\n * This is the TanStack Router equivalent of Angular's `secureRouteRoot()`.\n * It centralizes non-serializable runtime policy (handlers, redirects) at\n * the root level, while individual routes only declare serializable metadata\n * via `withTanStackRouteRequirements()`.\n *\n * The returned `evaluate()` function:\n * 1. Extracts requirements from the matched route chain (with composition)\n * 2. Finds the first unmet requirement using the provided `checkAuthenticated`\n * 3. Resolves the handler action (kind handler → default handler → block)\n *\n * @example\n * ```ts\n * import { createTanStackRouteSecurityPolicy } from \"@securitydept/client-react/tanstack-router\";\n *\n * const policy = createTanStackRouteSecurityPolicy({\n * requirementHandlers: {\n * frontend_oidc: (req) => {\n * redirectToOidcLogin(req);\n * return false;\n * },\n * },\n * defaultOnUnauthenticated: () => \"/login\",\n * });\n *\n * // In a route's beforeLoad or a React effect:\n * const result = policy.evaluate(matches, (req) => isAuthenticated(req.kind));\n * if (!result.allMet && typeof result.action === \"string\") {\n * navigate({ to: result.action });\n * }\n * ```\n */\nexport function createTanStackRouteSecurityPolicy(\n\toptions: TanStackRouteSecurityPolicyOptions = {},\n) {\n\treturn {\n\t\t/**\n\t\t * Evaluate the route security policy for a matched route chain.\n\t\t *\n\t\t * @param matches - The matched route chain (root → leaf).\n\t\t * @param checkAuthenticated - Returns true if a requirement is met.\n\t\t */\n\t\tevaluate(\n\t\t\tmatches: readonly TanStackRouteMatch[],\n\t\t\tcheckAuthenticated: (requirement: AuthRequirement) => boolean,\n\t\t\tcontext?: TanStackRouteUnauthenticatedContext,\n\t\t): TanStackRouteSecurityResult {\n\t\t\tconst effectiveRequirements = extractTanStackRouteRequirements(matches, {\n\t\t\t\trequirementsKey: options.requirementsKey,\n\t\t\t});\n\t\t\tconst handlerContext =\n\t\t\t\tcontext ?? createFallbackUnauthenticatedContext(matches);\n\n\t\t\tfor (const req of effectiveRequirements) {\n\t\t\t\tif (!checkAuthenticated(req)) {\n\t\t\t\t\tconst handler =\n\t\t\t\t\t\toptions.requirementHandlers?.[req.kind] ??\n\t\t\t\t\t\toptions.defaultOnUnauthenticated;\n\n\t\t\t\t\treturn {\n\t\t\t\t\t\tallMet: false,\n\t\t\t\t\t\tpendingRequirement: req,\n\t\t\t\t\t\taction: handler ? handler(req, handlerContext) : false,\n\t\t\t\t\t\teffectiveRequirements,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tallMet: true,\n\t\t\t\tpendingRequirement: undefined,\n\t\t\t\taction: undefined,\n\t\t\t\teffectiveRequirements,\n\t\t\t};\n\t\t},\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Route-security beforeLoad factory — canonical adopter-facing entry\n// ---------------------------------------------------------------------------\n\n/**\n * Navigation-blocked error thrown by {@link createSecureBeforeLoad} when a\n * requirement is unmet and the handler returns `false`.\n */\nexport class RouteSecurityBlockedError extends Error {\n\tconstructor(\n\t\tpublic readonly requirement: AuthRequirement,\n\t\tpublic readonly result: TanStackRouteSecurityResult,\n\t) {\n\t\tsuper(\n\t\t\t`[secureBeforeLoad] Navigation blocked: requirement \"${requirement.id}\" (kind: ${requirement.kind}) is not authenticated.`,\n\t\t);\n\t\tthis.name = \"RouteSecurityBlockedError\";\n\t}\n}\n\n/**\n * Minimal shape of TanStack Router's `beforeLoad` context.\n *\n * Only the fields actually consumed by {@link createSecureBeforeLoad} are\n * required. The full TanStack Router context has many more fields; this\n * interface lets the helper stay structurally compatible without importing\n * the full framework type.\n */\nexport interface SecureBeforeLoadContext {\n\t/** URL location of the target route. */\n\tlocation: TanStackRouteLocation;\n\t/**\n\t * Matched route entries for the current navigation.\n\t * Shape matches {@link TanStackRouteMatch}.\n\t */\n\tmatches: readonly TanStackRouteMatch[];\n\t/** Navigation cause (\"enter\", \"stay\", \"push\", \"replace\", etc.). */\n\tcause: string;\n}\n\n/**\n * Options for {@link createSecureBeforeLoad}.\n */\nexport interface CreateSecureBeforeLoadOptions\n\textends TanStackRouteSecurityPolicyOptions {\n\t/**\n\t * Returns true if the given auth requirement is currently satisfied.\n\t *\n\t * This is the auth-state bridge: the adopter supplies a function that\n\t * checks their auth store / context.\n\t */\n\tcheckAuthenticated: (requirement: AuthRequirement) => boolean;\n\n\t/**\n\t * TanStack Router's `redirect()` function.\n\t *\n\t * When a handler returns a string (redirect path), `createSecureBeforeLoad`\n\t * throws `redirect({ to: path })` to trigger TanStack Router's redirect\n\t * mechanism.\n\t *\n\t * Pass `redirect` from `@tanstack/react-router` here:\n\t * ```ts\n\t * import { redirect } from \"@tanstack/react-router\";\n\t * createSecureBeforeLoad({ redirect, ... });\n\t * ```\n\t *\n\t * If omitted, string actions cause a {@link RouteSecurityBlockedError}\n\t * with the redirect path in the error message.\n\t */\n\tredirect?: (opts: { to: string }) => never;\n}\n\n/**\n * Create a `beforeLoad`-compatible function that enforces route-security\n * requirements inside TanStack Router's execution semantics.\n *\n * **This is the canonical TanStack Router adopter-facing entry** — the\n * equivalent of Angular's `secureRouteRoot()`.\n *\n * How it works:\n * 1. Receives TanStack Router's `beforeLoad` context (including `matches`)\n * 2. Extracts the full requirement chain with composition semantics\n * 3. Finds the first unmet requirement\n * 4. Calls the handler (kind handler → default → block)\n * 5. If the handler returns a string path, throws `redirect({ to: path })`\n * 6. If the handler returns `false`, throws {@link RouteSecurityBlockedError}\n * 7. If all requirements are met, returns normally (navigation proceeds)\n *\n * @example\n * ```ts\n * import { redirect, createRoute } from \"@tanstack/react-router\";\n * import {\n * createSecureBeforeLoad,\n * withTanStackRouteRequirements,\n * } from \"@securitydept/client-react/tanstack-router\";\n *\n * // Root-level: non-serializable runtime policy\n * const securedBeforeLoad = createSecureBeforeLoad({\n * redirect,\n * checkAuthenticated: (req) => authStore.isAuthenticated(req.kind),\n * requirementHandlers: {\n * frontend_oidc: (req) => \"/login/oidc\",\n * session: () => \"/login\",\n * },\n * });\n *\n * // Root route — attach the beforeLoad\n * const rootRoute = createRootRoute({\n * beforeLoad: securedBeforeLoad,\n * });\n *\n * // Child routes — serializable declaration only\n * const dashboardRoute = createRoute({\n * getParentRoute: () => rootRoute,\n * path: \"dashboard\",\n * staticData: withTanStackRouteRequirements([\n * { id: \"session\", kind: \"session\" },\n * ]),\n * });\n *\n * const adminRoute = createRoute({\n * getParentRoute: () => dashboardRoute,\n * path: \"admin\",\n * staticData: withTanStackRouteRequirements([\n * { id: \"admin-oidc\", kind: \"frontend_oidc\" },\n * ]),\n * });\n * ```\n */\nexport function createSecureBeforeLoad(\n\toptions: CreateSecureBeforeLoadOptions,\n): (ctx: SecureBeforeLoadContext) => void | Promise<void> {\n\tconst policy = createTanStackRouteSecurityPolicy(options);\n\n\treturn function secureBeforeLoad(\n\t\tctx: SecureBeforeLoadContext,\n\t): void | Promise<void> {\n\t\tconst handlerContext = createUnauthenticatedContext(ctx);\n\t\tconst result = policy.evaluate(\n\t\t\tctx.matches,\n\t\t\toptions.checkAuthenticated,\n\t\t\thandlerContext,\n\t\t);\n\n\t\tif (result.allMet) return;\n\n\t\tconst { action, pendingRequirement } = result;\n\n\t\tif (!pendingRequirement) return;\n\n\t\tif (isPromiseLike(action)) {\n\t\t\treturn action.then((resolved) =>\n\t\t\t\thandleSecureBeforeLoadAction(\n\t\t\t\t\tresolved,\n\t\t\t\t\tpendingRequirement,\n\t\t\t\t\tresult,\n\t\t\t\t\toptions,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\treturn handleSecureBeforeLoadAction(\n\t\t\taction,\n\t\t\tpendingRequirement,\n\t\t\tresult,\n\t\t\toptions,\n\t\t);\n\t};\n}\n\n/**\n * Build a route-security handler for full-page external redirects.\n *\n * The callback should start the browser navigation, typically by calling an\n * auth client's `loginWithRedirect({ postAuthRedirectUri: context.attemptedUrl })`.\n * After that navigation has been started, the returned guard result never\n * settles so TanStack Router does not finalize an in-app blocked navigation\n * while the page is leaving for an external IdP.\n */\nexport function createExternalRedirectBeforeLoadHandler(\n\tstartRedirect: (\n\t\trequirement: AuthRequirement,\n\t\tcontext: TanStackRouteUnauthenticatedContext,\n\t) => void | Promise<void>,\n): TanStackRouteUnauthenticatedHandler {\n\treturn async (requirement, context) => {\n\t\tawait startRedirect(requirement, context);\n\t\treturn await neverSettlingBeforeLoadResult();\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nfunction createUnauthenticatedContext(\n\tctx: SecureBeforeLoadContext,\n): TanStackRouteUnauthenticatedContext {\n\treturn {\n\t\tlocation: ctx.location,\n\t\tattemptedUrl: ctx.location.href || ctx.location.pathname,\n\t\tmatches: ctx.matches,\n\t\tcause: ctx.cause,\n\t};\n}\n\nfunction createFallbackUnauthenticatedContext(\n\tmatches: readonly TanStackRouteMatch[],\n): TanStackRouteUnauthenticatedContext {\n\treturn {\n\t\tlocation: { pathname: \"\", href: \"\" },\n\t\tattemptedUrl: \"\",\n\t\tmatches,\n\t\tcause: undefined,\n\t};\n}\n\nfunction handleSecureBeforeLoadAction(\n\taction: TanStackRouteUnauthenticatedAction | undefined,\n\tpendingRequirement: AuthRequirement,\n\tresult: TanStackRouteSecurityResult,\n\toptions: CreateSecureBeforeLoadOptions,\n): void {\n\tif (typeof action === \"string\") {\n\t\tif (options.redirect) {\n\t\t\tthrow options.redirect({ to: action });\n\t\t}\n\t\t// No redirect function provided — throw a descriptive error.\n\t\tthrow new RouteSecurityBlockedError(pendingRequirement, result);\n\t}\n\n\t// action is false or undefined — block navigation.\n\tthrow new RouteSecurityBlockedError(pendingRequirement, result);\n}\n\nfunction isPromiseLike<T>(value: unknown): value is Promise<T> {\n\treturn (\n\t\ttypeof value === \"object\" &&\n\t\tvalue !== null &&\n\t\t\"then\" in value &&\n\t\ttypeof value.then === \"function\"\n\t);\n}\n\nfunction neverSettlingBeforeLoadResult(): Promise<never> {\n\treturn new Promise<never>(() => {\n\t\t// Intentionally never resolves: a full-page browser redirect is in progress.\n\t});\n}\n\nfunction isValidComposition(\n\traw: unknown,\n): raw is RequirementsClientSetComposition {\n\treturn (\n\t\traw === RequirementsClientSetComposition.Inherit ||\n\t\traw === RequirementsClientSetComposition.Merge ||\n\t\traw === RequirementsClientSetComposition.Replace\n\t);\n}\n\n// Unused import prevents TS from complaining if @tanstack/react-router types\n// are not available at build time (peerDep is optional for pure orchestration).\nexport type { AnyRoute, RegisteredRouter };\n// Re-export composition enum for adopter convenience\nexport { RequirementsClientSetComposition };\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/tanstack-router/index.ts"],"sourcesContent":["// TanStack React Router route-security contract for auth-coordination\n//\n// Canonical import path:\n// import { ... } from \"@securitydept/client-react/tanstack-router\"\n//\n// Provides the complete route-security contract for TanStack React Router,\n// aligned with the Angular Router contract in @securitydept/client-angular:\n//\n// Adopter-facing canonical path (highest level):\n// - createSecureBeforeLoad() — root-level beforeLoad factory that wires\n// non-serializable runtime policy (handlers, redirect logic) into\n// TanStack Router's execution semantics. This is the TanStack equivalent\n// of Angular's secureRouteRoot().\n// - withTanStackRouteRequirements() — child-route serializable metadata\n// declaration. This is the TanStack equivalent of Angular's secureRoute().\n//\n// Lower-level building blocks:\n// - extractTanStackRouteRequirements() — full-route aggregation with\n// merge/replace/inherit composition\n// - createTanStackRouteSecurityPolicy() — headless evaluator (no router\n// execution glue; for custom integrations)\n// - projectTanStackRouteMatches() / createTanStackRouteActivator() —\n// RouteRequirementOrchestrator projection helpers\n//\n// Architecture boundary:\n// - Does NOT own the router, lifecycle, or render UI.\n// - Does NOT re-export core orchestration primitives.\n// - Does NOT carry token-set-specific mapping, policy, or registry logic.\n// - The adopter wires this into their router setup.\n//\n// Relationship to Angular sibling:\n// - createSecureBeforeLoad() ↔ secureRouteRoot()\n// - withTanStackRouteRequirements() ↔ secureRoute() / withRouteRequirements()\n// - extractTanStackRouteRequirements() ↔ extractFullRouteRequirements()\n// - createTanStackRouteSecurityPolicy() ↔ createTokenSetRouteAggregationGuard()\n// - Both share RequirementsClientSetComposition from @securitydept/client/auth-coordination\n//\n// Stability: provisional\n\nimport type {\n\tAuthRequirement,\n\tRouteMatchNode,\n} from \"@securitydept/client/auth-coordination\";\nimport { RequirementsClientSetComposition } from \"@securitydept/client/auth-coordination\";\nimport type { AnyRoute, RegisteredRouter } from \"@tanstack/react-router\";\n\nexport type { AuthRequirement, RouteMatchNode };\n\n// ---------------------------------------------------------------------------\n// Route match shape\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal shape of a TanStack Router route match entry.\n *\n * Structurally compatible with the matches returned by `useMatches()`.\n * The adapter reads `staticData` (set via `beforeLoad` or `staticData` option)\n * to find auth requirements.\n */\nexport interface TanStackRouteMatch {\n\t/** Route identifier (the route's `id` field). */\n\trouteId: string;\n\t/**\n\t * Static route context, typically set via `beforeLoad` or `staticData`.\n\t * Auth requirements should be declared here under a well-known key.\n\t *\n\t * Typed as `object` (not `Record<string, unknown>`) to remain structurally\n\t * compatible with TanStack Router's open `StaticDataRouteOption` interface,\n\t * which does not carry an index signature.\n\t */\n\tstaticData?: object;\n\t/** Route context merged from parent. */\n\tcontext?: object;\n\t/** Route loader data, if available. */\n\tloaderData?: object;\n}\n\n// ---------------------------------------------------------------------------\n// Route data keys\n// ---------------------------------------------------------------------------\n\n/** Default key for auth requirements in TanStack Router `staticData`. */\nexport const DEFAULT_REQUIREMENTS_KEY = \"authRequirements\";\n\n/** Default key for the composition strategy in TanStack Router `staticData`. */\nexport const DEFAULT_COMPOSITION_KEY = \"authRequirementsComposition\";\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\n/** Options for {@link projectTanStackRouteMatches} and {@link createTanStackRouteActivator}. */\nexport interface TanStackRouterAdapterOptions {\n\t/**\n\t * Key used to read auth requirements from `staticData`.\n\t *\n\t * @default \"authRequirements\"\n\t */\n\trequirementsKey?: string;\n}\n\nexport interface TanStackRouteLocation {\n\tpathname: string;\n\thref: string;\n}\n\n/**\n * Runtime context passed to route unauthenticated handlers.\n *\n * `attemptedUrl` is the TanStack Router target URL for the navigation being\n * guarded. Use it as `postAuthRedirectUri` when a handler starts a full-page\n * auth redirect; do not infer this from `window.location`, because the current\n * document URL can still point at the previously committed route.\n */\nexport interface TanStackRouteUnauthenticatedContext {\n\treadonly location: TanStackRouteLocation;\n\treadonly attemptedUrl: string;\n\treadonly matches: readonly TanStackRouteMatch[];\n\treadonly cause: string | undefined;\n}\n\nexport type TanStackRouteUnauthenticatedAction = boolean | string;\n\nexport type TanStackRouteUnauthenticatedHandler = (\n\trequirement: AuthRequirement,\n\tcontext: TanStackRouteUnauthenticatedContext,\n) =>\n\t| TanStackRouteUnauthenticatedAction\n\t| Promise<TanStackRouteUnauthenticatedAction>;\n\nexport type TanStackRouteAuthenticatedCheck = (\n\trequirement: AuthRequirement,\n) => boolean | Promise<boolean>;\n\n// ---------------------------------------------------------------------------\n// Route metadata declaration helper\n// ---------------------------------------------------------------------------\n\n/**\n * Options for {@link withTanStackRouteRequirements}.\n */\nexport interface WithTanStackRouteRequirementsOptions {\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 staticData properties to merge into the returned object.\n\t */\n\textra?: Record<string, unknown>;\n}\n\n/**\n * Build a TanStack Router `staticData` object declaring auth requirements for\n * a single route segment.\n *\n * This is the TanStack Router equivalent of Angular's `withRouteRequirements()`.\n *\n * @example\n * ```ts\n * import { createRoute } from \"@tanstack/react-router\";\n * import { withTanStackRouteRequirements } from \"@securitydept/client-react/tanstack-router\";\n *\n * const dashboardRoute = createRoute({\n * getParentRoute: () => rootRoute,\n * path: \"dashboard\",\n * staticData: withTanStackRouteRequirements([\n * { id: \"session\", kind: \"session\" },\n * ]),\n * });\n *\n * // Child route replaces parent requirements:\n * const publicRoute = createRoute({\n * getParentRoute: () => dashboardRoute,\n * path: \"public\",\n * staticData: withTanStackRouteRequirements([], {\n * composition: RequirementsClientSetComposition.Replace,\n * }),\n * });\n * ```\n *\n * @param requirements - Auth requirements for this route segment.\n * @param options - Optional composition strategy and extra staticData.\n */\nexport function withTanStackRouteRequirements(\n\trequirements: AuthRequirement[],\n\toptions?: WithTanStackRouteRequirementsOptions,\n): Record<string, unknown> {\n\treturn {\n\t\t...(options?.extra ?? {}),\n\t\t[DEFAULT_REQUIREMENTS_KEY]: requirements,\n\t\t[DEFAULT_COMPOSITION_KEY]:\n\t\t\toptions?.composition ?? RequirementsClientSetComposition.Merge,\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Full-route aggregation with composition semantics\n// ---------------------------------------------------------------------------\n\n/**\n * Extract and compose auth requirements from a TanStack Router matched route\n * chain, respecting merge/replace/inherit composition at each segment.\n *\n * This is the TanStack Router equivalent of Angular's\n * `extractFullRouteRequirements()`. It walks the matched route chain from root\n * to leaf and applies the composition strategy declared at each segment:\n *\n * - `inherit` — keep parent requirements unchanged\n * - `merge` — append child requirements, replacing same-id parent entries\n * - `replace` — discard parent chain, use only child's requirements\n *\n * @param matches - Matched route chain from root to leaf (as from `useMatches()`).\n * @param options - Optional key override.\n */\nexport function extractTanStackRouteRequirements(\n\tmatches: readonly TanStackRouteMatch[],\n\toptions?: TanStackRouterAdapterOptions,\n): AuthRequirement[] {\n\tconst key = options?.requirementsKey ?? DEFAULT_REQUIREMENTS_KEY;\n\tlet result: AuthRequirement[] = [];\n\n\tfor (const match of matches) {\n\t\tconst raw = (match.staticData as Record<string, unknown>)?.[key];\n\t\tconst compositionRaw = (match.staticData as Record<string, unknown>)?.[\n\t\t\tDEFAULT_COMPOSITION_KEY\n\t\t];\n\t\tconst composition = isValidComposition(compositionRaw)\n\t\t\t? compositionRaw\n\t\t\t: RequirementsClientSetComposition.Merge;\n\t\tconst requirements: AuthRequirement[] = Array.isArray(raw) ? raw : [];\n\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// Composition resolution (shared logic with Angular sibling)\n// ---------------------------------------------------------------------------\n\n/**\n * Route-level auth requirement declaration for composition resolution.\n */\nexport interface RouteRequirementsDeclaration {\n\trequirements: readonly AuthRequirement[];\n\tcomposition: RequirementsClientSetComposition;\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 Angular's `resolveEffectiveRequirements()`:\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\n// ---------------------------------------------------------------------------\n// Projection (lower-level — preserved for backward compatibility)\n// ---------------------------------------------------------------------------\n\n/**\n * Project TanStack Router matched route entries into the SDK's\n * `RouteMatchNode[]` contract.\n *\n * Each match entry is mapped to a `RouteMatchNode`:\n * - `routeId` is taken from `match.routeId`\n * - `requirements` is read from `match.staticData[requirementsKey]`\n *\n * Entries with no requirements produce nodes with an empty requirements array,\n * preserving the full chain for shared-prefix diffing.\n *\n * @example\n * ```ts\n * import { useMatches } from \"@tanstack/react-router\";\n * import { projectTanStackRouteMatches } from \"@securitydept/client-react/tanstack-router\";\n *\n * const matches = useMatches();\n * const routeChain = projectTanStackRouteMatches(matches);\n * orchestrator.activateMatchedRoutes(routeChain);\n * ```\n */\nexport function projectTanStackRouteMatches(\n\tmatches: readonly TanStackRouteMatch[],\n\toptions?: TanStackRouterAdapterOptions,\n): RouteMatchNode[] {\n\tconst key = options?.requirementsKey ?? DEFAULT_REQUIREMENTS_KEY;\n\treturn matches.map((match) => {\n\t\tconst rawRequirements = (match.staticData as Record<string, unknown>)?.[\n\t\t\tkey\n\t\t];\n\t\tconst requirements: AuthRequirement[] = Array.isArray(rawRequirements)\n\t\t\t? rawRequirements\n\t\t\t: [];\n\t\treturn {\n\t\t\trouteId: match.routeId,\n\t\t\trequirements,\n\t\t};\n\t});\n}\n\n// ---------------------------------------------------------------------------\n// Activator factory (lower-level — preserved for backward compatibility)\n// ---------------------------------------------------------------------------\n\n/**\n * Create a route-change activator that projects TanStack Router matches and\n * activates them on the orchestrator.\n *\n * This is framework-agnostic (not a React hook) and can be called from a\n * `useEffect`, route subscriber, or any mechanism that notifies match changes.\n *\n * @example\n * ```ts\n * import { useMatches } from \"@@tanstack/react-router\";\n * import { createTanStackRouteActivator } from \"@securitydept/client-react/tanstack-router\";\n *\n * const activator = createTanStackRouteActivator(orchestrator);\n *\n * function RouteOrchestrationBridge() {\n * const matches = useMatches();\n * useEffect(() => activator.activate(matches), [matches]);\n * return null;\n * }\n * ```\n */\nexport function createTanStackRouteActivator(\n\torchestrator: {\n\t\tactivateMatchedRoutes(chain: readonly RouteMatchNode[]): void;\n\t\tdeactivateRoute(): void;\n\t},\n\toptions?: TanStackRouterAdapterOptions,\n) {\n\treturn {\n\t\t/** Project matches and activate the route chain on the orchestrator. */\n\t\tactivate(matches: readonly TanStackRouteMatch[]): void {\n\t\t\tconst chain = projectTanStackRouteMatches(matches, options);\n\t\t\torchestrator.activateMatchedRoutes(chain);\n\t\t},\n\t\t/** Deactivate the current route (e.g. on unmount or navigation away). */\n\t\tdeactivate(): void {\n\t\t\torchestrator.deactivateRoute();\n\t\t},\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Root-level route security runtime policy\n// ---------------------------------------------------------------------------\n\n/**\n * Root-level runtime policy for a secured TanStack Router route tree.\n *\n * This is the TanStack Router equivalent of Angular's\n * `SecureRouteRootSecurityOptions`. Non-serializable runtime callbacks\n * (planner host, onUnauthenticated handlers) live here, NOT in individual\n * route `staticData`.\n *\n * @example\n * ```ts\n * const routeSecurityPolicy = createTanStackRouteSecurityPolicy({\n * requirementHandlers: {\n * frontend_oidc: (failingRequirement) => {\n * redirectToLogin(failingRequirement);\n * return false;\n * },\n * },\n * });\n *\n * // Use in beforeLoad:\n * const protectedRoute = createRoute({\n * beforeLoad: ({ matches }) => {\n * routeSecurityPolicy.enforce(matches);\n * },\n * });\n * ```\n */\nexport interface TanStackRouteSecurityPolicyOptions {\n\t/**\n\t * Per-requirement-kind handlers.\n\t * Keys are `AuthRequirement.kind` strings.\n\t */\n\trequirementHandlers?: Record<string, TanStackRouteUnauthenticatedHandler>;\n\n\t/**\n\t * Fallback handler when no kind-specific handler matches.\n\t */\n\tdefaultOnUnauthenticated?: TanStackRouteUnauthenticatedHandler;\n\n\t/**\n\t * Custom key for reading requirements from staticData.\n\t * @default \"authRequirements\"\n\t */\n\trequirementsKey?: string;\n}\n\n/**\n * Route security evaluation result from\n * {@link createTanStackRouteSecurityPolicy}.\n */\nexport interface TanStackRouteSecurityResult {\n\t/** All requirements are met — allow navigation. */\n\treadonly allMet: boolean;\n\t/** The first unmet requirement, if any. */\n\treadonly pendingRequirement: AuthRequirement | undefined;\n\t/** Handler action for the pending requirement (from policy). */\n\treadonly action:\n\t\t| TanStackRouteUnauthenticatedAction\n\t\t| Promise<TanStackRouteUnauthenticatedAction>\n\t\t| undefined;\n\t/** The full effective requirements after composition. */\n\treadonly effectiveRequirements: readonly AuthRequirement[];\n}\n\n/**\n * Create a root-level route security policy that evaluates a TanStack Router\n * matched route chain and returns the enforcement decision.\n *\n * This is the TanStack Router equivalent of Angular's `secureRouteRoot()`.\n * It centralizes non-serializable runtime policy (handlers, redirects) at\n * the root level, while individual routes only declare serializable metadata\n * via `withTanStackRouteRequirements()`.\n *\n * The returned `evaluate()` function:\n * 1. Extracts requirements from the matched route chain (with composition)\n * 2. Finds the first unmet requirement using the provided `checkAuthenticated`\n * 3. Resolves the handler action (kind handler → default handler → block)\n *\n * @example\n * ```ts\n * import { createTanStackRouteSecurityPolicy } from \"@securitydept/client-react/tanstack-router\";\n *\n * const policy = createTanStackRouteSecurityPolicy({\n * requirementHandlers: {\n * frontend_oidc: (req) => {\n * redirectToOidcLogin(req);\n * return false;\n * },\n * },\n * defaultOnUnauthenticated: () => \"/login\",\n * });\n *\n * // In a route's beforeLoad or a React effect:\n * const result = policy.evaluate(matches, (req) => isAuthenticated(req.kind));\n * if (!result.allMet && typeof result.action === \"string\") {\n * navigate({ to: result.action });\n * }\n * ```\n */\nexport function createTanStackRouteSecurityPolicy(\n\toptions: TanStackRouteSecurityPolicyOptions = {},\n) {\n\treturn {\n\t\t/**\n\t\t * Evaluate the route security policy for a matched route chain.\n\t\t *\n\t\t * @param matches - The matched route chain (root → leaf).\n\t\t * @param checkAuthenticated - Returns true if a requirement is met.\n\t\t */\n\t\tevaluate(\n\t\t\tmatches: readonly TanStackRouteMatch[],\n\t\t\tcheckAuthenticated: (requirement: AuthRequirement) => boolean,\n\t\t\tcontext?: TanStackRouteUnauthenticatedContext,\n\t\t): TanStackRouteSecurityResult {\n\t\t\tconst effectiveRequirements = extractTanStackRouteRequirements(matches, {\n\t\t\t\trequirementsKey: options.requirementsKey,\n\t\t\t});\n\t\t\tconst handlerContext =\n\t\t\t\tcontext ?? createFallbackUnauthenticatedContext(matches);\n\n\t\t\tfor (const req of effectiveRequirements) {\n\t\t\t\tif (!checkAuthenticated(req)) {\n\t\t\t\t\treturn createTanStackUnauthenticatedResult(\n\t\t\t\t\t\treq,\n\t\t\t\t\t\teffectiveRequirements,\n\t\t\t\t\t\thandlerContext,\n\t\t\t\t\t\toptions,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tallMet: true,\n\t\t\t\tpendingRequirement: undefined,\n\t\t\t\taction: undefined,\n\t\t\t\teffectiveRequirements,\n\t\t\t};\n\t\t},\n\n\t\t/**\n\t\t * Async-capable evaluation for router guards that must wait for auth\n\t\t * freshness checks before deciding whether navigation is allowed.\n\t\t */\n\t\tasync evaluateAsync(\n\t\t\tmatches: readonly TanStackRouteMatch[],\n\t\t\tcheckAuthenticated: TanStackRouteAuthenticatedCheck,\n\t\t\tcontext?: TanStackRouteUnauthenticatedContext,\n\t\t): Promise<TanStackRouteSecurityResult> {\n\t\t\tconst effectiveRequirements = extractTanStackRouteRequirements(matches, {\n\t\t\t\trequirementsKey: options.requirementsKey,\n\t\t\t});\n\t\t\tconst handlerContext =\n\t\t\t\tcontext ?? createFallbackUnauthenticatedContext(matches);\n\n\t\t\tfor (const req of effectiveRequirements) {\n\t\t\t\tif (!(await checkAuthenticated(req))) {\n\t\t\t\t\treturn createTanStackUnauthenticatedResult(\n\t\t\t\t\t\treq,\n\t\t\t\t\t\teffectiveRequirements,\n\t\t\t\t\t\thandlerContext,\n\t\t\t\t\t\toptions,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tallMet: true,\n\t\t\t\tpendingRequirement: undefined,\n\t\t\t\taction: undefined,\n\t\t\t\teffectiveRequirements,\n\t\t\t};\n\t\t},\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Route-security beforeLoad factory — canonical adopter-facing entry\n// ---------------------------------------------------------------------------\n\n/**\n * Navigation-blocked error thrown by {@link createSecureBeforeLoad} when a\n * requirement is unmet and the handler returns `false`.\n */\nexport class RouteSecurityBlockedError extends Error {\n\tconstructor(\n\t\tpublic readonly requirement: AuthRequirement,\n\t\tpublic readonly result: TanStackRouteSecurityResult,\n\t) {\n\t\tsuper(\n\t\t\t`[secureBeforeLoad] Navigation blocked: requirement \"${requirement.id}\" (kind: ${requirement.kind}) is not authenticated.`,\n\t\t);\n\t\tthis.name = \"RouteSecurityBlockedError\";\n\t}\n}\n\n/**\n * Minimal shape of TanStack Router's `beforeLoad` context.\n *\n * Only the fields actually consumed by {@link createSecureBeforeLoad} are\n * required. The full TanStack Router context has many more fields; this\n * interface lets the helper stay structurally compatible without importing\n * the full framework type.\n */\nexport interface SecureBeforeLoadContext {\n\t/** URL location of the target route. */\n\tlocation: TanStackRouteLocation;\n\t/**\n\t * Matched route entries for the current navigation.\n\t * Shape matches {@link TanStackRouteMatch}.\n\t */\n\tmatches: readonly TanStackRouteMatch[];\n\t/** Navigation cause (\"enter\", \"stay\", \"push\", \"replace\", etc.). */\n\tcause: string;\n}\n\n/**\n * Options for {@link createSecureBeforeLoad}.\n */\nexport interface CreateSecureBeforeLoadOptions\n\textends TanStackRouteSecurityPolicyOptions {\n\t/**\n\t * Returns true if the given auth requirement is currently satisfied.\n\t *\n\t * This is the auth-state bridge: the adopter supplies a function that\n\t * checks their auth store / context.\n\t */\n\tcheckAuthenticated: TanStackRouteAuthenticatedCheck;\n\n\t/**\n\t * TanStack Router's `redirect()` function.\n\t *\n\t * When a handler returns a string (redirect path), `createSecureBeforeLoad`\n\t * throws `redirect({ to: path })` to trigger TanStack Router's redirect\n\t * mechanism.\n\t *\n\t * Pass `redirect` from `@tanstack/react-router` here:\n\t * ```ts\n\t * import { redirect } from \"@tanstack/react-router\";\n\t * createSecureBeforeLoad({ redirect, ... });\n\t * ```\n\t *\n\t * If omitted, string actions cause a {@link RouteSecurityBlockedError}\n\t * with the redirect path in the error message.\n\t */\n\tredirect?: (opts: { to: string }) => never;\n}\n\n/**\n * Create a `beforeLoad`-compatible function that enforces route-security\n * requirements inside TanStack Router's execution semantics.\n *\n * **This is the canonical TanStack Router adopter-facing entry** — the\n * equivalent of Angular's `secureRouteRoot()`.\n *\n * How it works:\n * 1. Receives TanStack Router's `beforeLoad` context (including `matches`)\n * 2. Extracts the full requirement chain with composition semantics\n * 3. Finds the first unmet requirement\n * 4. Calls the handler (kind handler → default → block)\n * 5. If the handler returns a string path, throws `redirect({ to: path })`\n * 6. If the handler returns `false`, throws {@link RouteSecurityBlockedError}\n * 7. If all requirements are met, returns normally (navigation proceeds)\n *\n * @example\n * ```ts\n * import { redirect, createRoute } from \"@tanstack/react-router\";\n * import {\n * createSecureBeforeLoad,\n * withTanStackRouteRequirements,\n * } from \"@securitydept/client-react/tanstack-router\";\n *\n * // Root-level: non-serializable runtime policy\n * const securedBeforeLoad = createSecureBeforeLoad({\n * redirect,\n * checkAuthenticated: (req) => authStore.isAuthenticated(req.kind),\n * requirementHandlers: {\n * frontend_oidc: (req) => \"/login/oidc\",\n * session: () => \"/login\",\n * },\n * });\n *\n * // Root route — attach the beforeLoad\n * const rootRoute = createRootRoute({\n * beforeLoad: securedBeforeLoad,\n * });\n *\n * // Child routes — serializable declaration only\n * const dashboardRoute = createRoute({\n * getParentRoute: () => rootRoute,\n * path: \"dashboard\",\n * staticData: withTanStackRouteRequirements([\n * { id: \"session\", kind: \"session\" },\n * ]),\n * });\n *\n * const adminRoute = createRoute({\n * getParentRoute: () => dashboardRoute,\n * path: \"admin\",\n * staticData: withTanStackRouteRequirements([\n * { id: \"admin-oidc\", kind: \"frontend_oidc\" },\n * ]),\n * });\n * ```\n */\nexport function createSecureBeforeLoad(\n\toptions: CreateSecureBeforeLoadOptions,\n): (ctx: SecureBeforeLoadContext) => void | Promise<void> {\n\treturn function secureBeforeLoad(\n\t\tctx: SecureBeforeLoadContext,\n\t): void | Promise<void> {\n\t\tconst handlerContext = createUnauthenticatedContext(ctx);\n\t\tconst result = evaluateRouteSecurityMaybeAsync(\n\t\t\tctx.matches,\n\t\t\toptions.checkAuthenticated,\n\t\t\thandlerContext,\n\t\t\toptions,\n\t\t);\n\n\t\tif (isPromiseLike(result)) {\n\t\t\treturn result.then((resolved) =>\n\t\t\t\thandleSecureBeforeLoadResult(resolved, options),\n\t\t\t);\n\t\t}\n\n\t\treturn handleSecureBeforeLoadResult(result, options);\n\t};\n}\n\nfunction handleSecureBeforeLoadResult(\n\tresult: TanStackRouteSecurityResult,\n\toptions: CreateSecureBeforeLoadOptions,\n): void | Promise<void> {\n\tif (result.allMet) return;\n\n\tconst { action, pendingRequirement } = result;\n\n\tif (!pendingRequirement) return;\n\n\tif (isPromiseLike(action)) {\n\t\treturn action.then((resolved) =>\n\t\t\thandleSecureBeforeLoadAction(\n\t\t\t\tresolved,\n\t\t\t\tpendingRequirement,\n\t\t\t\tresult,\n\t\t\t\toptions,\n\t\t\t),\n\t\t);\n\t}\n\n\treturn handleSecureBeforeLoadAction(\n\t\taction,\n\t\tpendingRequirement,\n\t\tresult,\n\t\toptions,\n\t);\n}\n\n/**\n * Build a route-security handler for full-page external redirects.\n *\n * The callback should start the browser navigation, typically by calling an\n * auth client's `loginWithRedirect({ postAuthRedirectUri: context.attemptedUrl })`.\n * After that navigation has been started, the returned guard result never\n * settles so TanStack Router does not finalize an in-app blocked navigation\n * while the page is leaving for an external IdP.\n */\nexport function createExternalRedirectBeforeLoadHandler(\n\tstartRedirect: (\n\t\trequirement: AuthRequirement,\n\t\tcontext: TanStackRouteUnauthenticatedContext,\n\t) => void | Promise<void>,\n): TanStackRouteUnauthenticatedHandler {\n\treturn async (requirement, context) => {\n\t\tawait startRedirect(requirement, context);\n\t\treturn await neverSettlingBeforeLoadResult();\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nfunction createUnauthenticatedContext(\n\tctx: SecureBeforeLoadContext,\n): TanStackRouteUnauthenticatedContext {\n\treturn {\n\t\tlocation: ctx.location,\n\t\tattemptedUrl: ctx.location.href || ctx.location.pathname,\n\t\tmatches: ctx.matches,\n\t\tcause: ctx.cause,\n\t};\n}\n\nfunction createFallbackUnauthenticatedContext(\n\tmatches: readonly TanStackRouteMatch[],\n): TanStackRouteUnauthenticatedContext {\n\treturn {\n\t\tlocation: { pathname: \"\", href: \"\" },\n\t\tattemptedUrl: \"\",\n\t\tmatches,\n\t\tcause: undefined,\n\t};\n}\n\nfunction createTanStackUnauthenticatedResult(\n\trequirement: AuthRequirement,\n\teffectiveRequirements: readonly AuthRequirement[],\n\tcontext: TanStackRouteUnauthenticatedContext,\n\toptions: TanStackRouteSecurityPolicyOptions,\n): TanStackRouteSecurityResult {\n\tconst handler =\n\t\toptions.requirementHandlers?.[requirement.kind] ??\n\t\toptions.defaultOnUnauthenticated;\n\treturn {\n\t\tallMet: false,\n\t\tpendingRequirement: requirement,\n\t\taction: handler ? handler(requirement, context) : false,\n\t\teffectiveRequirements,\n\t};\n}\n\nfunction evaluateRouteSecurityMaybeAsync(\n\tmatches: readonly TanStackRouteMatch[],\n\tcheckAuthenticated: TanStackRouteAuthenticatedCheck,\n\tcontext: TanStackRouteUnauthenticatedContext,\n\toptions: TanStackRouteSecurityPolicyOptions,\n): TanStackRouteSecurityResult | Promise<TanStackRouteSecurityResult> {\n\tconst effectiveRequirements = extractTanStackRouteRequirements(matches, {\n\t\trequirementsKey: options.requirementsKey,\n\t});\n\n\tfor (let index = 0; index < effectiveRequirements.length; index += 1) {\n\t\tconst requirement = effectiveRequirements[index];\n\t\tif (!requirement) continue;\n\t\tconst authenticated = checkAuthenticated(requirement);\n\t\tif (isPromiseLike<boolean>(authenticated)) {\n\t\t\treturn evaluateRouteSecurityFromAsyncCheck(\n\t\t\t\tauthenticated,\n\t\t\t\tindex,\n\t\t\t\teffectiveRequirements,\n\t\t\t\tcheckAuthenticated,\n\t\t\t\tcontext,\n\t\t\t\toptions,\n\t\t\t);\n\t\t}\n\t\tif (!authenticated) {\n\t\t\treturn createTanStackUnauthenticatedResult(\n\t\t\t\trequirement,\n\t\t\t\teffectiveRequirements,\n\t\t\t\tcontext,\n\t\t\t\toptions,\n\t\t\t);\n\t\t}\n\t}\n\n\treturn {\n\t\tallMet: true,\n\t\tpendingRequirement: undefined,\n\t\taction: undefined,\n\t\teffectiveRequirements,\n\t};\n}\n\nasync function evaluateRouteSecurityFromAsyncCheck(\n\tfirstAuthenticated: Promise<boolean>,\n\tfirstIndex: number,\n\teffectiveRequirements: readonly AuthRequirement[],\n\tcheckAuthenticated: TanStackRouteAuthenticatedCheck,\n\tcontext: TanStackRouteUnauthenticatedContext,\n\toptions: TanStackRouteSecurityPolicyOptions,\n): Promise<TanStackRouteSecurityResult> {\n\tconst firstRequirement = effectiveRequirements[firstIndex];\n\tif (firstRequirement && !(await firstAuthenticated)) {\n\t\treturn createTanStackUnauthenticatedResult(\n\t\t\tfirstRequirement,\n\t\t\teffectiveRequirements,\n\t\t\tcontext,\n\t\t\toptions,\n\t\t);\n\t}\n\n\tfor (\n\t\tlet index = firstIndex + 1;\n\t\tindex < effectiveRequirements.length;\n\t\tindex += 1\n\t) {\n\t\tconst requirement = effectiveRequirements[index];\n\t\tif (!requirement) continue;\n\t\tif (!(await checkAuthenticated(requirement))) {\n\t\t\treturn createTanStackUnauthenticatedResult(\n\t\t\t\trequirement,\n\t\t\t\teffectiveRequirements,\n\t\t\t\tcontext,\n\t\t\t\toptions,\n\t\t\t);\n\t\t}\n\t}\n\n\treturn {\n\t\tallMet: true,\n\t\tpendingRequirement: undefined,\n\t\taction: undefined,\n\t\teffectiveRequirements,\n\t};\n}\n\nfunction handleSecureBeforeLoadAction(\n\taction: TanStackRouteUnauthenticatedAction | undefined,\n\tpendingRequirement: AuthRequirement,\n\tresult: TanStackRouteSecurityResult,\n\toptions: CreateSecureBeforeLoadOptions,\n): void {\n\tif (typeof action === \"string\") {\n\t\tif (options.redirect) {\n\t\t\tthrow options.redirect({ to: action });\n\t\t}\n\t\t// No redirect function provided — throw a descriptive error.\n\t\tthrow new RouteSecurityBlockedError(pendingRequirement, result);\n\t}\n\n\t// action is false or undefined — block navigation.\n\tthrow new RouteSecurityBlockedError(pendingRequirement, result);\n}\n\nfunction isPromiseLike<T>(value: unknown): value is Promise<T> {\n\treturn (\n\t\ttypeof value === \"object\" &&\n\t\tvalue !== null &&\n\t\t\"then\" in value &&\n\t\ttypeof value.then === \"function\"\n\t);\n}\n\nfunction neverSettlingBeforeLoadResult(): Promise<never> {\n\treturn new Promise<never>(() => {\n\t\t// Intentionally never resolves: a full-page browser redirect is in progress.\n\t});\n}\n\nfunction isValidComposition(\n\traw: unknown,\n): raw is RequirementsClientSetComposition {\n\treturn (\n\t\traw === RequirementsClientSetComposition.Inherit ||\n\t\traw === RequirementsClientSetComposition.Merge ||\n\t\traw === RequirementsClientSetComposition.Replace\n\t);\n}\n\n// Unused import prevents TS from complaining if @tanstack/react-router types\n// are not available at build time (peerDep is optional for pure orchestration).\nexport type { AnyRoute, RegisteredRouter };\n// Re-export composition enum for adopter convenience\nexport { RequirementsClientSetComposition };\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@securitydept/client-react",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.5",
|
|
4
4
|
"description": "Client React of the SecurityDept TypeScript SDK, a layered authentication and authorization toolkit built as reusable Rust crates, a TypeScript client SDK workspace, and reference server/web apps.",
|
|
5
5
|
"author": "Zhou Yeheng <root@aitiotekt.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"dist"
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@securitydept/client": "0.2.0-beta.
|
|
44
|
+
"@securitydept/client": "0.2.0-beta.5"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"react": ">=19.0.0",
|