@sveltejs/kit 3.0.0-next.3 → 3.0.0-next.6
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/package.json +17 -17
- package/src/core/adapt/builder.js +18 -4
- package/src/core/adapt/index.js +1 -4
- package/src/core/config/index.js +64 -5
- package/src/core/config/options.js +73 -21
- package/src/core/env.js +35 -4
- package/src/core/postbuild/analyse.js +8 -12
- package/src/core/postbuild/crawl.js +22 -6
- package/src/core/postbuild/prerender.js +40 -23
- package/src/core/sync/create_manifest_data/index.js +23 -5
- package/src/core/sync/write_client_manifest.js +7 -0
- package/src/core/sync/write_server.js +13 -10
- package/src/core/sync/write_tsconfig.js +2 -4
- package/src/exports/index.js +32 -10
- package/src/exports/internal/env.js +1 -1
- package/src/exports/internal/index.js +6 -5
- package/src/exports/node/index.js +57 -6
- package/src/exports/public.d.ts +218 -114
- package/src/exports/vite/build/build_server.js +6 -1
- package/src/exports/vite/dev/index.js +10 -20
- package/src/exports/vite/index.js +306 -222
- package/src/exports/vite/preview/index.js +15 -7
- package/src/exports/vite/utils.js +8 -10
- package/src/runtime/app/env/types.d.ts +1 -1
- package/src/runtime/app/forms.js +22 -5
- package/src/runtime/app/paths/client.js +1 -3
- package/src/runtime/app/paths/public.d.ts +0 -28
- package/src/runtime/app/paths/server.js +7 -3
- package/src/runtime/app/server/remote/form.js +10 -3
- package/src/runtime/app/server/remote/query.js +3 -6
- package/src/runtime/app/server/remote/requested.js +8 -4
- package/src/runtime/app/server/remote/shared.js +5 -7
- package/src/runtime/client/client.js +190 -95
- package/src/runtime/client/fetcher.js +3 -2
- package/src/runtime/client/remote-functions/form.svelte.js +134 -24
- package/src/runtime/client/remote-functions/prerender.svelte.js +8 -2
- package/src/runtime/client/remote-functions/query/index.js +3 -2
- package/src/runtime/client/remote-functions/query/instance.svelte.js +26 -10
- package/src/runtime/client/remote-functions/query-batch.svelte.js +4 -3
- package/src/runtime/client/remote-functions/query-live/instance.svelte.js +26 -9
- package/src/runtime/client/remote-functions/shared.svelte.js +17 -7
- package/src/runtime/client/types.d.ts +9 -1
- package/src/runtime/client/utils.js +1 -1
- package/src/runtime/form-utils.js +84 -16
- package/src/runtime/server/cookie.js +22 -33
- package/src/runtime/server/csrf.js +65 -0
- package/src/runtime/server/data/index.js +7 -7
- package/src/runtime/server/env_module.js +0 -5
- package/src/runtime/server/index.js +1 -1
- package/src/runtime/server/page/actions.js +41 -26
- package/src/runtime/server/page/index.js +2 -1
- package/src/runtime/server/page/render.js +21 -23
- package/src/runtime/server/page/respond_with_error.js +7 -8
- package/src/runtime/server/remote.js +64 -27
- package/src/runtime/server/respond.js +81 -50
- package/src/runtime/server/utils.js +7 -7
- package/src/runtime/telemetry/otel.js +1 -1
- package/src/types/ambient.d.ts +5 -4
- package/src/types/global-private.d.ts +4 -4
- package/src/types/internal.d.ts +8 -10
- package/src/types/private.d.ts +33 -1
- package/src/types/synthetic/$lib.md +1 -1
- package/src/utils/error.js +11 -3
- package/src/utils/shared-iterator.js +5 -0
- package/src/utils/url.js +99 -2
- package/src/version.js +1 -1
- package/types/index.d.ts +340 -186
- package/types/index.d.ts.map +10 -9
|
@@ -58,6 +58,12 @@ export interface SvelteKitApp {
|
|
|
58
58
|
hash: boolean;
|
|
59
59
|
|
|
60
60
|
root: typeof SvelteComponent;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Lazily loads the contents of src/error.html, used as a last-resort
|
|
64
|
+
* error page when the root layout's load function throws during client-side rendering.
|
|
65
|
+
*/
|
|
66
|
+
get_error_template: () => Promise<(data: { status: number; message: string }) => string>;
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
export type NavigationIntent = {
|
|
@@ -77,6 +83,7 @@ export type NavigationResult = NavigationRedirect | NavigationFinished;
|
|
|
77
83
|
|
|
78
84
|
export type NavigationRedirect = {
|
|
79
85
|
type: 'redirect';
|
|
86
|
+
status: number;
|
|
80
87
|
location: string;
|
|
81
88
|
};
|
|
82
89
|
|
|
@@ -119,7 +126,8 @@ export interface NavigationState {
|
|
|
119
126
|
}
|
|
120
127
|
|
|
121
128
|
export interface HydrateOptions {
|
|
122
|
-
|
|
129
|
+
/** Provided in the case of a form action that returns `fail`, but otherwise derived from `error` */
|
|
130
|
+
status?: number;
|
|
123
131
|
error: App.Error | null;
|
|
124
132
|
node_ids: number[];
|
|
125
133
|
params: Record<string, string>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BROWSER, DEV } from 'esm-env';
|
|
2
2
|
import { writable } from 'svelte/store';
|
|
3
|
-
import { assets } from '$app/paths';
|
|
3
|
+
import { assets } from '$app/paths/internal/client';
|
|
4
4
|
import { version } from '$app/env';
|
|
5
5
|
import { noop } from '../../utils/functions.js';
|
|
6
6
|
import { PRELOAD_PRIORITIES } from './constants.js';
|
|
@@ -41,14 +41,11 @@ export function convert_formdata(data) {
|
|
|
41
41
|
|
|
42
42
|
if (is_array) key = key.slice(0, -2);
|
|
43
43
|
|
|
44
|
-
if (values.length > 1 && !is_array) {
|
|
45
|
-
throw new Error(`Form cannot contain duplicated keys — "${key}" has ${values.length} values`);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
44
|
// an empty `<input type="file">` will submit a non-existent file, bizarrely
|
|
49
45
|
values = values.filter(
|
|
50
46
|
(entry) => typeof entry === 'string' || entry.name !== '' || entry.size > 0
|
|
51
47
|
);
|
|
48
|
+
if (values.length === 0 && !is_array) continue;
|
|
52
49
|
|
|
53
50
|
if (key.startsWith('n:')) {
|
|
54
51
|
key = key.slice(2);
|
|
@@ -58,6 +55,10 @@ export function convert_formdata(data) {
|
|
|
58
55
|
values = values.map((v) => v === 'on');
|
|
59
56
|
}
|
|
60
57
|
|
|
58
|
+
if (values.length > 1 && !is_array) {
|
|
59
|
+
throw new Error(`Form cannot contain duplicated keys — "${key}" has ${values.length} values`);
|
|
60
|
+
}
|
|
61
|
+
|
|
61
62
|
set_nested_value(result, key, is_array ? values : values[0]);
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -608,18 +609,53 @@ function get_type_prefix(field_type, is_array, input_value) {
|
|
|
608
609
|
return '';
|
|
609
610
|
}
|
|
610
611
|
|
|
612
|
+
/**
|
|
613
|
+
* A deep-clone implementation specifically for form data, where
|
|
614
|
+
* we don't need to worry about cycles and whatnot
|
|
615
|
+
* @param {any} value
|
|
616
|
+
* @returns {any}
|
|
617
|
+
*/
|
|
618
|
+
function deep_clone(value) {
|
|
619
|
+
if (value !== null && typeof value === 'object') {
|
|
620
|
+
if (value instanceof Date) {
|
|
621
|
+
return new Date(value.getTime());
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (value instanceof File) {
|
|
625
|
+
return value;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (Array.isArray(value)) {
|
|
629
|
+
return value.map(deep_clone);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** @type {Record<string, any>} */
|
|
633
|
+
const clone = {};
|
|
634
|
+
for (const key of Object.keys(value)) {
|
|
635
|
+
clone[key] = deep_clone(value[key]);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return clone;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return value;
|
|
642
|
+
}
|
|
643
|
+
|
|
611
644
|
/**
|
|
612
645
|
* Creates a proxy-based field accessor for form data
|
|
613
646
|
* @param {any} target - Function or empty POJO
|
|
614
|
-
* @param {() => Record<string, any>}
|
|
615
|
-
* @param {(path: (string | number)[], value: any) => void}
|
|
647
|
+
* @param {() => Record<string, any>} get - Function to get current input data
|
|
648
|
+
* @param {(path: (string | number)[], value: any) => void} set - Function to set input data
|
|
616
649
|
* @param {(path?: (string | number)[], all?: boolean) => Record<string, InternalRemoteFormIssue[]>} get_issues - Function to get current issues
|
|
650
|
+
* @param {() => Record<string, boolean>} get_touched - Function to get touched fields
|
|
651
|
+
* @param {() => Record<string, boolean>} get_dirty - Function to get dirty fields
|
|
617
652
|
* @param {(string | number)[]} path - Current access path
|
|
618
653
|
* @returns {any} Proxy object with name(), value(), and issues() methods
|
|
619
654
|
*/
|
|
620
|
-
export function create_field_proxy(target,
|
|
655
|
+
export function create_field_proxy(target, get, set, get_issues, get_touched, get_dirty, path) {
|
|
621
656
|
const get_value = () => {
|
|
622
|
-
|
|
657
|
+
const value = deep_get(get(), path);
|
|
658
|
+
return deep_clone(value);
|
|
623
659
|
};
|
|
624
660
|
|
|
625
661
|
return new Proxy(target, {
|
|
@@ -628,24 +664,26 @@ export function create_field_proxy(target, get_input, set_input, get_issues, pat
|
|
|
628
664
|
|
|
629
665
|
// Handle array access like jobs[0]
|
|
630
666
|
if (/^\d+$/.test(prop)) {
|
|
631
|
-
return create_field_proxy({},
|
|
667
|
+
return create_field_proxy({}, get, set, get_issues, get_touched, get_dirty, [
|
|
632
668
|
...path,
|
|
633
669
|
parseInt(prop, 10)
|
|
634
670
|
]);
|
|
635
671
|
}
|
|
636
672
|
|
|
637
673
|
const key = build_path_string(path);
|
|
674
|
+
const next = [...path, prop];
|
|
638
675
|
|
|
639
676
|
if (prop === 'set') {
|
|
640
677
|
const set_func = function (/** @type {any} */ newValue) {
|
|
641
|
-
|
|
678
|
+
set(path, newValue);
|
|
642
679
|
return newValue;
|
|
643
680
|
};
|
|
644
|
-
|
|
681
|
+
|
|
682
|
+
return create_field_proxy(set_func, get, set, get_issues, get_touched, get_dirty, next);
|
|
645
683
|
}
|
|
646
684
|
|
|
647
685
|
if (prop === 'value') {
|
|
648
|
-
return create_field_proxy(get_value,
|
|
686
|
+
return create_field_proxy(get_value, get, set, get_issues, get_touched, get_dirty, next);
|
|
649
687
|
}
|
|
650
688
|
|
|
651
689
|
if (prop === 'issues' || prop === 'allIssues') {
|
|
@@ -659,15 +697,45 @@ export function create_field_proxy(target, get_input, set_input, get_issues, pat
|
|
|
659
697
|
}));
|
|
660
698
|
}
|
|
661
699
|
|
|
662
|
-
|
|
700
|
+
const issues = all_issues
|
|
663
701
|
?.filter((issue) => issue.name === key)
|
|
664
702
|
?.map((issue) => ({
|
|
665
703
|
path: issue.path,
|
|
666
704
|
message: issue.message
|
|
667
705
|
}));
|
|
706
|
+
|
|
707
|
+
return issues?.length ? issues : undefined;
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
return create_field_proxy(issues_func, get, set, get_issues, get_touched, get_dirty, next);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (prop === 'touched' || prop === 'dirty') {
|
|
714
|
+
const fn = () => {
|
|
715
|
+
const object = prop === 'dirty' ? get_dirty() : get_touched();
|
|
716
|
+
|
|
717
|
+
if (key === '') {
|
|
718
|
+
return Object.keys(object).length > 0;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (Object.hasOwn(object, key)) {
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
for (const candidate in object) {
|
|
726
|
+
if (!Object.hasOwn(object, candidate)) continue;
|
|
727
|
+
if (!candidate.startsWith(key)) continue;
|
|
728
|
+
|
|
729
|
+
const next = candidate[key.length];
|
|
730
|
+
if (next === '.' || next === '[') {
|
|
731
|
+
return true;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
return false;
|
|
668
736
|
};
|
|
669
737
|
|
|
670
|
-
return create_field_proxy(
|
|
738
|
+
return create_field_proxy(fn, get, set, get_issues, get_touched, get_dirty, next);
|
|
671
739
|
}
|
|
672
740
|
|
|
673
741
|
if (prop === 'as') {
|
|
@@ -833,11 +901,11 @@ export function create_field_proxy(target, get_input, set_input, get_issues, pat
|
|
|
833
901
|
});
|
|
834
902
|
};
|
|
835
903
|
|
|
836
|
-
return create_field_proxy(as_func,
|
|
904
|
+
return create_field_proxy(as_func, get, set, get_issues, get_touched, get_dirty, next);
|
|
837
905
|
}
|
|
838
906
|
|
|
839
907
|
// Handle property access (nested fields)
|
|
840
|
-
return create_field_proxy({},
|
|
908
|
+
return create_field_proxy({}, get, set, get_issues, get_touched, get_dirty, next);
|
|
841
909
|
}
|
|
842
910
|
});
|
|
843
911
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseCookie, parseSetCookie, stringifySetCookie } from 'cookie';
|
|
2
2
|
import { DEV } from 'esm-env';
|
|
3
3
|
import { normalize_path, resolve } from '../../utils/url.js';
|
|
4
4
|
import { add_data_suffix } from '../pathname.js';
|
|
@@ -39,7 +39,7 @@ function generate_cookie_key(domain, path, name) {
|
|
|
39
39
|
export function get_cookies(request, url) {
|
|
40
40
|
const header = request.headers.get('cookie') ?? '';
|
|
41
41
|
const initial_cookies = /** @type {Record<string, string>} */ (
|
|
42
|
-
|
|
42
|
+
parseCookie(header, { decode: (value) => value })
|
|
43
43
|
);
|
|
44
44
|
|
|
45
45
|
/** @type {string | undefined} */
|
|
@@ -48,7 +48,7 @@ export function get_cookies(request, url) {
|
|
|
48
48
|
/** @type {Map<string, import('./page/types.js').Cookie>} */
|
|
49
49
|
const new_cookies = new Map();
|
|
50
50
|
|
|
51
|
-
/** @type {import('cookie').
|
|
51
|
+
/** @type {Omit<import('cookie').SetCookie, 'name' | 'value'>} */
|
|
52
52
|
const defaults = {
|
|
53
53
|
httpOnly: true,
|
|
54
54
|
path: '/',
|
|
@@ -63,10 +63,6 @@ export function get_cookies(request, url) {
|
|
|
63
63
|
// typescript users. `@type {import('@sveltejs/kit').Cookies}` above is not
|
|
64
64
|
// sufficient to do so.
|
|
65
65
|
|
|
66
|
-
/**
|
|
67
|
-
* @param {string} name
|
|
68
|
-
* @param {import('cookie').ParseOptions} [opts]
|
|
69
|
-
*/
|
|
70
66
|
get(name, opts) {
|
|
71
67
|
// Look for the most specific matching cookie from new_cookies
|
|
72
68
|
const best_match = Array.from(new_cookies.values())
|
|
@@ -83,7 +79,7 @@ export function get_cookies(request, url) {
|
|
|
83
79
|
return best_match.options.maxAge === 0 ? undefined : best_match.value;
|
|
84
80
|
}
|
|
85
81
|
|
|
86
|
-
const req_cookies =
|
|
82
|
+
const req_cookies = parseCookie(header, { decode: opts?.decode });
|
|
87
83
|
const cookie = req_cookies[name]; // the decoded string or undefined
|
|
88
84
|
|
|
89
85
|
// in development, if the cookie was set during this session with `cookies.set`,
|
|
@@ -106,11 +102,8 @@ export function get_cookies(request, url) {
|
|
|
106
102
|
return cookie;
|
|
107
103
|
},
|
|
108
104
|
|
|
109
|
-
/**
|
|
110
|
-
* @param {import('cookie').ParseOptions} [opts]
|
|
111
|
-
*/
|
|
112
105
|
getAll(opts) {
|
|
113
|
-
const cookies =
|
|
106
|
+
const cookies = parseCookie(header, { decode: opts?.decode });
|
|
114
107
|
|
|
115
108
|
// Group cookies by name and find the most specific one for each name
|
|
116
109
|
const lookup = new Map();
|
|
@@ -141,29 +134,17 @@ export function get_cookies(request, url) {
|
|
|
141
134
|
);
|
|
142
135
|
},
|
|
143
136
|
|
|
144
|
-
/**
|
|
145
|
-
* @param {string} name
|
|
146
|
-
* @param {string} value
|
|
147
|
-
* @param {import('cookie').SerializeOptions} options
|
|
148
|
-
*/
|
|
149
137
|
set(name, value, options) {
|
|
150
138
|
set_internal(name, value, { ...defaults, ...options });
|
|
151
139
|
},
|
|
152
140
|
|
|
153
|
-
/**
|
|
154
|
-
* @param {string} name
|
|
155
|
-
* @param {import('cookie').SerializeOptions} options
|
|
156
|
-
*/
|
|
157
141
|
delete(name, options) {
|
|
158
142
|
cookies.set(name, '', { ...options, maxAge: 0 });
|
|
159
143
|
},
|
|
160
144
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
* @param {import('cookie').SerializeOptions} options
|
|
165
|
-
*/
|
|
166
|
-
serialize(name, value, options) {
|
|
145
|
+
parse: parseSetCookie,
|
|
146
|
+
|
|
147
|
+
serialize(name, value, { encode, ...options }) {
|
|
167
148
|
let path = options.path ?? '/';
|
|
168
149
|
|
|
169
150
|
if (!options.domain || options.domain === url.hostname) {
|
|
@@ -173,7 +154,7 @@ export function get_cookies(request, url) {
|
|
|
173
154
|
path = resolve(normalized_url, path);
|
|
174
155
|
}
|
|
175
156
|
|
|
176
|
-
return
|
|
157
|
+
return stringifySetCookie({ name, value, ...defaults, ...options, path }, { encode });
|
|
177
158
|
}
|
|
178
159
|
};
|
|
179
160
|
|
|
@@ -200,7 +181,7 @@ export function get_cookies(request, url) {
|
|
|
200
181
|
// explicit header has highest precedence
|
|
201
182
|
if (header) {
|
|
202
183
|
const parsed = /** @type {Record<string, string>} */ (
|
|
203
|
-
|
|
184
|
+
parseCookie(header, { decode: (value) => value })
|
|
204
185
|
);
|
|
205
186
|
for (const name in parsed) {
|
|
206
187
|
combined_cookies[name] = parsed[name];
|
|
@@ -238,7 +219,8 @@ export function get_cookies(request, url) {
|
|
|
238
219
|
new_cookies.set(cookie_key, cookie);
|
|
239
220
|
|
|
240
221
|
if (DEV) {
|
|
241
|
-
const
|
|
222
|
+
const { encode, ...rest } = cookie.options;
|
|
223
|
+
const serialized = stringifySetCookie({ name, value, ...rest }, { encode });
|
|
242
224
|
if (text_encoder.encode(serialized).byteLength > MAX_COOKIE_SIZE) {
|
|
243
225
|
throw new Error(`Cookie "${name}" is too large, and will be discarded by the browser`);
|
|
244
226
|
}
|
|
@@ -296,15 +278,22 @@ export function path_matches(path, constraint) {
|
|
|
296
278
|
*/
|
|
297
279
|
export function add_cookies_to_headers(headers, cookies) {
|
|
298
280
|
for (const new_cookie of cookies) {
|
|
299
|
-
const {
|
|
300
|
-
|
|
281
|
+
const {
|
|
282
|
+
name,
|
|
283
|
+
value,
|
|
284
|
+
options: { encode, ...options }
|
|
285
|
+
} = new_cookie;
|
|
286
|
+
headers.append('set-cookie', stringifySetCookie({ name, value, ...options }, { encode }));
|
|
301
287
|
|
|
302
288
|
// special case — for routes ending with .html, the route data lives in a sibling
|
|
303
289
|
// `.html__data.json` file rather than a child `/__data.json` file, which means
|
|
304
290
|
// we need to duplicate the cookie
|
|
305
291
|
if (options.path.endsWith('.html')) {
|
|
306
292
|
const path = add_data_suffix(options.path);
|
|
307
|
-
headers.append(
|
|
293
|
+
headers.append(
|
|
294
|
+
'set-cookie',
|
|
295
|
+
stringifySetCookie({ name, value, ...options, path }, { encode })
|
|
296
|
+
);
|
|
308
297
|
}
|
|
309
298
|
}
|
|
310
299
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { is_form_content_type } from '../../utils/http.js';
|
|
2
|
+
|
|
3
|
+
const mutating_form_methods = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The origin SvelteKit treats as "self" when validating the `Origin` header on
|
|
7
|
+
* cross-site requests.
|
|
8
|
+
*
|
|
9
|
+
* By default (`paths.origin` is `undefined`), SvelteKit derives the origin
|
|
10
|
+
* from `request.url` (which is set by the adapter, and ultimately by the
|
|
11
|
+
* platform). When `paths.origin` is configured — for example so that a preview
|
|
12
|
+
* deployment whose URL isn't known at build time, or an app behind a reverse
|
|
13
|
+
* proxy, can declare a canonical origin — that value takes precedence.
|
|
14
|
+
*
|
|
15
|
+
* @param {string | undefined} paths_origin the configured `kit.paths.origin`
|
|
16
|
+
* @param {string} url_origin the origin derived from `request.url`
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
export function get_self_origin(paths_origin, url_origin) {
|
|
20
|
+
return paths_origin || url_origin;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Determines whether a non-remote request should be rejected as a cross-site
|
|
25
|
+
* form submission (CSRF). Used by `respond.js` to gate form `POST`/`PUT`/
|
|
26
|
+
* `PATCH`/`DELETE` requests whose `Origin` header doesn't match the app's
|
|
27
|
+
* self-origin (and isn't in `trusted_origins`).
|
|
28
|
+
*
|
|
29
|
+
* @param {{
|
|
30
|
+
* request: Request;
|
|
31
|
+
* request_origin: string | null;
|
|
32
|
+
* self_origin: string;
|
|
33
|
+
* trusted_origins: string[];
|
|
34
|
+
* }} input
|
|
35
|
+
* @returns {boolean}
|
|
36
|
+
*/
|
|
37
|
+
export function is_csrf_forbidden({ request, request_origin, self_origin, trusted_origins }) {
|
|
38
|
+
return (
|
|
39
|
+
is_form_content_type(request) &&
|
|
40
|
+
mutating_form_methods.has(request.method) &&
|
|
41
|
+
request_origin !== self_origin &&
|
|
42
|
+
(!request_origin || !trusted_origins.includes(request_origin))
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Determines whether a remote-function request should be rejected as cross-site.
|
|
48
|
+
*
|
|
49
|
+
* Unlike form submissions, remote functions accept any content type (e.g.
|
|
50
|
+
* `application/json`), so the check is solely on the request method and origin:
|
|
51
|
+
* a non-`GET` request is forbidden when its `Origin` header doesn't match the
|
|
52
|
+
* app's self-origin. Unlike `is_csrf_forbidden`, entries in `trusted_origins`
|
|
53
|
+
* are *not* honoured — remote function endpoints are an implementation detail,
|
|
54
|
+
* not a public API, so cross-origin calls are forbidden regardless.
|
|
55
|
+
*
|
|
56
|
+
* @param {{
|
|
57
|
+
* request: Request;
|
|
58
|
+
* request_origin: string | null;
|
|
59
|
+
* self_origin: string;
|
|
60
|
+
* }} input
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
63
|
+
export function is_remote_forbidden({ request, request_origin, self_origin }) {
|
|
64
|
+
return request.method !== 'GET' && request_origin !== self_origin;
|
|
65
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { text } from '@sveltejs/kit';
|
|
2
|
-
import {
|
|
2
|
+
import { Redirect } from '@sveltejs/kit/internal';
|
|
3
3
|
import { normalize_error } from '../../../utils/error.js';
|
|
4
4
|
import { once } from '../../../utils/functions.js';
|
|
5
5
|
import { server_data_serializer_json } from '../page/data_serializer.js';
|
|
@@ -107,13 +107,11 @@ export async function render_data(
|
|
|
107
107
|
// Math.min because array isn't guaranteed to resolve in order
|
|
108
108
|
length = Math.min(length, i + 1);
|
|
109
109
|
|
|
110
|
+
const transformed = await handle_error_and_jsonify(event, event_state, options, error);
|
|
111
|
+
|
|
110
112
|
return /** @type {import('types').ServerErrorNode} */ ({
|
|
111
113
|
type: 'error',
|
|
112
|
-
error:
|
|
113
|
-
status:
|
|
114
|
-
error instanceof HttpError || error instanceof SvelteKitError
|
|
115
|
-
? error.status
|
|
116
|
-
: undefined
|
|
114
|
+
error: transformed
|
|
117
115
|
});
|
|
118
116
|
})
|
|
119
117
|
)
|
|
@@ -156,7 +154,8 @@ export async function render_data(
|
|
|
156
154
|
if (error instanceof Redirect) {
|
|
157
155
|
return redirect_json_response(error);
|
|
158
156
|
} else {
|
|
159
|
-
|
|
157
|
+
const transformed = await handle_error_and_jsonify(event, event_state, options, error);
|
|
158
|
+
return json_response(transformed, transformed.status);
|
|
160
159
|
}
|
|
161
160
|
}
|
|
162
161
|
}
|
|
@@ -182,6 +181,7 @@ export function redirect_json_response(redirect) {
|
|
|
182
181
|
return json_response(
|
|
183
182
|
/** @type {import('types').ServerRedirectNode} */ ({
|
|
184
183
|
type: 'redirect',
|
|
184
|
+
status: redirect.status,
|
|
185
185
|
location: redirect.location
|
|
186
186
|
})
|
|
187
187
|
);
|
|
@@ -16,7 +16,6 @@ let headers;
|
|
|
16
16
|
*/
|
|
17
17
|
export function get_public_env(request) {
|
|
18
18
|
const env = rendered_env;
|
|
19
|
-
const script = request.url.endsWith('.script.js');
|
|
20
19
|
|
|
21
20
|
payload ??= devalue.uneval(env);
|
|
22
21
|
etag ??= `W/${Date.now()}`;
|
|
@@ -29,9 +28,5 @@ export function get_public_env(request) {
|
|
|
29
28
|
return new Response(undefined, { status: 304, headers });
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
if (script) {
|
|
33
|
-
return new Response(`globalThis.__sveltekit_sw={env:${payload}}`, { headers });
|
|
34
|
-
}
|
|
35
|
-
|
|
36
31
|
return new Response(`export const env=${payload}`, { headers });
|
|
37
32
|
}
|
|
@@ -120,7 +120,7 @@ export class Server {
|
|
|
120
120
|
module.handleValidationError ||
|
|
121
121
|
(({ issues }) => {
|
|
122
122
|
console.error('Remote function schema validation failed:', issues);
|
|
123
|
-
return { message: 'Bad Request' };
|
|
123
|
+
return { message: 'Bad Request', status: 400 };
|
|
124
124
|
}),
|
|
125
125
|
reroute: module.reroute || noop,
|
|
126
126
|
transport: module.transport || {}
|
|
@@ -5,7 +5,7 @@ import { DEV } from 'esm-env';
|
|
|
5
5
|
import { json } from '@sveltejs/kit';
|
|
6
6
|
import { HttpError, Redirect, ActionFailure, SvelteKitError } from '@sveltejs/kit/internal';
|
|
7
7
|
import { with_request_store, merge_tracing } from '@sveltejs/kit/internal/server';
|
|
8
|
-
import {
|
|
8
|
+
import { normalize_error } from '../../../utils/error.js';
|
|
9
9
|
import { is_form_content_type, negotiate } from '../../../utils/http.js';
|
|
10
10
|
import { create_replacer, handle_error_and_jsonify } from '../utils.js';
|
|
11
11
|
import { record_span } from '../../telemetry/record_span.js';
|
|
@@ -36,13 +36,15 @@ export async function handle_action_json_request(event, event_state, options, se
|
|
|
36
36
|
`POST method not allowed. No form actions exist for ${DEV ? `the page at ${event.route.id}` : 'this page'}`
|
|
37
37
|
);
|
|
38
38
|
|
|
39
|
+
const error = await handle_error_and_jsonify(event, event_state, options, no_actions_error);
|
|
40
|
+
|
|
39
41
|
return action_json(
|
|
40
42
|
{
|
|
41
43
|
type: 'error',
|
|
42
|
-
error
|
|
44
|
+
error
|
|
43
45
|
},
|
|
44
46
|
{
|
|
45
|
-
status:
|
|
47
|
+
status: error.status,
|
|
46
48
|
headers: {
|
|
47
49
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405
|
|
48
50
|
// "The server must generate an Allow header field in a 405 status code response"
|
|
@@ -62,22 +64,27 @@ export async function handle_action_json_request(event, event_state, options, se
|
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
if (data instanceof ActionFailure) {
|
|
65
|
-
return action_json(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
data
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
67
|
+
return action_json(
|
|
68
|
+
{
|
|
69
|
+
type: 'failure',
|
|
70
|
+
status: data.status,
|
|
71
|
+
// @ts-expect-error we assign a string to what is supposed to be an object. That's ok
|
|
72
|
+
// because we don't use the object outside, and this way we have better code navigation
|
|
73
|
+
// through knowing where the related interface is used.
|
|
74
|
+
data: stringify_action_response(
|
|
75
|
+
data.data,
|
|
76
|
+
/** @type {string} */ (event.route.id),
|
|
77
|
+
options.hooks.transport
|
|
78
|
+
)
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
status: data.status
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
} else if (data) {
|
|
78
85
|
return action_json({
|
|
79
86
|
type: 'success',
|
|
80
|
-
status:
|
|
87
|
+
status: 200,
|
|
81
88
|
// @ts-expect-error see comment above
|
|
82
89
|
data: stringify_action_response(
|
|
83
90
|
data,
|
|
@@ -85,6 +92,9 @@ export async function handle_action_json_request(event, event_state, options, se
|
|
|
85
92
|
options.hooks.transport
|
|
86
93
|
)
|
|
87
94
|
});
|
|
95
|
+
} else {
|
|
96
|
+
// no data returned — use 204 No Content (without a body, per the spec)
|
|
97
|
+
return new Response(null, { status: 204 });
|
|
88
98
|
}
|
|
89
99
|
} catch (e) {
|
|
90
100
|
const err = normalize_error(e);
|
|
@@ -93,18 +103,20 @@ export async function handle_action_json_request(event, event_state, options, se
|
|
|
93
103
|
return action_json_redirect(err);
|
|
94
104
|
}
|
|
95
105
|
|
|
106
|
+
const transformed = await handle_error_and_jsonify(
|
|
107
|
+
event,
|
|
108
|
+
event_state,
|
|
109
|
+
options,
|
|
110
|
+
check_incorrect_fail_use(err)
|
|
111
|
+
);
|
|
112
|
+
|
|
96
113
|
return action_json(
|
|
97
114
|
{
|
|
98
115
|
type: 'error',
|
|
99
|
-
error:
|
|
100
|
-
event,
|
|
101
|
-
event_state,
|
|
102
|
-
options,
|
|
103
|
-
check_incorrect_fail_use(err)
|
|
104
|
-
)
|
|
116
|
+
error: transformed
|
|
105
117
|
},
|
|
106
118
|
{
|
|
107
|
-
status:
|
|
119
|
+
status: transformed.status
|
|
108
120
|
}
|
|
109
121
|
);
|
|
110
122
|
}
|
|
@@ -163,6 +175,7 @@ export async function handle_action_request(event, event_state, server) {
|
|
|
163
175
|
});
|
|
164
176
|
return {
|
|
165
177
|
type: 'error',
|
|
178
|
+
// We're lying a bit with the types here; this will be transformed into a proper App.Error object later
|
|
166
179
|
error: new SvelteKitError(
|
|
167
180
|
405,
|
|
168
181
|
'Method Not Allowed',
|
|
@@ -207,6 +220,7 @@ export async function handle_action_request(event, event_state, server) {
|
|
|
207
220
|
|
|
208
221
|
return {
|
|
209
222
|
type: 'error',
|
|
223
|
+
// @ts-expect-error We're lying a bit with the types here; this will be transformed into a proper App.Error object later
|
|
210
224
|
error: check_incorrect_fail_use(err)
|
|
211
225
|
};
|
|
212
226
|
}
|
|
@@ -243,11 +257,12 @@ async function call_action(event, event_state, actions) {
|
|
|
243
257
|
}
|
|
244
258
|
}
|
|
245
259
|
|
|
246
|
-
|
|
247
|
-
if (!action) {
|
|
260
|
+
if (!Object.hasOwn(actions, name)) {
|
|
248
261
|
throw new SvelteKitError(404, 'Not Found', `No action with name '${name}' found`);
|
|
249
262
|
}
|
|
250
263
|
|
|
264
|
+
const action = actions[name];
|
|
265
|
+
|
|
251
266
|
if (!is_form_content_type(event.request)) {
|
|
252
267
|
throw new SvelteKitError(
|
|
253
268
|
415,
|
|
@@ -269,6 +269,7 @@ export async function render_page(
|
|
|
269
269
|
if (state.prerendering && should_prerender_data) {
|
|
270
270
|
const body = JSON.stringify({
|
|
271
271
|
type: 'redirect',
|
|
272
|
+
status: err.status,
|
|
272
273
|
location: err.location
|
|
273
274
|
});
|
|
274
275
|
|
|
@@ -281,8 +282,8 @@ export async function render_page(
|
|
|
281
282
|
return redirect_response(err.status, err.location);
|
|
282
283
|
}
|
|
283
284
|
|
|
284
|
-
const status = get_status(err);
|
|
285
285
|
const error = await handle_error_and_jsonify(event, event_state, options, err);
|
|
286
|
+
const status = error.status;
|
|
286
287
|
|
|
287
288
|
while (i--) {
|
|
288
289
|
if (page.errors[i]) {
|