@securitydept/client-react 0.3.0-beta.1 → 0.3.0-beta.3
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 +26 -2
- package/dist/index.mjs +27 -2
- package/dist/index.mjs.map +1 -1
- package/dist/tanstack-router/index.mjs +14 -0
- package/dist/tanstack-router/index.mjs.map +1 -1
- package/package.json +7 -3
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ClientEnvironment, ClientEnvironmentService, PageClientEnvironment, WebClientEnvironment } from "@securitydept/client/web";
|
|
2
2
|
import { ReactNode } from "react";
|
|
3
3
|
import * as _$react_jsx_runtime0 from "react/jsx-runtime";
|
|
4
|
+
import { AuthGuardClientOption, CandidateSelector, PlannerHost, ScopedRequirementsClientSet, resolveEffectiveClientSet } from "@securitydept/client/auth-coordination";
|
|
4
5
|
|
|
6
|
+
//#region src/environment-service/index.d.ts
|
|
7
|
+
interface ClientEnvironmentServiceContextValue {
|
|
8
|
+
readClientEnvironment(): ClientEnvironment;
|
|
9
|
+
readWebEnvironment(): WebClientEnvironment;
|
|
10
|
+
readPageEnvironment(): PageClientEnvironment;
|
|
11
|
+
resolveClientEnvironment(): Promise<ClientEnvironment>;
|
|
12
|
+
resolveWebEnvironment(): Promise<WebClientEnvironment>;
|
|
13
|
+
resolvePageEnvironment(): Promise<PageClientEnvironment>;
|
|
14
|
+
reset(): void;
|
|
15
|
+
}
|
|
16
|
+
interface ClientEnvironmentServiceProviderProps {
|
|
17
|
+
children?: ReactNode;
|
|
18
|
+
service?: ClientEnvironmentServiceContextValue;
|
|
19
|
+
}
|
|
20
|
+
declare function ClientEnvironmentServiceProvider({
|
|
21
|
+
children,
|
|
22
|
+
service
|
|
23
|
+
}: ClientEnvironmentServiceProviderProps): _$react_jsx_runtime0.JSX.Element;
|
|
24
|
+
declare function useClientEnvironmentService<TClientEnvironment extends ClientEnvironment = ClientEnvironment, TWebEnvironment extends WebClientEnvironment = WebClientEnvironment, TPageEnvironment extends PageClientEnvironment = PageClientEnvironment>(): ClientEnvironmentService<TClientEnvironment, TWebEnvironment, TPageEnvironment>;
|
|
25
|
+
declare function useClientEnvironment<TClientEnvironment extends ClientEnvironment = ClientEnvironment, TWebEnvironment extends WebClientEnvironment = WebClientEnvironment, TPageEnvironment extends PageClientEnvironment = PageClientEnvironment>(): TClientEnvironment;
|
|
26
|
+
declare function useWebClientEnvironment<TClientEnvironment extends ClientEnvironment = ClientEnvironment, TWebEnvironment extends WebClientEnvironment = WebClientEnvironment, TPageEnvironment extends PageClientEnvironment = PageClientEnvironment>(): TWebEnvironment;
|
|
27
|
+
declare function usePageClientEnvironment<TClientEnvironment extends ClientEnvironment = ClientEnvironment, TWebEnvironment extends WebClientEnvironment = WebClientEnvironment, TPageEnvironment extends PageClientEnvironment = PageClientEnvironment>(): TPageEnvironment;
|
|
28
|
+
//#endregion
|
|
5
29
|
//#region src/planner-host/index.d.ts
|
|
6
30
|
/** Props for {@link AuthPlannerHostProvider}. */
|
|
7
31
|
interface AuthPlannerHostProviderProps {
|
|
@@ -84,5 +108,5 @@ declare function AuthRequirementsClientSetProvider({
|
|
|
84
108
|
*/
|
|
85
109
|
declare function useEffectiveClientSet(): readonly AuthGuardClientOption[];
|
|
86
110
|
//#endregion
|
|
87
|
-
export { AuthPlannerHostProvider, AuthPlannerHostProviderProps, AuthRequirementsClientSetProvider, AuthRequirementsClientSetProviderProps, resolveEffectiveClientSet, useAuthPlannerHost, useEffectiveClientSet };
|
|
111
|
+
export { AuthPlannerHostProvider, AuthPlannerHostProviderProps, AuthRequirementsClientSetProvider, AuthRequirementsClientSetProviderProps, ClientEnvironmentServiceProvider, ClientEnvironmentServiceProviderProps, resolveEffectiveClientSet, useAuthPlannerHost, useClientEnvironment, useClientEnvironmentService, useEffectiveClientSet, usePageClientEnvironment, useWebClientEnvironment };
|
|
88
112
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ClientEnvironmentService } from "@securitydept/client/web";
|
|
2
2
|
import { createContext, useContext, useMemo } from "react";
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
import { createPlannerHost, resolveEffectiveClientSet } from "@securitydept/client/auth-coordination";
|
|
5
|
+
//#region src/environment-service/index.tsx
|
|
6
|
+
const ClientEnvironmentServiceContext = createContext(null);
|
|
7
|
+
function ClientEnvironmentServiceProvider({ children, service }) {
|
|
8
|
+
const resolvedService = useMemo(() => service ?? new ClientEnvironmentService(), [service]);
|
|
9
|
+
return /* @__PURE__ */ jsx(ClientEnvironmentServiceContext.Provider, {
|
|
10
|
+
value: resolvedService,
|
|
11
|
+
children
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
function useClientEnvironmentService() {
|
|
15
|
+
const service = useContext(ClientEnvironmentServiceContext);
|
|
16
|
+
if (!service) throw new Error("[useClientEnvironmentService] No ClientEnvironmentServiceProvider found in the component tree. Wrap your app or route with <ClientEnvironmentServiceProvider>.");
|
|
17
|
+
return service;
|
|
18
|
+
}
|
|
19
|
+
function useClientEnvironment() {
|
|
20
|
+
return useClientEnvironmentService().readClientEnvironment();
|
|
21
|
+
}
|
|
22
|
+
function useWebClientEnvironment() {
|
|
23
|
+
return useClientEnvironmentService().readWebEnvironment();
|
|
24
|
+
}
|
|
25
|
+
function usePageClientEnvironment() {
|
|
26
|
+
return useClientEnvironmentService().readPageEnvironment();
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
4
29
|
//#region src/planner-host/index.tsx
|
|
5
30
|
const PlannerHostContext = createContext(null);
|
|
6
31
|
/**
|
|
@@ -80,6 +105,6 @@ function useEffectiveClientSet() {
|
|
|
80
105
|
return useContext(RequirementsClientSetContext);
|
|
81
106
|
}
|
|
82
107
|
//#endregion
|
|
83
|
-
export { AuthPlannerHostProvider, AuthRequirementsClientSetProvider, resolveEffectiveClientSet, useAuthPlannerHost, useEffectiveClientSet };
|
|
108
|
+
export { AuthPlannerHostProvider, AuthRequirementsClientSetProvider, ClientEnvironmentServiceProvider, resolveEffectiveClientSet, useAuthPlannerHost, useClientEnvironment, useClientEnvironmentService, useEffectiveClientSet, usePageClientEnvironment, useWebClientEnvironment };
|
|
84
109
|
|
|
85
110
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/environment-service/index.tsx","../src/planner-host/index.tsx"],"sourcesContent":["import type {\n\tClientEnvironment,\n\tPageClientEnvironment,\n\tWebClientEnvironment,\n} from \"@securitydept/client/web\";\nimport { ClientEnvironmentService } from \"@securitydept/client/web\";\nimport { createContext, type ReactNode, useContext, useMemo } from \"react\";\n\ninterface ClientEnvironmentServiceContextValue {\n\treadClientEnvironment(): ClientEnvironment;\n\treadWebEnvironment(): WebClientEnvironment;\n\treadPageEnvironment(): PageClientEnvironment;\n\tresolveClientEnvironment(): Promise<ClientEnvironment>;\n\tresolveWebEnvironment(): Promise<WebClientEnvironment>;\n\tresolvePageEnvironment(): Promise<PageClientEnvironment>;\n\treset(): void;\n}\n\nconst ClientEnvironmentServiceContext =\n\tcreateContext<ClientEnvironmentServiceContextValue | null>(null);\n\nexport interface ClientEnvironmentServiceProviderProps {\n\tchildren?: ReactNode;\n\tservice?: ClientEnvironmentServiceContextValue;\n}\n\nexport function ClientEnvironmentServiceProvider({\n\tchildren,\n\tservice,\n}: ClientEnvironmentServiceProviderProps) {\n\tconst resolvedService = useMemo(\n\t\t() => service ?? new ClientEnvironmentService(),\n\t\t[service],\n\t);\n\n\treturn (\n\t\t<ClientEnvironmentServiceContext.Provider value={resolvedService}>\n\t\t\t{children}\n\t\t</ClientEnvironmentServiceContext.Provider>\n\t);\n}\n\nexport function useClientEnvironmentService<\n\tTClientEnvironment extends ClientEnvironment = ClientEnvironment,\n\tTWebEnvironment extends WebClientEnvironment = WebClientEnvironment,\n\tTPageEnvironment extends PageClientEnvironment = PageClientEnvironment,\n>(): ClientEnvironmentService<\n\tTClientEnvironment,\n\tTWebEnvironment,\n\tTPageEnvironment\n> {\n\tconst service = useContext(ClientEnvironmentServiceContext);\n\tif (!service) {\n\t\tthrow new Error(\n\t\t\t\"[useClientEnvironmentService] No ClientEnvironmentServiceProvider found in the component tree. Wrap your app or route with <ClientEnvironmentServiceProvider>.\",\n\t\t);\n\t}\n\n\treturn service as ClientEnvironmentService<\n\t\tTClientEnvironment,\n\t\tTWebEnvironment,\n\t\tTPageEnvironment\n\t>;\n}\n\nexport function useClientEnvironment<\n\tTClientEnvironment extends ClientEnvironment = ClientEnvironment,\n\tTWebEnvironment extends WebClientEnvironment = WebClientEnvironment,\n\tTPageEnvironment extends PageClientEnvironment = PageClientEnvironment,\n>(): TClientEnvironment {\n\treturn useClientEnvironmentService<\n\t\tTClientEnvironment,\n\t\tTWebEnvironment,\n\t\tTPageEnvironment\n\t>().readClientEnvironment();\n}\n\nexport function useWebClientEnvironment<\n\tTClientEnvironment extends ClientEnvironment = ClientEnvironment,\n\tTWebEnvironment extends WebClientEnvironment = WebClientEnvironment,\n\tTPageEnvironment extends PageClientEnvironment = PageClientEnvironment,\n>(): TWebEnvironment {\n\treturn useClientEnvironmentService<\n\t\tTClientEnvironment,\n\t\tTWebEnvironment,\n\t\tTPageEnvironment\n\t>().readWebEnvironment();\n}\n\nexport function usePageClientEnvironment<\n\tTClientEnvironment extends ClientEnvironment = ClientEnvironment,\n\tTWebEnvironment extends WebClientEnvironment = WebClientEnvironment,\n\tTPageEnvironment extends PageClientEnvironment = PageClientEnvironment,\n>(): TPageEnvironment {\n\treturn useClientEnvironmentService<\n\t\tTClientEnvironment,\n\t\tTWebEnvironment,\n\t\tTPageEnvironment\n\t>().readPageEnvironment();\n}\n","// 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":";;;;;AAkBA,MAAM,kCACL,cAA2D,KAAK;AAOjE,SAAgB,iCAAiC,EAChD,UACA,WACyC;CACzC,MAAM,kBAAkB,cACjB,WAAW,IAAI,0BAA0B,EAC/C,CAAC,QAAQ,CACT;CAED,OACC,oBAAC,gCAAgC,UAAjC;EAA0C,OAAO;EAC/C;EACyC,CAAA;;AAI7C,SAAgB,8BAQd;CACD,MAAM,UAAU,WAAW,gCAAgC;CAC3D,IAAI,CAAC,SACJ,MAAM,IAAI,MACT,iKACA;CAGF,OAAO;;AAOR,SAAgB,uBAIQ;CACvB,OAAO,6BAIJ,CAAC,uBAAuB;;AAG5B,SAAgB,0BAIK;CACpB,OAAO,6BAIJ,CAAC,oBAAoB;;AAGzB,SAAgB,2BAIM;CACrB,OAAO,6BAIJ,CAAC,qBAAqB;;;;AChE1B,MAAM,qBAAqB,cAAkC,KAAK;;;;;;;;;;;;;;;;;;;;AAsClE,SAAgB,wBAAwB,EACvC,iBACA,aACA,YACgC;CAChC,MAAM,OAAO,cAAc;EAC1B,IAAI,aAAa,OAAO;EACxB,OAAO,kBAAkB,kBAAkB,EAAE,iBAAiB,GAAG,KAAA,EAAU;IACzE,CAAC,aAAa,gBAAgB,CAAC;CAElC,OACC,oBAAC,mBAAmB,UAApB;EAA6B,OAAO;EAClC;EAC4B,CAAA;;;;;;;;AAUhC,SAAgB,qBAAkC;CACjD,MAAM,OAAO,WAAW,mBAAmB;CAC3C,IAAI,CAAC,MACJ,MAAM,IAAI,MACT,sIAEA;CAEF,OAAO;;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;CAED,OACC,oBAAC,6BAA6B,UAA9B;EAAuC,OAAO;EAC5C;EACsC,CAAA;;;;;;;AAS1C,SAAgB,wBAA0D;CACzE,OAAO,WAAW,6BAA6B"}
|
|
@@ -152,10 +152,12 @@ function projectTanStackRouteMatches(matches, options) {
|
|
|
152
152
|
*/
|
|
153
153
|
function createTanStackRouteActivator(orchestrator, options) {
|
|
154
154
|
return {
|
|
155
|
+
/** Project matches and activate the route chain on the orchestrator. */
|
|
155
156
|
activate(matches) {
|
|
156
157
|
const chain = projectTanStackRouteMatches(matches, options);
|
|
157
158
|
orchestrator.activateMatchedRoutes(chain);
|
|
158
159
|
},
|
|
160
|
+
/** Deactivate the current route (e.g. on unmount or navigation away). */
|
|
159
161
|
deactivate() {
|
|
160
162
|
orchestrator.deactivateRoute();
|
|
161
163
|
}
|
|
@@ -198,6 +200,12 @@ function createTanStackRouteActivator(orchestrator, options) {
|
|
|
198
200
|
*/
|
|
199
201
|
function createTanStackRouteSecurityPolicy(options = {}) {
|
|
200
202
|
return {
|
|
203
|
+
/**
|
|
204
|
+
* Evaluate the route security policy for a matched route chain.
|
|
205
|
+
*
|
|
206
|
+
* @param matches - The matched route chain (root → leaf).
|
|
207
|
+
* @param checkAuthenticated - Returns true if a requirement is met.
|
|
208
|
+
*/
|
|
201
209
|
evaluate(matches, checkAuthenticated, context) {
|
|
202
210
|
const effectiveRequirements = extractTanStackRouteRequirements(matches, { requirementsKey: options.requirementsKey });
|
|
203
211
|
const handlerContext = context ?? createFallbackUnauthenticatedContext(matches);
|
|
@@ -209,6 +217,10 @@ function createTanStackRouteSecurityPolicy(options = {}) {
|
|
|
209
217
|
effectiveRequirements
|
|
210
218
|
};
|
|
211
219
|
},
|
|
220
|
+
/**
|
|
221
|
+
* Async-capable evaluation for router guards that must wait for auth
|
|
222
|
+
* freshness checks before deciding whether navigation is allowed.
|
|
223
|
+
*/
|
|
212
224
|
async evaluateAsync(matches, checkAuthenticated, context) {
|
|
213
225
|
const effectiveRequirements = extractTanStackRouteRequirements(matches, { requirementsKey: options.requirementsKey });
|
|
214
226
|
const handlerContext = context ?? createFallbackUnauthenticatedContext(matches);
|
|
@@ -227,6 +239,8 @@ function createTanStackRouteSecurityPolicy(options = {}) {
|
|
|
227
239
|
* requirement is unmet and the handler returns `false`.
|
|
228
240
|
*/
|
|
229
241
|
var RouteSecurityBlockedError = class extends Error {
|
|
242
|
+
requirement;
|
|
243
|
+
result;
|
|
230
244
|
constructor(requirement, result) {
|
|
231
245
|
super(`[secureBeforeLoad] Navigation blocked: requirement "${requirement.id}" (kind: ${requirement.kind}) is not authenticated.`);
|
|
232
246
|
this.requirement = requirement;
|
|
@@ -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\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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"}
|
|
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.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.3",
|
|
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.3.0-beta.
|
|
44
|
+
"@securitydept/client": "0.3.0-beta.3"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"react": ">=19.0.0",
|
|
@@ -52,8 +52,12 @@
|
|
|
52
52
|
"optional": true
|
|
53
53
|
}
|
|
54
54
|
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@tanstack/react-router": "^1.169.2",
|
|
57
|
+
"react": "^19.2.6"
|
|
58
|
+
},
|
|
55
59
|
"scripts": {
|
|
56
60
|
"build": "tsdown",
|
|
57
|
-
"typecheck": "tsc -b"
|
|
61
|
+
"typecheck": "tsc -b --force"
|
|
58
62
|
}
|
|
59
63
|
}
|