@securitydept/client-react 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.
- package/dist/index.d.mts +88 -0
- package/dist/index.mjs +85 -0
- package/dist/index.mjs.map +1 -0
- package/dist/tanstack-router/index.d.mts +395 -0
- package/dist/tanstack-router/index.mjs +308 -0
- package/dist/tanstack-router/index.mjs.map +1 -0
- package/package.json +37 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { AuthGuardClientOption, CandidateSelector, PlannerHost, ScopedRequirementsClientSet, resolveEffectiveClientSet } from "@securitydept/client/auth-coordination";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
import * as _$react_jsx_runtime0 from "react/jsx-runtime";
|
|
4
|
+
|
|
5
|
+
//#region src/planner-host/index.d.ts
|
|
6
|
+
/** Props for {@link AuthPlannerHostProvider}. */
|
|
7
|
+
interface AuthPlannerHostProviderProps {
|
|
8
|
+
/**
|
|
9
|
+
* Custom candidate selection strategy.
|
|
10
|
+
* @see {@link CandidateSelector}
|
|
11
|
+
*/
|
|
12
|
+
selectCandidate?: CandidateSelector;
|
|
13
|
+
/**
|
|
14
|
+
* Pre-constructed PlannerHost instance to use.
|
|
15
|
+
* If provided, `selectCandidate` is ignored.
|
|
16
|
+
*/
|
|
17
|
+
plannerHost?: PlannerHost;
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Provide a {@link PlannerHost} at the current React tree scope.
|
|
22
|
+
*
|
|
23
|
+
* Use at the app level or at feature-route boundaries to establish
|
|
24
|
+
* planner-host scopes. Child components inherit the nearest provider.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* // App-level (default sequential strategy)
|
|
29
|
+
* <AuthPlannerHostProvider>
|
|
30
|
+
* <App />
|
|
31
|
+
* </AuthPlannerHostProvider>
|
|
32
|
+
*
|
|
33
|
+
* // Feature-route with custom chooser
|
|
34
|
+
* <AuthPlannerHostProvider selectCandidate={showChooser}>
|
|
35
|
+
* <FeatureRoutes />
|
|
36
|
+
* </AuthPlannerHostProvider>
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
declare function AuthPlannerHostProvider({
|
|
40
|
+
selectCandidate,
|
|
41
|
+
plannerHost,
|
|
42
|
+
children
|
|
43
|
+
}: AuthPlannerHostProviderProps): _$react_jsx_runtime0.JSX.Element;
|
|
44
|
+
/**
|
|
45
|
+
* Look up the nearest {@link PlannerHost} from the React Context tree.
|
|
46
|
+
*
|
|
47
|
+
* Throws an explicit error if no provider is found, preventing
|
|
48
|
+
* silent fallback behavior.
|
|
49
|
+
*/
|
|
50
|
+
declare function useAuthPlannerHost(): PlannerHost;
|
|
51
|
+
/** Props for {@link AuthRequirementsClientSetProvider}. */
|
|
52
|
+
interface AuthRequirementsClientSetProviderProps {
|
|
53
|
+
/** The scoped set to provide at this level. */
|
|
54
|
+
scopedSet: ScopedRequirementsClientSet;
|
|
55
|
+
children: ReactNode;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Provide a {@link ScopedRequirementsClientSet} at the current React tree scope.
|
|
59
|
+
*
|
|
60
|
+
* Automatically resolves the effective client set by composing with the
|
|
61
|
+
* parent scope's options according to the declared composition strategy.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```tsx
|
|
65
|
+
* // Feature route: merge OIDC requirement with parent's session requirement
|
|
66
|
+
* <AuthRequirementsClientSetProvider
|
|
67
|
+
* scopedSet={{
|
|
68
|
+
* composition: RequirementsClientSetComposition.Merge,
|
|
69
|
+
* options: [oidcClientOption],
|
|
70
|
+
* }}
|
|
71
|
+
* >
|
|
72
|
+
* <ProtectedFeature />
|
|
73
|
+
* </AuthRequirementsClientSetProvider>
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
declare function AuthRequirementsClientSetProvider({
|
|
77
|
+
scopedSet,
|
|
78
|
+
children
|
|
79
|
+
}: AuthRequirementsClientSetProviderProps): _$react_jsx_runtime0.JSX.Element;
|
|
80
|
+
/**
|
|
81
|
+
* Get the effective requirements client set for the current React tree scope.
|
|
82
|
+
*
|
|
83
|
+
* Returns the resolved options after applying all parent/child composition.
|
|
84
|
+
*/
|
|
85
|
+
declare function useEffectiveClientSet(): readonly AuthGuardClientOption[];
|
|
86
|
+
//#endregion
|
|
87
|
+
export { AuthPlannerHostProvider, AuthPlannerHostProviderProps, AuthRequirementsClientSetProvider, AuthRequirementsClientSetProviderProps, resolveEffectiveClientSet, useAuthPlannerHost, useEffectiveClientSet };
|
|
88
|
+
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createPlannerHost, resolveEffectiveClientSet } from "@securitydept/client/auth-coordination";
|
|
2
|
+
import { createContext, useContext, useMemo } from "react";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
//#region src/planner-host/index.tsx
|
|
5
|
+
const PlannerHostContext = createContext(null);
|
|
6
|
+
/**
|
|
7
|
+
* Provide a {@link PlannerHost} at the current React tree scope.
|
|
8
|
+
*
|
|
9
|
+
* Use at the app level or at feature-route boundaries to establish
|
|
10
|
+
* planner-host scopes. Child components inherit the nearest provider.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* // App-level (default sequential strategy)
|
|
15
|
+
* <AuthPlannerHostProvider>
|
|
16
|
+
* <App />
|
|
17
|
+
* </AuthPlannerHostProvider>
|
|
18
|
+
*
|
|
19
|
+
* // Feature-route with custom chooser
|
|
20
|
+
* <AuthPlannerHostProvider selectCandidate={showChooser}>
|
|
21
|
+
* <FeatureRoutes />
|
|
22
|
+
* </AuthPlannerHostProvider>
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
function AuthPlannerHostProvider({ selectCandidate, plannerHost, children }) {
|
|
26
|
+
const host = useMemo(() => {
|
|
27
|
+
if (plannerHost) return plannerHost;
|
|
28
|
+
return createPlannerHost(selectCandidate ? { selectCandidate } : void 0);
|
|
29
|
+
}, [plannerHost, selectCandidate]);
|
|
30
|
+
return /* @__PURE__ */ jsx(PlannerHostContext.Provider, {
|
|
31
|
+
value: host,
|
|
32
|
+
children
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Look up the nearest {@link PlannerHost} from the React Context tree.
|
|
37
|
+
*
|
|
38
|
+
* Throws an explicit error if no provider is found, preventing
|
|
39
|
+
* silent fallback behavior.
|
|
40
|
+
*/
|
|
41
|
+
function useAuthPlannerHost() {
|
|
42
|
+
const host = useContext(PlannerHostContext);
|
|
43
|
+
if (!host) throw new Error("[useAuthPlannerHost] No AuthPlannerHostProvider found in the component tree. Wrap your app or route with <AuthPlannerHostProvider>.");
|
|
44
|
+
return host;
|
|
45
|
+
}
|
|
46
|
+
const RequirementsClientSetContext = createContext([]);
|
|
47
|
+
/**
|
|
48
|
+
* Provide a {@link ScopedRequirementsClientSet} at the current React tree scope.
|
|
49
|
+
*
|
|
50
|
+
* Automatically resolves the effective client set by composing with the
|
|
51
|
+
* parent scope's options according to the declared composition strategy.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```tsx
|
|
55
|
+
* // Feature route: merge OIDC requirement with parent's session requirement
|
|
56
|
+
* <AuthRequirementsClientSetProvider
|
|
57
|
+
* scopedSet={{
|
|
58
|
+
* composition: RequirementsClientSetComposition.Merge,
|
|
59
|
+
* options: [oidcClientOption],
|
|
60
|
+
* }}
|
|
61
|
+
* >
|
|
62
|
+
* <ProtectedFeature />
|
|
63
|
+
* </AuthRequirementsClientSetProvider>
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
function AuthRequirementsClientSetProvider({ scopedSet, children }) {
|
|
67
|
+
const parentOptions = useContext(RequirementsClientSetContext);
|
|
68
|
+
const effective = useMemo(() => resolveEffectiveClientSet(parentOptions, scopedSet), [parentOptions, scopedSet]);
|
|
69
|
+
return /* @__PURE__ */ jsx(RequirementsClientSetContext.Provider, {
|
|
70
|
+
value: effective,
|
|
71
|
+
children
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get the effective requirements client set for the current React tree scope.
|
|
76
|
+
*
|
|
77
|
+
* Returns the resolved options after applying all parent/child composition.
|
|
78
|
+
*/
|
|
79
|
+
function useEffectiveClientSet() {
|
|
80
|
+
return useContext(RequirementsClientSetContext);
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
export { AuthPlannerHostProvider, AuthRequirementsClientSetProvider, resolveEffectiveClientSet, useAuthPlannerHost, useEffectiveClientSet };
|
|
84
|
+
|
|
85
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/planner-host/index.tsx"],"sourcesContent":["// React planner-host integration — Context-based planner provider / lookup\n//\n// Canonical import path:\n// import { AuthPlannerHostProvider, useAuthPlannerHost, ... } from \"@securitydept/client-react\"\n//\n// Provides React Context glue for the shared planner-host contract:\n// - AuthPlannerHostProvider: provides a PlannerHost at a React tree scope\n// - useAuthPlannerHost(): hook to look up the nearest PlannerHost (fail-fast)\n// - AuthRequirementsClientSetProvider: provides scoped requirement client sets\n// - useEffectiveClientSet(): resolves the current scope's effective client set\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 React Context wiring for the shared contract\n//\n// Stability: provisional\n\nimport type {\n\tAuthGuardClientOption,\n\tCandidateSelector,\n\tPlannerHost,\n\tScopedRequirementsClientSet,\n} from \"@securitydept/client/auth-coordination\";\nimport {\n\tcreatePlannerHost,\n\tresolveEffectiveClientSet,\n} from \"@securitydept/client/auth-coordination\";\nimport { createContext, type ReactNode, useContext, useMemo } from \"react\";\n\n// ---------------------------------------------------------------------------\n// PlannerHost context\n// ---------------------------------------------------------------------------\n\nconst PlannerHostContext = createContext<PlannerHost | null>(null);\n\n/** Props for {@link AuthPlannerHostProvider}. */\nexport interface AuthPlannerHostProviderProps {\n\t/**\n\t * Custom candidate selection strategy.\n\t * @see {@link CandidateSelector}\n\t */\n\tselectCandidate?: CandidateSelector;\n\n\t/**\n\t * Pre-constructed PlannerHost instance to use.\n\t * If provided, `selectCandidate` is ignored.\n\t */\n\tplannerHost?: PlannerHost;\n\n\tchildren: ReactNode;\n}\n\n/**\n * Provide a {@link PlannerHost} at the current React tree scope.\n *\n * Use at the app level or at feature-route boundaries to establish\n * planner-host scopes. Child components inherit the nearest provider.\n *\n * @example\n * ```tsx\n * // App-level (default sequential strategy)\n * <AuthPlannerHostProvider>\n * <App />\n * </AuthPlannerHostProvider>\n *\n * // Feature-route with custom chooser\n * <AuthPlannerHostProvider selectCandidate={showChooser}>\n * <FeatureRoutes />\n * </AuthPlannerHostProvider>\n * ```\n */\nexport function AuthPlannerHostProvider({\n\tselectCandidate,\n\tplannerHost,\n\tchildren,\n}: AuthPlannerHostProviderProps) {\n\tconst host = useMemo(() => {\n\t\tif (plannerHost) return plannerHost;\n\t\treturn createPlannerHost(selectCandidate ? { selectCandidate } : undefined);\n\t}, [plannerHost, selectCandidate]);\n\n\treturn (\n\t\t<PlannerHostContext.Provider value={host}>\n\t\t\t{children}\n\t\t</PlannerHostContext.Provider>\n\t);\n}\n\n/**\n * Look up the nearest {@link PlannerHost} from the React Context tree.\n *\n * Throws an explicit error if no provider is found, preventing\n * silent fallback behavior.\n */\nexport function useAuthPlannerHost(): PlannerHost {\n\tconst host = useContext(PlannerHostContext);\n\tif (!host) {\n\t\tthrow new Error(\n\t\t\t\"[useAuthPlannerHost] No AuthPlannerHostProvider found in the component tree. \" +\n\t\t\t\t\"Wrap your app or route with <AuthPlannerHostProvider>.\",\n\t\t);\n\t}\n\treturn host;\n}\n\n// ---------------------------------------------------------------------------\n// Requirements client set context\n// ---------------------------------------------------------------------------\n\nconst RequirementsClientSetContext = createContext<\n\treadonly AuthGuardClientOption[]\n>([]);\n\n/** Props for {@link AuthRequirementsClientSetProvider}. */\nexport interface AuthRequirementsClientSetProviderProps {\n\t/** The scoped set to provide at this level. */\n\tscopedSet: ScopedRequirementsClientSet;\n\tchildren: ReactNode;\n}\n\n/**\n * Provide a {@link ScopedRequirementsClientSet} at the current React tree scope.\n *\n * Automatically resolves the effective client set by composing with the\n * parent scope's options according to the declared composition strategy.\n *\n * @example\n * ```tsx\n * // Feature route: merge OIDC requirement with parent's session requirement\n * <AuthRequirementsClientSetProvider\n * scopedSet={{\n * composition: RequirementsClientSetComposition.Merge,\n * options: [oidcClientOption],\n * }}\n * >\n * <ProtectedFeature />\n * </AuthRequirementsClientSetProvider>\n * ```\n */\nexport function AuthRequirementsClientSetProvider({\n\tscopedSet,\n\tchildren,\n}: AuthRequirementsClientSetProviderProps) {\n\tconst parentOptions = useContext(RequirementsClientSetContext);\n\tconst effective = useMemo(\n\t\t() => resolveEffectiveClientSet(parentOptions, scopedSet),\n\t\t[parentOptions, scopedSet],\n\t);\n\n\treturn (\n\t\t<RequirementsClientSetContext.Provider value={effective}>\n\t\t\t{children}\n\t\t</RequirementsClientSetContext.Provider>\n\t);\n}\n\n/**\n * Get the effective requirements client set for the current React tree scope.\n *\n * Returns the resolved options after applying all parent/child composition.\n */\nexport function useEffectiveClientSet(): readonly AuthGuardClientOption[] {\n\treturn useContext(RequirementsClientSetContext);\n}\n\n// Re-export composition constant for consumer convenience\nexport { resolveEffectiveClientSet };\n"],"mappings":";;;;AAkCA,MAAM,qBAAqB,cAAkC,KAAK;;;;;;;;;;;;;;;;;;;;AAsClE,SAAgB,wBAAwB,EACvC,iBACA,aACA,YACgC;CAChC,MAAM,OAAO,cAAc;AAC1B,MAAI,YAAa,QAAO;AACxB,SAAO,kBAAkB,kBAAkB,EAAE,iBAAiB,GAAG,KAAA,EAAU;IACzE,CAAC,aAAa,gBAAgB,CAAC;AAElC,QACC,oBAAC,mBAAmB,UAApB;EAA6B,OAAO;EAClC;EAC4B,CAAA;;;;;;;;AAUhC,SAAgB,qBAAkC;CACjD,MAAM,OAAO,WAAW,mBAAmB;AAC3C,KAAI,CAAC,KACJ,OAAM,IAAI,MACT,sIAEA;AAEF,QAAO;;AAOR,MAAM,+BAA+B,cAEnC,EAAE,CAAC;;;;;;;;;;;;;;;;;;;;AA4BL,SAAgB,kCAAkC,EACjD,WACA,YAC0C;CAC1C,MAAM,gBAAgB,WAAW,6BAA6B;CAC9D,MAAM,YAAY,cACX,0BAA0B,eAAe,UAAU,EACzD,CAAC,eAAe,UAAU,CAC1B;AAED,QACC,oBAAC,6BAA6B,UAA9B;EAAuC,OAAO;EAC5C;EACsC,CAAA;;;;;;;AAS1C,SAAgB,wBAA0D;AACzE,QAAO,WAAW,6BAA6B"}
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { AuthRequirement, RequirementsClientSetComposition, RouteMatchNode } from "@securitydept/client/auth-coordination";
|
|
2
|
+
import { AnyRoute, RegisteredRouter } from "@tanstack/react-router";
|
|
3
|
+
|
|
4
|
+
//#region src/tanstack-router/index.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Minimal shape of a TanStack Router route match entry.
|
|
7
|
+
*
|
|
8
|
+
* Structurally compatible with the matches returned by `useMatches()`.
|
|
9
|
+
* The adapter reads `staticData` (set via `beforeLoad` or `staticData` option)
|
|
10
|
+
* to find auth requirements.
|
|
11
|
+
*/
|
|
12
|
+
interface TanStackRouteMatch {
|
|
13
|
+
/** Route identifier (the route's `id` field). */
|
|
14
|
+
routeId: string;
|
|
15
|
+
/**
|
|
16
|
+
* Static route context, typically set via `beforeLoad` or `staticData`.
|
|
17
|
+
* Auth requirements should be declared here under a well-known key.
|
|
18
|
+
*
|
|
19
|
+
* Typed as `object` (not `Record<string, unknown>`) to remain structurally
|
|
20
|
+
* compatible with TanStack Router's open `StaticDataRouteOption` interface,
|
|
21
|
+
* which does not carry an index signature.
|
|
22
|
+
*/
|
|
23
|
+
staticData?: object;
|
|
24
|
+
/** Route context merged from parent. */
|
|
25
|
+
context?: object;
|
|
26
|
+
/** Route loader data, if available. */
|
|
27
|
+
loaderData?: object;
|
|
28
|
+
}
|
|
29
|
+
/** Default key for auth requirements in TanStack Router `staticData`. */
|
|
30
|
+
declare const DEFAULT_REQUIREMENTS_KEY = "authRequirements";
|
|
31
|
+
/** Default key for the composition strategy in TanStack Router `staticData`. */
|
|
32
|
+
declare const DEFAULT_COMPOSITION_KEY = "authRequirementsComposition";
|
|
33
|
+
/** Options for {@link projectTanStackRouteMatches} and {@link createTanStackRouteActivator}. */
|
|
34
|
+
interface TanStackRouterAdapterOptions {
|
|
35
|
+
/**
|
|
36
|
+
* Key used to read auth requirements from `staticData`.
|
|
37
|
+
*
|
|
38
|
+
* @default "authRequirements"
|
|
39
|
+
*/
|
|
40
|
+
requirementsKey?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Options for {@link withTanStackRouteRequirements}.
|
|
44
|
+
*/
|
|
45
|
+
interface WithTanStackRouteRequirementsOptions {
|
|
46
|
+
/**
|
|
47
|
+
* How this route segment composes with its parent route chain.
|
|
48
|
+
* @default RequirementsClientSetComposition.Merge
|
|
49
|
+
*/
|
|
50
|
+
composition?: RequirementsClientSetComposition;
|
|
51
|
+
/**
|
|
52
|
+
* Additional staticData properties to merge into the returned object.
|
|
53
|
+
*/
|
|
54
|
+
extra?: Record<string, unknown>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Build a TanStack Router `staticData` object declaring auth requirements for
|
|
58
|
+
* a single route segment.
|
|
59
|
+
*
|
|
60
|
+
* This is the TanStack Router equivalent of Angular's `withRouteRequirements()`.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* import { createRoute } from "@tanstack/react-router";
|
|
65
|
+
* import { withTanStackRouteRequirements } from "@securitydept/client-react/tanstack-router";
|
|
66
|
+
*
|
|
67
|
+
* const dashboardRoute = createRoute({
|
|
68
|
+
* getParentRoute: () => rootRoute,
|
|
69
|
+
* path: "dashboard",
|
|
70
|
+
* staticData: withTanStackRouteRequirements([
|
|
71
|
+
* { id: "session", kind: "session" },
|
|
72
|
+
* ]),
|
|
73
|
+
* });
|
|
74
|
+
*
|
|
75
|
+
* // Child route replaces parent requirements:
|
|
76
|
+
* const publicRoute = createRoute({
|
|
77
|
+
* getParentRoute: () => dashboardRoute,
|
|
78
|
+
* path: "public",
|
|
79
|
+
* staticData: withTanStackRouteRequirements([], {
|
|
80
|
+
* composition: RequirementsClientSetComposition.Replace,
|
|
81
|
+
* }),
|
|
82
|
+
* });
|
|
83
|
+
* ```
|
|
84
|
+
*
|
|
85
|
+
* @param requirements - Auth requirements for this route segment.
|
|
86
|
+
* @param options - Optional composition strategy and extra staticData.
|
|
87
|
+
*/
|
|
88
|
+
declare function withTanStackRouteRequirements(requirements: AuthRequirement[], options?: WithTanStackRouteRequirementsOptions): Record<string, unknown>;
|
|
89
|
+
/**
|
|
90
|
+
* Extract and compose auth requirements from a TanStack Router matched route
|
|
91
|
+
* chain, respecting merge/replace/inherit composition at each segment.
|
|
92
|
+
*
|
|
93
|
+
* This is the TanStack Router equivalent of Angular's
|
|
94
|
+
* `extractFullRouteRequirements()`. It walks the matched route chain from root
|
|
95
|
+
* to leaf and applies the composition strategy declared at each segment:
|
|
96
|
+
*
|
|
97
|
+
* - `inherit` — keep parent requirements unchanged
|
|
98
|
+
* - `merge` — append child requirements, replacing same-id parent entries
|
|
99
|
+
* - `replace` — discard parent chain, use only child's requirements
|
|
100
|
+
*
|
|
101
|
+
* @param matches - Matched route chain from root to leaf (as from `useMatches()`).
|
|
102
|
+
* @param options - Optional key override.
|
|
103
|
+
*/
|
|
104
|
+
declare function extractTanStackRouteRequirements(matches: readonly TanStackRouteMatch[], options?: TanStackRouterAdapterOptions): AuthRequirement[];
|
|
105
|
+
/**
|
|
106
|
+
* Route-level auth requirement declaration for composition resolution.
|
|
107
|
+
*/
|
|
108
|
+
interface RouteRequirementsDeclaration {
|
|
109
|
+
requirements: readonly AuthRequirement[];
|
|
110
|
+
composition: RequirementsClientSetComposition;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Resolve the effective auth requirements by composing the parent chain with a
|
|
114
|
+
* child route declaration.
|
|
115
|
+
*
|
|
116
|
+
* Composition semantics mirror Angular's `resolveEffectiveRequirements()`:
|
|
117
|
+
* - `inherit` keeps the parent requirements unchanged
|
|
118
|
+
* - `merge` appends child requirements and replaces same-id parent entries
|
|
119
|
+
* - `replace` discards the parent chain and uses only the child's requirements
|
|
120
|
+
*/
|
|
121
|
+
declare function resolveEffectiveRequirements(parentRequirements: readonly AuthRequirement[], child: RouteRequirementsDeclaration): AuthRequirement[];
|
|
122
|
+
/**
|
|
123
|
+
* Project TanStack Router matched route entries into the SDK's
|
|
124
|
+
* `RouteMatchNode[]` contract.
|
|
125
|
+
*
|
|
126
|
+
* Each match entry is mapped to a `RouteMatchNode`:
|
|
127
|
+
* - `routeId` is taken from `match.routeId`
|
|
128
|
+
* - `requirements` is read from `match.staticData[requirementsKey]`
|
|
129
|
+
*
|
|
130
|
+
* Entries with no requirements produce nodes with an empty requirements array,
|
|
131
|
+
* preserving the full chain for shared-prefix diffing.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```ts
|
|
135
|
+
* import { useMatches } from "@tanstack/react-router";
|
|
136
|
+
* import { projectTanStackRouteMatches } from "@securitydept/client-react/tanstack-router";
|
|
137
|
+
*
|
|
138
|
+
* const matches = useMatches();
|
|
139
|
+
* const routeChain = projectTanStackRouteMatches(matches);
|
|
140
|
+
* orchestrator.activateMatchedRoutes(routeChain);
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
declare function projectTanStackRouteMatches(matches: readonly TanStackRouteMatch[], options?: TanStackRouterAdapterOptions): RouteMatchNode[];
|
|
144
|
+
/**
|
|
145
|
+
* Create a route-change activator that projects TanStack Router matches and
|
|
146
|
+
* activates them on the orchestrator.
|
|
147
|
+
*
|
|
148
|
+
* This is framework-agnostic (not a React hook) and can be called from a
|
|
149
|
+
* `useEffect`, route subscriber, or any mechanism that notifies match changes.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* import { useMatches } from "@@tanstack/react-router";
|
|
154
|
+
* import { createTanStackRouteActivator } from "@securitydept/client-react/tanstack-router";
|
|
155
|
+
*
|
|
156
|
+
* const activator = createTanStackRouteActivator(orchestrator);
|
|
157
|
+
*
|
|
158
|
+
* function RouteOrchestrationBridge() {
|
|
159
|
+
* const matches = useMatches();
|
|
160
|
+
* useEffect(() => activator.activate(matches), [matches]);
|
|
161
|
+
* return null;
|
|
162
|
+
* }
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
declare function createTanStackRouteActivator(orchestrator: {
|
|
166
|
+
activateMatchedRoutes(chain: readonly RouteMatchNode[]): void;
|
|
167
|
+
deactivateRoute(): void;
|
|
168
|
+
}, options?: TanStackRouterAdapterOptions): {
|
|
169
|
+
/** Project matches and activate the route chain on the orchestrator. */activate(matches: readonly TanStackRouteMatch[]): void; /** Deactivate the current route (e.g. on unmount or navigation away). */
|
|
170
|
+
deactivate(): void;
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Root-level runtime policy for a secured TanStack Router route tree.
|
|
174
|
+
*
|
|
175
|
+
* This is the TanStack Router equivalent of Angular's
|
|
176
|
+
* `SecureRouteRootSecurityOptions`. Non-serializable runtime callbacks
|
|
177
|
+
* (planner host, onUnauthenticated handlers) live here, NOT in individual
|
|
178
|
+
* route `staticData`.
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```ts
|
|
182
|
+
* const routeSecurityPolicy = createTanStackRouteSecurityPolicy({
|
|
183
|
+
* requirementHandlers: {
|
|
184
|
+
* frontend_oidc: (failingRequirement) => {
|
|
185
|
+
* redirectToLogin(failingRequirement);
|
|
186
|
+
* return false;
|
|
187
|
+
* },
|
|
188
|
+
* },
|
|
189
|
+
* });
|
|
190
|
+
*
|
|
191
|
+
* // Use in beforeLoad:
|
|
192
|
+
* const protectedRoute = createRoute({
|
|
193
|
+
* beforeLoad: ({ matches }) => {
|
|
194
|
+
* routeSecurityPolicy.enforce(matches);
|
|
195
|
+
* },
|
|
196
|
+
* });
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
199
|
+
interface TanStackRouteSecurityPolicyOptions {
|
|
200
|
+
/**
|
|
201
|
+
* Per-requirement-kind handlers.
|
|
202
|
+
* Keys are `AuthRequirement.kind` strings.
|
|
203
|
+
*/
|
|
204
|
+
requirementHandlers?: Record<string, (requirement: AuthRequirement) => boolean | string>;
|
|
205
|
+
/**
|
|
206
|
+
* Fallback handler when no kind-specific handler matches.
|
|
207
|
+
*/
|
|
208
|
+
defaultOnUnauthenticated?: (requirement: AuthRequirement) => boolean | string;
|
|
209
|
+
/**
|
|
210
|
+
* Custom key for reading requirements from staticData.
|
|
211
|
+
* @default "authRequirements"
|
|
212
|
+
*/
|
|
213
|
+
requirementsKey?: string;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Route security evaluation result from
|
|
217
|
+
* {@link createTanStackRouteSecurityPolicy}.
|
|
218
|
+
*/
|
|
219
|
+
interface TanStackRouteSecurityResult {
|
|
220
|
+
/** All requirements are met — allow navigation. */
|
|
221
|
+
readonly allMet: boolean;
|
|
222
|
+
/** The first unmet requirement, if any. */
|
|
223
|
+
readonly pendingRequirement: AuthRequirement | undefined;
|
|
224
|
+
/** Handler action for the pending requirement (from policy). */
|
|
225
|
+
readonly action: boolean | string | undefined;
|
|
226
|
+
/** The full effective requirements after composition. */
|
|
227
|
+
readonly effectiveRequirements: readonly AuthRequirement[];
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Create a root-level route security policy that evaluates a TanStack Router
|
|
231
|
+
* matched route chain and returns the enforcement decision.
|
|
232
|
+
*
|
|
233
|
+
* This is the TanStack Router equivalent of Angular's `secureRouteRoot()`.
|
|
234
|
+
* It centralizes non-serializable runtime policy (handlers, redirects) at
|
|
235
|
+
* the root level, while individual routes only declare serializable metadata
|
|
236
|
+
* via `withTanStackRouteRequirements()`.
|
|
237
|
+
*
|
|
238
|
+
* The returned `evaluate()` function:
|
|
239
|
+
* 1. Extracts requirements from the matched route chain (with composition)
|
|
240
|
+
* 2. Finds the first unmet requirement using the provided `checkAuthenticated`
|
|
241
|
+
* 3. Resolves the handler action (kind handler → default handler → block)
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```ts
|
|
245
|
+
* import { createTanStackRouteSecurityPolicy } from "@securitydept/client-react/tanstack-router";
|
|
246
|
+
*
|
|
247
|
+
* const policy = createTanStackRouteSecurityPolicy({
|
|
248
|
+
* requirementHandlers: {
|
|
249
|
+
* frontend_oidc: (req) => {
|
|
250
|
+
* redirectToOidcLogin(req);
|
|
251
|
+
* return false;
|
|
252
|
+
* },
|
|
253
|
+
* },
|
|
254
|
+
* defaultOnUnauthenticated: () => "/login",
|
|
255
|
+
* });
|
|
256
|
+
*
|
|
257
|
+
* // In a route's beforeLoad or a React effect:
|
|
258
|
+
* const result = policy.evaluate(matches, (req) => isAuthenticated(req.kind));
|
|
259
|
+
* if (!result.allMet && typeof result.action === "string") {
|
|
260
|
+
* navigate({ to: result.action });
|
|
261
|
+
* }
|
|
262
|
+
* ```
|
|
263
|
+
*/
|
|
264
|
+
declare function createTanStackRouteSecurityPolicy(options?: TanStackRouteSecurityPolicyOptions): {
|
|
265
|
+
/**
|
|
266
|
+
* Evaluate the route security policy for a matched route chain.
|
|
267
|
+
*
|
|
268
|
+
* @param matches - The matched route chain (root → leaf).
|
|
269
|
+
* @param checkAuthenticated - Returns true if a requirement is met.
|
|
270
|
+
*/
|
|
271
|
+
evaluate(matches: readonly TanStackRouteMatch[], checkAuthenticated: (requirement: AuthRequirement) => boolean): TanStackRouteSecurityResult;
|
|
272
|
+
};
|
|
273
|
+
/**
|
|
274
|
+
* Navigation-blocked error thrown by {@link createSecureBeforeLoad} when a
|
|
275
|
+
* requirement is unmet and the handler returns `false`.
|
|
276
|
+
*/
|
|
277
|
+
declare class RouteSecurityBlockedError extends Error {
|
|
278
|
+
readonly requirement: AuthRequirement;
|
|
279
|
+
readonly result: TanStackRouteSecurityResult;
|
|
280
|
+
constructor(requirement: AuthRequirement, result: TanStackRouteSecurityResult);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Minimal shape of TanStack Router's `beforeLoad` context.
|
|
284
|
+
*
|
|
285
|
+
* Only the fields actually consumed by {@link createSecureBeforeLoad} are
|
|
286
|
+
* required. The full TanStack Router context has many more fields; this
|
|
287
|
+
* interface lets the helper stay structurally compatible without importing
|
|
288
|
+
* the full framework type.
|
|
289
|
+
*/
|
|
290
|
+
interface SecureBeforeLoadContext {
|
|
291
|
+
/** URL location of the target route. */
|
|
292
|
+
location: {
|
|
293
|
+
pathname: string;
|
|
294
|
+
href: string;
|
|
295
|
+
};
|
|
296
|
+
/**
|
|
297
|
+
* Matched route entries for the current navigation.
|
|
298
|
+
* Shape matches {@link TanStackRouteMatch}.
|
|
299
|
+
*/
|
|
300
|
+
matches: readonly TanStackRouteMatch[];
|
|
301
|
+
/** Navigation cause ("enter", "stay", "push", "replace", etc.). */
|
|
302
|
+
cause: string;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Options for {@link createSecureBeforeLoad}.
|
|
306
|
+
*/
|
|
307
|
+
interface CreateSecureBeforeLoadOptions extends TanStackRouteSecurityPolicyOptions {
|
|
308
|
+
/**
|
|
309
|
+
* Returns true if the given auth requirement is currently satisfied.
|
|
310
|
+
*
|
|
311
|
+
* This is the auth-state bridge: the adopter supplies a function that
|
|
312
|
+
* checks their auth store / context.
|
|
313
|
+
*/
|
|
314
|
+
checkAuthenticated: (requirement: AuthRequirement) => boolean;
|
|
315
|
+
/**
|
|
316
|
+
* TanStack Router's `redirect()` function.
|
|
317
|
+
*
|
|
318
|
+
* When a handler returns a string (redirect path), `createSecureBeforeLoad`
|
|
319
|
+
* throws `redirect({ to: path })` to trigger TanStack Router's redirect
|
|
320
|
+
* mechanism.
|
|
321
|
+
*
|
|
322
|
+
* Pass `redirect` from `@tanstack/react-router` here:
|
|
323
|
+
* ```ts
|
|
324
|
+
* import { redirect } from "@tanstack/react-router";
|
|
325
|
+
* createSecureBeforeLoad({ redirect, ... });
|
|
326
|
+
* ```
|
|
327
|
+
*
|
|
328
|
+
* If omitted, string actions cause a {@link RouteSecurityBlockedError}
|
|
329
|
+
* with the redirect path in the error message.
|
|
330
|
+
*/
|
|
331
|
+
redirect?: (opts: {
|
|
332
|
+
to: string;
|
|
333
|
+
}) => never;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Create a `beforeLoad`-compatible function that enforces route-security
|
|
337
|
+
* requirements inside TanStack Router's execution semantics.
|
|
338
|
+
*
|
|
339
|
+
* **This is the canonical TanStack Router adopter-facing entry** — the
|
|
340
|
+
* equivalent of Angular's `secureRouteRoot()`.
|
|
341
|
+
*
|
|
342
|
+
* How it works:
|
|
343
|
+
* 1. Receives TanStack Router's `beforeLoad` context (including `matches`)
|
|
344
|
+
* 2. Extracts the full requirement chain with composition semantics
|
|
345
|
+
* 3. Finds the first unmet requirement
|
|
346
|
+
* 4. Calls the handler (kind handler → default → block)
|
|
347
|
+
* 5. If the handler returns a string path, throws `redirect({ to: path })`
|
|
348
|
+
* 6. If the handler returns `false`, throws {@link RouteSecurityBlockedError}
|
|
349
|
+
* 7. If all requirements are met, returns normally (navigation proceeds)
|
|
350
|
+
*
|
|
351
|
+
* @example
|
|
352
|
+
* ```ts
|
|
353
|
+
* import { redirect, createRoute } from "@tanstack/react-router";
|
|
354
|
+
* import {
|
|
355
|
+
* createSecureBeforeLoad,
|
|
356
|
+
* withTanStackRouteRequirements,
|
|
357
|
+
* } from "@securitydept/client-react/tanstack-router";
|
|
358
|
+
*
|
|
359
|
+
* // Root-level: non-serializable runtime policy
|
|
360
|
+
* const securedBeforeLoad = createSecureBeforeLoad({
|
|
361
|
+
* redirect,
|
|
362
|
+
* checkAuthenticated: (req) => authStore.isAuthenticated(req.kind),
|
|
363
|
+
* requirementHandlers: {
|
|
364
|
+
* frontend_oidc: (req) => "/login/oidc",
|
|
365
|
+
* session: () => "/login",
|
|
366
|
+
* },
|
|
367
|
+
* });
|
|
368
|
+
*
|
|
369
|
+
* // Root route — attach the beforeLoad
|
|
370
|
+
* const rootRoute = createRootRoute({
|
|
371
|
+
* beforeLoad: securedBeforeLoad,
|
|
372
|
+
* });
|
|
373
|
+
*
|
|
374
|
+
* // Child routes — serializable declaration only
|
|
375
|
+
* const dashboardRoute = createRoute({
|
|
376
|
+
* getParentRoute: () => rootRoute,
|
|
377
|
+
* path: "dashboard",
|
|
378
|
+
* staticData: withTanStackRouteRequirements([
|
|
379
|
+
* { id: "session", kind: "session" },
|
|
380
|
+
* ]),
|
|
381
|
+
* });
|
|
382
|
+
*
|
|
383
|
+
* const adminRoute = createRoute({
|
|
384
|
+
* getParentRoute: () => dashboardRoute,
|
|
385
|
+
* path: "admin",
|
|
386
|
+
* staticData: withTanStackRouteRequirements([
|
|
387
|
+
* { id: "admin-oidc", kind: "frontend_oidc" },
|
|
388
|
+
* ]),
|
|
389
|
+
* });
|
|
390
|
+
* ```
|
|
391
|
+
*/
|
|
392
|
+
declare function createSecureBeforeLoad(options: CreateSecureBeforeLoadOptions): (ctx: SecureBeforeLoadContext) => void;
|
|
393
|
+
//#endregion
|
|
394
|
+
export { type AnyRoute, type AuthRequirement, CreateSecureBeforeLoadOptions, DEFAULT_COMPOSITION_KEY, DEFAULT_REQUIREMENTS_KEY, type RegisteredRouter, RequirementsClientSetComposition, type RouteMatchNode, RouteRequirementsDeclaration, RouteSecurityBlockedError, SecureBeforeLoadContext, TanStackRouteMatch, TanStackRouteSecurityPolicyOptions, TanStackRouteSecurityResult, TanStackRouterAdapterOptions, WithTanStackRouteRequirementsOptions, createSecureBeforeLoad, createTanStackRouteActivator, createTanStackRouteSecurityPolicy, extractTanStackRouteRequirements, projectTanStackRouteMatches, resolveEffectiveRequirements, withTanStackRouteRequirements };
|
|
395
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { RequirementsClientSetComposition } from "@securitydept/client/auth-coordination";
|
|
2
|
+
//#region src/tanstack-router/index.ts
|
|
3
|
+
/** Default key for auth requirements in TanStack Router `staticData`. */
|
|
4
|
+
const DEFAULT_REQUIREMENTS_KEY = "authRequirements";
|
|
5
|
+
/** Default key for the composition strategy in TanStack Router `staticData`. */
|
|
6
|
+
const DEFAULT_COMPOSITION_KEY = "authRequirementsComposition";
|
|
7
|
+
/**
|
|
8
|
+
* Build a TanStack Router `staticData` object declaring auth requirements for
|
|
9
|
+
* a single route segment.
|
|
10
|
+
*
|
|
11
|
+
* This is the TanStack Router equivalent of Angular's `withRouteRequirements()`.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { createRoute } from "@tanstack/react-router";
|
|
16
|
+
* import { withTanStackRouteRequirements } from "@securitydept/client-react/tanstack-router";
|
|
17
|
+
*
|
|
18
|
+
* const dashboardRoute = createRoute({
|
|
19
|
+
* getParentRoute: () => rootRoute,
|
|
20
|
+
* path: "dashboard",
|
|
21
|
+
* staticData: withTanStackRouteRequirements([
|
|
22
|
+
* { id: "session", kind: "session" },
|
|
23
|
+
* ]),
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* // Child route replaces parent requirements:
|
|
27
|
+
* const publicRoute = createRoute({
|
|
28
|
+
* getParentRoute: () => dashboardRoute,
|
|
29
|
+
* path: "public",
|
|
30
|
+
* staticData: withTanStackRouteRequirements([], {
|
|
31
|
+
* composition: RequirementsClientSetComposition.Replace,
|
|
32
|
+
* }),
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @param requirements - Auth requirements for this route segment.
|
|
37
|
+
* @param options - Optional composition strategy and extra staticData.
|
|
38
|
+
*/
|
|
39
|
+
function withTanStackRouteRequirements(requirements, options) {
|
|
40
|
+
return {
|
|
41
|
+
...options?.extra ?? {},
|
|
42
|
+
[DEFAULT_REQUIREMENTS_KEY]: requirements,
|
|
43
|
+
[DEFAULT_COMPOSITION_KEY]: options?.composition ?? RequirementsClientSetComposition.Merge
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Extract and compose auth requirements from a TanStack Router matched route
|
|
48
|
+
* chain, respecting merge/replace/inherit composition at each segment.
|
|
49
|
+
*
|
|
50
|
+
* This is the TanStack Router equivalent of Angular's
|
|
51
|
+
* `extractFullRouteRequirements()`. It walks the matched route chain from root
|
|
52
|
+
* to leaf and applies the composition strategy declared at each segment:
|
|
53
|
+
*
|
|
54
|
+
* - `inherit` — keep parent requirements unchanged
|
|
55
|
+
* - `merge` — append child requirements, replacing same-id parent entries
|
|
56
|
+
* - `replace` — discard parent chain, use only child's requirements
|
|
57
|
+
*
|
|
58
|
+
* @param matches - Matched route chain from root to leaf (as from `useMatches()`).
|
|
59
|
+
* @param options - Optional key override.
|
|
60
|
+
*/
|
|
61
|
+
function extractTanStackRouteRequirements(matches, options) {
|
|
62
|
+
const key = options?.requirementsKey ?? "authRequirements";
|
|
63
|
+
let result = [];
|
|
64
|
+
for (const match of matches) {
|
|
65
|
+
const raw = match.staticData?.[key];
|
|
66
|
+
const compositionRaw = match.staticData?.[DEFAULT_COMPOSITION_KEY];
|
|
67
|
+
const composition = isValidComposition(compositionRaw) ? compositionRaw : RequirementsClientSetComposition.Merge;
|
|
68
|
+
result = resolveEffectiveRequirements(result, {
|
|
69
|
+
composition,
|
|
70
|
+
requirements: Array.isArray(raw) ? raw : []
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Resolve the effective auth requirements by composing the parent chain with a
|
|
77
|
+
* child route declaration.
|
|
78
|
+
*
|
|
79
|
+
* Composition semantics mirror Angular's `resolveEffectiveRequirements()`:
|
|
80
|
+
* - `inherit` keeps the parent requirements unchanged
|
|
81
|
+
* - `merge` appends child requirements and replaces same-id parent entries
|
|
82
|
+
* - `replace` discards the parent chain and uses only the child's requirements
|
|
83
|
+
*/
|
|
84
|
+
function resolveEffectiveRequirements(parentRequirements, child) {
|
|
85
|
+
switch (child.composition) {
|
|
86
|
+
case RequirementsClientSetComposition.Inherit: return [...parentRequirements];
|
|
87
|
+
case RequirementsClientSetComposition.Merge: {
|
|
88
|
+
const merged = /* @__PURE__ */ new Map();
|
|
89
|
+
for (const requirement of parentRequirements) merged.set(requirement.id, requirement);
|
|
90
|
+
for (const requirement of child.requirements) merged.set(requirement.id, requirement);
|
|
91
|
+
return [...merged.values()];
|
|
92
|
+
}
|
|
93
|
+
case RequirementsClientSetComposition.Replace: return [...child.requirements];
|
|
94
|
+
default: {
|
|
95
|
+
const _exhaustive = child.composition;
|
|
96
|
+
throw new Error(`[resolveEffectiveRequirements] Unknown composition: ${_exhaustive}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Project TanStack Router matched route entries into the SDK's
|
|
102
|
+
* `RouteMatchNode[]` contract.
|
|
103
|
+
*
|
|
104
|
+
* Each match entry is mapped to a `RouteMatchNode`:
|
|
105
|
+
* - `routeId` is taken from `match.routeId`
|
|
106
|
+
* - `requirements` is read from `match.staticData[requirementsKey]`
|
|
107
|
+
*
|
|
108
|
+
* Entries with no requirements produce nodes with an empty requirements array,
|
|
109
|
+
* preserving the full chain for shared-prefix diffing.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```ts
|
|
113
|
+
* import { useMatches } from "@tanstack/react-router";
|
|
114
|
+
* import { projectTanStackRouteMatches } from "@securitydept/client-react/tanstack-router";
|
|
115
|
+
*
|
|
116
|
+
* const matches = useMatches();
|
|
117
|
+
* const routeChain = projectTanStackRouteMatches(matches);
|
|
118
|
+
* orchestrator.activateMatchedRoutes(routeChain);
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
function projectTanStackRouteMatches(matches, options) {
|
|
122
|
+
const key = options?.requirementsKey ?? "authRequirements";
|
|
123
|
+
return matches.map((match) => {
|
|
124
|
+
const rawRequirements = match.staticData?.[key];
|
|
125
|
+
const requirements = Array.isArray(rawRequirements) ? rawRequirements : [];
|
|
126
|
+
return {
|
|
127
|
+
routeId: match.routeId,
|
|
128
|
+
requirements
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Create a route-change activator that projects TanStack Router matches and
|
|
134
|
+
* activates them on the orchestrator.
|
|
135
|
+
*
|
|
136
|
+
* This is framework-agnostic (not a React hook) and can be called from a
|
|
137
|
+
* `useEffect`, route subscriber, or any mechanism that notifies match changes.
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```ts
|
|
141
|
+
* import { useMatches } from "@@tanstack/react-router";
|
|
142
|
+
* import { createTanStackRouteActivator } from "@securitydept/client-react/tanstack-router";
|
|
143
|
+
*
|
|
144
|
+
* const activator = createTanStackRouteActivator(orchestrator);
|
|
145
|
+
*
|
|
146
|
+
* function RouteOrchestrationBridge() {
|
|
147
|
+
* const matches = useMatches();
|
|
148
|
+
* useEffect(() => activator.activate(matches), [matches]);
|
|
149
|
+
* return null;
|
|
150
|
+
* }
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
function createTanStackRouteActivator(orchestrator, options) {
|
|
154
|
+
return {
|
|
155
|
+
activate(matches) {
|
|
156
|
+
const chain = projectTanStackRouteMatches(matches, options);
|
|
157
|
+
orchestrator.activateMatchedRoutes(chain);
|
|
158
|
+
},
|
|
159
|
+
deactivate() {
|
|
160
|
+
orchestrator.deactivateRoute();
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Create a root-level route security policy that evaluates a TanStack Router
|
|
166
|
+
* matched route chain and returns the enforcement decision.
|
|
167
|
+
*
|
|
168
|
+
* This is the TanStack Router equivalent of Angular's `secureRouteRoot()`.
|
|
169
|
+
* It centralizes non-serializable runtime policy (handlers, redirects) at
|
|
170
|
+
* the root level, while individual routes only declare serializable metadata
|
|
171
|
+
* via `withTanStackRouteRequirements()`.
|
|
172
|
+
*
|
|
173
|
+
* The returned `evaluate()` function:
|
|
174
|
+
* 1. Extracts requirements from the matched route chain (with composition)
|
|
175
|
+
* 2. Finds the first unmet requirement using the provided `checkAuthenticated`
|
|
176
|
+
* 3. Resolves the handler action (kind handler → default handler → block)
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```ts
|
|
180
|
+
* import { createTanStackRouteSecurityPolicy } from "@securitydept/client-react/tanstack-router";
|
|
181
|
+
*
|
|
182
|
+
* const policy = createTanStackRouteSecurityPolicy({
|
|
183
|
+
* requirementHandlers: {
|
|
184
|
+
* frontend_oidc: (req) => {
|
|
185
|
+
* redirectToOidcLogin(req);
|
|
186
|
+
* return false;
|
|
187
|
+
* },
|
|
188
|
+
* },
|
|
189
|
+
* defaultOnUnauthenticated: () => "/login",
|
|
190
|
+
* });
|
|
191
|
+
*
|
|
192
|
+
* // In a route's beforeLoad or a React effect:
|
|
193
|
+
* const result = policy.evaluate(matches, (req) => isAuthenticated(req.kind));
|
|
194
|
+
* if (!result.allMet && typeof result.action === "string") {
|
|
195
|
+
* navigate({ to: result.action });
|
|
196
|
+
* }
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
199
|
+
function createTanStackRouteSecurityPolicy(options = {}) {
|
|
200
|
+
return { evaluate(matches, checkAuthenticated) {
|
|
201
|
+
const effectiveRequirements = extractTanStackRouteRequirements(matches, { requirementsKey: options.requirementsKey });
|
|
202
|
+
for (const req of effectiveRequirements) if (!checkAuthenticated(req)) {
|
|
203
|
+
const handler = options.requirementHandlers?.[req.kind] ?? options.defaultOnUnauthenticated;
|
|
204
|
+
return {
|
|
205
|
+
allMet: false,
|
|
206
|
+
pendingRequirement: req,
|
|
207
|
+
action: handler ? handler(req) : false,
|
|
208
|
+
effectiveRequirements
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
allMet: true,
|
|
213
|
+
pendingRequirement: void 0,
|
|
214
|
+
action: void 0,
|
|
215
|
+
effectiveRequirements
|
|
216
|
+
};
|
|
217
|
+
} };
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Navigation-blocked error thrown by {@link createSecureBeforeLoad} when a
|
|
221
|
+
* requirement is unmet and the handler returns `false`.
|
|
222
|
+
*/
|
|
223
|
+
var RouteSecurityBlockedError = class extends Error {
|
|
224
|
+
constructor(requirement, result) {
|
|
225
|
+
super(`[secureBeforeLoad] Navigation blocked: requirement "${requirement.id}" (kind: ${requirement.kind}) is not authenticated.`);
|
|
226
|
+
this.requirement = requirement;
|
|
227
|
+
this.result = result;
|
|
228
|
+
this.name = "RouteSecurityBlockedError";
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
232
|
+
* Create a `beforeLoad`-compatible function that enforces route-security
|
|
233
|
+
* requirements inside TanStack Router's execution semantics.
|
|
234
|
+
*
|
|
235
|
+
* **This is the canonical TanStack Router adopter-facing entry** — the
|
|
236
|
+
* equivalent of Angular's `secureRouteRoot()`.
|
|
237
|
+
*
|
|
238
|
+
* How it works:
|
|
239
|
+
* 1. Receives TanStack Router's `beforeLoad` context (including `matches`)
|
|
240
|
+
* 2. Extracts the full requirement chain with composition semantics
|
|
241
|
+
* 3. Finds the first unmet requirement
|
|
242
|
+
* 4. Calls the handler (kind handler → default → block)
|
|
243
|
+
* 5. If the handler returns a string path, throws `redirect({ to: path })`
|
|
244
|
+
* 6. If the handler returns `false`, throws {@link RouteSecurityBlockedError}
|
|
245
|
+
* 7. If all requirements are met, returns normally (navigation proceeds)
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* ```ts
|
|
249
|
+
* import { redirect, createRoute } from "@tanstack/react-router";
|
|
250
|
+
* import {
|
|
251
|
+
* createSecureBeforeLoad,
|
|
252
|
+
* withTanStackRouteRequirements,
|
|
253
|
+
* } from "@securitydept/client-react/tanstack-router";
|
|
254
|
+
*
|
|
255
|
+
* // Root-level: non-serializable runtime policy
|
|
256
|
+
* const securedBeforeLoad = createSecureBeforeLoad({
|
|
257
|
+
* redirect,
|
|
258
|
+
* checkAuthenticated: (req) => authStore.isAuthenticated(req.kind),
|
|
259
|
+
* requirementHandlers: {
|
|
260
|
+
* frontend_oidc: (req) => "/login/oidc",
|
|
261
|
+
* session: () => "/login",
|
|
262
|
+
* },
|
|
263
|
+
* });
|
|
264
|
+
*
|
|
265
|
+
* // Root route — attach the beforeLoad
|
|
266
|
+
* const rootRoute = createRootRoute({
|
|
267
|
+
* beforeLoad: securedBeforeLoad,
|
|
268
|
+
* });
|
|
269
|
+
*
|
|
270
|
+
* // Child routes — serializable declaration only
|
|
271
|
+
* const dashboardRoute = createRoute({
|
|
272
|
+
* getParentRoute: () => rootRoute,
|
|
273
|
+
* path: "dashboard",
|
|
274
|
+
* staticData: withTanStackRouteRequirements([
|
|
275
|
+
* { id: "session", kind: "session" },
|
|
276
|
+
* ]),
|
|
277
|
+
* });
|
|
278
|
+
*
|
|
279
|
+
* const adminRoute = createRoute({
|
|
280
|
+
* getParentRoute: () => dashboardRoute,
|
|
281
|
+
* path: "admin",
|
|
282
|
+
* staticData: withTanStackRouteRequirements([
|
|
283
|
+
* { id: "admin-oidc", kind: "frontend_oidc" },
|
|
284
|
+
* ]),
|
|
285
|
+
* });
|
|
286
|
+
* ```
|
|
287
|
+
*/
|
|
288
|
+
function createSecureBeforeLoad(options) {
|
|
289
|
+
const policy = createTanStackRouteSecurityPolicy(options);
|
|
290
|
+
return function secureBeforeLoad(ctx) {
|
|
291
|
+
const result = policy.evaluate(ctx.matches, options.checkAuthenticated);
|
|
292
|
+
if (result.allMet) return;
|
|
293
|
+
const { action, pendingRequirement } = result;
|
|
294
|
+
if (!pendingRequirement) return;
|
|
295
|
+
if (typeof action === "string") {
|
|
296
|
+
if (options.redirect) throw options.redirect({ to: action });
|
|
297
|
+
throw new RouteSecurityBlockedError(pendingRequirement, result);
|
|
298
|
+
}
|
|
299
|
+
throw new RouteSecurityBlockedError(pendingRequirement, result);
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function isValidComposition(raw) {
|
|
303
|
+
return raw === RequirementsClientSetComposition.Inherit || raw === RequirementsClientSetComposition.Merge || raw === RequirementsClientSetComposition.Replace;
|
|
304
|
+
}
|
|
305
|
+
//#endregion
|
|
306
|
+
export { DEFAULT_COMPOSITION_KEY, DEFAULT_REQUIREMENTS_KEY, RequirementsClientSetComposition, RouteSecurityBlockedError, createSecureBeforeLoad, createTanStackRouteActivator, createTanStackRouteSecurityPolicy, extractTanStackRouteRequirements, projectTanStackRouteMatches, resolveEffectiveRequirements, withTanStackRouteRequirements };
|
|
307
|
+
|
|
308
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +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\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<\n\t\tstring,\n\t\t(requirement: AuthRequirement) => boolean | string\n\t>;\n\n\t/**\n\t * Fallback handler when no kind-specific handler matches.\n\t */\n\tdefaultOnUnauthenticated?: (requirement: AuthRequirement) => boolean | string;\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: boolean | string | 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): TanStackRouteSecurityResult {\n\t\t\tconst effectiveRequirements = extractTanStackRouteRequirements(matches, {\n\t\t\t\trequirementsKey: options.requirementsKey,\n\t\t\t});\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) : 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: { pathname: string; href: string };\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 {\n\tconst policy = createTanStackRouteSecurityPolicy(options);\n\n\treturn function secureBeforeLoad(ctx: SecureBeforeLoadContext): void {\n\t\tconst result = policy.evaluate(ctx.matches, options.checkAuthenticated);\n\n\t\tif (result.allMet) return;\n\n\t\tconst { action, pendingRequirement } = result;\n\n\t\tif (!pendingRequirement) return;\n\n\t\tif (typeof action === \"string\") {\n\t\t\tif (options.redirect) {\n\t\t\t\tthrow options.redirect({ to: action });\n\t\t\t}\n\t\t\t// No redirect function provided — throw a descriptive error.\n\t\t\tthrow new RouteSecurityBlockedError(pendingRequirement, result);\n\t\t}\n\n\t\t// action is false or undefined — block navigation.\n\t\tthrow new RouteSecurityBlockedError(pendingRequirement, result);\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\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":";;;AAkFA,MAAa,2BAA2B;;AAGxC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoEvC,SAAgB,8BACf,cACA,SAC0B;AAC1B,QAAO;EACN,GAAI,SAAS,SAAS,EAAE;GACvB,2BAA2B;GAC3B,0BACA,SAAS,eAAe,iCAAiC;EAC1D;;;;;;;;;;;;;;;;;AAsBF,SAAgB,iCACf,SACA,SACoB;CACpB,MAAM,MAAM,SAAS,mBAAA;CACrB,IAAI,SAA4B,EAAE;AAElC,MAAK,MAAM,SAAS,SAAS;EAC5B,MAAM,MAAO,MAAM,aAAyC;EAC5D,MAAM,iBAAkB,MAAM,aAC7B;EAED,MAAM,cAAc,mBAAmB,eAAe,GACnD,iBACA,iCAAiC;AAGpC,WAAS,6BAA6B,QAAQ;GAC7C;GACA,cAJuC,MAAM,QAAQ,IAAI,GAAG,MAAM,EAAE;GAKpE,CAAC;;AAEH,QAAO;;;;;;;;;;;AAwBR,SAAgB,6BACf,oBACA,OACoB;AACpB,SAAQ,MAAM,aAAd;EACC,KAAK,iCAAiC,QACrC,QAAO,CAAC,GAAG,mBAAmB;EAE/B,KAAK,iCAAiC,OAAO;GAC5C,MAAM,yBAAS,IAAI,KAA8B;AACjD,QAAK,MAAM,eAAe,mBACzB,QAAO,IAAI,YAAY,IAAI,YAAY;AAExC,QAAK,MAAM,eAAe,MAAM,aAC/B,QAAO,IAAI,YAAY,IAAI,YAAY;AAExC,UAAO,CAAC,GAAG,OAAO,QAAQ,CAAC;;EAG5B,KAAK,iCAAiC,QACrC,QAAO,CAAC,GAAG,MAAM,aAAa;EAE/B,SAAS;GACR,MAAM,cAAqB,MAAM;AACjC,SAAM,IAAI,MACT,uDAAuD,cACvD;;;;;;;;;;;;;;;;;;;;;;;;;AA8BJ,SAAgB,4BACf,SACA,SACmB;CACnB,MAAM,MAAM,SAAS,mBAAA;AACrB,QAAO,QAAQ,KAAK,UAAU;EAC7B,MAAM,kBAAmB,MAAM,aAC9B;EAED,MAAM,eAAkC,MAAM,QAAQ,gBAAgB,GACnE,kBACA,EAAE;AACL,SAAO;GACN,SAAS,MAAM;GACf;GACA;GACA;;;;;;;;;;;;;;;;;;;;;;;AA4BH,SAAgB,6BACf,cAIA,SACC;AACD,QAAO;EAEN,SAAS,SAA8C;GACtD,MAAM,QAAQ,4BAA4B,SAAS,QAAQ;AAC3D,gBAAa,sBAAsB,MAAM;;EAG1C,aAAmB;AAClB,gBAAa,iBAAiB;;EAE/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0GF,SAAgB,kCACf,UAA8C,EAAE,EAC/C;AACD,QAAO,EAON,SACC,SACA,oBAC8B;EAC9B,MAAM,wBAAwB,iCAAiC,SAAS,EACvE,iBAAiB,QAAQ,iBACzB,CAAC;AAEF,OAAK,MAAM,OAAO,sBACjB,KAAI,CAAC,mBAAmB,IAAI,EAAE;GAC7B,MAAM,UACL,QAAQ,sBAAsB,IAAI,SAClC,QAAQ;AAET,UAAO;IACN,QAAQ;IACR,oBAAoB;IACpB,QAAQ,UAAU,QAAQ,IAAI,GAAG;IACjC;IACA;;AAIH,SAAO;GACN,QAAQ;GACR,oBAAoB,KAAA;GACpB,QAAQ,KAAA;GACR;GACA;IAEF;;;;;;AAWF,IAAa,4BAAb,cAA+C,MAAM;CACpD,YACC,aACA,QACC;AACD,QACC,uDAAuD,YAAY,GAAG,WAAW,YAAY,KAAK,yBAClG;AALe,OAAA,cAAA;AACA,OAAA,SAAA;AAKhB,OAAK,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiHd,SAAgB,uBACf,SACyC;CACzC,MAAM,SAAS,kCAAkC,QAAQ;AAEzD,QAAO,SAAS,iBAAiB,KAAoC;EACpE,MAAM,SAAS,OAAO,SAAS,IAAI,SAAS,QAAQ,mBAAmB;AAEvE,MAAI,OAAO,OAAQ;EAEnB,MAAM,EAAE,QAAQ,uBAAuB;AAEvC,MAAI,CAAC,mBAAoB;AAEzB,MAAI,OAAO,WAAW,UAAU;AAC/B,OAAI,QAAQ,SACX,OAAM,QAAQ,SAAS,EAAE,IAAI,QAAQ,CAAC;AAGvC,SAAM,IAAI,0BAA0B,oBAAoB,OAAO;;AAIhE,QAAM,IAAI,0BAA0B,oBAAoB,OAAO;;;AAQjE,SAAS,mBACR,KAC0C;AAC1C,QACC,QAAQ,iCAAiC,WACzC,QAAQ,iCAAiC,SACzC,QAAQ,iCAAiC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@securitydept/client-react",
|
|
3
|
+
"version": "0.2.0-beta.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"monorepo-tsc": "./src/planner-host/index.tsx",
|
|
9
|
+
"types": "./dist/index.d.mts",
|
|
10
|
+
"import": "./dist/index.mjs"
|
|
11
|
+
},
|
|
12
|
+
"./tanstack-router": {
|
|
13
|
+
"monorepo-tsc": "./src/tanstack-router/index.ts",
|
|
14
|
+
"types": "./dist/tanstack-router/index.d.mts",
|
|
15
|
+
"import": "./dist/tanstack-router/index.mjs"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsdown",
|
|
23
|
+
"typecheck": "tsc -b"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@securitydept/client": "workspace:*"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"react": ">=19.0.0",
|
|
30
|
+
"@tanstack/react-router": ">=1.0.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependenciesMeta": {
|
|
33
|
+
"@tanstack/react-router": {
|
|
34
|
+
"optional": true
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|