@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.
Files changed (51) hide show
  1. package/dist/esm/client/hydrateStart.js +4 -2
  2. package/dist/esm/client/hydrateStart.js.map +1 -1
  3. package/dist/esm/client-rpc/serverFnFetcher.d.ts +21 -0
  4. package/dist/esm/client-rpc/serverFnFetcher.js +94 -71
  5. package/dist/esm/client-rpc/serverFnFetcher.js.map +1 -1
  6. package/dist/esm/createCsrfMiddleware.d.ts +46 -0
  7. package/dist/esm/createCsrfMiddleware.js +63 -0
  8. package/dist/esm/createCsrfMiddleware.js.map +1 -0
  9. package/dist/esm/createMiddleware.d.ts +4 -0
  10. package/dist/esm/createMiddleware.js.map +1 -1
  11. package/dist/esm/createServerFn.d.ts +44 -31
  12. package/dist/esm/createServerFn.js +1 -1
  13. package/dist/esm/createServerFn.js.map +1 -1
  14. package/dist/esm/fake-entries/plugin-adapters.d.ts +3 -0
  15. package/dist/esm/fake-entries/plugin-adapters.js +7 -0
  16. package/dist/esm/fake-entries/plugin-adapters.js.map +1 -0
  17. package/dist/esm/fake-entries/router.d.ts +1 -0
  18. package/dist/esm/fake-entries/router.js +6 -0
  19. package/dist/esm/fake-entries/router.js.map +1 -0
  20. package/dist/esm/{fake-start-entry.d.ts → fake-entries/start.d.ts} +0 -1
  21. package/dist/esm/fake-entries/start.js +6 -0
  22. package/dist/esm/fake-entries/start.js.map +1 -0
  23. package/dist/esm/getDefaultSerovalPlugins.d.ts +2 -1
  24. package/dist/esm/getDefaultSerovalPlugins.js.map +1 -1
  25. package/dist/esm/index.d.ts +4 -1
  26. package/dist/esm/index.js +3 -1
  27. package/dist/esm/tests/createCsrfMiddleware.test.d.ts +1 -0
  28. package/package.json +9 -10
  29. package/skills/start-core/SKILL.md +11 -7
  30. package/skills/start-core/auth-server-primitives/SKILL.md +410 -0
  31. package/skills/start-core/deployment/SKILL.md +9 -0
  32. package/skills/start-core/execution-model/SKILL.md +68 -19
  33. package/skills/start-core/middleware/SKILL.md +42 -9
  34. package/skills/start-core/server-functions/SKILL.md +115 -17
  35. package/src/client/hydrateStart.ts +12 -6
  36. package/src/client-rpc/serverFnFetcher.ts +132 -103
  37. package/src/createCsrfMiddleware.ts +197 -0
  38. package/src/createMiddleware.ts +4 -0
  39. package/src/createServerFn.ts +192 -63
  40. package/src/fake-entries/plugin-adapters.ts +4 -0
  41. package/src/fake-entries/router.ts +1 -0
  42. package/src/{fake-start-entry.ts → fake-entries/start.ts} +0 -1
  43. package/src/getDefaultSerovalPlugins.ts +2 -1
  44. package/src/index.tsx +16 -0
  45. package/src/start-entry.d.ts +9 -2
  46. package/src/tests/createCsrfMiddleware.test.ts +290 -0
  47. package/src/tests/createServerFn.test-d.ts +152 -2
  48. package/src/tests/createServerMiddleware.test-d.ts +16 -3
  49. package/bin/intent.js +0 -25
  50. package/dist/esm/fake-start-entry.js +0 -7
  51. 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
+ }
@@ -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.