Repository: sveltejs/kit
Pull request #6502 (2022-09-01 08:19:05)

[ci] disable playwright cache and use correct browser in adapter-static test
alternative / comparison for #6485

no playwright caching

### Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
- [ ] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
- [ ] This message body should clearly illustrate what problems it solves.
- [ ] Ideally, include a test that fails without this PR but passes with it.

### Tests
- [ ] Run the tests with `pnpm test` and lint the project with `pnpm lint` and `pnpm check`

### Changesets
- [ ] If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running `pnpm changeset` and following the prompts. All changesets should be `patch` until SvelteKit 1.0


changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: 0c04e937ea02f739bd435763609efc59e5cc1482

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/dominikg/kit/new/ci/disable-playwright-cache?filename=.changeset/wicked-candles-swim.md&value=---%0A%22%40sveltejs%2Fadapter-static%22%3A%20patch%0A%22%40sveltejs%2Fkit%22%3A%20patch%0A---%0A%0A%5Bci%5D%20disable%20playwright%20cache%20and%20use%20correct%20browser%20in%20adapter-static%20test%0A)



Patch:
diff --git a/.changeset/ninety-tables-switch.md b/.changeset/ninety-tables-switch.md
new file mode 100644
index 000000000..37bd6b1f8
--- /dev/null
+++ b/.changeset/ninety-tables-switch.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[breaking] call `invalidate(fn)` predicates with a URL instead of a string
diff --git a/.changeset/odd-months-grow.md b/.changeset/odd-months-grow.md
new file mode 100644
index 000000000..d8bd476bd
--- /dev/null
+++ b/.changeset/odd-months-grow.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Prefer JSON responses when returning errors if accept header is `*/*`
diff --git a/.changeset/pre.json b/.changeset/pre.json
index 96e4bf9f8..400adc439 100644
--- a/.changeset/pre.json
+++ b/.changeset/pre.json
@@ -671,6 +671,7 @@
     "ninety-singers-invite",
     "ninety-snakes-melt",
     "ninety-suns-relax",
+    "ninety-tables-switch",
     "ninety-vans-talk",
     "ninety-walls-heal",
     "odd-bottles-drop",
@@ -683,6 +684,7 @@
     "odd-islands-heal",
     "odd-ligers-march",
     "odd-ligers-swim",
+    "odd-months-grow",
     "odd-pumas-grow",
     "odd-sloths-sit",
     "odd-tools-add",
@@ -841,6 +843,7 @@
     "rude-geese-roll",
     "rude-grapes-perform",
     "rude-masks-kneel",
+    "rude-nails-work",
     "rude-olives-wink",
     "rude-rockets-rescue",
     "rude-starfishes-press",
@@ -896,6 +899,7 @@
     "shiny-scissors-listen",
     "shiny-sloths-juggle",
     "shiny-teachers-nail",
+    "shiny-vans-jam",
     "short-beds-punch",
     "short-buttons-mix",
     "short-comics-unite",
@@ -1016,6 +1020,7 @@
     "stale-humans-cough",
     "stale-mirrors-attend",
     "stale-plums-heal",
+    "strange-apples-vanish",
     "strange-hounds-invite",
     "strange-kiwis-tell",
     "strange-poems-design",
diff --git a/.changeset/rude-nails-work.md b/.changeset/rude-nails-work.md
new file mode 100644
index 000000000..e11a00cbf
--- /dev/null
+++ b/.changeset/rude-nails-work.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[breaking] replace invalidate() with invalidateAll()
diff --git a/.changeset/shiny-vans-jam.md b/.changeset/shiny-vans-jam.md
new file mode 100644
index 000000000..84664ebdb
--- /dev/null
+++ b/.changeset/shiny-vans-jam.md
@@ -0,0 +1,5 @@
+---
+'create-svelte': patch
+---
+
+Use `invalidateAll()`
diff --git a/.changeset/strange-apples-vanish.md b/.changeset/strange-apples-vanish.md
new file mode 100644
index 000000000..ff1256d8c
--- /dev/null
+++ b/.changeset/strange-apples-vanish.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[breaking] block cross-site form POSTs by default. disable with config.kit.csrf.checkOrigin
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5691cd5e5..1120e690d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,6 +9,8 @@ on:
 env:
   TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
   TURBO_TEAM: svelte
+  # we call `pnpm playwright install` instead
+  PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
 
 # cancel in-progress runs on new commits to same PR (gitub.event.number)
 concurrency:
@@ -18,9 +20,6 @@ concurrency:
 jobs:
   Lint:
     runs-on: ubuntu-latest
-    env:
-      # not needed for linting
-      PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
     steps:
       - uses: actions/checkout@v3
       - uses: pnpm/action-setup@v2.2.2
@@ -46,14 +45,12 @@ jobs:
             e2e-browser: 'firefox'
           - node-version: 16
             os: macOS-latest
-            e2e-browser: 'safari'
+            e2e-browser: 'webkit'
           - node-version: 18
             os: ubuntu-latest
             e2e-browser: 'chromium'
     env:
       TURBO_CACHE_KEY: ${{ matrix.os }}-${{ matrix.node-version }}
-      # Install playwright's binray under node_modules so it will be cached together
-      PLAYWRIGHT_BROWSERS_PATH: '0'
       KIT_E2E_BROWSER: ${{matrix.e2e-browser}}
     steps:
       - run: git config --global core.autocrlf false
@@ -64,7 +61,7 @@ jobs:
           node-version: ${{ matrix.node-version }}
           cache: pnpm
       - run: pnpm install --frozen-lockfile
-      - run: pnpm playwright install
+      - run: pnpm playwright install ${{ matrix.e2e-browser }}
       - run: pnpm test
       - name: Archive test results
         if: failure()
diff --git a/documentation/docs/05-load.md b/documentation/docs/05-load.md
index eb83c9111..0aaf5389d 100644
--- a/documentation/docs/05-load.md
+++ b/documentation/docs/05-load.md
@@ -99,12 +99,14 @@ An instance of [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL), co
 
 #### depends
 
-This function declares that the `load` function has a _dependency_ on one or more URLs, which can subsequently be used with [`invalidate()`](/docs/modules#$app-navigation-invalidate) to cause `load` to rerun.
+This function declares that the `load` function has a _dependency_ on one or more URLs or custom identifiers, which can subsequently be used with [`invalidate()`](/docs/modules#$app-navigation-invalidate) to cause `load` to rerun.
 
 Most of the time you won't need this, as `fetch` calls `depends` on your behalf — it's only necessary if you're using a custom API client that bypasses `fetch`.
 
 URLs can be absolute or relative to the page being loaded, and must be [encoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding).
 
+Custom identifiers have to be prefixed with one or more lowercase letters followed by a colon to conform to the [URI specification](https://www.rfc-editor.org/rfc/rfc3986.html)
+
 ```js
 // @filename: ambient.d.ts
 declare module '$lib/api' {
@@ -121,7 +123,11 @@ import * as api from '$lib/api';
 
 /** @type {import('./$types').PageLoad} */
 export async function load({ depends }) {
-	depends(`${api.base}/foo`, `${api.base}/bar`);
+	depends(
+		`${api.base}/foo`,
+		`${api.base}/bar`,
+		'my-stuff:foo'
+	);
 
 	return {
 		foo: api.client.get('/foo'),
diff --git a/documentation/docs/14-configuration.md b/documentation/docs/14-configuration.md
index 1893883bb..6fe48cd98 100644
--- a/documentation/docs/14-configuration.md
+++ b/documentation/docs/14-configuration.md
@@ -25,6 +25,9 @@ const config = {
 				// ...
 			}
 		},
+		csrf: {
+			checkOrigin: true
+		},
 		env: {
 			dir: process.cwd(),
 			publicPrefix: 'PUBLIC_'
@@ -161,6 +164,14 @@ When pages are prerendered, the CSP header is added via a `<meta http-equiv>` ta
 
 > Note that most [Svelte transitions](https://svelte.dev/tutorial/transition) work by creating an inline `<style>` element. If you use these in your app, you must either leave the `style-src` directive unspecified or add `unsafe-inline`.
 
+### csrf
+
+Protection against [cross-site request forgery](https://owasp.org/www-community/attacks/csrf) attacks:
+
+- `checkOrigin` — if `true`, SvelteKit will check the incoming `origin` header for `POST` form submissions and verify that it matches the server's origin
+
+To allow people to make `POST` form submissions to your app from other origins, you will need to disable this option. Be careful!
+
 ### env
 
 Environment variable configuration:
diff --git a/packages/adapter-static/package.json b/packages/adapter-static/package.json
index cf797a1b6..33dee37a6 100644
--- a/packages/adapter-static/package.json
+++ b/packages/adapter-static/package.json
@@ -27,7 +27,6 @@
 	"devDependencies": {
 		"@sveltejs/kit": "workspace:*",
 		"@types/node": "^16.11.36",
-		"playwright-chromium": "^1.25.0",
 		"sirv": "^2.0.2",
 		"svelte": "^3.48.0",
 		"typescript": "^4.8.2",
diff --git a/packages/create-svelte/CHANGELOG.md b/packages/create-svelte/CHANGELOG.md
index 9d7c46493..1aa9c2f85 100644
--- a/packages/create-svelte/CHANGELOG.md
+++ b/packages/create-svelte/CHANGELOG.md
@@ -1,5 +1,11 @@
 # create-svelte
 
+## 2.0.0-next.171
+
+### Patch Changes
+
+- Use `invalidateAll()` ([#6493](https://github.com/sveltejs/kit/pull/6493))
+
 ## 2.0.0-next.170
 
 ### Patch Changes
diff --git a/packages/create-svelte/package.json b/packages/create-svelte/package.json
index 70c06139b..cc80e79b5 100644
--- a/packages/create-svelte/package.json
+++ b/packages/create-svelte/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "create-svelte",
-	"version": "2.0.0-next.170",
+	"version": "2.0.0-next.171",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/create-svelte/templates/default/src/lib/form.ts b/packages/create-svelte/templates/default/src/lib/form.ts
index c38dee6ec..6723c612f 100644
--- a/packages/create-svelte/templates/default/src/lib/form.ts
+++ b/packages/create-svelte/templates/default/src/lib/form.ts
@@ -1,4 +1,4 @@
-import { invalidate } from '$app/navigation';
+import { invalidateAll } from '$app/navigation';
 
 // this action (https://svelte.dev/tutorial/actions) allows us to
 // progressively enhance a <form> that already works without JS
@@ -83,7 +83,7 @@ export function enhance(
 
 			if (response.ok) {
 				if (result) result({ data, form, response });
-				invalidate();
+				invalidateAll();
 			} else if (error) {
 				error({ data, form, error: null, response });
 			} else {
diff --git a/packages/create-svelte/templates/default/vercel.json b/packages/create-svelte/templates/default/vercel.json
deleted file mode 100644
index 564eeeb2c..000000000
--- a/packages/create-svelte/templates/default/vercel.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
-	"github": {
-		"silent": true
-	}
-}
diff --git a/packages/kit/CHANGELOG.md b/packages/kit/CHANGELOG.md
index 8c86a5604..f5b25350d 100644
--- a/packages/kit/CHANGELOG.md
+++ b/packages/kit/CHANGELOG.md
@@ -1,5 +1,21 @@
 # @sveltejs/kit
 
+## 1.0.0-next.462
+
+### Patch Changes
+
+- [breaking] block cross-site form POSTs by default. disable with config.kit.csrf.checkOrigin ([#6510](https://github.com/sveltejs/kit/pull/6510))
+
+## 1.0.0-next.461
+
+### Patch Changes
+
+- [breaking] call `invalidate(fn)` predicates with a URL instead of a string ([#6493](https://github.com/sveltejs/kit/pull/6493))
+
+* Prefer JSON responses when returning errors if accept header is `*/*` ([#6497](https://github.com/sveltejs/kit/pull/6497))
+
+- [breaking] replace invalidate() with invalidateAll() ([#6493](https://github.com/sveltejs/kit/pull/6493))
+
 ## 1.0.0-next.460
 
 ### Patch Changes
diff --git a/packages/kit/package.json b/packages/kit/package.json
index 4a84a24be..cab35c96e 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/kit",
-	"version": "1.0.0-next.460",
+	"version": "1.0.0-next.462",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js
index c324dd347..4ece16a53 100644
--- a/packages/kit/src/core/config/index.spec.js
+++ b/packages/kit/src/core/config/index.spec.js
@@ -72,6 +72,9 @@ const get_defaults = (prefix = '') => ({
 			directives: directive_defaults,
 			reportOnly: directive_defaults
 		},
+		csrf: {
+			checkOrigin: true
+		},
 		endpointExtensions: undefined,
 		env: {
 			dir: process.cwd(),
diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js
index 9b4eefd5c..a442fa1d7 100644
--- a/packages/kit/src/core/config/options.js
+++ b/packages/kit/src/core/config/options.js
@@ -125,6 +125,10 @@ const options = object(
 				reportOnly: directives
 			}),
 
+			csrf: object({
+				checkOrigin: boolean(true)
+			}),
+
 			// TODO: remove this for the 1.0 release
 			endpointExtensions: error(
 				(keypath) => `${keypath} has been renamed to config.kit.moduleExtensions`
diff --git a/packages/kit/src/exports/vite/build/build_server.js b/packages/kit/src/exports/vite/build/build_server.js
index 231243312..137d358ee 100644
--- a/packages/kit/src/exports/vite/build/build_server.js
+++ b/packages/kit/src/exports/vite/build/build_server.js
@@ -54,6 +54,9 @@ export class Server {
 	constructor(manifest) {
 		this.options = {
 			csp: ${s(config.kit.csp)},
+			csrf: {
+				check_origin: ${s(config.kit.csrf.checkOrigin)},
+			},
 			dev: false,
 			get_stack: error => String(error), // for security
 			handle_error: (error, event) => {
diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js
index cc6b0bd23..4ce5b8fb9 100644
--- a/packages/kit/src/exports/vite/dev/index.js
+++ b/packages/kit/src/exports/vite/dev/index.js
@@ -377,6 +377,9 @@ export async function dev(vite, vite_config, svelte_config, illegal_imports) {
 					request,
 					{
 						csp: svelte_config.kit.csp,
+						csrf: {
+							check_origin: svelte_config.kit.csrf.checkOrigin
+						},
 						dev: true,
 						get_stack: (error) => fix_stack_trace(error),
 						handle_error: (error, event) => {
diff --git a/packages/kit/src/runtime/app/navigation.js b/packages/kit/src/runtime/app/navigation.js
index d97e6dea6..974e83a13 100644
--- a/packages/kit/src/runtime/app/navigation.js
+++ b/packages/kit/src/runtime/app/navigation.js
@@ -16,6 +16,7 @@ export const disableScrollHandling = ssr
 	: client.disable_scroll_handling;
 export const goto = ssr ? guard('goto') : client.goto;
 export const invalidate = ssr ? guard('invalidate') : client.invalidate;
+export const invalidateAll = ssr ? guard('invalidateAll') : client.invalidateAll;
 export const prefetch = ssr ? guard('prefetch') : client.prefetch;
 export const prefetchRoutes = ssr ? guard('prefetchRoutes') : client.prefetch_routes;
 export const beforeNavigate = ssr ? () => {} : client.before_navigate;
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index 6b95fbbfd..190348e84 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -70,7 +70,7 @@ function check_for_removed_attributes() {
  * @returns {import('./types').Client}
  */
 export function create_client({ target, base, trailing_slash }) {
-	/** @type {Array<((href: string) => boolean)>} */
+	/** @type {Array<((url: URL) => boolean)>} */
 	const invalidated = [];
 
 	/** @type {{id: string | null, promise: Promise<import('./types').NavigationResult | undefined> | null}} */
@@ -103,6 +103,7 @@ export function create_client({ target, base, trailing_slash }) {
 
 	/** @type {Promise<void> | null} */
 	let invalidating = null;
+	let force_invalidation = false;
 
 	/** @type {import('svelte').SvelteComponent} */
 	let root;
@@ -139,6 +140,19 @@ export function create_client({ target, base, trailing_slash }) {
 	/** @type {{}} */
 	let token;
 
+	function invalidate() {
+		if (!invalidating) {
+			invalidating = Promise.resolve().then(async () => {
+				await update(new URL(location.href), []);
+
+				invalidating = null;
+				force_invalidation = false;
+			});
+		}
+
+		return invalidating;
+	}
+
 	/**
 	 * @param {string | URL} url
 	 * @param {{ noscroll?: boolean; replaceState?: boolean; keepfocus?: boolean; state?: any }} opts
@@ -639,6 +653,8 @@ export function create_client({ target, base, trailing_slash }) {
 	 * @param {{ url: boolean, params: string[] }} changed
 	 */
 	function has_changed(changed, parent_changed, uses) {
+		if (force_invalidation) return true;
+
 		if (!uses) return false;
 
 		if (uses.parent && parent_changed) return true;
@@ -648,8 +664,8 @@ export function create_client({ target, base, trailing_slash }) {
 			if (uses.params.has(param)) return true;
 		}
 
-		for (const dep of uses.dependencies) {
-			if (invalidated.some((fn) => fn(dep))) return true;
+		for (const href of uses.dependencies) {
+			if (invalidated.some((fn) => fn(new URL(href)))) return true;
 		}
 
 		return false;
@@ -1057,28 +1073,25 @@ export function create_client({ target, base, trailing_slash }) {
 
 		invalidate: (resource) => {
 			if (resource === undefined) {
-				// Force rerun of all load functions, regardless of their dependencies
-				for (const node of current.branch) {
-					node?.server?.uses.dependencies.add('');
-					node?.shared?.uses.dependencies.add('');
-				}
-				invalidated.push(() => true);
-			} else if (typeof resource === 'function') {
+				// TODO remove for 1.0
+				throw new Error(
+					'`invalidate()` (with no arguments) has been replaced by `invalidateAll()`'
+				);
+			}
+
+			if (typeof resource === 'function') {
 				invalidated.push(resource);
 			} else {
 				const { href } = new URL(resource, location.href);
-				invalidated.push((dep) => dep === href);
+				invalidated.push((url) => url.href === href);
 			}
 
-			if (!invalidating) {
-				invalidating = Promise.resolve().then(async () => {
-					await update(new URL(location.href), []);
-
-					invalidating = null;
-				});
-			}
+			return invalidate();
+		},
 
-			return invalidating;
+		invalidateAll: () => {
+			force_invalidation = true;
+			return invalidate();
 		},
 
 		prefetch: async (href) => {
diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts
index e86bc3ae0..f2f5a3617 100644
--- a/packages/kit/src/runtime/client/types.d.ts
+++ b/packages/kit/src/runtime/client/types.d.ts
@@ -3,6 +3,7 @@ import {
 	beforeNavigate,
 	goto,
 	invalidate,
+	invalidateAll,
 	prefetch,
 	prefetchRoutes
 } from '$app/navigation';
@@ -17,6 +18,7 @@ export interface Client {
 	disable_scroll_handling: () => void;
 	goto: typeof goto;
 	invalidate: typeof invalidate;
+	invalidateAll: typeof invalidateAll;
 	prefetch: typeof prefetch;
 	prefetch_routes: typeof prefetchRoutes;
 
diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js
index 413903640..ec142c6ec 100644
--- a/packages/kit/src/runtime/server/index.js
+++ b/packages/kit/src/runtime/server/index.js
@@ -18,6 +18,21 @@ const default_transform = ({ html }) => html;
 export async function respond(request, options, state) {
 	let url = new URL(request.url);
 
+	if (options.csrf.check_origin) {
+		const type = request.headers.get('content-type')?.split(';')[0];
+
+		const forbidden =
+			request.method === 'POST' &&
+			request.headers.get('origin') !== url.origin &&
+			(type === 'application/x-www-form-urlencoded' || type === 'multipart/form-data');
+
+		if (forbidden) {
+			return new Response(`Cross-site ${request.method} form submissions are forbidden`, {
+				status: 403
+			});
+		}
+	}
+
 	const { parameter, allowed } = options.method_override;
 	const method_override = url.searchParams.get(parameter)?.toUpperCase();
 
diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js
index d4659cdbe..437de80e5 100644
--- a/packages/kit/src/runtime/server/utils.js
+++ b/packages/kit/src/runtime/server/utils.js
@@ -194,8 +194,8 @@ export function handle_fatal_error(event, options, error) {
 
 	// ideally we'd use sec-fetch-dest instead, but Safari — quelle surprise — doesn't support it
 	const type = negotiate(event.request.headers.get('accept') || 'text/html', [
-		'text/html',
-		'application/json'
+		'application/json',
+		'text/html'
 	]);
 
 	if (event.url.pathname.endsWith(DATA_SUFFIX) || type === 'application/json') {
diff --git a/packages/kit/types/ambient.d.ts b/packages/kit/types/ambient.d.ts
index 15d742150..0dca0aa9f 100644
--- a/packages/kit/types/ambient.d.ts
+++ b/packages/kit/types/ambient.d.ts
@@ -113,10 +113,25 @@ declare module '$app/navigation' {
 		opts?: { replaceState?: boolean; noscroll?: boolean; keepfocus?: boolean; state?: any }
 	): Promise<void>;
 	/**
-	 * Causes any `load` functions belonging to the currently active page to re-run if they `fetch` the resource in question. If no argument is given, all resources will be invalidated. Returns a `Promise` that resolves when the page is subsequently updated.
-	 * @param dependency The invalidated resource
+	 * Causes any `load` functions belonging to the currently active page to re-run if they depend on the `url` in question, via `fetch` or `depends`. Returns a `Promise` that resolves when the page is subsequently updated.
+	 *
+	 * If the argument is given as a `string` or `URL`, it must resolve to the same URL that was passed to `fetch` or `depends` (including query parameters).
+	 * To create a custom identifier, use a string beginning with `[a-z]+:` (e.g. `custom:state`) — this is a valid URL.
+	 *
+	 * The `function` argument can be used define a custom predicate. It receives the full `URL` and causes `load` to rerun if `true` is returned.
+	 * This can be useful if you want to invalidate based on a pattern instead of a exact match.
+	 *
+	 * ```ts
+	 * // Example: Match '/path' regardless of the query parameters
+	 * invalidate((url) => url.pathname === '/path');
+	 * ```
+	 * @param url The invalidated URL
+	 */
+	export function invalidate(url: string | URL | ((url: URL) => boolean)): Promise<void>;
+	/**
+	 * Causes all `load` functions belonging to the currently active page to re-run. Returns a `Promise` that resolves when the page is subsequently updated.
 	 */
-	export function invalidate(dependency?: string | ((href: string) => boolean)): Promise<void>;
+	export function invalidateAll(): Promise<void>;
 	/**
 	 * Programmatically prefetches the given page, which means
 	 *  1. ensuring that the code for the page is loaded, and
diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts
index ffe84ca00..cbbd7fb73 100644
--- a/packages/kit/types/index.d.ts
+++ b/packages/kit/types/index.d.ts
@@ -128,6 +128,9 @@ export interface KitConfig {
 		directives?: CspDirectives;
 		reportOnly?: CspDirectives;
 	};
+	csrf?: {
+		checkOrigin?: boolean;
+	};
 	env?: {
 		dir?: string;
 		publicPrefix?: string;
diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts
index 234af54f8..508a7bb6e 100644
--- a/packages/kit/types/internal.d.ts
+++ b/packages/kit/types/internal.d.ts
@@ -290,6 +290,9 @@ export type SSRNodeLoader = () => Promise<SSRNode>;
 
 export interface SSROptions {
 	csp: ValidatedConfig['kit']['csp'];
+	csrf: {
+		check_origin: boolean;
+	};
 	dev: boolean;
 	get_stack: (error: Error) => string | undefined;
 	handle_error(error: Error & { frame?: string }, event: RequestEvent): void;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6d729cd68..995152fcc 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -147,7 +147,6 @@ importers:
     specifiers:
       '@sveltejs/kit': workspace:*
       '@types/node': ^16.11.36
-      playwright-chromium: ^1.25.0
       sirv: ^2.0.2
       svelte: ^3.48.0
       typescript: ^4.8.2
@@ -156,7 +155,6 @@ importers:
     devDependencies:
       '@sveltejs/kit': link:../kit
       '@types/node': 16.11.42
-      playwright-chromium: 1.25.0
       sirv: 2.0.2
       svelte: 3.48.0
       typescript: 4.8.2
@@ -3277,15 +3275,6 @@ packages:
       find-up: 4.1.0
     dev: true
 
-  /playwright-chromium/1.25.0:
-    resolution: {integrity: sha512-FH9ho3noAWVStCJx4XW78+D8QW0A99WDp53DDkYeVdEpJqCmAIKHCSE6dl5XtaDKrZPYC1ZG5hGXQh1K5H/p+g==}
-    engines: {node: '>=14'}
-    hasBin: true
-    requiresBuild: true
-    dependencies:
-      playwright-core: 1.25.0
-    dev: true
-
   /playwright-core/1.25.0:
     resolution: {integrity: sha512-kZ3Jwaf3wlu0GgU0nB8UMQ+mXFTqBIFz9h1svTlNduNKjnbPXFxw7mJanLVjqxHJRn62uBfmgBj93YHidk2N5Q==}
     engines: {node: '>=14'}
diff --git a/sites/kit.svelte.dev/vercel.json b/sites/kit.svelte.dev/vercel.json
deleted file mode 100644
index 564eeeb2c..000000000
--- a/sites/kit.svelte.dev/vercel.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
-	"github": {
-		"silent": true
-	}
-}


Test patch:
diff --git a/packages/adapter-static/test/utils.js b/packages/adapter-static/test/utils.js
index 5f7bd122e..1ace81f46 100644
--- a/packages/adapter-static/test/utils.js
+++ b/packages/adapter-static/test/utils.js
@@ -3,9 +3,26 @@ import fs from 'fs';
 import http from 'http';
 import { fileURLToPath } from 'url';
 import sirv from 'sirv';
-import { chromium } from 'playwright-chromium';
+import { chromium, webkit, firefox } from 'playwright';
 import * as uvu from 'uvu';
 
+const known_browsers = {
+	chromium: chromium,
+	firefox: firefox,
+	webkit: webkit
+};
+const test_browser_name = /** @type {keyof typeof } */ (process.env.KIT_E2E_BROWSER ?? 'chromium');
+
+const test_browser = known_browsers[test_browser_name];
+
+if (!test_browser) {
+	throw new Error(
+		`invalid test browser specified: KIT_E2E_BROWSER=${
+			process.env.KIT_E2E_BROWSER
+		}. Allowed values: ${Object.keys().join(', ')}`
+	);
+}
+
 /**
  * @typedef {{
  *   cwd: string;
@@ -55,7 +72,7 @@ export function run(app, callback) {
 
 			context.port = port;
 			context.base = `http://localhost:${context.port}`;
-			context.browser = await chromium.launch();
+			context.browser = await test_browser.launch();
 			context.page = await context.browser.newPage();
 		} catch (e) {
 			// TODO remove unnecessary try-catch https://github.com/lukeed/uvu/pull/61
@@ -64,8 +81,20 @@ export function run(app, callback) {
 	});
 
 	suite.after(async (context) => {
-		context.server.close();
-		context.browser.close();
+		if (context.browser) {
+			try {
+				await context.browser.close();
+			} catch (e) {
+				console.error('failed to close test browser', e);
+			}
+		}
+		if (context.server) {
+			try {
+				await context.server.close();
+			} catch (e) {
+				console.error('failed to close test server', e);
+			}
+		}
 	});
 
 	callback(suite);
diff --git a/packages/kit/test/apps/basics/src/routes/csrf/+server.js b/packages/kit/test/apps/basics/src/routes/csrf/+server.js
new file mode 100644
index 000000000..cfbef1424
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/csrf/+server.js
@@ -0,0 +1,4 @@
+/** @type {import('./$types').RequestHandler} */
+export function POST() {
+	return new Response(undefined, { status: 201 });
+}
diff --git a/packages/kit/test/apps/basics/src/routes/load/change-detection/one/[x]/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/change-detection/one/[x]/+page.svelte
index 9acf3f221..11134eaf5 100644
--- a/packages/kit/test/apps/basics/src/routes/load/change-detection/one/[x]/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/load/change-detection/one/[x]/+page.svelte
@@ -1,5 +1,5 @@
 <script>
-	import { invalidate } from '$app/navigation';
+	import { invalidate, invalidateAll } from '$app/navigation';
 
 	/** @type {import('./$types').PageData} */
 	export let data;
@@ -10,7 +10,7 @@
 <button
 	on:click={async () => {
 		window.invalidated = false;
-		await invalidate((dep) => dep.includes('change-detection/data.json'));
+		await invalidate((url) => url.pathname.includes('change-detection/data.json'));
 		window.invalidated = true;
 	}}>invalidate change-detection/data.json</button
 >
@@ -18,7 +18,7 @@
 <button
 	on:click={async () => {
 		window.invalidated = false;
-		await invalidate();
+		await invalidateAll();
 		window.invalidated = true;
 	}}>invalidate all</button
 >
diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/forced/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/invalidation/forced/+page.svelte
index 0f544b39f..b66f248de 100644
--- a/packages/kit/test/apps/basics/src/routes/load/invalidation/forced/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/load/invalidation/forced/+page.svelte
@@ -1,5 +1,5 @@
 <script>
-	import { invalidate } from '$app/navigation';
+	import { invalidateAll } from '$app/navigation';
 
 	/** @type {import('./$types').PageData} */
 	export let data;
@@ -7,4 +7,4 @@
 
 <h1>a: {data.a}, b: {data.b}</h1>
 
-<button on:click={() => invalidate()}>invalidate</button>
+<button on:click={invalidateAll}>invalidate</button>
diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js
index 53c83b05c..6f5c87ec4 100644
--- a/packages/kit/test/apps/basics/test/client.test.js
+++ b/packages/kit/test/apps/basics/test/client.test.js
@@ -572,7 +572,7 @@ test.describe('Shadow DOM', () => {
 		await clicknav('div[id="clickme"]');
 		expect(await page.textContent('h1')).toBe('a');
 
-		expect(requests).toEqual([]);
+		expect(requests.filter((url) => !url.endsWith('/favicon.png'))).toEqual([]);
 	});
 });
 
diff --git a/packages/kit/test/apps/basics/test/server.test.js b/packages/kit/test/apps/basics/test/server.test.js
index c409a799e..9cf989154 100644
--- a/packages/kit/test/apps/basics/test/server.test.js
+++ b/packages/kit/test/apps/basics/test/server.test.js
@@ -1,5 +1,6 @@
 import { expect } from '@playwright/test';
 import { test } from '../../../utils.js';
+import { fetch } from 'undici';
 import { createHash, randomBytes } from 'node:crypto';
 
 /** @typedef {import('@playwright/test').Response} Response */
@@ -22,6 +23,20 @@ test.describe('Content-Type', () => {
 	});
 });
 
+test.describe('CSRF', () => {
+	test('Blocks requests with incorrect origin', async ({ baseURL }) => {
+		const res = await fetch(`${baseURL}/csrf`, {
+			method: 'POST',
+			headers: {
+				'content-type': 'application/x-www-form-urlencoded'
+			}
+		});
+
+		expect(res.status).toBe(403);
+		expect(await res.text()).toBe('Cross-site POST form submissions are forbidden');
+	});
+});
+
 test.describe('Endpoints', () => {
 	test('HEAD with matching headers but without body', async ({ request }) => {
 		const url = '/endpoint-output/body';
@@ -149,15 +164,39 @@ test.describe('Errors', () => {
 	});
 
 	test('throw error(...) in endpoint', async ({ request, read_errors }) => {
-		const res = await request.get('/errors/endpoint-throw-error');
+		// HTML
+		{
+			const res = await request.get('/errors/endpoint-throw-error', {
+				headers: {
+					accept: 'text/html'
+				}
+			});
+
+			const error = read_errors('/errors/endpoint-throw-error');
+			expect(error).toBe(undefined);
+
+			expect(res.status()).toBe(401);
+			expect(await res.text()).toContain(
+				'This is the static error page with the following message: You shall not pass'
+			);
+		}
 
-		const error = read_errors('/errors/endpoint-throw-error');
-		expect(error).toBe(undefined);
+		// JSON (default)
+		{
+			const res = await request.get('/errors/endpoint-throw-error');
 
-		expect(await res?.text()).toContain(
-			'This is the static error page with the following message: You shall not pass'
-		);
-		expect(res?.status()).toBe(401);
+			const error = read_errors('/errors/endpoint-throw-error');
+			expect(error).toBe(undefined);
+
+			expect(res.status()).toBe(401);
+			expect(await res.json()).toEqual({
+				status: 401,
+				message: 'You shall not pass',
+
+				// TODO this is gross, fix it
+				__is_http_error: true
+			});
+		}
 	});
 
 	test('throw redirect(...) in endpoint', async ({ page, read_errors }) => {
@@ -170,13 +209,38 @@ test.describe('Errors', () => {
 		expect(await page.textContent('h1')).toBe('the answer is 42');
 	});
 
-	test('error thrown in handle results in a rendered error page', async ({ request }) => {
-		const res = await request.get('/errors/error-in-handle');
+	test('error thrown in handle results in a rendered error page or JSON response', async ({
+		request
+	}) => {
+		// HTML
+		{
+			const res = await request.get('/errors/error-in-handle', {
+				headers: {
+					accept: 'text/html'
+				}
+			});
+
+			expect(res.status()).toBe(500);
+			expect(await res.text()).toContain(
+				'This is the static error page with the following message: Error in handle'
+			);
+		}
+
+		// JSON (default)
+		{
+			const res = await request.get('/errors/error-in-handle');
 
-		expect(await res.text()).toContain(
-			'This is the static error page with the following message: Error in handle'
-		);
-		expect(res.status()).toBe(500);
+			const error = await res.json();
+
+			expect(typeof error.stack).toBe('string');
+			delete error.stack;
+
+			expect(res.status()).toBe(500);
+			expect(error).toEqual({
+				name: 'Error',
+				message: 'Error in handle'
+			});
+		}
 	});
 });
 
diff --git a/packages/kit/test/utils.js b/packages/kit/test/utils.js
index 3e23d49bb..cf7f24889 100644
--- a/packages/kit/test/utils.js
+++ b/packages/kit/test/utils.js
@@ -115,7 +115,7 @@ export const test = base.extend({
 const known_devices = {
 	chromium: devices['Desktop Chrome'],
 	firefox: devices['Desktop Firefox'],
-	safari: devices['Desktop Safari']
+	webkit: devices['Desktop Safari']
 };
 const test_browser = /** @type {keyof typeof known_devices} */ (
 	process.env.KIT_E2E_BROWSER ?? 'chromium'