@securitydept/client-react 0.3.0-beta.3 → 0.3.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/index.d.mts +98 -105
- package/dist/index.mjs +398 -90
- package/dist/index.mjs.map +1 -1
- package/dist/tanstack-router/index.d.mts +76 -415
- package/dist/tanstack-router/index.mjs +95 -393
- package/dist/tanstack-router/index.mjs.map +1 -1
- package/package.json +14 -7
|
@@ -1,416 +1,118 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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 : [];
|
|
1
|
+
import { ENVIRONMENT_TOKEN, PipelineOutcome, RequirementPlannerHost, RouteCompositionRequirementPlanner, RouterNavigationMode, UriReferenceString, UriReferenceStringSchema, injectRequirementPlannerHost, readSecuritydeptRouteMetadata, throwValidationClientError, validateTraitInput, writeSecuritydeptRouteMetadata } from "@securitydept/client";
|
|
2
|
+
import { redirect } from "@tanstack/react-router";
|
|
3
|
+
import { type } from "arktype";
|
|
4
|
+
//#region src/tanstack-router/auth-coordination/route-metadata.ts
|
|
5
|
+
function projectTanStackRouteSegments(matches) {
|
|
6
|
+
return matches.map((match, index) => {
|
|
7
|
+
const metadata = readSecuritydeptRouteMetadata(match.staticData);
|
|
126
8
|
return {
|
|
127
|
-
routeId: match.routeId
|
|
128
|
-
requirements
|
|
9
|
+
routeId: match.routeId ?? match.id ?? match.pathname ?? `tanstack-route-${index}`,
|
|
10
|
+
requirements: metadata?.requirements ?? [],
|
|
11
|
+
composition: metadata?.composition
|
|
129
12
|
};
|
|
130
13
|
});
|
|
131
14
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
/** Project matches and activate the route chain on the orchestrator. */
|
|
156
|
-
activate(matches) {
|
|
157
|
-
const chain = projectTanStackRouteMatches(matches, options);
|
|
158
|
-
orchestrator.activateMatchedRoutes(chain);
|
|
159
|
-
},
|
|
160
|
-
/** Deactivate the current route (e.g. on unmount or navigation away). */
|
|
161
|
-
deactivate() {
|
|
162
|
-
orchestrator.deactivateRoute();
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Create a root-level route security policy that evaluates a TanStack Router
|
|
168
|
-
* matched route chain and returns the enforcement decision.
|
|
169
|
-
*
|
|
170
|
-
* This is the TanStack Router equivalent of Angular's `secureRouteRoot()`.
|
|
171
|
-
* It centralizes non-serializable runtime policy (handlers, redirects) at
|
|
172
|
-
* the root level, while individual routes only declare serializable metadata
|
|
173
|
-
* via `withTanStackRouteRequirements()`.
|
|
174
|
-
*
|
|
175
|
-
* The returned `evaluate()` function:
|
|
176
|
-
* 1. Extracts requirements from the matched route chain (with composition)
|
|
177
|
-
* 2. Finds the first unmet requirement using the provided `checkAuthenticated`
|
|
178
|
-
* 3. Resolves the handler action (kind handler → default handler → block)
|
|
179
|
-
*
|
|
180
|
-
* @example
|
|
181
|
-
* ```ts
|
|
182
|
-
* import { createTanStackRouteSecurityPolicy } from "@securitydept/client-react/tanstack-router";
|
|
183
|
-
*
|
|
184
|
-
* const policy = createTanStackRouteSecurityPolicy({
|
|
185
|
-
* requirementHandlers: {
|
|
186
|
-
* frontend_oidc: (req) => {
|
|
187
|
-
* redirectToOidcLogin(req);
|
|
188
|
-
* return false;
|
|
189
|
-
* },
|
|
190
|
-
* },
|
|
191
|
-
* defaultOnUnauthenticated: () => "/login",
|
|
192
|
-
* });
|
|
193
|
-
*
|
|
194
|
-
* // In a route's beforeLoad or a React effect:
|
|
195
|
-
* const result = policy.evaluate(matches, (req) => isAuthenticated(req.kind));
|
|
196
|
-
* if (!result.allMet && typeof result.action === "string") {
|
|
197
|
-
* navigate({ to: result.action });
|
|
198
|
-
* }
|
|
199
|
-
* ```
|
|
200
|
-
*/
|
|
201
|
-
function createTanStackRouteSecurityPolicy(options = {}) {
|
|
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
|
-
*/
|
|
209
|
-
evaluate(matches, checkAuthenticated, context) {
|
|
210
|
-
const effectiveRequirements = extractTanStackRouteRequirements(matches, { requirementsKey: options.requirementsKey });
|
|
211
|
-
const handlerContext = context ?? createFallbackUnauthenticatedContext(matches);
|
|
212
|
-
for (const req of effectiveRequirements) if (!checkAuthenticated(req)) return createTanStackUnauthenticatedResult(req, effectiveRequirements, handlerContext, options);
|
|
213
|
-
return {
|
|
214
|
-
allMet: true,
|
|
215
|
-
pendingRequirement: void 0,
|
|
216
|
-
action: void 0,
|
|
217
|
-
effectiveRequirements
|
|
218
|
-
};
|
|
219
|
-
},
|
|
220
|
-
/**
|
|
221
|
-
* Async-capable evaluation for router guards that must wait for auth
|
|
222
|
-
* freshness checks before deciding whether navigation is allowed.
|
|
223
|
-
*/
|
|
224
|
-
async evaluateAsync(matches, checkAuthenticated, context) {
|
|
225
|
-
const effectiveRequirements = extractTanStackRouteRequirements(matches, { requirementsKey: options.requirementsKey });
|
|
226
|
-
const handlerContext = context ?? createFallbackUnauthenticatedContext(matches);
|
|
227
|
-
for (const req of effectiveRequirements) if (!await checkAuthenticated(req)) return createTanStackUnauthenticatedResult(req, effectiveRequirements, handlerContext, options);
|
|
228
|
-
return {
|
|
229
|
-
allMet: true,
|
|
230
|
-
pendingRequirement: void 0,
|
|
231
|
-
action: void 0,
|
|
232
|
-
effectiveRequirements
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Navigation-blocked error thrown by {@link createSecureBeforeLoad} when a
|
|
239
|
-
* requirement is unmet and the handler returns `false`.
|
|
240
|
-
*/
|
|
241
|
-
var RouteSecurityBlockedError = class extends Error {
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/tanstack-router/auth-coordination/route-guard.ts
|
|
17
|
+
var TanStackRouteSecurityBlockedError = class extends Error {
|
|
242
18
|
requirement;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
super(`[secureBeforeLoad] Navigation blocked: requirement "${requirement.id}" (kind: ${requirement.kind}) is not authenticated.`);
|
|
19
|
+
constructor(requirement) {
|
|
20
|
+
super(`TanStack route security blocked requirement: ${requirement.id}`);
|
|
246
21
|
this.requirement = requirement;
|
|
247
|
-
this.
|
|
248
|
-
this.name = "RouteSecurityBlockedError";
|
|
22
|
+
this.name = "TanStackRouteSecurityBlockedError";
|
|
249
23
|
}
|
|
250
24
|
};
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
*
|
|
267
|
-
* @example
|
|
268
|
-
* ```ts
|
|
269
|
-
* import { redirect, createRoute } from "@tanstack/react-router";
|
|
270
|
-
* import {
|
|
271
|
-
* createSecureBeforeLoad,
|
|
272
|
-
* withTanStackRouteRequirements,
|
|
273
|
-
* } from "@securitydept/client-react/tanstack-router";
|
|
274
|
-
*
|
|
275
|
-
* // Root-level: non-serializable runtime policy
|
|
276
|
-
* const securedBeforeLoad = createSecureBeforeLoad({
|
|
277
|
-
* redirect,
|
|
278
|
-
* checkAuthenticated: (req) => authStore.isAuthenticated(req.kind),
|
|
279
|
-
* requirementHandlers: {
|
|
280
|
-
* frontend_oidc: (req) => "/login/oidc",
|
|
281
|
-
* session: () => "/login",
|
|
282
|
-
* },
|
|
283
|
-
* });
|
|
284
|
-
*
|
|
285
|
-
* // Root route — attach the beforeLoad
|
|
286
|
-
* const rootRoute = createRootRoute({
|
|
287
|
-
* beforeLoad: securedBeforeLoad,
|
|
288
|
-
* });
|
|
289
|
-
*
|
|
290
|
-
* // Child routes — serializable declaration only
|
|
291
|
-
* const dashboardRoute = createRoute({
|
|
292
|
-
* getParentRoute: () => rootRoute,
|
|
293
|
-
* path: "dashboard",
|
|
294
|
-
* staticData: withTanStackRouteRequirements([
|
|
295
|
-
* { id: "session", kind: "session" },
|
|
296
|
-
* ]),
|
|
297
|
-
* });
|
|
298
|
-
*
|
|
299
|
-
* const adminRoute = createRoute({
|
|
300
|
-
* getParentRoute: () => dashboardRoute,
|
|
301
|
-
* path: "admin",
|
|
302
|
-
* staticData: withTanStackRouteRequirements([
|
|
303
|
-
* { id: "admin-oidc", kind: "frontend_oidc" },
|
|
304
|
-
* ]),
|
|
305
|
-
* });
|
|
306
|
-
* ```
|
|
307
|
-
*/
|
|
308
|
-
function createSecureBeforeLoad(options) {
|
|
309
|
-
return function secureBeforeLoad(ctx) {
|
|
310
|
-
const handlerContext = createUnauthenticatedContext(ctx);
|
|
311
|
-
const result = evaluateRouteSecurityMaybeAsync(ctx.matches, options.checkAuthenticated, handlerContext, options);
|
|
312
|
-
if (isPromiseLike(result)) return result.then((resolved) => handleSecureBeforeLoadResult(resolved, options));
|
|
313
|
-
return handleSecureBeforeLoadResult(result, options);
|
|
25
|
+
function createTanStackBeforeLoad(options) {
|
|
26
|
+
return async (context) => {
|
|
27
|
+
const injector = context.context.securitydeptInjector;
|
|
28
|
+
const parentHost = injectRequirementPlannerHost({ injector });
|
|
29
|
+
const host = options?.plannerHost ?? (options?.behaviour ? RequirementPlannerHost.fromBehaviour(typeof options.behaviour === "function" ? options.behaviour() : options.behaviour, {
|
|
30
|
+
parent: parentHost ?? void 0,
|
|
31
|
+
environment: injector.get(ENVIRONMENT_TOKEN, null) ?? void 0
|
|
32
|
+
}) : parentHost) ?? RequirementPlannerHost.fromBehaviour({});
|
|
33
|
+
const segments = projectTanStackRouteSegments(context.matches);
|
|
34
|
+
const result = await RouteCompositionRequirementPlanner.fromRouteSegments(host, segments, { url: context.location.href }).runUntilSettled();
|
|
35
|
+
switch (result.outcome) {
|
|
36
|
+
case PipelineOutcome.Settled: return;
|
|
37
|
+
case PipelineOutcome.Blocked: throw new TanStackRouteSecurityBlockedError(result.requirement);
|
|
38
|
+
case PipelineOutcome.Redirect: throw redirect({ to: result.location });
|
|
39
|
+
}
|
|
314
40
|
};
|
|
315
41
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (
|
|
321
|
-
return
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Build a route-security handler for full-page external redirects.
|
|
325
|
-
*
|
|
326
|
-
* The callback should start the browser navigation, typically by calling an
|
|
327
|
-
* auth client's `loginWithRedirect({ postAuthRedirectUri: context.attemptedUrl })`.
|
|
328
|
-
* After that navigation has been started, the returned guard result never
|
|
329
|
-
* settles so TanStack Router does not finalize an in-app blocked navigation
|
|
330
|
-
* while the page is leaving for an external IdP.
|
|
331
|
-
*/
|
|
332
|
-
function createExternalRedirectBeforeLoadHandler(startRedirect) {
|
|
333
|
-
return async (requirement, context) => {
|
|
334
|
-
await startRedirect(requirement, context);
|
|
335
|
-
return await neverSettlingBeforeLoadResult();
|
|
336
|
-
};
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/tanstack-router/auth-coordination/secure-routes.ts
|
|
44
|
+
function createTanStackRouterContext(options = {}) {
|
|
45
|
+
const securitydeptInjector = options.injector ?? options.environment?.injector;
|
|
46
|
+
if (!securitydeptInjector) throw new Error("[createTanStackRouterContext] Missing SecurityDept injector. Pass `injector` or `environment`.");
|
|
47
|
+
return { securitydeptInjector };
|
|
337
48
|
}
|
|
338
|
-
function
|
|
339
|
-
return {
|
|
340
|
-
location: ctx.location,
|
|
341
|
-
attemptedUrl: ctx.location.href || ctx.location.pathname,
|
|
342
|
-
matches: ctx.matches,
|
|
343
|
-
cause: ctx.cause
|
|
344
|
-
};
|
|
49
|
+
function secureRoute(security, options = {}) {
|
|
50
|
+
return { staticData: writeRequirementStaticData(security, options.staticData) };
|
|
345
51
|
}
|
|
346
|
-
function
|
|
52
|
+
function secureRouteRoot(security, options = {}) {
|
|
53
|
+
const securityBeforeLoad = createTanStackBeforeLoad(security.plannerHost ? { plannerHost: security.plannerHost } : security.behaviour ? { behaviour: security.behaviour } : void 0);
|
|
347
54
|
return {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
href: ""
|
|
351
|
-
},
|
|
352
|
-
attemptedUrl: "",
|
|
353
|
-
matches,
|
|
354
|
-
cause: void 0
|
|
55
|
+
staticData: writeRequirementStaticData(security, options.staticData),
|
|
56
|
+
beforeLoad: securityBeforeLoad
|
|
355
57
|
};
|
|
356
58
|
}
|
|
357
|
-
function
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
effectiveRequirements
|
|
364
|
-
};
|
|
59
|
+
function writeRequirementStaticData(security, base) {
|
|
60
|
+
const patch = {};
|
|
61
|
+
if (security.requirements !== void 0) patch.requirements = security.requirements;
|
|
62
|
+
if (security.composition !== void 0) patch.composition = security.composition;
|
|
63
|
+
if (Object.keys(patch).length === 0) return { ...base ?? {} };
|
|
64
|
+
return writeSecuritydeptRouteMetadata(base, patch);
|
|
365
65
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/tanstack-router/router.ts
|
|
68
|
+
const TanStackRouterNavigationLikeSchema = type({
|
|
69
|
+
state: "object?",
|
|
70
|
+
navigate: "Function"
|
|
71
|
+
});
|
|
72
|
+
const ResolvedRouterForTanStackRouterCreateOptionsSchema = type({
|
|
73
|
+
router: TanStackRouterNavigationLikeSchema,
|
|
74
|
+
currentUrl: UriReferenceStringSchema.or("null")
|
|
75
|
+
});
|
|
76
|
+
function createRouterForTanStackRouter(options) {
|
|
77
|
+
const routerInput = options.router;
|
|
78
|
+
const resolvedCreateOptions = {
|
|
79
|
+
router: options.router,
|
|
80
|
+
get currentUrl() {
|
|
81
|
+
if (Object.hasOwn(options, "currentUrl")) return options.currentUrl ?? null;
|
|
82
|
+
return routerInput?.state?.location?.href ?? null;
|
|
83
|
+
}
|
|
380
84
|
};
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
85
|
+
validateTraitInput({
|
|
86
|
+
value: resolvedCreateOptions,
|
|
87
|
+
bundledSchema: ResolvedRouterForTanStackRouterCreateOptionsSchema,
|
|
88
|
+
validator: options.validators?.router,
|
|
89
|
+
onInvalid: (failure) => throwValidationClientError({
|
|
90
|
+
code: "client_react.tanstack_router.invalid_router_options",
|
|
91
|
+
source: "client-react",
|
|
92
|
+
messagePrefix: "createRouterForTanStackRouter could not validate routerForTanStackRouterCreateOptions",
|
|
93
|
+
failure
|
|
94
|
+
})
|
|
95
|
+
});
|
|
96
|
+
const { router } = resolvedCreateOptions;
|
|
390
97
|
return {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
98
|
+
currentUrl() {
|
|
99
|
+
const currentUrl = resolvedCreateOptions.currentUrl;
|
|
100
|
+
return currentUrl == null ? null : UriReferenceString.parse(currentUrl);
|
|
101
|
+
},
|
|
102
|
+
async navigate(request) {
|
|
103
|
+
if (request.mode === RouterNavigationMode.External) {
|
|
104
|
+
await router.navigate({ href: request.url.toString() });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
await router.navigate({
|
|
108
|
+
to: request.url.toString(),
|
|
109
|
+
replace: request.mode === RouterNavigationMode.Replace,
|
|
110
|
+
state: request.state
|
|
111
|
+
});
|
|
112
|
+
}
|
|
395
113
|
};
|
|
396
114
|
}
|
|
397
|
-
function handleSecureBeforeLoadAction(action, pendingRequirement, result, options) {
|
|
398
|
-
if (typeof action === "string") {
|
|
399
|
-
if (options.redirect) throw options.redirect({ to: action });
|
|
400
|
-
throw new RouteSecurityBlockedError(pendingRequirement, result);
|
|
401
|
-
}
|
|
402
|
-
throw new RouteSecurityBlockedError(pendingRequirement, result);
|
|
403
|
-
}
|
|
404
|
-
function isPromiseLike(value) {
|
|
405
|
-
return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
|
|
406
|
-
}
|
|
407
|
-
function neverSettlingBeforeLoadResult() {
|
|
408
|
-
return new Promise(() => {});
|
|
409
|
-
}
|
|
410
|
-
function isValidComposition(raw) {
|
|
411
|
-
return raw === RequirementsClientSetComposition.Inherit || raw === RequirementsClientSetComposition.Merge || raw === RequirementsClientSetComposition.Replace;
|
|
412
|
-
}
|
|
413
115
|
//#endregion
|
|
414
|
-
export {
|
|
116
|
+
export { TanStackRouteSecurityBlockedError, createRouterForTanStackRouter, createTanStackBeforeLoad, createTanStackRouterContext, projectTanStackRouteSegments, secureRoute, secureRouteRoot };
|
|
415
117
|
|
|
416
118
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -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":["defineType"],"sources":["../../src/tanstack-router/auth-coordination/route-metadata.ts","../../src/tanstack-router/auth-coordination/route-guard.ts","../../src/tanstack-router/auth-coordination/secure-routes.ts","../../src/tanstack-router/router.ts"],"sourcesContent":["import {\n\ttype AuthRequirement,\n\ttype RouteTreeSegment,\n\treadSecuritydeptRouteMetadata,\n} from \"@securitydept/client\";\n\nexport interface TanStackRouteMatchLike {\n\treadonly id?: string;\n\treadonly routeId?: string;\n\treadonly pathname?: string;\n\treadonly staticData?: object;\n}\n\nexport function projectTanStackRouteSegments<\n\tTAuthRequirement extends AuthRequirement = AuthRequirement,\n>(\n\tmatches: readonly TanStackRouteMatchLike[],\n): RouteTreeSegment<TAuthRequirement>[] {\n\treturn matches.map((match, index) => {\n\t\tconst metadata = readSecuritydeptRouteMetadata<TAuthRequirement>(\n\t\t\tmatch.staticData,\n\t\t);\n\t\treturn {\n\t\t\trouteId:\n\t\t\t\tmatch.routeId ??\n\t\t\t\tmatch.id ??\n\t\t\t\tmatch.pathname ??\n\t\t\t\t`tanstack-route-${index}`,\n\t\t\trequirements: metadata?.requirements ?? [],\n\t\t\tcomposition: metadata?.composition,\n\t\t};\n\t});\n}\n","import {\n\ttype AuthRequirement,\n\tENVIRONMENT_TOKEN,\n\tinjectRequirementPlannerHost,\n\tPipelineOutcome,\n\ttype RequirementBehaviourWithRouteContext,\n\tRequirementPlannerHost,\n\ttype RequirementPlannerHostBehaviour,\n\ttype RouteBehaviourContextExtra,\n\tRouteCompositionRequirementPlanner,\n\ttype SecuritydeptInjectorTrait,\n} from \"@securitydept/client\";\nimport { redirect } from \"@tanstack/react-router\";\nimport {\n\tprojectTanStackRouteSegments,\n\ttype TanStackRouteMatchLike,\n} from \"./route-metadata\";\n\nexport interface SecuritydeptTanStackRouterContext {\n\treadonly securitydeptInjector: SecuritydeptInjectorTrait;\n\treadonly router?: {\n\t\treadonly state: {\n\t\t\treadonly matches: readonly TanStackRouteMatchLike[];\n\t\t};\n\t};\n}\n\nexport interface TanStackBeforeLoadContextLike {\n\treadonly context: SecuritydeptTanStackRouterContext;\n\treadonly matches: readonly TanStackRouteMatchLike[];\n\treadonly location: {\n\t\treadonly href: string;\n\t};\n}\n\nexport interface CreateTanStackBeforeLoadWithPlannerHostOptions<\n\tTAuthRequirement extends AuthRequirement = AuthRequirement,\n\tTBehaviour extends Partial<\n\t\tRequirementBehaviourWithRouteContext<TAuthRequirement>\n\t> = Partial<RequirementBehaviourWithRouteContext<TAuthRequirement>>,\n> {\n\treadonly plannerHost: RequirementPlannerHost<TBehaviour>;\n\treadonly behaviour?: never;\n}\n\nexport interface CreateTanStackBeforeLoadWithBehaviourOptions<\n\tTAuthRequirement extends AuthRequirement = AuthRequirement,\n\tTBehaviour extends Partial<\n\t\tRequirementBehaviourWithRouteContext<TAuthRequirement>\n\t> = Partial<RequirementBehaviourWithRouteContext<TAuthRequirement>>,\n> {\n\treadonly plannerHost?: never;\n\treadonly behaviour?: RequirementPlannerHostBehaviour<\n\t\tTAuthRequirement,\n\t\tRouteBehaviourContextExtra,\n\t\tTBehaviour\n\t>;\n}\n\nexport type CreateTanStackBeforeLoadOptions<\n\tTAuthRequirement extends AuthRequirement = AuthRequirement,\n\tTBehaviour extends Partial<\n\t\tRequirementBehaviourWithRouteContext<TAuthRequirement>\n\t> = Partial<RequirementBehaviourWithRouteContext<TAuthRequirement>>,\n> =\n\t| CreateTanStackBeforeLoadWithPlannerHostOptions<TAuthRequirement, TBehaviour>\n\t| CreateTanStackBeforeLoadWithBehaviourOptions<TAuthRequirement, TBehaviour>;\n\nexport class TanStackRouteSecurityBlockedError extends Error {\n\tconstructor(readonly requirement: AuthRequirement) {\n\t\tsuper(`TanStack route security blocked requirement: ${requirement.id}`);\n\t\tthis.name = \"TanStackRouteSecurityBlockedError\";\n\t}\n}\n\nexport function createTanStackBeforeLoad<\n\tTAuthRequirement extends AuthRequirement = AuthRequirement,\n\tTBehaviour extends Partial<\n\t\tRequirementBehaviourWithRouteContext<TAuthRequirement>\n\t> = Partial<RequirementBehaviourWithRouteContext<TAuthRequirement>>,\n>(options?: CreateTanStackBeforeLoadOptions<TAuthRequirement, TBehaviour>) {\n\treturn async (context: TanStackBeforeLoadContextLike): Promise<void> => {\n\t\tconst injector = context.context.securitydeptInjector;\n\t\tconst parentHost = injectRequirementPlannerHost<\n\t\t\tTAuthRequirement,\n\t\t\tRouteBehaviourContextExtra,\n\t\t\tTBehaviour\n\t\t>({ injector });\n\t\tconst host =\n\t\t\toptions?.plannerHost ??\n\t\t\t(options?.behaviour\n\t\t\t\t? RequirementPlannerHost.fromBehaviour(\n\t\t\t\t\t\ttypeof options.behaviour === \"function\"\n\t\t\t\t\t\t\t? options.behaviour()\n\t\t\t\t\t\t\t: options.behaviour,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tparent: parentHost ?? undefined,\n\t\t\t\t\t\t\tenvironment: injector.get(ENVIRONMENT_TOKEN, null) ?? undefined,\n\t\t\t\t\t\t},\n\t\t\t\t\t)\n\t\t\t\t: parentHost) ??\n\t\t\tRequirementPlannerHost.fromBehaviour({} as TBehaviour);\n\t\tconst segments = projectTanStackRouteSegments<TAuthRequirement>(\n\t\t\tcontext.matches,\n\t\t);\n\t\tconst planner = RouteCompositionRequirementPlanner.fromRouteSegments(\n\t\t\thost,\n\t\t\tsegments,\n\t\t\t{\n\t\t\t\turl: context.location.href,\n\t\t\t},\n\t\t);\n\t\tconst result = await planner.runUntilSettled();\n\t\tswitch (result.outcome) {\n\t\t\tcase PipelineOutcome.Settled:\n\t\t\t\treturn;\n\t\t\tcase PipelineOutcome.Blocked:\n\t\t\t\tthrow new TanStackRouteSecurityBlockedError(result.requirement);\n\t\t\tcase PipelineOutcome.Redirect:\n\t\t\t\tthrow redirect({ to: result.location });\n\t\t}\n\t};\n}\n","import {\n\ttype AuthRequirement,\n\ttype FoundationEnvironment,\n\ttype RequirementBehaviourWithRouteContext,\n\ttype RequirementPlannerHost,\n\ttype RequirementPlannerHostBehaviour,\n\ttype RequirementsComposition,\n\ttype RouteBehaviourContextExtra,\n\ttype SecuritydeptInjectorTrait,\n\ttype SecuritydeptRouteMetadata,\n\twriteSecuritydeptRouteMetadata,\n} from \"@securitydept/client\";\nimport {\n\tcreateTanStackBeforeLoad,\n\ttype SecuritydeptTanStackRouterContext,\n\ttype TanStackBeforeLoadContextLike,\n} from \"./route-guard\";\n\nexport interface CreateTanStackRouterContextOptions {\n\treadonly environment?: FoundationEnvironment;\n\treadonly injector?: SecuritydeptInjectorTrait;\n}\n\nexport interface TanStackRouteSecurityOptions<\n\tTAuthRequirement extends AuthRequirement = AuthRequirement,\n\tTBehaviour extends Partial<\n\t\tRequirementBehaviourWithRouteContext<TAuthRequirement>\n\t> = Partial<RequirementBehaviourWithRouteContext<TAuthRequirement>>,\n> {\n\treadonly requirements?: readonly TAuthRequirement[];\n\treadonly composition?: RequirementsComposition;\n\treadonly plannerHost?: RequirementPlannerHost<TBehaviour>;\n\treadonly behaviour?: RequirementPlannerHostBehaviour<\n\t\tTAuthRequirement,\n\t\tRouteBehaviourContextExtra,\n\t\tTBehaviour\n\t>;\n}\n\nexport interface CreateTanStackRouteSecurityPatchOptions {\n\treadonly staticData?: object;\n}\n\nexport interface TanStackRouteSecurityPatch {\n\treadonly staticData: Record<string, unknown>;\n}\n\nexport interface TanStackRouteRootSecurityPatch\n\textends TanStackRouteSecurityPatch {\n\treadonly beforeLoad: (\n\t\tcontext: TanStackBeforeLoadContextLike,\n\t) => Promise<void>;\n}\n\nexport function createTanStackRouterContext(\n\toptions: CreateTanStackRouterContextOptions = {},\n): Pick<SecuritydeptTanStackRouterContext, \"securitydeptInjector\"> {\n\tconst securitydeptInjector =\n\t\toptions.injector ?? options.environment?.injector;\n\tif (!securitydeptInjector) {\n\t\tthrow new Error(\n\t\t\t\"[createTanStackRouterContext] Missing SecurityDept injector. Pass `injector` or `environment`.\",\n\t\t);\n\t}\n\treturn { securitydeptInjector };\n}\n\nexport function secureRoute<\n\tTAuthRequirement extends AuthRequirement = AuthRequirement,\n\tTBehaviour extends Partial<\n\t\tRequirementBehaviourWithRouteContext<TAuthRequirement>\n\t> = Partial<RequirementBehaviourWithRouteContext<TAuthRequirement>>,\n>(\n\tsecurity: TanStackRouteSecurityOptions<TAuthRequirement, TBehaviour>,\n\toptions: CreateTanStackRouteSecurityPatchOptions = {},\n): TanStackRouteSecurityPatch {\n\treturn {\n\t\tstaticData: writeRequirementStaticData(security, options.staticData),\n\t};\n}\n\nexport function secureRouteRoot<\n\tTAuthRequirement extends AuthRequirement = AuthRequirement,\n\tTBehaviour extends Partial<\n\t\tRequirementBehaviourWithRouteContext<TAuthRequirement>\n\t> = Partial<RequirementBehaviourWithRouteContext<TAuthRequirement>>,\n>(\n\tsecurity: TanStackRouteSecurityOptions<TAuthRequirement, TBehaviour>,\n\toptions: CreateTanStackRouteSecurityPatchOptions = {},\n): TanStackRouteRootSecurityPatch {\n\tconst securityBeforeLoad = createTanStackBeforeLoad<\n\t\tTAuthRequirement,\n\t\tTBehaviour\n\t>(\n\t\tsecurity.plannerHost\n\t\t\t? { plannerHost: security.plannerHost }\n\t\t\t: security.behaviour\n\t\t\t\t? { behaviour: security.behaviour }\n\t\t\t\t: undefined,\n\t);\n\treturn {\n\t\tstaticData: writeRequirementStaticData(security, options.staticData),\n\t\tbeforeLoad: securityBeforeLoad,\n\t};\n}\n\nfunction writeRequirementStaticData<\n\tTAuthRequirement extends AuthRequirement,\n\tTBehaviour extends Partial<\n\t\tRequirementBehaviourWithRouteContext<TAuthRequirement>\n\t>,\n>(\n\tsecurity: TanStackRouteSecurityOptions<TAuthRequirement, TBehaviour>,\n\tbase: object | undefined,\n): Record<string, unknown> {\n\tconst patch: SecuritydeptRouteMetadata<TAuthRequirement> = {};\n\tif (security.requirements !== undefined) {\n\t\tpatch.requirements = security.requirements;\n\t}\n\tif (security.composition !== undefined) {\n\t\tpatch.composition = security.composition;\n\t}\n\tif (Object.keys(patch).length === 0) {\n\t\treturn { ...(base ?? {}) };\n\t}\n\treturn writeSecuritydeptRouteMetadata(base, patch);\n}\n","import {\n\ttype EnvironmentValidators,\n\tRouterNavigationMode,\n\ttype RouterNavigationRequest,\n\ttype RouterTrait,\n\tthrowValidationClientError,\n\tUriReferenceString,\n\tUriReferenceStringSchema,\n\tvalidateTraitInput,\n\ttype WithTraitInputValidator,\n} from \"@securitydept/client\";\nimport { type AnyRouter, type NavigateOptions } from \"@tanstack/react-router\";\nimport { type as defineType } from \"arktype\";\n\nexport interface TanStackRouterLocationLike {\n\treadonly href: string;\n}\n\nexport interface TanStackRouterNavigationLike {\n\treadonly state?: {\n\t\treadonly location?: TanStackRouterLocationLike;\n\t};\n\tnavigate(options: TanStackRouterNavigateOptions): Promise<unknown> | unknown;\n}\n\nexport type TanStackRouterNavigateOptions = NavigateOptions<\n\tAnyRouter,\n\tstring,\n\tstring | undefined,\n\tstring,\n\t\"\"\n>;\n\nexport interface CreateRouterForTanStackRouterOptions {\n\treadonly router: TanStackRouterNavigationLike;\n\treadonly currentUrl?: string | null;\n}\n\nexport interface ResolvedRouterForTanStackRouterCreateOptions {\n\treadonly router: TanStackRouterNavigationLike;\n\treadonly currentUrl: string | null;\n}\n\nconst TanStackRouterNavigationLikeSchema = defineType({\n\tstate: \"object?\",\n\tnavigate: \"Function\",\n});\n\nconst ResolvedRouterForTanStackRouterCreateOptionsSchema = defineType({\n\trouter: TanStackRouterNavigationLikeSchema,\n\tcurrentUrl: UriReferenceStringSchema.or(\"null\"),\n});\n\nexport function createRouterForTanStackRouter(\n\toptions: CreateRouterForTanStackRouterOptions &\n\t\tWithTraitInputValidator<Pick<EnvironmentValidators, \"router\">>,\n): RouterTrait {\n\tconst routerInput = options.router as\n\t\t| TanStackRouterNavigationLike\n\t\t| null\n\t\t| undefined;\n\tconst resolvedCreateOptions: ResolvedRouterForTanStackRouterCreateOptions = {\n\t\trouter: options.router,\n\t\tget currentUrl() {\n\t\t\tif (Object.hasOwn(options, \"currentUrl\")) {\n\t\t\t\treturn options.currentUrl ?? null;\n\t\t\t}\n\n\t\t\treturn routerInput?.state?.location?.href ?? null;\n\t\t},\n\t};\n\n\tvalidateTraitInput({\n\t\tvalue: resolvedCreateOptions,\n\t\tbundledSchema: ResolvedRouterForTanStackRouterCreateOptionsSchema,\n\t\tvalidator: options.validators?.router,\n\t\tonInvalid: (failure) =>\n\t\t\tthrowValidationClientError({\n\t\t\t\tcode: \"client_react.tanstack_router.invalid_router_options\",\n\t\t\t\tsource: \"client-react\",\n\t\t\t\tmessagePrefix:\n\t\t\t\t\t\"createRouterForTanStackRouter could not validate routerForTanStackRouterCreateOptions\",\n\t\t\t\tfailure,\n\t\t\t}),\n\t});\n\n\tconst { router } = resolvedCreateOptions;\n\treturn {\n\t\tcurrentUrl() {\n\t\t\tconst currentUrl = resolvedCreateOptions.currentUrl;\n\t\t\treturn currentUrl == null ? null : UriReferenceString.parse(currentUrl);\n\t\t},\n\t\tasync navigate(request: RouterNavigationRequest) {\n\t\t\tif (request.mode === RouterNavigationMode.External) {\n\t\t\t\tawait router.navigate({ href: request.url.toString() });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait router.navigate({\n\t\t\t\tto: request.url.toString(),\n\t\t\t\treplace: request.mode === RouterNavigationMode.Replace,\n\t\t\t\tstate: request.state as TanStackRouterNavigateOptions[\"state\"],\n\t\t\t});\n\t\t},\n\t};\n}\n"],"mappings":"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"}
|