@benjavicente/start-client-core 1.167.9 → 1.168.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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 +10 -11
- 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,197 @@
|
|
|
1
|
+
import { createIsomorphicFn } from '@benjavicente/start-fn-stubs'
|
|
2
|
+
import { createMiddleware } from './createMiddleware'
|
|
3
|
+
import type {
|
|
4
|
+
RequestMiddlewareAfterServer,
|
|
5
|
+
RequestServerOptions,
|
|
6
|
+
} from './createMiddleware'
|
|
7
|
+
import type { Register } from '@benjavicente/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> & typeof ctx
|
|
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.
|