@tanstack/start-client-core 1.143.9 → 1.144.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/client-rpc/frame-decoder.d.ts +23 -0
- package/dist/esm/client-rpc/frame-decoder.js +243 -0
- package/dist/esm/client-rpc/frame-decoder.js.map +1 -0
- package/dist/esm/client-rpc/serverFnFetcher.js +61 -15
- package/dist/esm/client-rpc/serverFnFetcher.js.map +1 -1
- package/dist/esm/constants.d.ts +29 -0
- package/dist/esm/constants.js +40 -1
- package/dist/esm/constants.js.map +1 -1
- package/dist/esm/createServerFn.d.ts +1 -2
- package/dist/esm/createServerFn.js +104 -70
- package/dist/esm/createServerFn.js.map +1 -1
- package/dist/esm/getDefaultSerovalPlugins.d.ts +1 -1
- package/dist/esm/index.d.ts +6 -2
- package/dist/esm/index.js +15 -4
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/safeObjectMerge.d.ts +10 -0
- package/dist/esm/safeObjectMerge.js +30 -0
- package/dist/esm/safeObjectMerge.js.map +1 -0
- package/package.json +4 -4
- package/src/client-rpc/frame-decoder.ts +389 -0
- package/src/client-rpc/serverFnFetcher.ts +116 -16
- package/src/constants.ts +60 -0
- package/src/createServerFn.ts +145 -99
- package/src/index.tsx +12 -1
- package/src/safeObjectMerge.ts +38 -0
package/src/createServerFn.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { isNotFound, isRedirect } from '@tanstack/router-core'
|
|
2
1
|
import { mergeHeaders } from '@tanstack/router-core/ssr/client'
|
|
3
2
|
|
|
3
|
+
import { isRedirect, parseRedirect } from '@tanstack/router-core'
|
|
4
4
|
import { TSS_SERVER_FUNCTION_FACTORY } from './constants'
|
|
5
5
|
import { getStartOptions } from './getStartOptions'
|
|
6
6
|
import { getStartContextServerOnly } from './getStartContextServerOnly'
|
|
7
|
-
import
|
|
7
|
+
import { createNullProtoObject, safeObjectMerge } from './safeObjectMerge'
|
|
8
8
|
import type {
|
|
9
9
|
AnyValidator,
|
|
10
10
|
Constrain,
|
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
ValidateSerializableInput,
|
|
17
17
|
Validator,
|
|
18
18
|
} from '@tanstack/router-core'
|
|
19
|
+
import type { TSS_SERVER_FUNCTION } from './constants'
|
|
19
20
|
import type {
|
|
20
21
|
AnyFunctionMiddleware,
|
|
21
22
|
AnyRequestMiddleware,
|
|
@@ -112,17 +113,22 @@ export const createServerFn: CreateServerFn<Register> = (options, __opts) => {
|
|
|
112
113
|
return Object.assign(
|
|
113
114
|
async (opts?: CompiledFetcherFnOptions) => {
|
|
114
115
|
// Start by executing the client-side middleware chain
|
|
115
|
-
|
|
116
|
+
const result = await executeMiddleware(resolvedMiddleware, 'client', {
|
|
116
117
|
...extractedFn,
|
|
117
118
|
...newOptions,
|
|
118
119
|
data: opts?.data as any,
|
|
119
120
|
headers: opts?.headers,
|
|
120
121
|
signal: opts?.signal,
|
|
121
|
-
context:
|
|
122
|
-
}).then((d) => {
|
|
123
|
-
if (d.error) throw d.error
|
|
124
|
-
return d.result
|
|
122
|
+
context: createNullProtoObject(),
|
|
125
123
|
})
|
|
124
|
+
|
|
125
|
+
const redirect = parseRedirect(result.error)
|
|
126
|
+
if (redirect) {
|
|
127
|
+
throw redirect
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (result.error) throw result.error
|
|
131
|
+
return result.result
|
|
126
132
|
},
|
|
127
133
|
{
|
|
128
134
|
// This copies over the URL, function ID
|
|
@@ -133,25 +139,30 @@ export const createServerFn: CreateServerFn<Register> = (options, __opts) => {
|
|
|
133
139
|
const startContext = getStartContextServerOnly()
|
|
134
140
|
const serverContextAfterGlobalMiddlewares =
|
|
135
141
|
startContext.contextAfterGlobalMiddlewares
|
|
142
|
+
// Use safeObjectMerge for opts.context which comes from client
|
|
136
143
|
const ctx = {
|
|
137
144
|
...extractedFn,
|
|
138
145
|
...opts,
|
|
139
|
-
context:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
146
|
+
context: safeObjectMerge(
|
|
147
|
+
serverContextAfterGlobalMiddlewares,
|
|
148
|
+
opts.context,
|
|
149
|
+
),
|
|
143
150
|
signal,
|
|
144
151
|
request: startContext.request,
|
|
145
152
|
}
|
|
146
153
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
154
|
+
const result = await executeMiddleware(
|
|
155
|
+
resolvedMiddleware,
|
|
156
|
+
'server',
|
|
157
|
+
ctx,
|
|
158
|
+
).then((d) => ({
|
|
159
|
+
// Only send the result and sendContext back to the client
|
|
160
|
+
result: d.result,
|
|
161
|
+
error: d.error,
|
|
162
|
+
context: d.sendContext,
|
|
163
|
+
}))
|
|
164
|
+
|
|
165
|
+
return result
|
|
155
166
|
},
|
|
156
167
|
},
|
|
157
168
|
) as any
|
|
@@ -173,12 +184,23 @@ export async function executeMiddleware(
|
|
|
173
184
|
opts: ServerFnMiddlewareOptions,
|
|
174
185
|
): Promise<ServerFnMiddlewareResult> {
|
|
175
186
|
const globalMiddlewares = getStartOptions()?.functionMiddleware || []
|
|
176
|
-
|
|
187
|
+
let flattenedMiddlewares = flattenMiddlewares([
|
|
177
188
|
...globalMiddlewares,
|
|
178
189
|
...middlewares,
|
|
179
190
|
])
|
|
180
191
|
|
|
181
|
-
|
|
192
|
+
// On server, filter out middlewares that already executed in the request phase
|
|
193
|
+
// to prevent duplicate execution (issue #5239)
|
|
194
|
+
if (env === 'server') {
|
|
195
|
+
const startContext = getStartContextServerOnly({ throwIfNotFound: false })
|
|
196
|
+
if (startContext?.executedRequestMiddlewares) {
|
|
197
|
+
flattenedMiddlewares = flattenedMiddlewares.filter(
|
|
198
|
+
(m) => !startContext.executedRequestMiddlewares.has(m),
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const callNextMiddleware: NextFn = async (ctx) => {
|
|
182
204
|
// Get the next middleware
|
|
183
205
|
const nextMiddleware = flattenedMiddlewares.shift()
|
|
184
206
|
|
|
@@ -187,54 +209,110 @@ export async function executeMiddleware(
|
|
|
187
209
|
return ctx
|
|
188
210
|
}
|
|
189
211
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
ctx.data
|
|
199
|
-
|
|
200
|
-
|
|
212
|
+
// Execute the middleware
|
|
213
|
+
try {
|
|
214
|
+
if (
|
|
215
|
+
'inputValidator' in nextMiddleware.options &&
|
|
216
|
+
nextMiddleware.options.inputValidator &&
|
|
217
|
+
env === 'server'
|
|
218
|
+
) {
|
|
219
|
+
// Execute the middleware's input function
|
|
220
|
+
ctx.data = await execValidator(
|
|
221
|
+
nextMiddleware.options.inputValidator,
|
|
222
|
+
ctx.data,
|
|
223
|
+
)
|
|
224
|
+
}
|
|
201
225
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
226
|
+
let middlewareFn: MiddlewareFn | undefined = undefined
|
|
227
|
+
if (env === 'client') {
|
|
228
|
+
if ('client' in nextMiddleware.options) {
|
|
229
|
+
middlewareFn = nextMiddleware.options.client as
|
|
230
|
+
| MiddlewareFn
|
|
231
|
+
| undefined
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// env === 'server'
|
|
235
|
+
else if ('server' in nextMiddleware.options) {
|
|
236
|
+
middlewareFn = nextMiddleware.options.server as MiddlewareFn | undefined
|
|
206
237
|
}
|
|
207
|
-
}
|
|
208
|
-
// env === 'server'
|
|
209
|
-
else if ('server' in nextMiddleware.options) {
|
|
210
|
-
middlewareFn = nextMiddleware.options.server as MiddlewareFn | undefined
|
|
211
|
-
}
|
|
212
238
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
239
|
+
if (middlewareFn) {
|
|
240
|
+
const userNext = async (
|
|
241
|
+
userCtx: ServerFnMiddlewareResult | undefined = {} as any,
|
|
242
|
+
) => {
|
|
243
|
+
// Return the next middleware
|
|
244
|
+
// Use safeObjectMerge for context objects to prevent prototype pollution
|
|
245
|
+
const nextCtx = {
|
|
246
|
+
...ctx,
|
|
247
|
+
...userCtx,
|
|
248
|
+
context: safeObjectMerge(ctx.context, userCtx.context),
|
|
249
|
+
sendContext: safeObjectMerge(ctx.sendContext, userCtx.sendContext),
|
|
250
|
+
headers: mergeHeaders(ctx.headers, userCtx.headers),
|
|
251
|
+
result:
|
|
252
|
+
userCtx.result !== undefined
|
|
253
|
+
? userCtx.result
|
|
254
|
+
: userCtx instanceof Response
|
|
255
|
+
? userCtx
|
|
256
|
+
: (ctx as any).result,
|
|
257
|
+
error: userCtx.error ?? (ctx as any).error,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
return await callNextMiddleware(nextCtx)
|
|
262
|
+
} catch (error: any) {
|
|
218
263
|
return {
|
|
219
|
-
...
|
|
264
|
+
...nextCtx,
|
|
220
265
|
error,
|
|
221
266
|
}
|
|
222
267
|
}
|
|
268
|
+
}
|
|
223
269
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
270
|
+
// Execute the middleware
|
|
271
|
+
const result = await middlewareFn({
|
|
272
|
+
...ctx,
|
|
273
|
+
next: userNext as any,
|
|
274
|
+
} as any)
|
|
275
|
+
|
|
276
|
+
// If result is NOT a ctx object, we need to return it as
|
|
277
|
+
// the { result }
|
|
278
|
+
if (isRedirect(result)) {
|
|
279
|
+
return {
|
|
280
|
+
...ctx,
|
|
281
|
+
error: result,
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (result instanceof Response) {
|
|
286
|
+
return {
|
|
287
|
+
...ctx,
|
|
288
|
+
result,
|
|
289
|
+
}
|
|
290
|
+
}
|
|
228
291
|
|
|
229
|
-
|
|
292
|
+
if (!(result as any)) {
|
|
293
|
+
throw new Error(
|
|
294
|
+
'User middleware returned undefined. You must call next() or return a result in your middlewares.',
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return result
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return callNextMiddleware(ctx)
|
|
302
|
+
} catch (error: any) {
|
|
303
|
+
return {
|
|
304
|
+
...ctx,
|
|
305
|
+
error,
|
|
306
|
+
}
|
|
307
|
+
}
|
|
230
308
|
}
|
|
231
309
|
|
|
232
310
|
// Start the middleware chain
|
|
233
|
-
return
|
|
311
|
+
return callNextMiddleware({
|
|
234
312
|
...opts,
|
|
235
313
|
headers: opts.headers || {},
|
|
236
314
|
sendContext: opts.sendContext || {},
|
|
237
|
-
context: opts.context ||
|
|
315
|
+
context: opts.context || createNullProtoObject(),
|
|
238
316
|
})
|
|
239
317
|
}
|
|
240
318
|
|
|
@@ -571,18 +649,21 @@ export interface ServerFnTypes<
|
|
|
571
649
|
allOutput: IntersectAllValidatorOutputs<TMiddlewares, TInputValidator>
|
|
572
650
|
}
|
|
573
651
|
|
|
574
|
-
export function flattenMiddlewares
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
const seen = new Set<
|
|
578
|
-
const flattened: Array<
|
|
652
|
+
export function flattenMiddlewares<
|
|
653
|
+
T extends AnyFunctionMiddleware | AnyRequestMiddleware,
|
|
654
|
+
>(middlewares: Array<T>, maxDepth: number = 100): Array<T> {
|
|
655
|
+
const seen = new Set<T>()
|
|
656
|
+
const flattened: Array<T> = []
|
|
579
657
|
|
|
580
|
-
const recurse = (
|
|
581
|
-
|
|
582
|
-
|
|
658
|
+
const recurse = (middleware: Array<T>, depth: number) => {
|
|
659
|
+
if (depth > maxDepth) {
|
|
660
|
+
throw new Error(
|
|
661
|
+
`Middleware nesting depth exceeded maximum of ${maxDepth}. Check for circular references.`,
|
|
662
|
+
)
|
|
663
|
+
}
|
|
583
664
|
middleware.forEach((m) => {
|
|
584
665
|
if (m.options.middleware) {
|
|
585
|
-
recurse(m.options.middleware)
|
|
666
|
+
recurse(m.options.middleware as Array<T>, depth + 1)
|
|
586
667
|
}
|
|
587
668
|
|
|
588
669
|
if (!seen.has(m)) {
|
|
@@ -592,7 +673,7 @@ export function flattenMiddlewares(
|
|
|
592
673
|
})
|
|
593
674
|
}
|
|
594
675
|
|
|
595
|
-
recurse(middlewares)
|
|
676
|
+
recurse(middlewares, 0)
|
|
596
677
|
|
|
597
678
|
return flattened
|
|
598
679
|
}
|
|
@@ -622,41 +703,6 @@ export type MiddlewareFn = (
|
|
|
622
703
|
},
|
|
623
704
|
) => Promise<ServerFnMiddlewareResult>
|
|
624
705
|
|
|
625
|
-
export const applyMiddleware = async (
|
|
626
|
-
middlewareFn: MiddlewareFn,
|
|
627
|
-
ctx: ServerFnMiddlewareOptions,
|
|
628
|
-
nextFn: NextFn,
|
|
629
|
-
) => {
|
|
630
|
-
return middlewareFn({
|
|
631
|
-
...ctx,
|
|
632
|
-
next: (async (
|
|
633
|
-
userCtx: ServerFnMiddlewareResult | undefined = {} as any,
|
|
634
|
-
) => {
|
|
635
|
-
// Return the next middleware
|
|
636
|
-
return nextFn({
|
|
637
|
-
...ctx,
|
|
638
|
-
...userCtx,
|
|
639
|
-
context: {
|
|
640
|
-
...ctx.context,
|
|
641
|
-
...userCtx.context,
|
|
642
|
-
},
|
|
643
|
-
sendContext: {
|
|
644
|
-
...ctx.sendContext,
|
|
645
|
-
...(userCtx.sendContext ?? {}),
|
|
646
|
-
},
|
|
647
|
-
headers: mergeHeaders(ctx.headers, userCtx.headers),
|
|
648
|
-
result:
|
|
649
|
-
userCtx.result !== undefined
|
|
650
|
-
? userCtx.result
|
|
651
|
-
: userCtx instanceof Response
|
|
652
|
-
? userCtx
|
|
653
|
-
: (ctx as any).result,
|
|
654
|
-
error: userCtx.error ?? (ctx as any).error,
|
|
655
|
-
})
|
|
656
|
-
}) as any,
|
|
657
|
-
} as any)
|
|
658
|
-
}
|
|
659
|
-
|
|
660
706
|
export function execValidator(
|
|
661
707
|
validator: AnyValidator,
|
|
662
708
|
input: unknown,
|
package/src/index.tsx
CHANGED
|
@@ -2,6 +2,9 @@ export type { JsonResponse } from '@tanstack/router-core/ssr/client'
|
|
|
2
2
|
|
|
3
3
|
export { hydrate, json, mergeHeaders } from '@tanstack/router-core/ssr/client'
|
|
4
4
|
|
|
5
|
+
export { RawStream } from '@tanstack/router-core'
|
|
6
|
+
export type { OnRawStreamCallback } from '@tanstack/router-core'
|
|
7
|
+
|
|
5
8
|
export {
|
|
6
9
|
createIsomorphicFn,
|
|
7
10
|
createServerOnlyFn,
|
|
@@ -72,7 +75,6 @@ export type {
|
|
|
72
75
|
RequiredFetcher,
|
|
73
76
|
} from './createServerFn'
|
|
74
77
|
export {
|
|
75
|
-
applyMiddleware,
|
|
76
78
|
execValidator,
|
|
77
79
|
flattenMiddlewares,
|
|
78
80
|
executeMiddleware,
|
|
@@ -81,9 +83,17 @@ export {
|
|
|
81
83
|
export {
|
|
82
84
|
TSS_FORMDATA_CONTEXT,
|
|
83
85
|
TSS_SERVER_FUNCTION,
|
|
86
|
+
TSS_CONTENT_TYPE_FRAMED,
|
|
87
|
+
TSS_CONTENT_TYPE_FRAMED_VERSIONED,
|
|
88
|
+
TSS_FRAMED_PROTOCOL_VERSION,
|
|
89
|
+
FrameType,
|
|
90
|
+
FRAME_HEADER_SIZE,
|
|
84
91
|
X_TSS_SERIALIZED,
|
|
85
92
|
X_TSS_RAW_RESPONSE,
|
|
93
|
+
X_TSS_CONTEXT,
|
|
94
|
+
validateFramedProtocolVersion,
|
|
86
95
|
} from './constants'
|
|
96
|
+
export type { FrameType as FrameTypeValue } from './constants'
|
|
87
97
|
|
|
88
98
|
export type * from './serverRoute'
|
|
89
99
|
|
|
@@ -100,3 +110,4 @@ export type { Register } from '@tanstack/router-core'
|
|
|
100
110
|
export { getRouterInstance } from './getRouterInstance'
|
|
101
111
|
export { getDefaultSerovalPlugins } from './getDefaultSerovalPlugins'
|
|
102
112
|
export { getGlobalStartContext } from './getGlobalStartContext'
|
|
113
|
+
export { safeObjectMerge, createNullProtoObject } from './safeObjectMerge'
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
function isSafeKey(key: string): boolean {
|
|
2
|
+
return key !== '__proto__' && key !== 'constructor' && key !== 'prototype'
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Merge target and source into a new null-proto object, filtering dangerous keys.
|
|
7
|
+
*/
|
|
8
|
+
export function safeObjectMerge<T extends Record<string, unknown>>(
|
|
9
|
+
target: T | undefined,
|
|
10
|
+
source: Record<string, unknown> | null | undefined,
|
|
11
|
+
): T {
|
|
12
|
+
const result = Object.create(null) as T
|
|
13
|
+
if (target) {
|
|
14
|
+
for (const key of Object.keys(target)) {
|
|
15
|
+
if (isSafeKey(key)) result[key as keyof T] = target[key] as T[keyof T]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (source && typeof source === 'object') {
|
|
19
|
+
for (const key of Object.keys(source)) {
|
|
20
|
+
if (isSafeKey(key)) result[key as keyof T] = source[key] as T[keyof T]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return result
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a null-prototype object, optionally copying from source.
|
|
28
|
+
*/
|
|
29
|
+
export function createNullProtoObject<T extends object>(
|
|
30
|
+
source?: T,
|
|
31
|
+
): { [K in keyof T]: T[K] } {
|
|
32
|
+
if (!source) return Object.create(null)
|
|
33
|
+
const obj = Object.create(null)
|
|
34
|
+
for (const key of Object.keys(source)) {
|
|
35
|
+
if (isSafeKey(key)) obj[key] = (source as Record<string, unknown>)[key]
|
|
36
|
+
}
|
|
37
|
+
return obj
|
|
38
|
+
}
|