@benjavicente/start-client-core 1.167.9 → 1.168.2
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/hydrateStart.js +4 -2
- package/dist/esm/client/hydrateStart.js.map +1 -1
- package/dist/esm/client-rpc/serverFnFetcher.d.ts +21 -0
- package/dist/esm/client-rpc/serverFnFetcher.js +94 -71
- package/dist/esm/client-rpc/serverFnFetcher.js.map +1 -1
- 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/createServerFn.d.ts +44 -31
- package/dist/esm/createServerFn.js +1 -1
- package/dist/esm/createServerFn.js.map +1 -1
- package/dist/esm/fake-entries/plugin-adapters.d.ts +3 -0
- package/dist/esm/fake-entries/plugin-adapters.js +7 -0
- package/dist/esm/fake-entries/plugin-adapters.js.map +1 -0
- package/dist/esm/fake-entries/router.d.ts +1 -0
- package/dist/esm/fake-entries/router.js +6 -0
- package/dist/esm/fake-entries/router.js.map +1 -0
- package/dist/esm/{fake-start-entry.d.ts → fake-entries/start.d.ts} +0 -1
- package/dist/esm/fake-entries/start.js +6 -0
- package/dist/esm/fake-entries/start.js.map +1 -0
- package/dist/esm/getDefaultSerovalPlugins.d.ts +2 -1
- package/dist/esm/getDefaultSerovalPlugins.js.map +1 -1
- package/dist/esm/index.d.ts +4 -1
- package/dist/esm/index.js +3 -1
- package/dist/esm/tests/createCsrfMiddleware.test.d.ts +1 -0
- package/package.json +9 -10
- 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 +68 -19
- package/skills/start-core/middleware/SKILL.md +42 -9
- package/skills/start-core/server-functions/SKILL.md +115 -17
- package/src/client/hydrateStart.ts +12 -6
- package/src/client-rpc/serverFnFetcher.ts +132 -103
- package/src/createCsrfMiddleware.ts +197 -0
- package/src/createMiddleware.ts +4 -0
- package/src/createServerFn.ts +192 -63
- package/src/fake-entries/plugin-adapters.ts +4 -0
- package/src/fake-entries/router.ts +1 -0
- package/src/{fake-start-entry.ts → fake-entries/start.ts} +0 -1
- package/src/getDefaultSerovalPlugins.ts +2 -1
- package/src/index.tsx +16 -0
- package/src/start-entry.d.ts +9 -2
- package/src/tests/createCsrfMiddleware.test.ts +290 -0
- package/src/tests/createServerFn.test-d.ts +152 -2
- package/src/tests/createServerMiddleware.test-d.ts +16 -3
- package/bin/intent.js +0 -25
- package/dist/esm/fake-start-entry.js +0 -7
- package/dist/esm/fake-start-entry.js.map +0 -1
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
createCsrfMiddleware,
|
|
4
|
+
csrfSymbol,
|
|
5
|
+
getCsrfRequestValidationResult,
|
|
6
|
+
isCsrfRequestAllowed,
|
|
7
|
+
} from '../createCsrfMiddleware'
|
|
8
|
+
import type { RequestServerOptions } from '../createMiddleware'
|
|
9
|
+
import type { Register } from '@benjavicente/router-core'
|
|
10
|
+
|
|
11
|
+
const requestOrigin = 'https://app.example.com'
|
|
12
|
+
|
|
13
|
+
function trackHeaders(init: Record<string, string>) {
|
|
14
|
+
const headers = new Map(
|
|
15
|
+
Object.entries(init).map(([key, value]) => [key.toLowerCase(), value]),
|
|
16
|
+
)
|
|
17
|
+
const reads: Array<string> = []
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
reads,
|
|
21
|
+
request: {
|
|
22
|
+
url: `${requestOrigin}/_serverFn/test`,
|
|
23
|
+
headers: {
|
|
24
|
+
get(name: string) {
|
|
25
|
+
reads.push(name)
|
|
26
|
+
return headers.get(name.toLowerCase()) ?? null
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
} as Request,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function createContext(init: {
|
|
34
|
+
headers?: Record<string, string>
|
|
35
|
+
handlerType?: 'serverFn' | 'router'
|
|
36
|
+
origin?: string
|
|
37
|
+
}): RequestServerOptions<Register, undefined> {
|
|
38
|
+
const { request } = trackHeaders(init.headers ?? {})
|
|
39
|
+
return {
|
|
40
|
+
request,
|
|
41
|
+
pathname: new URL(request.url).pathname,
|
|
42
|
+
context: undefined,
|
|
43
|
+
next: (() => undefined) as any,
|
|
44
|
+
handlerType: init.handlerType ?? 'serverFn',
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function runMiddleware(
|
|
49
|
+
middleware: ReturnType<typeof createCsrfMiddleware>,
|
|
50
|
+
ctx: RequestServerOptions<Register, undefined>,
|
|
51
|
+
) {
|
|
52
|
+
const next = vi.fn(() => ({ request: ctx.request, pathname: ctx.pathname }))
|
|
53
|
+
const result = await middleware.options.server!({
|
|
54
|
+
...ctx,
|
|
55
|
+
next,
|
|
56
|
+
} as any)
|
|
57
|
+
return { result, next }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('getCsrfRequestValidationResult', () => {
|
|
61
|
+
it('allows same-origin fetch metadata without reading Origin or Referer', async () => {
|
|
62
|
+
const { request, reads } = trackHeaders({
|
|
63
|
+
'Sec-Fetch-Site': 'same-origin',
|
|
64
|
+
Origin: 'https://evil.example.com',
|
|
65
|
+
Referer: 'https://evil.example.com/path',
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
await expect(
|
|
69
|
+
getCsrfRequestValidationResult({}, createContextFromRequest(request)),
|
|
70
|
+
).resolves.toBe(true)
|
|
71
|
+
expect(reads).toEqual(['Sec-Fetch-Site'])
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it.each(['same-site', 'cross-site', 'none', 'invalid'])(
|
|
75
|
+
'rejects %s fetch metadata',
|
|
76
|
+
async (fetchSite) => {
|
|
77
|
+
const ctx = createContext({
|
|
78
|
+
headers: {
|
|
79
|
+
'Sec-Fetch-Site': fetchSite,
|
|
80
|
+
Origin: requestOrigin,
|
|
81
|
+
Referer: `${requestOrigin}/path`,
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
await expect(getCsrfRequestValidationResult({}, ctx)).resolves.toBe(false)
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
it('allows matching Origin without reading Referer', async () => {
|
|
90
|
+
const { request, reads } = trackHeaders({
|
|
91
|
+
Origin: requestOrigin,
|
|
92
|
+
Referer: 'https://evil.example.com/path',
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
await expect(
|
|
96
|
+
getCsrfRequestValidationResult({}, createContextFromRequest(request)),
|
|
97
|
+
).resolves.toBe(true)
|
|
98
|
+
expect(reads).toEqual(['Sec-Fetch-Site', 'Origin'])
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('rejects mismatched Origin without reading Referer', async () => {
|
|
102
|
+
const { request, reads } = trackHeaders({
|
|
103
|
+
Origin: 'https://evil.example.com',
|
|
104
|
+
Referer: `${requestOrigin}/path`,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
await expect(
|
|
108
|
+
getCsrfRequestValidationResult({}, createContextFromRequest(request)),
|
|
109
|
+
).resolves.toBe(false)
|
|
110
|
+
expect(reads).toEqual(['Sec-Fetch-Site', 'Origin'])
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it.each([
|
|
114
|
+
requestOrigin,
|
|
115
|
+
`${requestOrigin}/path`,
|
|
116
|
+
`${requestOrigin}?query=1`,
|
|
117
|
+
`${requestOrigin}#hash`,
|
|
118
|
+
])('allows same-origin Referer fallback: %s', async (referer) => {
|
|
119
|
+
const ctx = createContext({ headers: { Referer: referer } })
|
|
120
|
+
|
|
121
|
+
await expect(getCsrfRequestValidationResult({}, ctx)).resolves.toBe(true)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it.each([
|
|
125
|
+
'https://evil.example.com/path',
|
|
126
|
+
`${requestOrigin}.evil/path`,
|
|
127
|
+
`${requestOrigin}:443/path`,
|
|
128
|
+
])('rejects cross-origin Referer fallback: %s', async (referer) => {
|
|
129
|
+
const ctx = createContext({ headers: { Referer: referer } })
|
|
130
|
+
|
|
131
|
+
await expect(getCsrfRequestValidationResult({}, ctx)).resolves.toBe(false)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('returns undefined for requests without origin check headers', async () => {
|
|
135
|
+
const ctx = createContext({})
|
|
136
|
+
|
|
137
|
+
await expect(
|
|
138
|
+
getCsrfRequestValidationResult({}, ctx),
|
|
139
|
+
).resolves.toBeUndefined()
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('rejects empty Referer header as known invalid origin check', async () => {
|
|
143
|
+
const ctx = createContext({ headers: { Referer: '' } })
|
|
144
|
+
|
|
145
|
+
await expect(getCsrfRequestValidationResult({}, ctx)).resolves.toBe(false)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('rejects missing origin check headers by default', async () => {
|
|
149
|
+
const ctx = createContext({})
|
|
150
|
+
|
|
151
|
+
await expect(isCsrfRequestAllowed({}, ctx)).resolves.toBe(false)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('allows missing origin check headers with the opt-in', async () => {
|
|
155
|
+
const ctx = createContext({})
|
|
156
|
+
|
|
157
|
+
await expect(
|
|
158
|
+
isCsrfRequestAllowed({ allowRequestsWithoutOriginCheck: true }, ctx),
|
|
159
|
+
).resolves.toBe(true)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('does not allow invalid origin check headers with the opt-in', async () => {
|
|
163
|
+
const ctx = createContext({ headers: { 'Sec-Fetch-Site': 'cross-site' } })
|
|
164
|
+
|
|
165
|
+
await expect(
|
|
166
|
+
isCsrfRequestAllowed({ allowRequestsWithoutOriginCheck: true }, ctx),
|
|
167
|
+
).resolves.toBe(false)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('uses custom origin matchers', async () => {
|
|
171
|
+
const ctx = createContext({
|
|
172
|
+
headers: { Origin: 'https://preview.example.com' },
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
await expect(
|
|
176
|
+
getCsrfRequestValidationResult(
|
|
177
|
+
{ origin: ['https://app.example.com', 'https://preview.example.com'] },
|
|
178
|
+
ctx,
|
|
179
|
+
),
|
|
180
|
+
).resolves.toBe(true)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('uses custom Sec-Fetch-Site matchers', async () => {
|
|
184
|
+
const ctx = createContext({ headers: { 'Sec-Fetch-Site': 'same-site' } })
|
|
185
|
+
|
|
186
|
+
await expect(
|
|
187
|
+
getCsrfRequestValidationResult(
|
|
188
|
+
{ secFetchSite: ['same-origin', 'same-site'] },
|
|
189
|
+
ctx,
|
|
190
|
+
),
|
|
191
|
+
).resolves.toBe(true)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('reads request URL origin only when needed', async () => {
|
|
195
|
+
const getUrl = vi.fn(() => `${requestOrigin}/_serverFn/test`)
|
|
196
|
+
const sameOriginFetch = createContext({
|
|
197
|
+
headers: { 'Sec-Fetch-Site': 'same-origin' },
|
|
198
|
+
})
|
|
199
|
+
Object.defineProperty(sameOriginFetch.request, 'url', { get: getUrl })
|
|
200
|
+
|
|
201
|
+
await expect(
|
|
202
|
+
getCsrfRequestValidationResult({}, sameOriginFetch),
|
|
203
|
+
).resolves.toBe(true)
|
|
204
|
+
expect(getUrl).not.toHaveBeenCalled()
|
|
205
|
+
|
|
206
|
+
const originFallback = createContext({
|
|
207
|
+
headers: { Origin: requestOrigin },
|
|
208
|
+
})
|
|
209
|
+
Object.defineProperty(originFallback.request, 'url', { get: getUrl })
|
|
210
|
+
|
|
211
|
+
await expect(
|
|
212
|
+
getCsrfRequestValidationResult({}, originFallback),
|
|
213
|
+
).resolves.toBe(true)
|
|
214
|
+
expect(getUrl).toHaveBeenCalledTimes(1)
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
describe('createCsrfMiddleware', () => {
|
|
219
|
+
it('marks middleware with csrfSymbol in non-production environments', () => {
|
|
220
|
+
const middleware = createCsrfMiddleware()
|
|
221
|
+
|
|
222
|
+
expect(csrfSymbol in middleware).toBe(true)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('protects router requests by default', async () => {
|
|
226
|
+
const middleware = createCsrfMiddleware()
|
|
227
|
+
const ctx = createContext({ handlerType: 'router' })
|
|
228
|
+
|
|
229
|
+
const { result, next } = await runMiddleware(middleware, ctx)
|
|
230
|
+
|
|
231
|
+
expect(next).not.toHaveBeenCalled()
|
|
232
|
+
expect(result).toBeInstanceOf(Response)
|
|
233
|
+
expect((result as Response).status).toBe(403)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('protects server function requests by default', async () => {
|
|
237
|
+
const middleware = createCsrfMiddleware()
|
|
238
|
+
const ctx = createContext({ handlerType: 'serverFn' })
|
|
239
|
+
|
|
240
|
+
const { result, next } = await runMiddleware(middleware, ctx)
|
|
241
|
+
|
|
242
|
+
expect(next).not.toHaveBeenCalled()
|
|
243
|
+
expect(result).toBeInstanceOf(Response)
|
|
244
|
+
expect((result as Response).status).toBe(403)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('filters requests', async () => {
|
|
248
|
+
const middleware = createCsrfMiddleware({ filter: () => false })
|
|
249
|
+
const ctx = createContext({ handlerType: 'serverFn' })
|
|
250
|
+
|
|
251
|
+
const { next } = await runMiddleware(middleware, ctx)
|
|
252
|
+
|
|
253
|
+
expect(next).toHaveBeenCalledTimes(1)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('can filter to server function requests', async () => {
|
|
257
|
+
const middleware = createCsrfMiddleware({
|
|
258
|
+
filter: (ctx) => ctx.handlerType === 'serverFn',
|
|
259
|
+
})
|
|
260
|
+
const ctx = createContext({ handlerType: 'router' })
|
|
261
|
+
|
|
262
|
+
const { next } = await runMiddleware(middleware, ctx)
|
|
263
|
+
|
|
264
|
+
expect(next).toHaveBeenCalledTimes(1)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('uses custom failure responses', async () => {
|
|
268
|
+
const middleware = createCsrfMiddleware({
|
|
269
|
+
failureResponse: new Response('CSRF failed', { status: 419 }),
|
|
270
|
+
})
|
|
271
|
+
const ctx = createContext({ handlerType: 'serverFn' })
|
|
272
|
+
|
|
273
|
+
const { result } = await runMiddleware(middleware, ctx)
|
|
274
|
+
|
|
275
|
+
expect((result as Response).status).toBe(419)
|
|
276
|
+
await expect((result as Response).text()).resolves.toBe('CSRF failed')
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
function createContextFromRequest(
|
|
281
|
+
request: Request,
|
|
282
|
+
): RequestServerOptions<Register, undefined> {
|
|
283
|
+
return {
|
|
284
|
+
request,
|
|
285
|
+
pathname: new URL(request.url).pathname,
|
|
286
|
+
context: undefined,
|
|
287
|
+
next: (() => undefined) as any,
|
|
288
|
+
handlerType: 'serverFn',
|
|
289
|
+
}
|
|
290
|
+
}
|
|
@@ -6,6 +6,7 @@ import type { ServerFnMeta } from '../constants'
|
|
|
6
6
|
import type {
|
|
7
7
|
Constrain,
|
|
8
8
|
Register,
|
|
9
|
+
SerializationError,
|
|
9
10
|
TsrSerializable,
|
|
10
11
|
ValidateSerializableInput,
|
|
11
12
|
Validator,
|
|
@@ -512,7 +513,134 @@ test('createServerFn returns undefined', () => {
|
|
|
512
513
|
test('createServerFn cannot return function', () => {
|
|
513
514
|
expectTypeOf(createServerFn().handler<{ func: () => 'func' }>)
|
|
514
515
|
.parameter(0)
|
|
515
|
-
.returns.toEqualTypeOf<{
|
|
516
|
+
.returns.toEqualTypeOf<{
|
|
517
|
+
func: SerializationError<'Function may not be serializable'>
|
|
518
|
+
}>()
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
test('createServerFn strict false can validate and return function', () => {
|
|
522
|
+
const fn = createServerFn({ method: 'GET', strict: false })
|
|
523
|
+
.inputValidator((input: { func: () => 'input' }) => ({
|
|
524
|
+
output: input.func(),
|
|
525
|
+
}))
|
|
526
|
+
.handler(({ data }) => {
|
|
527
|
+
expectTypeOf(data).toEqualTypeOf<{ output: 'input' }>()
|
|
528
|
+
|
|
529
|
+
return {
|
|
530
|
+
func: () => 'func' as const,
|
|
531
|
+
}
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
expectTypeOf(fn).parameter(0).toEqualTypeOf<{
|
|
535
|
+
data: { func: () => 'input' }
|
|
536
|
+
headers?: HeadersInit
|
|
537
|
+
signal?: AbortSignal
|
|
538
|
+
fetch?: CustomFetch
|
|
539
|
+
}>()
|
|
540
|
+
|
|
541
|
+
expectTypeOf(fn({ data: { func: () => 'input' } })).toEqualTypeOf<
|
|
542
|
+
Promise<{
|
|
543
|
+
func: () => 'func'
|
|
544
|
+
}>
|
|
545
|
+
>()
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
test('createServerFn strict false factory preserves strictness', () => {
|
|
549
|
+
const createServerFnWithoutSerializationCheck = createServerFn({
|
|
550
|
+
strict: false,
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
const myServerFn = createServerFnWithoutSerializationCheck()
|
|
554
|
+
.inputValidator((input: { func: () => 'input' }) => ({
|
|
555
|
+
output: input.func(),
|
|
556
|
+
}))
|
|
557
|
+
.handler(({ data }) => {
|
|
558
|
+
expectTypeOf(data).toEqualTypeOf<{ output: 'input' }>()
|
|
559
|
+
|
|
560
|
+
return {
|
|
561
|
+
func: () => 'func' as const,
|
|
562
|
+
}
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
expectTypeOf(myServerFn).parameter(0).toEqualTypeOf<{
|
|
566
|
+
data: { func: () => 'input' }
|
|
567
|
+
headers?: HeadersInit
|
|
568
|
+
signal?: AbortSignal
|
|
569
|
+
fetch?: CustomFetch
|
|
570
|
+
}>()
|
|
571
|
+
|
|
572
|
+
expectTypeOf(myServerFn({ data: { func: () => 'input' } })).toEqualTypeOf<
|
|
573
|
+
Promise<{
|
|
574
|
+
func: () => 'func'
|
|
575
|
+
}>
|
|
576
|
+
>()
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
test('createServerFn strict input false can validate function', () => {
|
|
580
|
+
const fn = createServerFn({ strict: { input: false } })
|
|
581
|
+
.inputValidator((input: { func: () => 'input' }) => ({
|
|
582
|
+
output: input.func(),
|
|
583
|
+
}))
|
|
584
|
+
.handler(({ data }) => {
|
|
585
|
+
expectTypeOf(data).toEqualTypeOf<{ output: 'input' }>()
|
|
586
|
+
|
|
587
|
+
return {
|
|
588
|
+
value: 'serializable' as const,
|
|
589
|
+
}
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
expectTypeOf(fn).parameter(0).toEqualTypeOf<{
|
|
593
|
+
data: { func: () => 'input' }
|
|
594
|
+
headers?: HeadersInit
|
|
595
|
+
signal?: AbortSignal
|
|
596
|
+
fetch?: CustomFetch
|
|
597
|
+
}>()
|
|
598
|
+
|
|
599
|
+
expectTypeOf(fn({ data: { func: () => 'input' } })).toEqualTypeOf<
|
|
600
|
+
Promise<{
|
|
601
|
+
value: 'serializable'
|
|
602
|
+
}>
|
|
603
|
+
>()
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
test('createServerFn strict output false can return function', () => {
|
|
607
|
+
const fn = createServerFn({ strict: { output: false } }).handler(() => ({
|
|
608
|
+
func: () => 'func' as const,
|
|
609
|
+
}))
|
|
610
|
+
|
|
611
|
+
expectTypeOf(fn()).toEqualTypeOf<
|
|
612
|
+
Promise<{
|
|
613
|
+
func: () => 'func'
|
|
614
|
+
}>
|
|
615
|
+
>()
|
|
616
|
+
|
|
617
|
+
const promiseFn = createServerFn({ strict: { output: false } }).handler(() =>
|
|
618
|
+
Promise.resolve({
|
|
619
|
+
func: () => 'func' as const,
|
|
620
|
+
}),
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
expectTypeOf(promiseFn()).toEqualTypeOf<
|
|
624
|
+
Promise<{
|
|
625
|
+
func: () => 'func'
|
|
626
|
+
}>
|
|
627
|
+
>()
|
|
628
|
+
})
|
|
629
|
+
|
|
630
|
+
test('ServerFnReturnType skips serialization when strict is false', () => {
|
|
631
|
+
expectTypeOf<
|
|
632
|
+
ServerFnReturnType<Register, { func: () => 'func' }, false>
|
|
633
|
+
>().toEqualTypeOf<{
|
|
634
|
+
func: () => 'func'
|
|
635
|
+
}>()
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
test('ServerFnReturnType skips serialization when strict output is false', () => {
|
|
639
|
+
expectTypeOf<
|
|
640
|
+
ServerFnReturnType<Register, { func: () => 'func' }, { output: false }>
|
|
641
|
+
>().toEqualTypeOf<{
|
|
642
|
+
func: () => 'func'
|
|
643
|
+
}>()
|
|
516
644
|
})
|
|
517
645
|
|
|
518
646
|
test('createServerFn cannot validate function', () => {
|
|
@@ -525,7 +653,29 @@ test('createServerFn cannot validate function', () => {
|
|
|
525
653
|
.toEqualTypeOf<
|
|
526
654
|
Constrain<
|
|
527
655
|
(input: { func: () => 'string' }) => { output: 'string' },
|
|
528
|
-
Validator<
|
|
656
|
+
Validator<
|
|
657
|
+
{ func: SerializationError<'Function may not be serializable'> },
|
|
658
|
+
any
|
|
659
|
+
>
|
|
660
|
+
>
|
|
661
|
+
>()
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
test('createServerFn strict output false still checks input', () => {
|
|
665
|
+
const validator = createServerFn({
|
|
666
|
+
method: 'GET',
|
|
667
|
+
strict: { output: false },
|
|
668
|
+
}).inputValidator<(input: { func: () => 'string' }) => { output: 'string' }>
|
|
669
|
+
|
|
670
|
+
expectTypeOf(validator)
|
|
671
|
+
.parameter(0)
|
|
672
|
+
.toEqualTypeOf<
|
|
673
|
+
Constrain<
|
|
674
|
+
(input: { func: () => 'string' }) => { output: 'string' },
|
|
675
|
+
Validator<
|
|
676
|
+
{ func: SerializationError<'Function may not be serializable'> },
|
|
677
|
+
any
|
|
678
|
+
>
|
|
529
679
|
>
|
|
530
680
|
>()
|
|
531
681
|
})
|
|
@@ -2,7 +2,7 @@ import { expectTypeOf, test } from 'vitest'
|
|
|
2
2
|
import { createMiddleware } from '../createMiddleware'
|
|
3
3
|
import type { RequestServerNextFn } from '../createMiddleware'
|
|
4
4
|
import type { ConstrainValidator, CustomFetch } from '../createServerFn'
|
|
5
|
-
import type { Register } from '@benjavicente/router-core'
|
|
5
|
+
import type { Register, SerializationError } from '@benjavicente/router-core'
|
|
6
6
|
import type { ServerFnMeta } from '../constants'
|
|
7
7
|
|
|
8
8
|
test('createServeMiddleware removes middleware after middleware,', () => {
|
|
@@ -573,7 +573,10 @@ test('createMiddleware sendContext cannot send a function', () => {
|
|
|
573
573
|
.parameter(0)
|
|
574
574
|
.exclude<undefined>()
|
|
575
575
|
.toHaveProperty('sendContext')
|
|
576
|
-
.toEqualTypeOf<
|
|
576
|
+
.toEqualTypeOf<
|
|
577
|
+
| { func: SerializationError<'Function may not be serializable'> }
|
|
578
|
+
| undefined
|
|
579
|
+
>()
|
|
577
580
|
|
|
578
581
|
return next()
|
|
579
582
|
})
|
|
@@ -582,7 +585,10 @@ test('createMiddleware sendContext cannot send a function', () => {
|
|
|
582
585
|
.parameter(0)
|
|
583
586
|
.exclude<undefined>()
|
|
584
587
|
.toHaveProperty('sendContext')
|
|
585
|
-
.toEqualTypeOf<
|
|
588
|
+
.toEqualTypeOf<
|
|
589
|
+
| { func: SerializationError<'Function may not be serializable'> }
|
|
590
|
+
| undefined
|
|
591
|
+
>()
|
|
586
592
|
|
|
587
593
|
return next()
|
|
588
594
|
})
|
|
@@ -669,6 +675,7 @@ test('createMiddleware with type request, no middleware or context', () => {
|
|
|
669
675
|
next: RequestServerNextFn<{}, undefined>
|
|
670
676
|
pathname: string
|
|
671
677
|
context: undefined
|
|
678
|
+
handlerType: 'serverFn' | 'router'
|
|
672
679
|
serverFnMeta?: ServerFnMeta
|
|
673
680
|
}>()
|
|
674
681
|
|
|
@@ -692,6 +699,7 @@ test('createMiddleware with type request, no middleware with context', () => {
|
|
|
692
699
|
next: RequestServerNextFn<{}, undefined>
|
|
693
700
|
pathname: string
|
|
694
701
|
context: undefined
|
|
702
|
+
handlerType: 'serverFn' | 'router'
|
|
695
703
|
serverFnMeta?: ServerFnMeta
|
|
696
704
|
}>()
|
|
697
705
|
|
|
@@ -716,6 +724,7 @@ test('createMiddleware with type request, middleware and context', () => {
|
|
|
716
724
|
next: RequestServerNextFn<{}, undefined>
|
|
717
725
|
pathname: string
|
|
718
726
|
context: undefined
|
|
727
|
+
handlerType: 'serverFn' | 'router'
|
|
719
728
|
serverFnMeta?: ServerFnMeta
|
|
720
729
|
}>()
|
|
721
730
|
|
|
@@ -740,6 +749,7 @@ test('createMiddleware with type request, middleware and context', () => {
|
|
|
740
749
|
next: RequestServerNextFn<{}, undefined>
|
|
741
750
|
pathname: string
|
|
742
751
|
context: { a: string }
|
|
752
|
+
handlerType: 'serverFn' | 'router'
|
|
743
753
|
serverFnMeta?: ServerFnMeta
|
|
744
754
|
}>()
|
|
745
755
|
|
|
@@ -763,6 +773,7 @@ test('createMiddleware with type request can return Response directly', () => {
|
|
|
763
773
|
next: RequestServerNextFn<{}, undefined>
|
|
764
774
|
pathname: string
|
|
765
775
|
context: undefined
|
|
776
|
+
handlerType: 'serverFn' | 'router'
|
|
766
777
|
serverFnMeta?: ServerFnMeta
|
|
767
778
|
}>()
|
|
768
779
|
|
|
@@ -783,6 +794,7 @@ test('createMiddleware with type request can return Promise<Response>', () => {
|
|
|
783
794
|
next: RequestServerNextFn<{}, undefined>
|
|
784
795
|
pathname: string
|
|
785
796
|
context: undefined
|
|
797
|
+
handlerType: 'serverFn' | 'router'
|
|
786
798
|
serverFnMeta?: ServerFnMeta
|
|
787
799
|
}>()
|
|
788
800
|
|
|
@@ -798,6 +810,7 @@ test('createMiddleware with type request can return sync Response', () => {
|
|
|
798
810
|
next: RequestServerNextFn<{}, undefined>
|
|
799
811
|
pathname: string
|
|
800
812
|
context: undefined
|
|
813
|
+
handlerType: 'serverFn' | 'router'
|
|
801
814
|
serverFnMeta?: ServerFnMeta
|
|
802
815
|
}>()
|
|
803
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
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"fake-start-entry.js","names":[],"sources":["../../src/fake-start-entry.ts"],"sourcesContent":["export const startInstance = undefined\nexport const getRouter = () => {}\n"],"mappings":";AAAA,IAAa,gBAAgB,KAAA;AAC7B,IAAa,kBAAkB"}
|