@tanstack/start-client-core 1.168.1 → 1.169.0
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/esm/createCsrfMiddleware.d.ts +46 -0
- package/dist/esm/createCsrfMiddleware.js +63 -0
- package/dist/esm/createCsrfMiddleware.js.map +1 -0
- package/dist/esm/createMiddleware.d.ts +4 -0
- package/dist/esm/createMiddleware.js.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -1
- package/package.json +5 -9
- package/skills/start-core/SKILL.md +11 -7
- package/skills/start-core/auth-server-primitives/SKILL.md +410 -0
- package/skills/start-core/deployment/SKILL.md +9 -0
- package/skills/start-core/execution-model/SKILL.md +62 -13
- package/skills/start-core/middleware/SKILL.md +37 -4
- package/skills/start-core/server-functions/SKILL.md +115 -17
- package/src/createCsrfMiddleware.ts +197 -0
- package/src/createMiddleware.ts +4 -0
- package/src/index.tsx +11 -0
- package/src/start-entry.d.ts +2 -2
- package/src/tests/createServerMiddleware.test-d.ts +7 -0
- package/bin/intent.js +0 -25
|
@@ -19,6 +19,7 @@ sources:
|
|
|
19
19
|
|
|
20
20
|
Server functions are type-safe RPCs created with `createServerFn`. They run exclusively on the server but can be called from anywhere — loaders, components, hooks, event handlers, or other server functions.
|
|
21
21
|
|
|
22
|
+
> **CRITICAL**: Server functions are RPC endpoints. They are reachable by direct POST regardless of which route renders the calling UI. **Auth must be enforced inside the handler (or via middleware) — a route `beforeLoad` does NOT protect the RPC.** See [start-core/auth-server-primitives](../auth-server-primitives/SKILL.md) for the session/middleware pattern.
|
|
22
23
|
> **CRITICAL**: Loaders are ISOMORPHIC — they run on BOTH client and server. Database queries, file system access, and secret API keys MUST go inside `createServerFn`, NOT in loaders directly.
|
|
23
24
|
> **CRITICAL**: Do not use `"use server"` directives, `getServerSideProps`, or any Next.js/Remix server patterns. TanStack Start uses `createServerFn` exclusively.
|
|
24
25
|
|
|
@@ -199,16 +200,29 @@ import {
|
|
|
199
200
|
setResponseStatus,
|
|
200
201
|
} from '@tanstack/react-start/server'
|
|
201
202
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const authHeader = getRequestHeader('Authorization')
|
|
205
|
-
|
|
203
|
+
// Public, non-personalized data — safe to cache shared across users.
|
|
204
|
+
const getPublicData = createServerFn({ method: 'GET' }).handler(async () => {
|
|
206
205
|
setResponseHeaders({
|
|
206
|
+
// 'public' is correct ONLY when the response does not depend on identity.
|
|
207
|
+
// For anything tied to a session/user/tenant, use 'private' or 'no-store'.
|
|
207
208
|
'Cache-Control': 'public, max-age=300',
|
|
208
209
|
})
|
|
209
210
|
setResponseStatus(200)
|
|
211
|
+
return fetchPublicData()
|
|
212
|
+
})
|
|
210
213
|
|
|
211
|
-
|
|
214
|
+
// Authenticated data — must NOT be 'public'.
|
|
215
|
+
const getMyData = createServerFn({ method: 'GET' }).handler(async () => {
|
|
216
|
+
const authHeader = getRequestHeader('Authorization')
|
|
217
|
+
// ... auth check ...
|
|
218
|
+
|
|
219
|
+
setResponseHeaders({
|
|
220
|
+
// 'private' = only the user-agent may cache. Vary by Cookie/Authorization
|
|
221
|
+
// so any intermediary that does cache keys by identity, not URL alone.
|
|
222
|
+
'Cache-Control': 'private, max-age=60',
|
|
223
|
+
Vary: 'Cookie, Authorization',
|
|
224
|
+
})
|
|
225
|
+
return fetchPersonalizedData()
|
|
212
226
|
})
|
|
213
227
|
```
|
|
214
228
|
|
|
@@ -254,7 +268,33 @@ Static imports of server functions are safe — the build replaces implementatio
|
|
|
254
268
|
|
|
255
269
|
## Common Mistakes
|
|
256
270
|
|
|
257
|
-
### 1. CRITICAL:
|
|
271
|
+
### 1. CRITICAL: Relying on a route guard to protect a server function
|
|
272
|
+
|
|
273
|
+
A `beforeLoad` redirect protects the **route's UI**, not the **RPC**. `createServerFn` exposes a callable endpoint that an attacker can hit directly — no need to load the route at all. Auth on the route is necessary but not sufficient.
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
// WRONG — the route guard doesn't reach the handler
|
|
277
|
+
const getMyOrders = createServerFn({ method: 'GET' }).handler(async () => {
|
|
278
|
+
return db.orders.findMany() // ← anyone can call the RPC
|
|
279
|
+
})
|
|
280
|
+
export const Route = createFileRoute('/_authenticated/orders')({
|
|
281
|
+
beforeLoad: ({ context }) => {
|
|
282
|
+
if (!context.auth.isAuthenticated) throw redirect({ to: '/login' })
|
|
283
|
+
},
|
|
284
|
+
loader: () => getMyOrders(),
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
// CORRECT — auth enforced on the handler itself
|
|
288
|
+
const getMyOrders = createServerFn({ method: 'GET' })
|
|
289
|
+
.middleware([authMiddleware])
|
|
290
|
+
.handler(async ({ context }) => {
|
|
291
|
+
return db.orders.findMany({ where: { userId: context.session.userId } })
|
|
292
|
+
})
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Apply `authMiddleware` (or an equivalent in-handler check) to **every** `createServerFn` that needs auth. See [start-core/auth-server-primitives](../auth-server-primitives/SKILL.md) for the full session/middleware pattern and [start-core/middleware](../middleware/SKILL.md) for composing the factory.
|
|
296
|
+
|
|
297
|
+
### 2. CRITICAL: Putting server-only code in loaders
|
|
258
298
|
|
|
259
299
|
```tsx
|
|
260
300
|
// WRONG — loader is ISOMORPHIC, runs on BOTH client and server
|
|
@@ -276,22 +316,47 @@ export const Route = createFileRoute('/posts')({
|
|
|
276
316
|
})
|
|
277
317
|
```
|
|
278
318
|
|
|
279
|
-
###
|
|
319
|
+
### 3. CRITICAL: Using Next.js / Remix / React Router DOM patterns
|
|
320
|
+
|
|
321
|
+
If the file lives at `src/pages/`, `app/layout.tsx`, `_app/`, or imports anything from `react-router-dom` or `next/`, it is wrong-framework code. TanStack Start uses `src/routes/` + `createFileRoute` + `createServerFn`.
|
|
280
322
|
|
|
281
323
|
```tsx
|
|
282
324
|
// WRONG — "use server" is a React directive, not used in TanStack Start
|
|
283
325
|
'use server'
|
|
284
326
|
export async function getUser() { ... }
|
|
285
327
|
|
|
286
|
-
// WRONG — getServerSideProps is Next.js
|
|
328
|
+
// WRONG — getServerSideProps is Next.js Pages Router
|
|
287
329
|
export async function getServerSideProps() { ... }
|
|
288
330
|
|
|
289
|
-
//
|
|
331
|
+
// WRONG — Next.js App Router server component data fetching
|
|
332
|
+
export default async function Page() {
|
|
333
|
+
const data = await fetch(...).then(r => r.json())
|
|
334
|
+
return <div>{data}</div>
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// WRONG — Remix
|
|
338
|
+
export async function loader({ request }) { ... }
|
|
339
|
+
export async function action({ request }) { ... }
|
|
340
|
+
|
|
341
|
+
// WRONG — react-router-dom (a different library)
|
|
342
|
+
import { Link, useNavigate } from 'react-router-dom'
|
|
343
|
+
|
|
344
|
+
// CORRECT — TanStack Start
|
|
345
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
346
|
+
import { Link, useNavigate, createFileRoute } from '@tanstack/react-router'
|
|
347
|
+
|
|
290
348
|
const getUser = createServerFn({ method: 'GET' })
|
|
291
349
|
.handler(async () => { ... })
|
|
350
|
+
|
|
351
|
+
export const Route = createFileRoute('/users/$id')({
|
|
352
|
+
loader: ({ params }) => getUser({ data: { id: params.id } }),
|
|
353
|
+
component: UserPage,
|
|
354
|
+
})
|
|
292
355
|
```
|
|
293
356
|
|
|
294
|
-
|
|
357
|
+
If you see `src/pages/`, `app/layout.tsx`, or `react-router-dom` in agent output, the agent is generating for the wrong framework. Build will fail or routes will conflict at runtime.
|
|
358
|
+
|
|
359
|
+
### 4. HIGH: Dynamic imports for server functions
|
|
295
360
|
|
|
296
361
|
```tsx
|
|
297
362
|
// WRONG — can cause bundler issues
|
|
@@ -301,7 +366,7 @@ const { getUser } = await import('~/utils/users.functions')
|
|
|
301
366
|
import { getUser } from '~/utils/users.functions'
|
|
302
367
|
```
|
|
303
368
|
|
|
304
|
-
###
|
|
369
|
+
### 5. HIGH: Awaiting server function without calling it
|
|
305
370
|
|
|
306
371
|
`createServerFn` returns a function — it must be invoked with `()`:
|
|
307
372
|
|
|
@@ -316,20 +381,53 @@ const data = await getItems()
|
|
|
316
381
|
const data = await getItems({ data: { id: '1' } })
|
|
317
382
|
```
|
|
318
383
|
|
|
319
|
-
###
|
|
384
|
+
### 6. CRITICAL: Caching authenticated responses with `Cache-Control: public`
|
|
385
|
+
|
|
386
|
+
`Cache-Control: public, max-age=N` tells every CDN, proxy, and shared cache between you and the user that this response can be served to anyone. If the response depends on the session (user, tenant, role), the first user's response gets cached and replayed to the next user — a cross-tenant data leak.
|
|
387
|
+
|
|
388
|
+
```tsx
|
|
389
|
+
// WRONG — auth'd response, public cache, leaks to next user via CDN
|
|
390
|
+
const getMyOrders = createServerFn({ method: 'GET' }).handler(async () => {
|
|
391
|
+
const session = await requireSession() // identity-dependent
|
|
392
|
+
setResponseHeaders({ 'Cache-Control': 'public, max-age=300' })
|
|
393
|
+
return db.orders.findMany({ where: { userId: session.userId } })
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
// CORRECT — private + Vary so any cache that does store it keys by identity
|
|
397
|
+
const getMyOrders = createServerFn({ method: 'GET' }).handler(async () => {
|
|
398
|
+
const session = await requireSession()
|
|
399
|
+
setResponseHeaders({
|
|
400
|
+
'Cache-Control': 'private, max-age=60',
|
|
401
|
+
Vary: 'Cookie, Authorization',
|
|
402
|
+
})
|
|
403
|
+
return db.orders.findMany({ where: { userId: session.userId } })
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
// ALSO CORRECT — opt out entirely for sensitive data
|
|
407
|
+
setResponseHeaders({ 'Cache-Control': 'no-store' })
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
Rule of thumb: if the handler reads a session/cookie/auth header or branches on identity, the response is **not** `public`. Default to `private` (or `no-store` for sensitive data); reach for `public` only on responses that are byte-for-byte identical regardless of who asks. See also [start-core/deployment](../deployment/SKILL.md) for ISR/Cache-Control on full pages.
|
|
320
411
|
|
|
321
|
-
|
|
412
|
+
### 7. MEDIUM: When to wrap with `useServerFn`
|
|
413
|
+
|
|
414
|
+
`useServerFn` is **required** when the server function uses `throw redirect()` or `throw notFound()` — the hook wires the throw into the router so the redirect actually navigates. For server functions that just return data (call them directly or via `useMutation`/`useQuery`), the hook is optional.
|
|
322
415
|
|
|
323
416
|
```tsx
|
|
324
|
-
//
|
|
417
|
+
// Plain data — direct call is fine (also fine to pass to useMutation/useQuery)
|
|
325
418
|
<button onClick={() => deletePost({ data: { id } })}>Delete</button>
|
|
419
|
+
useMutation({ mutationFn: deletePost })
|
|
326
420
|
|
|
327
|
-
//
|
|
328
|
-
const
|
|
329
|
-
<button onClick={() =>
|
|
421
|
+
// Throws redirect/notFound — MUST wrap with useServerFn so the router handles the throw
|
|
422
|
+
const signupFn = useServerFn(signup) // signup throws redirect on success
|
|
423
|
+
<button onClick={() => signupFn({ data: form })}>Sign up</button>
|
|
330
424
|
```
|
|
331
425
|
|
|
426
|
+
If in doubt: wrap with `useServerFn`. It's a no-op for plain-data functions and the safe default when a function might later add a redirect.
|
|
427
|
+
|
|
332
428
|
## Cross-References
|
|
333
429
|
|
|
334
430
|
- [start-core/execution-model](../execution-model/SKILL.md) — understanding where code runs
|
|
335
431
|
- [start-core/middleware](../middleware/SKILL.md) — composing server functions with middleware
|
|
432
|
+
- [start-core/auth-server-primitives](../auth-server-primitives/SKILL.md) — sessions, cookies, OAuth, CSRF, rate limiting (the server-side half of auth; `getCurrentUser`/`useSession`-style helpers belong here, not at module scope)
|
|
433
|
+
- [router-core/auth-and-guards](../../../../router-core/skills/router-core/auth-and-guards/SKILL.md) — the routing side: route guards do NOT protect server functions, so always re-check auth in the handler or via middleware
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { createIsomorphicFn } from '@tanstack/start-fn-stubs'
|
|
2
|
+
import { createMiddleware } from './createMiddleware'
|
|
3
|
+
import type {
|
|
4
|
+
RequestMiddlewareAfterServer,
|
|
5
|
+
RequestServerOptions,
|
|
6
|
+
} from './createMiddleware'
|
|
7
|
+
import type { Register } from '@tanstack/router-core'
|
|
8
|
+
|
|
9
|
+
export const csrfSymbol = Symbol.for('tanstack-start:csrf-middleware')
|
|
10
|
+
|
|
11
|
+
export type CsrfSecFetchSite =
|
|
12
|
+
| 'same-origin'
|
|
13
|
+
| 'same-site'
|
|
14
|
+
| 'cross-site'
|
|
15
|
+
| 'none'
|
|
16
|
+
|
|
17
|
+
export type CsrfMatcher<TValue, TRegister = Register, TMiddlewares = unknown> =
|
|
18
|
+
| TValue
|
|
19
|
+
| Array<TValue>
|
|
20
|
+
| ((
|
|
21
|
+
value: TValue | (string & {}),
|
|
22
|
+
ctx: RequestServerOptions<TRegister, TMiddlewares>,
|
|
23
|
+
) => boolean | Promise<boolean>)
|
|
24
|
+
|
|
25
|
+
export interface CsrfMiddlewareOptions<
|
|
26
|
+
TRegister = Register,
|
|
27
|
+
TMiddlewares = unknown,
|
|
28
|
+
> {
|
|
29
|
+
/**
|
|
30
|
+
* Return `true` to validate this request, or `false` to skip validation.
|
|
31
|
+
*
|
|
32
|
+
* @default undefined, which validates every request handled by this middleware.
|
|
33
|
+
*/
|
|
34
|
+
filter?: (
|
|
35
|
+
ctx: RequestServerOptions<TRegister, TMiddlewares>,
|
|
36
|
+
) => boolean | Promise<boolean>
|
|
37
|
+
/**
|
|
38
|
+
* Allowed Origin values. Defaults to the trusted request origin.
|
|
39
|
+
*/
|
|
40
|
+
origin?: CsrfMatcher<string, TRegister, TMiddlewares>
|
|
41
|
+
/**
|
|
42
|
+
* Allowed Sec-Fetch-Site values.
|
|
43
|
+
*
|
|
44
|
+
* @default 'same-origin'
|
|
45
|
+
*/
|
|
46
|
+
secFetchSite?: CsrfMatcher<CsrfSecFetchSite, TRegister, TMiddlewares>
|
|
47
|
+
/**
|
|
48
|
+
* Whether to use Referer as a fallback when Sec-Fetch-Site and Origin are absent.
|
|
49
|
+
*
|
|
50
|
+
* @default true
|
|
51
|
+
*/
|
|
52
|
+
referer?:
|
|
53
|
+
| boolean
|
|
54
|
+
| ((
|
|
55
|
+
referer: string,
|
|
56
|
+
ctx: RequestServerOptions<TRegister, TMiddlewares>,
|
|
57
|
+
) => boolean | Promise<boolean>)
|
|
58
|
+
/**
|
|
59
|
+
* Allow requests when Sec-Fetch-Site, Origin, and Referer are all missing.
|
|
60
|
+
*
|
|
61
|
+
* @default false
|
|
62
|
+
*/
|
|
63
|
+
allowRequestsWithoutOriginCheck?: boolean
|
|
64
|
+
/**
|
|
65
|
+
* Optional response returned when CSRF validation fails.
|
|
66
|
+
*
|
|
67
|
+
* @default new Response('Forbidden', { status: 403 })
|
|
68
|
+
*/
|
|
69
|
+
failureResponse?:
|
|
70
|
+
| Response
|
|
71
|
+
| ((
|
|
72
|
+
ctx: RequestServerOptions<TRegister, TMiddlewares>,
|
|
73
|
+
) => Response | Promise<Response>)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type CreateCsrfMiddleware = <TRegister, TMiddlewares>(
|
|
77
|
+
opts?: CsrfMiddlewareOptions<TRegister, TMiddlewares>,
|
|
78
|
+
) => RequestMiddlewareAfterServer<{}, undefined, undefined>
|
|
79
|
+
|
|
80
|
+
const innerCreateCsrfMiddleware: CreateCsrfMiddleware = (opts = {}) => {
|
|
81
|
+
const middleware = createMiddleware().server(async (ctx) => {
|
|
82
|
+
const csrfCtx = ctx as RequestServerOptions<any, any>
|
|
83
|
+
|
|
84
|
+
if (opts.filter && !(await opts.filter(csrfCtx))) {
|
|
85
|
+
return ctx.next()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (await isCsrfRequestAllowed(opts, csrfCtx)) {
|
|
89
|
+
return ctx.next()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return getFailureResponse(opts, csrfCtx)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
96
|
+
Object.defineProperty(middleware, csrfSymbol, { value: true })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return middleware
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const createCsrfMiddleware: CreateCsrfMiddleware =
|
|
103
|
+
createIsomorphicFn().server(innerCreateCsrfMiddleware) as CreateCsrfMiddleware
|
|
104
|
+
|
|
105
|
+
export async function isCsrfRequestAllowed<TRegister, TMiddlewares>(
|
|
106
|
+
opts: CsrfMiddlewareOptions<TRegister, TMiddlewares>,
|
|
107
|
+
ctx: RequestServerOptions<TRegister, TMiddlewares>,
|
|
108
|
+
): Promise<boolean> {
|
|
109
|
+
const result = await getCsrfRequestValidationResult(opts, ctx)
|
|
110
|
+
return (
|
|
111
|
+
result === true ||
|
|
112
|
+
(result === undefined && opts.allowRequestsWithoutOriginCheck === true)
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function getCsrfRequestValidationResult<TRegister, TMiddlewares>(
|
|
117
|
+
opts: CsrfMiddlewareOptions<TRegister, TMiddlewares>,
|
|
118
|
+
ctx: RequestServerOptions<TRegister, TMiddlewares>,
|
|
119
|
+
): Promise<boolean | undefined> {
|
|
120
|
+
const fetchSite = ctx.request.headers.get('Sec-Fetch-Site')
|
|
121
|
+
if (fetchSite !== null) {
|
|
122
|
+
return matchValue(opts.secFetchSite ?? 'same-origin', fetchSite, ctx)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const origin = ctx.request.headers.get('Origin')
|
|
126
|
+
if (origin !== null) {
|
|
127
|
+
if (opts.origin) {
|
|
128
|
+
return matchValue(opts.origin, origin, ctx)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return origin === new URL(ctx.request.url).origin
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const referer = ctx.request.headers.get('Referer')
|
|
135
|
+
if (referer === null || opts.referer === false) {
|
|
136
|
+
return undefined
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (typeof opts.referer === 'function') {
|
|
140
|
+
return opts.referer(referer, ctx)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (opts.origin) {
|
|
144
|
+
const refererOrigin = getOriginFromUrl(referer)
|
|
145
|
+
return (
|
|
146
|
+
refererOrigin !== undefined && matchValue(opts.origin, refererOrigin, ctx)
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return isRefererSameOrigin(referer, new URL(ctx.request.url).origin)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function matchValue<TValue extends string, TRegister, TMiddlewares>(
|
|
154
|
+
matcher: CsrfMatcher<TValue, TRegister, TMiddlewares>,
|
|
155
|
+
value: string,
|
|
156
|
+
ctx: RequestServerOptions<TRegister, TMiddlewares>,
|
|
157
|
+
): Promise<boolean> {
|
|
158
|
+
if (typeof matcher === 'function') {
|
|
159
|
+
return matcher(value, ctx)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (Array.isArray(matcher)) {
|
|
163
|
+
// typescript is dumb for array.includes()
|
|
164
|
+
return matcher.includes(value as TValue)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return value === matcher
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getOriginFromUrl(url: string): string | undefined {
|
|
171
|
+
try {
|
|
172
|
+
return new URL(url).origin
|
|
173
|
+
} catch {
|
|
174
|
+
return undefined
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isRefererSameOrigin(referer: string, requestOrigin: string): boolean {
|
|
179
|
+
if (referer === requestOrigin) return true
|
|
180
|
+
if (!referer.startsWith(requestOrigin)) return false
|
|
181
|
+
if (referer.length === requestOrigin.length) return true
|
|
182
|
+
const code = referer.charCodeAt(requestOrigin.length)
|
|
183
|
+
return code === 47 /* '/' */ || code === 63 /* '?' */ || code === 35 /* '#' */
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function getFailureResponse<TRegister, TMiddlewares>(
|
|
187
|
+
opts: CsrfMiddlewareOptions<TRegister, TMiddlewares>,
|
|
188
|
+
ctx: RequestServerOptions<TRegister, TMiddlewares>,
|
|
189
|
+
): Promise<Response> {
|
|
190
|
+
if (typeof opts.failureResponse === 'function') {
|
|
191
|
+
return opts.failureResponse(ctx)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
opts.failureResponse?.clone() ?? new Response('Forbidden', { status: 403 })
|
|
196
|
+
)
|
|
197
|
+
}
|
package/src/createMiddleware.ts
CHANGED
|
@@ -770,6 +770,10 @@ export interface RequestServerOptions<TRegister, TMiddlewares> {
|
|
|
770
770
|
pathname: string
|
|
771
771
|
context: Expand<AssignAllServerRequestContext<TRegister, TMiddlewares>>
|
|
772
772
|
next: RequestServerNextFn<TRegister, TMiddlewares>
|
|
773
|
+
/**
|
|
774
|
+
* Type of Start handler currently processing this request.
|
|
775
|
+
*/
|
|
776
|
+
handlerType: 'serverFn' | 'router'
|
|
773
777
|
/**
|
|
774
778
|
* Metadata about the server function being invoked.
|
|
775
779
|
* This is only present when the request is handling a server function call.
|
package/src/index.tsx
CHANGED
|
@@ -84,6 +84,17 @@ export {
|
|
|
84
84
|
flattenMiddlewares,
|
|
85
85
|
executeMiddleware,
|
|
86
86
|
} from './createServerFn'
|
|
87
|
+
export {
|
|
88
|
+
createCsrfMiddleware,
|
|
89
|
+
csrfSymbol,
|
|
90
|
+
getCsrfRequestValidationResult,
|
|
91
|
+
isCsrfRequestAllowed,
|
|
92
|
+
} from './createCsrfMiddleware'
|
|
93
|
+
export type {
|
|
94
|
+
CsrfMatcher,
|
|
95
|
+
CsrfMiddlewareOptions,
|
|
96
|
+
CsrfSecFetchSite,
|
|
97
|
+
} from './createCsrfMiddleware'
|
|
87
98
|
|
|
88
99
|
export {
|
|
89
100
|
TSS_FORMDATA_CONTEXT,
|
package/src/start-entry.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
declare module '#tanstack-start-entry' {
|
|
2
|
-
import type { StartEntry } from '
|
|
2
|
+
import type { StartEntry } from './startEntry'
|
|
3
3
|
|
|
4
4
|
export const startInstance: StartEntry['startInstance']
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
declare module '#tanstack-router-entry' {
|
|
8
|
-
import type { RouterEntry } from '
|
|
8
|
+
import type { RouterEntry } from './startEntry'
|
|
9
9
|
|
|
10
10
|
export const getRouter: RouterEntry['getRouter']
|
|
11
11
|
}
|
|
@@ -675,6 +675,7 @@ test('createMiddleware with type request, no middleware or context', () => {
|
|
|
675
675
|
next: RequestServerNextFn<{}, undefined>
|
|
676
676
|
pathname: string
|
|
677
677
|
context: undefined
|
|
678
|
+
handlerType: 'serverFn' | 'router'
|
|
678
679
|
serverFnMeta?: ServerFnMeta
|
|
679
680
|
}>()
|
|
680
681
|
|
|
@@ -698,6 +699,7 @@ test('createMiddleware with type request, no middleware with context', () => {
|
|
|
698
699
|
next: RequestServerNextFn<{}, undefined>
|
|
699
700
|
pathname: string
|
|
700
701
|
context: undefined
|
|
702
|
+
handlerType: 'serverFn' | 'router'
|
|
701
703
|
serverFnMeta?: ServerFnMeta
|
|
702
704
|
}>()
|
|
703
705
|
|
|
@@ -722,6 +724,7 @@ test('createMiddleware with type request, middleware and context', () => {
|
|
|
722
724
|
next: RequestServerNextFn<{}, undefined>
|
|
723
725
|
pathname: string
|
|
724
726
|
context: undefined
|
|
727
|
+
handlerType: 'serverFn' | 'router'
|
|
725
728
|
serverFnMeta?: ServerFnMeta
|
|
726
729
|
}>()
|
|
727
730
|
|
|
@@ -746,6 +749,7 @@ test('createMiddleware with type request, middleware and context', () => {
|
|
|
746
749
|
next: RequestServerNextFn<{}, undefined>
|
|
747
750
|
pathname: string
|
|
748
751
|
context: { a: string }
|
|
752
|
+
handlerType: 'serverFn' | 'router'
|
|
749
753
|
serverFnMeta?: ServerFnMeta
|
|
750
754
|
}>()
|
|
751
755
|
|
|
@@ -769,6 +773,7 @@ test('createMiddleware with type request can return Response directly', () => {
|
|
|
769
773
|
next: RequestServerNextFn<{}, undefined>
|
|
770
774
|
pathname: string
|
|
771
775
|
context: undefined
|
|
776
|
+
handlerType: 'serverFn' | 'router'
|
|
772
777
|
serverFnMeta?: ServerFnMeta
|
|
773
778
|
}>()
|
|
774
779
|
|
|
@@ -789,6 +794,7 @@ test('createMiddleware with type request can return Promise<Response>', () => {
|
|
|
789
794
|
next: RequestServerNextFn<{}, undefined>
|
|
790
795
|
pathname: string
|
|
791
796
|
context: undefined
|
|
797
|
+
handlerType: 'serverFn' | 'router'
|
|
792
798
|
serverFnMeta?: ServerFnMeta
|
|
793
799
|
}>()
|
|
794
800
|
|
|
@@ -804,6 +810,7 @@ test('createMiddleware with type request can return sync Response', () => {
|
|
|
804
810
|
next: RequestServerNextFn<{}, undefined>
|
|
805
811
|
pathname: string
|
|
806
812
|
context: undefined
|
|
813
|
+
handlerType: 'serverFn' | 'router'
|
|
807
814
|
serverFnMeta?: ServerFnMeta
|
|
808
815
|
}>()
|
|
809
816
|
|
package/bin/intent.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Auto-generated by @tanstack/intent setup
|
|
3
|
-
// Exposes the intent end-user CLI for consumers of this library.
|
|
4
|
-
// Commit this file, then add to your package.json:
|
|
5
|
-
// "bin": { "intent": "./bin/intent.js" }
|
|
6
|
-
try {
|
|
7
|
-
await import('@tanstack/intent/intent-library')
|
|
8
|
-
} catch (e) {
|
|
9
|
-
const isModuleNotFound =
|
|
10
|
-
e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND'
|
|
11
|
-
const missingIntentLibrary =
|
|
12
|
-
typeof e?.message === 'string' && e.message.includes('@tanstack/intent')
|
|
13
|
-
|
|
14
|
-
if (isModuleNotFound && missingIntentLibrary) {
|
|
15
|
-
console.error('@tanstack/intent is not installed.')
|
|
16
|
-
console.error('')
|
|
17
|
-
console.error('Install it as a dev dependency:')
|
|
18
|
-
console.error(' npm add -D @tanstack/intent')
|
|
19
|
-
console.error('')
|
|
20
|
-
console.error('Or run directly:')
|
|
21
|
-
console.error(' npx @tanstack/intent@latest list')
|
|
22
|
-
process.exit(1)
|
|
23
|
-
}
|
|
24
|
-
throw e
|
|
25
|
-
}
|