Repository: withastro/astro
Pull request #13514: feat(cloudflare): add KV session storage support
Author: login: ascorbic
Labels: name: pkg: integration
description: Related to any renderer integration (scope)
name: pr preview
description: Apply this label to a PR to generate a preview release
Created: 2025-03-28T15:41:29Z | Merged: 2025-03-31T09:05:44Z

## Changes

Adds support for automatically-provisioning session storage for Cloudflare KV

Automatically configures Cloudflare KV storage when experimental sessions are enabled


If the `experimental.session` flag is enabled when using the Cloudflare adapter, Astro will automatically configure the session storage using the Cloudflare KV driver. You can still manually configure the session storage if you need to use a different driver or want to customize the session storage configuration. If you want to use sessions, you will need to create the KV namespace and declare it in your wrangler config. You can do this using the Wrangler CLI:

```sh
npx wrangler kv namespace create SESSION
```

This will log the id of the created namespace. You can then add it to your `wrangler.json`/`wrangler.toml` file like this:

```jsonc
// wrangler.json
{
  "kv_namespaces": [
    {
      "binding": "SESSION",
      "id": "<your kv namespace id here>"
    }
  ]
}
```

By default it uses the binding name `SESSION`, but if you want to use a different binding name you can do so by passing the `sessionKVBindingName` option to the adapter. For example:

```js
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
  output: 'server',
  site: `http://example.com`,
  adapter: cloudflare({
    platformProxy: {
      enabled: true,
    },
    sessionKVBindingName: 'MY_SESSION',
  }),
  experimental: {
    session: true,
  }
});

```

## Testing

Adds fixture and test suite. To try it on your own site, use [the preview release of `@astrojs/cloudflare`](https://github.com/withastro/astro/pull/13514#issuecomment-2761728375)

<!-- How was this change tested? -->
<!-- DON'T DELETE THIS SECTION! If no tests added, explain why. -->

## Docs
Needs updated docs
<!-- Could this affect a user’s behavior? We probably need to update docs! -->
<!-- If docs will be needed or you’re not sure, uncomment the next line: -->
<!-- /cc @withastro/maintainers-docs for feedback! -->

<!-- DON'T DELETE THIS SECTION! If no docs added, explain why.-->
<!-- https://github.com/withastro/docs -->



Diff:
diff --git a/.changeset/twenty-dogs-listen.md b/.changeset/twenty-dogs-listen.md
new file mode 100644
index 000000000000..de765d110b9d
--- /dev/null
+++ b/.changeset/twenty-dogs-listen.md
@@ -0,0 +1,51 @@
+---
+'@astrojs/cloudflare': minor
+---
+
+Automatically configures Cloudflare KV storage when experimental sessions are enabled
+
+
+If the `experimental.session` flag is enabled when using the Cloudflare adapter, Astro will automatically configure the session storage using the Cloudflare KV driver. You can still manually configure the session storage if you need to use a different driver or want to customize the session storage configuration. If you want to use sessions, you will need to create the KV namespace and declare it in your wrangler config. You can do this using the Wrangler CLI:
+
+```sh
+npx wrangler kv namespace create SESSION
+```
+
+This will log the id of the created namespace. You can then add it to your `wrangler.json`/`wrangler.toml` file like this:
+
+```jsonc
+// wrangler.json
+{
+  "kv_namespaces": [
+    {
+      "binding": "SESSION",
+      "id": "<your kv namespace id here>"
+    }
+  ]
+}
+```
+
+By default it uses the binding name `SESSION`, but if you want to use a different binding name you can do so by passing the `sessionKVBindingName` option to the adapter. For example:
+
+```js
+import { defineConfig } from 'astro/config';
+import cloudflare from '@astrojs/cloudflare';
+export default defineConfig({
+  output: 'server',
+  site: `http://example.com`,
+  adapter: cloudflare({
+    platformProxy: {
+      enabled: true,
+    },
+    sessionKVBindingName: 'MY_SESSION',
+  }),
+  experimental: {
+    session: true,
+  }
+});
+
+```
+
+See [the Cloudflare KV docs](https://developers.cloudflare.com/kv/concepts/kv-namespaces/) for more details on setting up KV namespaces.
+
+See [the experimental session docs](https://docs.astro.build/en/reference/experimental-flags/sessions/) for more information on configuring session storage.
diff --git a/packages/integrations/cloudflare/package.json b/packages/integrations/cloudflare/package.json
index 72ef4ac05055..5978877cb7e6 100644
--- a/packages/integrations/cloudflare/package.json
+++ b/packages/integrations/cloudflare/package.json
@@ -36,14 +36,14 @@
   "dependencies": {
     "@astrojs/internal-helpers": "workspace:*",
     "@astrojs/underscore-redirects": "workspace:*",
-    "@cloudflare/workers-types": "^4.20250317.0",
+    "@cloudflare/workers-types": "^4.20250327.0",
     "esbuild": "^0.25.0",
     "estree-walker": "^3.0.3",
     "magic-string": "^0.30.17",
-    "miniflare": "^4.20250317.0",
+    "miniflare": "^4.20250321.1",
     "tinyglobby": "^0.2.12",
     "vite": "^6.2.3",
-    "wrangler": "^4.2.0"
+    "wrangler": "^4.5.1"
   },
   "peerDependencies": {
     "astro": "^5.0.0"
@@ -52,6 +52,7 @@
     "astro": "workspace:*",
     "astro-scripts": "workspace:*",
     "cheerio": "1.0.0",
+    "devalue": "^5.1.1",
     "execa": "^8.0.1",
     "rollup": "^4.35.0",
     "strip-ansi": "^7.1.0"
diff --git a/packages/integrations/cloudflare/src/entrypoints/server.ts b/packages/integrations/cloudflare/src/entrypoints/server.ts
index 231dd87fa391..b3cc6daf50e5 100644
--- a/packages/integrations/cloudflare/src/entrypoints/server.ts
+++ b/packages/integrations/cloudflare/src/entrypoints/server.ts
@@ -26,6 +26,16 @@ export interface Runtime<T extends object = object> {
 	};
 }
 
+declare global {
+	// This is not a real global, but is injected using Vite define to allow us to specify the session binding name in the config.
+	// eslint-disable-next-line no-var
+	var __ASTRO_SESSION_BINDING_NAME: string;
+
+	// Just used to pass the KV binding to unstorage.
+	// eslint-disable-next-line no-var
+	var __env__: Partial<Env>;
+}
+
 export function createExports(manifest: SSRManifest) {
 	const app = new App(manifest);
 
@@ -35,7 +45,12 @@ export function createExports(manifest: SSRManifest) {
 		context: ExecutionContext,
 	) => {
 		const { pathname } = new URL(request.url);
-
+		const bindingName = globalThis.__ASTRO_SESSION_BINDING_NAME;
+		// Assigning the KV binding to globalThis allows unstorage to access it for session storage.
+		// unstorage checks in globalThis and globalThis.__env__ for the binding.
+		globalThis.__env__ ??= {};
+		globalThis.__env__[bindingName] = env[bindingName];
+		
 		// static assets fallback, in case default _routes.json is not used
 		if (manifest.assets.has(pathname)) {
 			return env.ASSETS.fetch(request.url.replace(/\.html$/, ''));
diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts
index 346cc4021287..3147b31fea5a 100644
--- a/packages/integrations/cloudflare/src/index.ts
+++ b/packages/integrations/cloudflare/src/index.ts
@@ -25,6 +25,7 @@ import {
 import { createGetEnv } from './utils/env.js';
 import { createRoutesFile, getParts } from './utils/generate-routes-json.js';
 import { setImageConfig } from './utils/image-config.js';
+import { fileURLToPath } from 'node:url';
 
 export type { Runtime } from './entrypoints/server.js';
 
@@ -71,6 +72,34 @@ export type Options = {
 	 * for reference on how these file types are exported
 	 */
 	cloudflareModules?: boolean;
+
+	/**
+	 * By default, Astro will be configured to use Cloudflare KV to store session data. If you want to use sessions,
+	 * you must create a KV namespace and declare it in your wrangler config file. You can do this with the wrangler command:
+	 *
+	 * ```sh
+	 * npx wrangler kv namespace create SESSION
+	 * ```
+	 *
+	 * This will log the id of the created namespace. You can then add it to your `wrangler.json` file like this:
+	 *
+	 * ```json
+	 * {
+	 *   "kv_namespaces": [
+	 *     {
+	 *       "binding": "SESSION",
+	 *       "id": "<your kv namespace id here>"
+	 *     }
+	 *   ]
+	 * }
+	 * ```
+	 * By default, the driver looks for the binding named `SESSION`, but you can override this by providing a different name here.
+	 *
+	 * See https://developers.cloudflare.com/kv/concepts/kv-namespaces/ for more details on using KV namespaces.
+	 *
+	 */
+
+	sessionKVBindingName?: string;
 };
 
 function wrapWithSlashes(path: string): string {
@@ -110,7 +139,41 @@ export default function createIntegration(args?: Options): AstroIntegration {
 				logger,
 				addWatchFile,
 				addMiddleware,
+				createCodegenDir,
 			}) => {
+				let session = config.session;
+
+				const isBuild = command === 'build';
+
+				if (config.experimental.session && !session?.driver) {
+					const sessionDir = isBuild ? undefined : createCodegenDir();
+					const bindingName = args?.sessionKVBindingName ?? 'SESSION';
+					logger.info(
+						`Configuring experimental session support using ${isBuild ? 'Cloudflare KV' : 'filesystem storage'}. Be sure to define a KV binding named "${bindingName}".`,
+					);
+					logger.info(
+						`If you see the error "Invalid binding \`${bindingName}\`" in your build output, you need to add the binding to your wrangler config file.`,
+					);
+					session = isBuild
+						? {
+								...session,
+								driver: 'cloudflare-kv-binding',
+								options: {
+									binding: bindingName,
+									...session?.options,
+								},
+							}
+						: {
+								...session,
+								driver: 'fs-lite',
+								options: {
+									base: fileURLToPath(new URL('sessions', sessionDir)),
+									...session?.options,
+								},
+							};
+				}
+
+
 				updateConfig({
 					build: {
 						client: new URL(`.${wrapWithSlashes(config.base)}`, config.outDir),
@@ -118,6 +181,7 @@ export default function createIntegration(args?: Options): AstroIntegration {
 						serverEntry: 'index.js',
 						redirects: false,
 					},
+					session,
 					vite: {
 						plugins: [
 							// https://developers.cloudflare.com/pages/functions/module-support/
@@ -254,6 +318,10 @@ export default function createIntegration(args?: Options): AstroIntegration {
 					// in a global way, so we shim their access as `process.env.*`. This is not the recommended way for users to access environment variables. But we'll add this for compatibility for chosen variables. Mainly to support `@astrojs/db`
 					vite.define = {
 						'process.env': 'process.env',
+						// Allows the request handler to know what the binding name is
+						'globalThis.__ASTRO_SESSION_BINDING_NAME': JSON.stringify(
+							args?.sessionKVBindingName ?? 'SESSION',
+						),
 						...vite.define,
 					};
 				}
diff --git a/packages/integrations/cloudflare/test/fixtures/astro-dev-platform/package.json b/packages/integrations/cloudflare/test/fixtures/astro-dev-platform/package.json
index a406e7c61e11..a39e254fa061 100644
--- a/packages/integrations/cloudflare/test/fixtures/astro-dev-platform/package.json
+++ b/packages/integrations/cloudflare/test/fixtures/astro-dev-platform/package.json
@@ -7,6 +7,6 @@
     "astro": "workspace:*"
   },
   "devDependencies": {
-    "wrangler": "^3.112.0"
+    "wrangler": "^4.5.1"
   }
 }
diff --git a/packages/integrations/cloudflare/test/fixtures/astro-env/package.json b/packages/integrations/cloudflare/test/fixtures/astro-env/package.json
index d3b910161425..4a9fd822b670 100644
--- a/packages/integrations/cloudflare/test/fixtures/astro-env/package.json
+++ b/packages/integrations/cloudflare/test/fixtures/astro-env/package.json
@@ -7,6 +7,6 @@
     "astro": "workspace:*"
   },
   "devDependencies": {
-    "wrangler": "^4.4.1"
+    "wrangler": "^4.5.1"
   }
 }
diff --git a/packages/integrations/cloudflare/test/fixtures/sessions/astro.config.mjs b/packages/integrations/cloudflare/test/fixtures/sessions/astro.config.mjs
new file mode 100644
index 000000000000..7d37e976209d
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/sessions/astro.config.mjs
@@ -0,0 +1,15 @@
+// @ts-check
+import { defineConfig } from 'astro/config';
+import cloudflare from '@astrojs/cloudflare';
+export default defineConfig({
+  output: 'server',
+  site: `http://example.com`,
+  adapter: cloudflare({
+    platformProxy: {
+      enabled: true,
+    },
+  }),
+  experimental: {
+    session: true,
+  }
+});
diff --git a/packages/integrations/cloudflare/test/fixtures/sessions/package.json b/packages/integrations/cloudflare/test/fixtures/sessions/package.json
new file mode 100644
index 000000000000..d9a6f3af8ffa
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/sessions/package.json
@@ -0,0 +1,17 @@
+{
+  "name": "@test/astro-cloudflare-sessions",
+  "version": "0.0.0",
+  "private": true,
+  "dependencies": {
+    "@astrojs/cloudflare": "workspace:*"
+  },
+  "devDependencies": {
+    "astro": "workspace:*",
+    "wrangler": "^4.5.1"
+  },
+  "scripts": {
+    "build": "astro build",
+    "preview": "astro build && wrangler dev",
+    "start": "astro dev"
+  }
+}
diff --git a/packages/integrations/cloudflare/test/fixtures/sessions/src/actions/index.ts b/packages/integrations/cloudflare/test/fixtures/sessions/src/actions/index.ts
new file mode 100644
index 000000000000..856f68ba8fb1
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/sessions/src/actions/index.ts
@@ -0,0 +1,36 @@
+import { defineAction } from 'astro:actions';
+import { z } from 'astro:schema';
+
+export const server = {
+	addToCart: defineAction({
+		accept: 'form',
+		input: z.object({ productId: z.string() }),
+		handler: async (input, context) => {
+			const cart: Array<string> = (await context.session.get('cart')) || [];
+			cart.push(input.productId);
+			await context.session.set('cart', cart);
+			return { cart, message: 'Product added to cart at ' + new Date().toTimeString() };
+		},
+	}),
+	getCart: defineAction({
+		handler: async (input, context) => {
+			return await context.session.get('cart');
+		},
+	}),
+	clearCart: defineAction({
+		accept: 'json',
+		handler: async (input, context) => {
+			await context.session.set('cart', []);
+			return { cart: [], message: 'Cart cleared at ' + new Date().toTimeString() };
+		},
+	}),
+	addUrl: defineAction({
+		input: z.object({ favoriteUrl: z.string().url() }),
+		handler: async (input, context) => {
+			const previousFavoriteUrl = await context.session.get<URL>('favoriteUrl');
+			const url = new URL(input.favoriteUrl);
+			context.session.set('favoriteUrl', url);
+			return { message: 'Favorite URL set to ' + url.href + ' from ' + (previousFavoriteUrl?.href ?? "nothing") };
+		}
+	})
+}
diff --git a/packages/integrations/cloudflare/test/fixtures/sessions/src/middleware.ts b/packages/integrations/cloudflare/test/fixtures/sessions/src/middleware.ts
new file mode 100644
index 000000000000..7f56f11f364f
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/sessions/src/middleware.ts
@@ -0,0 +1,49 @@
+import { defineMiddleware } from 'astro:middleware';
+import { getActionContext } from 'astro:actions';
+
+const ACTION_SESSION_KEY = 'actionResult'
+
+export const onRequest = defineMiddleware(async (context, next) => {
+  // Skip requests for prerendered pages
+  if (context.isPrerendered) return next();
+
+  const { action, setActionResult, serializeActionResult } =
+    getActionContext(context);
+
+		console.log(action?.name)
+
+	const actionPayload = await context.session.get(ACTION_SESSION_KEY);
+
+  if (actionPayload) {
+    setActionResult(actionPayload.actionName, actionPayload.actionResult);
+    context.session.delete(ACTION_SESSION_KEY);
+    return next();
+  }
+
+  // If an action was called from an HTML form action,
+  // call the action handler and redirect to the destination page
+  if (action?.calledFrom === "form") {
+    const actionResult = await action.handler();
+
+    context.session.set(ACTION_SESSION_KEY, {
+      actionName: action.name,
+      actionResult: serializeActionResult(actionResult),
+    });
+
+
+    // Redirect back to the previous page on error
+    if (actionResult.error) {
+      const referer = context.request.headers.get("Referer");
+      if (!referer) {
+        throw new Error(
+          "Internal: Referer unexpectedly missing from Action POST request.",
+        );
+      }
+      return context.redirect(referer);
+    }
+    // Redirect to the destination page on success
+    return context.redirect(context.originPathname);
+  }
+
+  return next();
+});
diff --git a/packages/integrations/cloudflare/test/fixtures/sessions/src/pages/api.ts b/packages/integrations/cloudflare/test/fixtures/sessions/src/pages/api.ts
new file mode 100644
index 000000000000..21793c78a74f
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/sessions/src/pages/api.ts
@@ -0,0 +1,13 @@
+import type { APIRoute } from 'astro';
+
+export const GET: APIRoute = async (context) => {
+	const url = new URL(context.url, 'http://localhost');
+	let value = url.searchParams.get('set');
+	if (value) {
+		 context.session.set('value', value);
+	} else {
+		value = await context.session.get('value');
+	}
+	const cart = await context.session.get('cart');
+	return Response.json({ value, cart });
+};
diff --git a/packages/integrations/cloudflare/test/fixtures/sessions/src/pages/cart.astro b/packages/integrations/cloudflare/test/fixtures/sessions/src/pages/cart.astro
new file mode 100644
index 000000000000..e69a9e5e15b1
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/sessions/src/pages/cart.astro
@@ -0,0 +1,24 @@
+---
+import { actions } from "astro:actions";
+
+const result = Astro.getActionResult(actions.addToCart);
+
+const cart = result?.data?.cart ?? await Astro.session.get('cart');
+const message = result?.data?.message ?? 'Add something to your cart!';
+---
+<p>Cart: <span id="cart">{JSON.stringify(cart)}</span></p>
+<p id="message">{message}</p>
+<form action={actions.addToCart} method="POST">
+	<input type="text" name="productId" value="shoe" />
+	<button type="submit">Add to Cart</button>
+</form>
+<input type="button" value="Clear Cart" id="clearCart" />
+<script>
+	import { actions } from "astro:actions";
+	async function clearCart() {
+		const result = await actions.clearCart({});
+		document.getElementById('cart').textContent = JSON.stringify(result.data.cart);
+		document.getElementById('message').textContent = result.data.message;
+	}
+	document.getElementById('clearCart').addEventListener('click', clearCart);
+</script>
diff --git a/packages/integrations/cloudflare/test/fixtures/sessions/src/pages/destroy.ts b/packages/integrations/cloudflare/test/fixtures/sessions/src/pages/destroy.ts
new file mode 100644
index 000000000000..e83f6e4b6c31
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/sessions/src/pages/destroy.ts
@@ -0,0 +1,6 @@
+import type { APIRoute } from 'astro';
+
+export const GET: APIRoute = async (context) => {
+	await context.session.destroy();
+	return Response.json({});
+};
diff --git a/packages/integrations/cloudflare/test/fixtures/sessions/src/pages/index.astro b/packages/integrations/cloudflare/test/fixtures/sessions/src/pages/index.astro
new file mode 100644
index 000000000000..30d6a1618796
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/sessions/src/pages/index.astro
@@ -0,0 +1,13 @@
+---
+const value = await Astro.session.get('value');
+---
+<html lang="en">
+<head>
+	<meta charset="utf-8" />
+	<title>Hi</title>
+</head>
+
+<h1>Hi</h1>
+<p>{value}</p>
+<a href="/cart" style="font-size: 36px">🛒</a>
+</html>
diff --git a/packages/integrations/cloudflare/test/fixtures/sessions/src/pages/regenerate.ts b/packages/integrations/cloudflare/test/fixtures/sessions/src/pages/regenerate.ts
new file mode 100644
index 000000000000..6f2240588e4f
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/sessions/src/pages/regenerate.ts
@@ -0,0 +1,6 @@
+import type { APIRoute } from 'astro';
+
+export const GET: APIRoute = async (context) => {
+	await context.session.regenerate();
+	return Response.json({});
+};
diff --git a/packages/integrations/cloudflare/test/fixtures/sessions/src/pages/update.ts b/packages/integrations/cloudflare/test/fixtures/sessions/src/pages/update.ts
new file mode 100644
index 000000000000..71b058e75321
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/sessions/src/pages/update.ts
@@ -0,0 +1,10 @@
+import type { APIRoute } from 'astro';
+
+export const GET: APIRoute = async (context) => {
+	const previousObject = await context.session.get("key") ?? { value: "none" };
+	const previousValue = previousObject.value;
+	const sessionData = { value: "expected" };
+	context.session.set("key", sessionData);
+	sessionData.value = "unexpected";
+	return Response.json({previousValue});
+};
diff --git a/packages/integrations/cloudflare/test/fixtures/sessions/wrangler.json b/packages/integrations/cloudflare/test/fixtures/sessions/wrangler.json
new file mode 100644
index 000000000000..479d56c32dd2
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/sessions/wrangler.json
@@ -0,0 +1,22 @@
+{
+  "name": "astro-cf-session",
+  "compatibility_date": "2024-11-01",
+  "compatibility_flags": [
+    "nodejs_compat"
+  ],
+  "main": "./dist/_worker.js/index.js",
+  "assets": {
+    "directory": "./dist",
+    "binding": "ASSETS"
+  },
+  "observability": {
+    "enabled": true
+  },
+  "kv_namespaces": [
+    {
+      "binding": "SESSION",
+      "id": "<SESSION_ID>"
+    }
+  ],
+  "upload_source_maps": true
+}
diff --git a/packages/integrations/cloudflare/test/sessions.test.js b/packages/integrations/cloudflare/test/sessions.test.js
new file mode 100644
index 000000000000..ab0ddf4669e4
--- /dev/null
+++ b/packages/integrations/cloudflare/test/sessions.test.js
@@ -0,0 +1,93 @@
+import assert from 'node:assert/strict';
+import { after, before, describe, it } from 'node:test';
+import * as devalue from 'devalue';
+import { fileURLToPath } from 'node:url';
+import { astroCli, wranglerCli } from './_test-utils.js';
+
+const root = new URL('./fixtures/sessions/', import.meta.url);
+
+describe('astro:env', () => {
+	let wrangler;
+
+	before(async () => {
+		await astroCli(fileURLToPath(root), 'build');
+
+		wrangler = wranglerCli(fileURLToPath(root));
+		await new Promise((resolve) => {
+			wrangler.stdout.on('data', (data) => {
+				// console.log('[stdout]', data.toString());
+				if (data.toString().includes('http://127.0.0.1:8788')) resolve();
+			});
+			wrangler.stderr.on('data', (_data) => {
+				// console.log('[stderr]', _data.toString());
+			});
+		});
+	});
+
+	after(() => {
+		wrangler.kill();
+	});
+
+	it('can regenerate session cookies upon request', async () => {
+		const firstResponse = await fetch('http://127.0.0.1:8788/regenerate', { method: 'GET' });
+		const firstHeaders = firstResponse.headers.get('set-cookie').split(',');
+		const firstSessionId = firstHeaders[0].split(';')[0].split('=')[1];
+
+		const secondResponse = await fetch('http://127.0.0.1:8788/regenerate', {
+			method: 'GET',
+			headers: {
+				cookie: `astro-session=${firstSessionId}`,
+			},
+		});
+		const secondHeaders = secondResponse.headers.get('set-cookie').split(',');
+		const secondSessionId = secondHeaders[0].split(';')[0].split('=')[1];
+		assert.notEqual(firstSessionId, secondSessionId);
+	});
+
+	it('can save session data by value', async () => {
+		const firstResponse = await fetch('http://127.0.0.1:8788/update', { method: 'GET' });
+		const firstValue = await firstResponse.json();
+		assert.equal(firstValue.previousValue, 'none');
+
+		const firstHeaders = firstResponse.headers.get('set-cookie').split(',');
+		const firstSessionId = firstHeaders[0].split(';')[0].split('=')[1];
+		const secondResponse = await fetch('http://127.0.0.1:8788/update', {
+			method: 'GET',
+			headers: {
+				cookie: `astro-session=${firstSessionId}`,
+			},
+		});
+		const secondValue = await secondResponse.json();
+		assert.equal(secondValue.previousValue, 'expected');
+	});
+
+	it('can save and restore URLs in session data', async () => {
+		const firstResponse = await fetch('http://127.0.0.1:8788/_actions/addUrl', {
+			method: 'POST',
+			headers: {
+				'Content-Type': 'application/json',
+			},
+			body: JSON.stringify({ favoriteUrl: 'https://domain.invalid' }),
+		});
+
+		assert.equal(firstResponse.ok, true);
+		const firstHeaders = firstResponse.headers.get('set-cookie').split(',');
+		const firstSessionId = firstHeaders[0].split(';')[0].split('=')[1];
+
+		const data = devalue.parse(await firstResponse.text());
+		assert.equal(data.message, 'Favorite URL set to https://domain.invalid/ from nothing');
+		const secondResponse = await fetch('http://127.0.0.1:8788/_actions/addUrl', {
+			method: 'POST',
+			headers: {
+				'Content-Type': 'application/json',
+				cookie: `astro-session=${firstSessionId}`,
+			},
+			body: JSON.stringify({ favoriteUrl: 'https://example.com' }),
+		});
+		const secondData = devalue.parse(await secondResponse.text());
+		assert.equal(
+			secondData.message,
+			'Favorite URL set to https://example.com/ from https://domain.invalid/',
+		);
+	});
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index fb712975dffe..e7823354a846 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4295,7 +4295,7 @@ importers:
         version: 1.0.2
       drizzle-orm:
         specifier: ^0.31.2
-        version: 0.31.4(@cloudflare/workers-types@4.20250320.0)(@libsql/client@0.15.0)(@types/react@18.3.20)(react@19.0.0)
+        version: 0.31.4(@cloudflare/workers-types@4.20250327.0)(@libsql/client@0.15.0)(@types/react@18.3.20)(react@19.0.0)
       github-slugger:
         specifier: ^2.0.0
         version: 2.0.0
@@ -4556,8 +4556,8 @@ importers:
         specifier: workspace:*
         version: link:../../underscore-redirects
       '@cloudflare/workers-types':
-        specifier: ^4.20250317.0
-        version: 4.20250320.0
+        specifier: ^4.20250327.0
+        version: 4.20250327.0
       esbuild:
         specifier: ^0.25.0
         version: 0.25.0
@@ -4568,8 +4568,8 @@ importers:
         specifier: ^0.30.17
         version: 0.30.17
       miniflare:
-        specifier: ^4.20250317.0
-        version: 4.20250321.0
+        specifier: ^4.20250321.1
+        version: 4.20250321.1
       tinyglobby:
         specifier: ^0.2.12
         version: 0.2.12
@@ -4577,8 +4577,8 @@ importers:
         specifier: ^6.2.3
         version: 6.2.3(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.0)(yaml@2.5.1)
       wrangler:
-        specifier: ^4.2.0
-        version: 4.4.1(@cloudflare/workers-types@4.20250320.0)
+        specifier: ^4.5.1
+        version: 4.5.1(@cloudflare/workers-types@4.20250327.0)
     devDependencies:
       astro:
         specifier: workspace:*
@@ -4589,6 +4589,9 @@ importers:
       cheerio:
         specifier: 1.0.0
         version: 1.0.0
+      devalue:
+        specifier: ^5.1.1
+        version: 5.1.1
       execa:
         specifier: ^8.0.1
         version: 8.0.1
@@ -4609,8 +4612,8 @@ importers:
         version: link:../../../../../astro
     devDependencies:
       wrangler:
-        specifier: ^3.112.0
-        version: 3.112.0(@cloudflare/workers-types@4.20250320.0)
+        specifier: ^4.5.1
+        version: 4.5.1(@cloudflare/workers-types@4.20250327.0)
 
   packages/integrations/cloudflare/test/fixtures/astro-env:
     dependencies:
@@ -4622,8 +4625,8 @@ importers:
         version: link:../../../../../astro
     devDependencies:
       wrangler:
-        specifier: ^4.4.1
-        version: 4.4.1(@cloudflare/workers-types@4.20250320.0)
+        specifier: ^4.5.1
+        version: 4.5.1(@cloudflare/workers-types@4.20250327.0)
 
   packages/integrations/cloudflare/test/fixtures/compile-image-service:
     dependencies:
@@ -4670,6 +4673,19 @@ importers:
         specifier: workspace:*
         version: link:../../../../../astro
 
+  packages/integrations/cloudflare/test/fixtures/sessions:
+    dependencies:
+      '@astrojs/cloudflare':
+        specifier: workspace:*
+        version: link:../../..
+    devDependencies:
+      astro:
+        specifier: workspace:*
+        version: link:../../../../../astro
+      wrangler:
+        specifier: ^4.5.1
+        version: 4.5.1(@cloudflare/workers-types@4.20250327.0)
+
   packages/integrations/cloudflare/test/fixtures/with-base:
     dependencies:
       '@astrojs/cloudflare':
@@ -6654,85 +6670,51 @@ packages:
     bundledDependencies:
       - is-unicode-supported
 
-  '@cloudflare/kv-asset-handler@0.3.4':
-    resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==}
-    engines: {node: '>=16.13'}
-
   '@cloudflare/kv-asset-handler@0.4.0':
     resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==}
     engines: {node: '>=18.0.0'}
 
-  '@cloudflare/unenv-preset@2.3.0':
-    resolution: {integrity: sha512-AaKYnbFpHaVDZGh3Hjy3oLYd12+LZw9aupAOudYJ+tjekahxcIqlSAr0zK9kPOdtgn10tzaqH7QJFUWcLE+k7g==}
+  '@cloudflare/unenv-preset@2.3.1':
+    resolution: {integrity: sha512-Xq57Qd+ADpt6hibcVBO0uLG9zzRgyRhfCUgBT9s+g3+3Ivg5zDyVgLFy40ES1VdNcu8rPNSivm9A+kGP5IVaPg==}
     peerDependencies:
       unenv: 2.0.0-rc.15
-      workerd: ^1.20250311.0
+      workerd: ^1.20250320.0
     peerDependenciesMeta:
       workerd:
         optional: true
 
-  '@cloudflare/workerd-darwin-64@1.20250214.0':
-    resolution: {integrity: sha512-cDvvedWDc5zrgDnuXe2qYcz/TwBvzmweO55C7XpPuAWJ9Oqxv81PkdekYxD8mH989aQ/GI5YD0Fe6fDYlM+T3Q==}
-    engines: {node: '>=16'}
-    cpu: [x64]
-    os: [darwin]
-
   '@cloudflare/workerd-darwin-64@1.20250321.0':
     resolution: {integrity: sha512-y273GfLaNCxkL8hTfo0c8FZKkOPdq+CPZAKJXPWB+YpS1JCOULu6lNTptpD7ZtF14dTYPkn5Weug31TTlviJmw==}
     engines: {node: '>=16'}
     cpu: [x64]
     os: [darwin]
 
-  '@cloudflare/workerd-darwin-arm64@1.20250214.0':
-    resolution: {integrity: sha512-NytCvRveVzu0mRKo+tvZo3d/gCUway3B2ZVqSi/TS6NXDGBYIJo7g6s3BnTLS74kgyzeDOjhu9j/RBJBS809qw==}
-    engines: {node: '>=16'}
-    cpu: [arm64]
-    os: [darwin]
-
   '@cloudflare/workerd-darwin-arm64@1.20250321.0':
     resolution: {integrity: sha512-qvf7/gkkQq7fAsoMlntJSimN/WfwQqxi2oL0aWZMGodTvs/yRHO2I4oE0eOihVdK1BXyBHJXNxEvNDBjF0+Yuw==}
     engines: {node: '>=16'}
     cpu: [arm64]
     os: [darwin]
 
-  '@cloudflare/workerd-linux-64@1.20250214.0':
-    resolution: {integrity: sha512-pQ7+aHNHj8SiYEs4d/6cNoimE5xGeCMfgU1yfDFtA9YGN9Aj2BITZgOWPec+HW7ZkOy9oWlNrO6EvVjGgB4tbQ==}
-    engines: {node: '>=16'}
-    cpu: [x64]
-    os: [linux]
-
   '@cloudflare/workerd-linux-64@1.20250321.0':
     resolution: {integrity: sha512-AEp3xjWFrNPO/h0StCOgOb0bWCcNThnkESpy91Wf4mfUF2p7tOCdp37Nk/1QIRqSxnfv4Hgxyi7gcWud9cJuMw==}
     engines: {node: '>=16'}
     cpu: [x64]
     os: [linux]
 
-  '@cloudflare/workerd-linux-arm64@1.20250214.0':
-    resolution: {integrity: sha512-Vhlfah6Yd9ny1npNQjNgElLIjR6OFdEbuR3LCfbLDCwzWEBFhIf7yC+Tpp/a0Hq7kLz3sLdktaP7xl3PJhyOjA==}
-    engines: {node: '>=16'}
-    cpu: [arm64]
-    os: [linux]
-
   '@cloudflare/workerd-linux-arm64@1.20250321.0':
     resolution: {integrity: sha512-wRWyMIoPIS1UBXCisW0FYTgGsfZD4AVS0hXA5nuLc0c21CvzZpmmTjqEWMcwPFenwy/MNL61NautVOC4qJqQ3Q==}
     engines: {node: '>=16'}
     cpu: [arm64]
     os: [linux]
 
-  '@cloudflare/workerd-windows-64@1.20250214.0':
-    resolution: {integrity: sha512-GMwMyFbkjBKjYJoKDhGX8nuL4Gqe3IbVnVWf2Q6086CValyIknupk5J6uQWGw2EBU3RGO3x4trDXT5WphQJZDQ==}
-    engines: {node: '>=16'}
-    cpu: [x64]
-    os: [win32]
-
   '@cloudflare/workerd-windows-64@1.20250321.0':
     resolution: {integrity: sha512-8vYP3QYO0zo2faUDfWl88jjfUvz7Si9GS3mUYaTh/TR9LcAUtsO7muLxPamqEyoxNFtbQgy08R4rTid94KRi3w==}
     engines: {node: '>=16'}
     cpu: [x64]
     os: [win32]
 
-  '@cloudflare/workers-types@4.20250320.0':
-    resolution: {integrity: sha512-0qYPnnF36eEzes/uLBrIE7pbyOucnyNVTlcPY4zgakZT0BBdrHYoTDGH+9VqwfcMW7mLu2PY2daYGIFsgycX2A==}
+  '@cloudflare/workers-types@4.20250327.0':
+    resolution: {integrity: sha512-rkoGnSY/GgBLCuhjZMIC3mt0jjqqvL17uOK92OI4eivmE+pMFOAchowDxIWOzDyYe5vwNCakbCeIM/FrSmwGJA==}
 
   '@codspeed/core@4.0.0':
     resolution: {integrity: sha512-B3zwdwLG8rcV0ORfYKX1wDP6ZCWf9C6ySidSf61q2vm9v5Lj2cWwRvj7vX+w/UyFHWKjp/zSyWTEed/r3Fv4Tg==}
@@ -7027,16 +7009,6 @@ packages:
   '@emnapi/runtime@1.3.1':
     resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==}
 
-  '@esbuild-plugins/node-globals-polyfill@0.2.3':
-    resolution: {integrity: sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==}
-    peerDependencies:
-      esbuild: '*'
-
-  '@esbuild-plugins/node-modules-polyfill@0.2.2':
-    resolution: {integrity: sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==}
-    peerDependencies:
-      esbuild: '*'
-
   '@esbuild/aix-ppc64@0.24.2':
     resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==}
     engines: {node: '>=18'}
@@ -7049,12 +7021,6 @@ packages:
     cpu: [ppc64]
     os: [aix]
 
-  '@esbuild/android-arm64@0.17.19':
-    resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [android]
-
   '@esbuild/android-arm64@0.24.2':
     resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==}
     engines: {node: '>=18'}
@@ -7067,12 +7033,6 @@ packages:
     cpu: [arm64]
     os: [android]
 
-  '@esbuild/android-arm@0.17.19':
-    resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==}
-    engines: {node: '>=12'}
-    cpu: [arm]
-    os: [android]
-
   '@esbuild/android-arm@0.24.2':
     resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==}
     engines: {node: '>=18'}
@@ -7085,12 +7045,6 @@ packages:
     cpu: [arm]
     os: [android]
 
-  '@esbuild/android-x64@0.17.19':
-    resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [android]
-
   '@esbuild/android-x64@0.24.2':
     resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==}
     engines: {node: '>=18'}
@@ -7103,12 +7057,6 @@ packages:
     cpu: [x64]
     os: [android]
 
-  '@esbuild/darwin-arm64@0.17.19':
-    resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [darwin]
-
   '@esbuild/darwin-arm64@0.24.2':
     resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==}
     engines: {node: '>=18'}
@@ -7121,12 +7069,6 @@ packages:
     cpu: [arm64]
     os: [darwin]
 
-  '@esbuild/darwin-x64@0.17.19':
-    resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [darwin]
-
   '@esbuild/darwin-x64@0.24.2':
     resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==}
     engines: {node: '>=18'}
@@ -7139,12 +7081,6 @@ packages:
     cpu: [x64]
     os: [darwin]
 
-  '@esbuild/freebsd-arm64@0.17.19':
-    resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [freebsd]
-
   '@esbuild/freebsd-arm64@0.24.2':
     resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==}
     engines: {node: '>=18'}
@@ -7157,12 +7093,6 @@ packages:
     cpu: [arm64]
     os: [freebsd]
 
-  '@esbuild/freebsd-x64@0.17.19':
-    resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [freebsd]
-
   '@esbuild/freebsd-x64@0.24.2':
     resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==}
     engines: {node: '>=18'}
@@ -7175,12 +7105,6 @@ packages:
     cpu: [x64]
     os: [freebsd]
 
-  '@esbuild/linux-arm64@0.17.19':
-    resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [linux]
-
   '@esbuild/linux-arm64@0.24.2':
     resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==}
     engines: {node: '>=18'}
@@ -7193,12 +7117,6 @@ packages:
     cpu: [arm64]
     os: [linux]
 
-  '@esbuild/linux-arm@0.17.19':
-    resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==}
-    engines: {node: '>=12'}
-    cpu: [arm]
-    os: [linux]
-
   '@esbuild/linux-arm@0.24.2':
     resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==}
     engines: {node: '>=18'}
@@ -7211,12 +7129,6 @@ packages:
     cpu: [arm]
     os: [linux]
 
-  '@esbuild/linux-ia32@0.17.19':
-    resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==}
-    engines: {node: '>=12'}
-    cpu: [ia32]
-    os: [linux]
-
   '@esbuild/linux-ia32@0.24.2':
     resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==}
     engines: {node: '>=18'}
@@ -7229,12 +7141,6 @@ packages:
     cpu: [ia32]
     os: [linux]
 
-  '@esbuild/linux-loong64@0.17.19':
-    resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==}
-    engines: {node: '>=12'}
-    cpu: [loong64]
-    os: [linux]
-
   '@esbuild/linux-loong64@0.24.2':
     resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==}
     engines: {node: '>=18'}
@@ -7247,12 +7153,6 @@ packages:
     cpu: [loong64]
     os: [linux]
 
-  '@esbuild/linux-mips64el@0.17.19':
-    resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==}
-    engines: {node: '>=12'}
-    cpu: [mips64el]
-    os: [linux]
-
   '@esbuild/linux-mips64el@0.24.2':
     resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==}
     engines: {node: '>=18'}
@@ -7265,12 +7165,6 @@ packages:
     cpu: [mips64el]
     os: [linux]
 
-  '@esbuild/linux-ppc64@0.17.19':
-    resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==}
-    engines: {node: '>=12'}
-    cpu: [ppc64]
-    os: [linux]
-
   '@esbuild/linux-ppc64@0.24.2':
     resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==}
     engines: {node: '>=18'}
@@ -7283,12 +7177,6 @@ packages:
     cpu: [ppc64]
     os: [linux]
 
-  '@esbuild/linux-riscv64@0.17.19':
-    resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==}
-    engines: {node: '>=12'}
-    cpu: [riscv64]
-    os: [linux]
-
   '@esbuild/linux-riscv64@0.24.2':
     resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==}
     engines: {node: '>=18'}
@@ -7301,12 +7189,6 @@ packages:
     cpu: [riscv64]
     os: [linux]
 
-  '@esbuild/linux-s390x@0.17.19':
-    resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==}
-    engines: {node: '>=12'}
-    cpu: [s390x]
-    os: [linux]
-
   '@esbuild/linux-s390x@0.24.2':
     resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==}
     engines: {node: '>=18'}
@@ -7319,12 +7201,6 @@ packages:
     cpu: [s390x]
     os: [linux]
 
-  '@esbuild/linux-x64@0.17.19':
-    resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [linux]
-
   '@esbuild/linux-x64@0.24.2':
     resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==}
     engines: {node: '>=18'}
@@ -7349,12 +7225,6 @@ packages:
     cpu: [arm64]
     os: [netbsd]
 
-  '@esbuild/netbsd-x64@0.17.19':
-    resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [netbsd]
-
   '@esbuild/netbsd-x64@0.24.2':
     resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==}
     engines: {node: '>=18'}
@@ -7379,12 +7249,6 @@ packages:
     cpu: [arm64]
     os: [openbsd]
 
-  '@esbuild/openbsd-x64@0.17.19':
-    resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [openbsd]
-
   '@esbuild/openbsd-x64@0.24.2':
     resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==}
     engines: {node: '>=18'}
@@ -7397,12 +7261,6 @@ packages:
     cpu: [x64]
     os: [openbsd]
 
-  '@esbuild/sunos-x64@0.17.19':
-    resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [sunos]
-
   '@esbuild/sunos-x64@0.24.2':
     resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==}
     engines: {node: '>=18'}
@@ -7415,12 +7273,6 @@ packages:
     cpu: [x64]
     os: [sunos]
 
-  '@esbuild/win32-arm64@0.17.19':
-    resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [win32]
-
   '@esbuild/win32-arm64@0.24.2':
     resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==}
     engines: {node: '>=18'}
@@ -7433,12 +7285,6 @@ packages:
     cpu: [arm64]
     os: [win32]
 
-  '@esbuild/win32-ia32@0.17.19':
-    resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==}
-    engines: {node: '>=12'}
-    cpu: [ia32]
-    os: [win32]
-
   '@esbuild/win32-ia32@0.24.2':
     resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==}
     engines: {node: '>=18'}
@@ -7451,12 +7297,6 @@ packages:
     cpu: [ia32]
     os: [win32]
 
-  '@esbuild/win32-x64@0.17.19':
-    resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [win32]
-
   '@esbuild/win32-x64@0.24.2':
     resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==}
     engines: {node: '>=18'}
@@ -9006,9 +8846,6 @@ packages:
   concat-map@0.0.1:
     resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
 
-  confbox@0.1.8:
-    resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
-
   consola@3.2.3:
     resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==}
     engines: {node: ^14.18.0 || >=16.10.0}
@@ -9448,11 +9285,6 @@ packages:
   esast-util-from-js@2.0.1:
     resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==}
 
-  esbuild@0.17.19:
-    resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==}
-    engines: {node: '>=12'}
-    hasBin: true
-
   esbuild@0.24.2:
     resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==}
     engines: {node: '>=18'}
@@ -9555,9 +9387,6 @@ packages:
   estree-util-visit@2.0.0:
     resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==}
 
-  estree-walker@0.6.1:
-    resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==}
-
   estree-walker@2.0.2:
     resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
 
@@ -10193,6 +10022,7 @@ packages:
 
   libsql@0.5.1:
     resolution: {integrity: sha512-ePnm5zj6T//GKiTY/v5b0a272NX73hqdRORmD8gzz1nUui9051dtTt6t0XCrIqxwJAHSmQiZcfAx3YSASn9Y+A==}
+    cpu: [x64, arm64, wasm32]
     os: [darwin, linux, win32]
 
   lightningcss-darwin-arm64@1.29.2:
@@ -10344,9 +10174,6 @@ packages:
     resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
     hasBin: true
 
-  magic-string@0.25.9:
-    resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
-
   magic-string@0.30.17:
     resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
 
@@ -10611,13 +10438,8 @@ packages:
     resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
     engines: {node: '>=12'}
 
-  miniflare@3.20250214.2:
-    resolution: {integrity: sha512-t+lT4p2lbOcKv4PS3sx1F/wcDAlbEYZCO2VooLp4H7JErWWYIi9yjD3UillC3CGOpiBahVg5nrPCoFltZf6UlA==}
-    engines: {node: '>=16.13'}
-    hasBin: true
-
-  miniflare@4.20250321.0:
-    resolution: {integrity: sha512-os+NJA7Eqi00BJHdVhzIa+3PMotnCtZg3hiUIRYcsZF5W7He8SK2EkV8csAb+npZq3jZ4SNpDebO01swM5dcWw==}
+  miniflare@4.20250321.1:
+    resolution: {integrity: sha512-pQuVtF6vQ1zMvPCo3Z19mzSFjgnlEnybzNzAJZipsqIk6kMXpYBZq+d8cWmeQFkBYlgeZKeKJ4EBKT6KapfTNg==}
     engines: {node: '>=18.0.0'}
     hasBin: true
 
@@ -10667,9 +10489,6 @@ packages:
     engines: {node: '>=10'}
     hasBin: true
 
-  mlly@1.7.4:
-    resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==}
-
   mri@1.2.0:
     resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
     engines: {node: '>=4'}
@@ -10812,9 +10631,6 @@ packages:
   ofetch@1.4.1:
     resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==}
 
-  ohash@1.1.4:
-    resolution: {integrity: sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==}
-
   ohash@2.0.11:
     resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
 
@@ -10997,9 +10813,6 @@ packages:
     resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
     engines: {node: '>=8'}
 
-  pathe@1.1.2:
-    resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
-
   pathe@2.0.3:
     resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
 
@@ -11033,9 +10846,6 @@ packages:
     resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
     engines: {node: '>= 6'}
 
-  pkg-types@1.3.1:
-    resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
-
   playwright-core@1.51.1:
     resolution: {integrity: sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==}
     engines: {node: '>=18'}
@@ -11555,16 +11365,6 @@ packages:
     resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==}
     hasBin: true
 
-  rollup-plugin-inject@3.0.2:
-    resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==}
-    deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.
-
-  rollup-plugin-node-polyfills@0.2.1:
-    resolution: {integrity: sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==}
-
-  rollup-pluginutils@2.8.2:
-    resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==}
-
   rollup@4.37.0:
     resolution: {integrity: sha512-iAtQy/L4QFU+rTJ1YUjXqJOJzuwEghqWzCEYD2FEghT7Gsy1VdABntrO4CLopA5IkflTyqNiLNwPcOJ3S7UKLg==}
     engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@@ -11761,10 +11561,6 @@ packages:
     resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==}
     engines: {node: '>= 8'}
 
-  sourcemap-codec@1.4.8:
-    resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
-    deprecated: Please use @jridgewell/sourcemap-codec instead
-
   space-separated-tokens@2.0.2:
     resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
 
@@ -12133,9 +11929,6 @@ packages:
     resolution: {integrity: sha512-NFQG741e8mJ0fLQk90xKxFdaSM7z4+IQpAgsFI36bCDY9Z2+aXXZjVy2uUksMouWfMI9+w5ejOq5zYYTBCQJDQ==}
     engines: {node: '>=20.18.1'}
 
-  unenv@2.0.0-rc.1:
-    resolution: {integrity: sha512-PU5fb40H8X149s117aB4ytbORcCvlASdtF97tfls4BPIyj4PeVxvpSuy1jAptqYHqB0vb2w2sHvzM0XWcp2OKg==}
-
   unenv@2.0.0-rc.15:
     resolution: {integrity: sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA==}
 
@@ -12607,28 +12400,13 @@ packages:
     resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
     engines: {node: '>=0.10.0'}
 
-  workerd@1.20250214.0:
-    resolution: {integrity: sha512-QWcqXZLiMpV12wiaVnb3nLmfs/g4ZsFQq2mX85z546r3AX4CTIkXl0VP50W3CwqLADej3PGYiRDOTelDOwVG1g==}
-    engines: {node: '>=16'}
-    hasBin: true
-
   workerd@1.20250321.0:
     resolution: {integrity: sha512-vyuz9pdJ+7o1lC79vQ2UVRLXPARa2Lq94PbTfqEcYQeSxeR9X+YqhNq2yysv8Zs5vpokmexLCtMniPp9u+2LVQ==}
     engines: {node: '>=16'}
     hasBin: true
 
-  wrangler@3.112.0:
-    resolution: {integrity: sha512-PNQWGze3ODlWwG33LPr8kNhbht3eB3L9fogv+fapk2fjaqj0kNweRapkwmvtz46ojcqWzsxmTe4nOC0hIVUfPA==}
-    engines: {node: '>=16.17.0'}
-    hasBin: true
-    peerDependencies:
-      '@cloudflare/workers-types': ^4.20250214.0
-    peerDependenciesMeta:
-      '@cloudflare/workers-types':
-        optional: true
-
-  wrangler@4.4.1:
-    resolution: {integrity: sha512-EFwr7hiVeAmPOuOGQ7HFfeaLKLxEXQMJ86kyn6RFB8pGjMEUtvZMsVa9cPubKkKgNi3WcDEFeFLalclGyq+tGA==}
+  wrangler@4.5.1:
+    resolution: {integrity: sha512-7ct52LPiKnjqtC6SteKr0aherOS6CmvEurGXxGoDjgIOuoAPjN7jjYAw3eEIReGPlcQXmhpPLuunVFkWNhCoZA==}
     engines: {node: '>=18.0.0'}
     hasBin: true
     peerDependencies:
@@ -13301,51 +13079,32 @@ snapshots:
       picocolors: 1.1.1
       sisteransi: 1.0.5
 
-  '@cloudflare/kv-asset-handler@0.3.4':
-    dependencies:
-      mime: 3.0.0
-
   '@cloudflare/kv-asset-handler@0.4.0':
     dependencies:
       mime: 3.0.0
 
-  '@cloudflare/unenv-preset@2.3.0(unenv@2.0.0-rc.15)(workerd@1.20250321.0)':
+  '@cloudflare/unenv-preset@2.3.1(unenv@2.0.0-rc.15)(workerd@1.20250321.0)':
     dependencies:
       unenv: 2.0.0-rc.15
     optionalDependencies:
       workerd: 1.20250321.0
 
-  '@cloudflare/workerd-darwin-64@1.20250214.0':
-    optional: true
-
   '@cloudflare/workerd-darwin-64@1.20250321.0':
     optional: true
 
-  '@cloudflare/workerd-darwin-arm64@1.20250214.0':
-    optional: true
-
   '@cloudflare/workerd-darwin-arm64@1.20250321.0':
     optional: true
 
-  '@cloudflare/workerd-linux-64@1.20250214.0':
-    optional: true
-
   '@cloudflare/workerd-linux-64@1.20250321.0':
     optional: true
 
-  '@cloudflare/workerd-linux-arm64@1.20250214.0':
-    optional: true
-
   '@cloudflare/workerd-linux-arm64@1.20250321.0':
     optional: true
 
-  '@cloudflare/workerd-windows-64@1.20250214.0':
-    optional: true
-
   '@cloudflare/workerd-windows-64@1.20250321.0':
     optional: true
 
-  '@cloudflare/workers-types@4.20250320.0': {}
+  '@cloudflare/workers-types@4.20250327.0': {}
 
   '@codspeed/core@4.0.0':
     dependencies:
@@ -13651,160 +13410,102 @@ snapshots:
       tslib: 2.6.2
     optional: true
 
-  '@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.17.19)':
-    dependencies:
-      esbuild: 0.17.19
-
-  '@esbuild-plugins/node-modules-polyfill@0.2.2(esbuild@0.17.19)':
-    dependencies:
-      esbuild: 0.17.19
-      escape-string-regexp: 4.0.0
-      rollup-plugin-node-polyfills: 0.2.1
-
   '@esbuild/aix-ppc64@0.24.2':
     optional: true
 
   '@esbuild/aix-ppc64@0.25.0':
     optional: true
 
-  '@esbuild/android-arm64@0.17.19':
-    optional: true
-
   '@esbuild/android-arm64@0.24.2':
     optional: true
 
   '@esbuild/android-arm64@0.25.0':
     optional: true
 
-  '@esbuild/android-arm@0.17.19':
-    optional: true
-
   '@esbuild/android-arm@0.24.2':
     optional: true
 
   '@esbuild/android-arm@0.25.0':
     optional: true
 
-  '@esbuild/android-x64@0.17.19':
-    optional: true
-
   '@esbuild/android-x64@0.24.2':
     optional: true
 
   '@esbuild/android-x64@0.25.0':
     optional: true
 
-  '@esbuild/darwin-arm64@0.17.19':
-    optional: true
-
   '@esbuild/darwin-arm64@0.24.2':
     optional: true
 
   '@esbuild/darwin-arm64@0.25.0':
     optional: true
 
-  '@esbuild/darwin-x64@0.17.19':
-    optional: true
-
   '@esbuild/darwin-x64@0.24.2':
     optional: true
 
   '@esbuild/darwin-x64@0.25.0':
     optional: true
 
-  '@esbuild/freebsd-arm64@0.17.19':
-    optional: true
-
   '@esbuild/freebsd-arm64@0.24.2':
     optional: true
 
   '@esbuild/freebsd-arm64@0.25.0':
     optional: true
 
-  '@esbuild/freebsd-x64@0.17.19':
-    optional: true
-
   '@esbuild/freebsd-x64@0.24.2':
     optional: true
 
   '@esbuild/freebsd-x64@0.25.0':
     optional: true
 
-  '@esbuild/linux-arm64@0.17.19':
-    optional: true
-
   '@esbuild/linux-arm64@0.24.2':
     optional: true
 
   '@esbuild/linux-arm64@0.25.0':
     optional: true
 
-  '@esbuild/linux-arm@0.17.19':
-    optional: true
-
   '@esbuild/linux-arm@0.24.2':
     optional: true
 
   '@esbuild/linux-arm@0.25.0':
     optional: true
 
-  '@esbuild/linux-ia32@0.17.19':
-    optional: true
-
   '@esbuild/linux-ia32@0.24.2':
     optional: true
 
   '@esbuild/linux-ia32@0.25.0':
     optional: true
 
-  '@esbuild/linux-loong64@0.17.19':
-    optional: true
-
   '@esbuild/linux-loong64@0.24.2':
     optional: true
 
   '@esbuild/linux-loong64@0.25.0':
     optional: true
 
-  '@esbuild/linux-mips64el@0.17.19':
-    optional: true
-
   '@esbuild/linux-mips64el@0.24.2':
     optional: true
 
   '@esbuild/linux-mips64el@0.25.0':
     optional: true
 
-  '@esbuild/linux-ppc64@0.17.19':
-    optional: true
-
   '@esbuild/linux-ppc64@0.24.2':
     optional: true
 
   '@esbuild/linux-ppc64@0.25.0':
     optional: true
 
-  '@esbuild/linux-riscv64@0.17.19':
-    optional: true
-
   '@esbuild/linux-riscv64@0.24.2':
     optional: true
 
   '@esbuild/linux-riscv64@0.25.0':
     optional: true
 
-  '@esbuild/linux-s390x@0.17.19':
-    optional: true
-
   '@esbuild/linux-s390x@0.24.2':
     optional: true
 
   '@esbuild/linux-s390x@0.25.0':
     optional: true
 
-  '@esbuild/linux-x64@0.17.19':
-    optional: true
-
   '@esbuild/linux-x64@0.24.2':
     optional: true
 
@@ -13817,9 +13518,6 @@ snapshots:
   '@esbuild/netbsd-arm64@0.25.0':
     optional: true
 
-  '@esbuild/netbsd-x64@0.17.19':
-    optional: true
-
   '@esbuild/netbsd-x64@0.24.2':
     optional: true
 
@@ -13832,45 +13530,30 @@ snapshots:
   '@esbuild/openbsd-arm64@0.25.0':
     optional: true
 
-  '@esbuild/openbsd-x64@0.17.19':
-    optional: true
-
   '@esbuild/openbsd-x64@0.24.2':
     optional: true
 
   '@esbuild/openbsd-x64@0.25.0':
     optional: true
 
-  '@esbuild/sunos-x64@0.17.19':
-    optional: true
-
   '@esbuild/sunos-x64@0.24.2':
     optional: true
 
   '@esbuild/sunos-x64@0.25.0':
     optional: true
 
-  '@esbuild/win32-arm64@0.17.19':
-    optional: true
-
   '@esbuild/win32-arm64@0.24.2':
     optional: true
 
   '@esbuild/win32-arm64@0.25.0':
     optional: true
 
-  '@esbuild/win32-ia32@0.17.19':
-    optional: true
-
   '@esbuild/win32-ia32@0.24.2':
     optional: true
 
   '@esbuild/win32-ia32@0.25.0':
     optional: true
 
-  '@esbuild/win32-x64@0.17.19':
-    optional: true
-
   '@esbuild/win32-x64@0.24.2':
     optional: true
 
@@ -15509,8 +15192,6 @@ snapshots:
 
   concat-map@0.0.1: {}
 
-  confbox@0.1.8: {}
-
   consola@3.2.3: {}
 
   content-disposition@0.5.4:
@@ -15712,9 +15393,9 @@ snapshots:
 
   dotenv@8.6.0: {}
 
-  drizzle-orm@0.31.4(@cloudflare/workers-types@4.20250320.0)(@libsql/client@0.15.0)(@types/react@18.3.20)(react@19.0.0):
+  drizzle-orm@0.31.4(@cloudflare/workers-types@4.20250327.0)(@libsql/client@0.15.0)(@types/react@18.3.20)(react@19.0.0):
     optionalDependencies:
-      '@cloudflare/workers-types': 4.20250320.0
+      '@cloudflare/workers-types': 4.20250327.0
       '@libsql/client': 0.15.0
       '@types/react': 18.3.20
       react: 19.0.0
@@ -15797,31 +15478,6 @@ snapshots:
       esast-util-from-estree: 2.0.0
       vfile-message: 4.0.2
 
-  esbuild@0.17.19:
-    optionalDependencies:
-      '@esbuild/android-arm': 0.17.19
-      '@esbuild/android-arm64': 0.17.19
-      '@esbuild/android-x64': 0.17.19
-      '@esbuild/darwin-arm64': 0.17.19
-      '@esbuild/darwin-x64': 0.17.19
-      '@esbuild/freebsd-arm64': 0.17.19
-      '@esbuild/freebsd-x64': 0.17.19
-      '@esbuild/linux-arm': 0.17.19
-      '@esbuild/linux-arm64': 0.17.19
-      '@esbuild/linux-ia32': 0.17.19
-      '@esbuild/linux-loong64': 0.17.19
-      '@esbuild/linux-mips64el': 0.17.19
-      '@esbuild/linux-ppc64': 0.17.19
-      '@esbuild/linux-riscv64': 0.17.19
-      '@esbuild/linux-s390x': 0.17.19
-      '@esbuild/linux-x64': 0.17.19
-      '@esbuild/netbsd-x64': 0.17.19
-      '@esbuild/openbsd-x64': 0.17.19
-      '@esbuild/sunos-x64': 0.17.19
-      '@esbuild/win32-arm64': 0.17.19
-      '@esbuild/win32-ia32': 0.17.19
-      '@esbuild/win32-x64': 0.17.19
-
   esbuild@0.24.2:
     optionalDependencies:
       '@esbuild/aix-ppc64': 0.24.2
@@ -16003,8 +15659,6 @@ snapshots:
       '@types/estree-jsx': 1.0.5
       '@types/unist': 3.0.3
 
-  estree-walker@0.6.1: {}
-
   estree-walker@2.0.2: {}
 
   estree-walker@3.0.3:
@@ -16896,10 +16550,6 @@ snapshots:
 
   lz-string@1.5.0: {}
 
-  magic-string@0.25.9:
-    dependencies:
-      sourcemap-codec: 1.4.8
-
   magic-string@0.30.17:
     dependencies:
       '@jridgewell/sourcemap-codec': 1.5.0
@@ -17441,24 +17091,7 @@ snapshots:
 
   mimic-fn@4.0.0: {}
 
-  miniflare@3.20250214.2:
-    dependencies:
-      '@cspotcode/source-map-support': 0.8.1
-      acorn: 8.14.0
-      acorn-walk: 8.3.2
-      exit-hook: 2.2.1
-      glob-to-regexp: 0.4.1
-      stoppable: 1.1.0
-      undici: 5.28.5
-      workerd: 1.20250214.0
-      ws: 8.18.0
-      youch: 3.2.3
-      zod: 3.22.3
-    transitivePeerDependencies:
-      - bufferutil
-      - utf-8-validate
-
-  miniflare@4.20250321.0:
+  miniflare@4.20250321.1:
     dependencies:
       '@cspotcode/source-map-support': 0.8.1
       acorn: 8.14.0
@@ -17511,13 +17144,6 @@ snapshots:
 
   mkdirp@3.0.1: {}
 
-  mlly@1.7.4:
-    dependencies:
-      acorn: 8.14.1
-      pathe: 2.0.3
-      pkg-types: 1.3.1
-      ufo: 1.5.4
-
   mri@1.2.0: {}
 
   mrmime@2.0.1: {}
@@ -17633,8 +17259,6 @@ snapshots:
       node-fetch-native: 1.6.6
       ufo: 1.5.4
 
-  ohash@1.1.4: {}
-
   ohash@2.0.11: {}
 
   on-finished@2.4.1:
@@ -17814,8 +17438,6 @@ snapshots:
 
   path-type@4.0.0: {}
 
-  pathe@1.1.2: {}
-
   pathe@2.0.3: {}
 
   pathval@2.0.0: {}
@@ -17834,12 +17456,6 @@ snapshots:
 
   pirates@4.0.6: {}
 
-  pkg-types@1.3.1:
-    dependencies:
-      confbox: 0.1.8
-      mlly: 1.7.4
-      pathe: 2.0.3
-
   playwright-core@1.51.1: {}
 
   playwright@1.51.1:
@@ -18494,20 +18110,6 @@ snapshots:
     dependencies:
       glob: 10.4.5
 
-  rollup-plugin-inject@3.0.2:
-    dependencies:
-      estree-walker: 0.6.1
-      magic-string: 0.25.9
-      rollup-pluginutils: 2.8.2
-
-  rollup-plugin-node-polyfills@0.2.1:
-    dependencies:
-      rollup-plugin-inject: 3.0.2
-
-  rollup-pluginutils@2.8.2:
-    dependencies:
-      estree-walker: 0.6.1
-
   rollup@4.37.0:
     dependencies:
       '@types/estree': 1.0.6
@@ -18793,8 +18395,6 @@ snapshots:
 
   source-map@0.7.4: {}
 
-  sourcemap-codec@1.4.8: {}
-
   space-separated-tokens@2.0.2: {}
 
   spawndamnit@3.0.1:
@@ -19161,14 +18761,6 @@ snapshots:
 
   undici@7.5.0: {}
 
-  unenv@2.0.0-rc.1:
-    dependencies:
-      defu: 6.1.4
-      mlly: 1.7.4
-      ohash: 1.1.4
-      pathe: 1.1.2
-      ufo: 1.5.4
-
   unenv@2.0.0-rc.15:
     dependencies:
       defu: 6.1.4
@@ -19641,14 +19233,6 @@ snapshots:
 
   word-wrap@1.2.5: {}
 
-  workerd@1.20250214.0:
-    optionalDependencies:
-      '@cloudflare/workerd-darwin-64': 1.20250214.0
-      '@cloudflare/workerd-darwin-arm64': 1.20250214.0
-      '@cloudflare/workerd-linux-64': 1.20250214.0
-      '@cloudflare/workerd-linux-arm64': 1.20250214.0
-      '@cloudflare/workerd-windows-64': 1.20250214.0
-
   workerd@1.20250321.0:
     optionalDependencies:
       '@cloudflare/workerd-darwin-64': 1.20250321.0
@@ -19657,37 +19241,18 @@ snapshots:
       '@cloudflare/workerd-linux-arm64': 1.20250321.0
       '@cloudflare/workerd-windows-64': 1.20250321.0
 
-  wrangler@3.112.0(@cloudflare/workers-types@4.20250320.0):
-    dependencies:
-      '@cloudflare/kv-asset-handler': 0.3.4
-      '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19)
-      '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19)
-      blake3-wasm: 2.1.5
-      esbuild: 0.17.19
-      miniflare: 3.20250214.2
-      path-to-regexp: 6.3.0
-      unenv: 2.0.0-rc.1
-      workerd: 1.20250214.0
-    optionalDependencies:
-      '@cloudflare/workers-types': 4.20250320.0
-      fsevents: 2.3.3
-      sharp: 0.33.5
-    transitivePeerDependencies:
-      - bufferutil
-      - utf-8-validate
-
-  wrangler@4.4.1(@cloudflare/workers-types@4.20250320.0):
+  wrangler@4.5.1(@cloudflare/workers-types@4.20250327.0):
     dependencies:
       '@cloudflare/kv-asset-handler': 0.4.0
-      '@cloudflare/unenv-preset': 2.3.0(unenv@2.0.0-rc.15)(workerd@1.20250321.0)
+      '@cloudflare/unenv-preset': 2.3.1(unenv@2.0.0-rc.15)(workerd@1.20250321.0)
       blake3-wasm: 2.1.5
       esbuild: 0.24.2
-      miniflare: 4.20250321.0
+      miniflare: 4.20250321.1
       path-to-regexp: 6.3.0
       unenv: 2.0.0-rc.15
       workerd: 1.20250321.0
     optionalDependencies:
-      '@cloudflare/workers-types': 4.20250320.0
+      '@cloudflare/workers-types': 4.20250327.0
       fsevents: 2.3.3
       sharp: 0.33.5
     transitivePeerDependencies:


Reviews:
user: login: ematipico
body: Awesome 🔥 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-28T17:15:26Z
user: login: florian-lefebvre
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T17:19:03Z
user: login: ascorbic
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T17:26:27Z
user: login: ascorbic
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T17:52:50Z
user: login: ascorbic
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T17:53:40Z
user: login: ematipico
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-31T08:31:02Z

Review comments:
diff_hunk: @@ -26,6 +27,13 @@ export interface Runtime<T extends object = object> {
 	};
 }
 
+declare global {
path: packages/integrations/cloudflare/src/entrypoints/server.ts
user: login: ematipico
body: Can we comment on the usage and the why of these two globals? 
created_at: 2025-03-28T17:08:57Z
author_association: MEMBER
diff_hunk: @@ -71,6 +72,35 @@ export type Options = {
 	 * for reference on how these file types are exported
 	 */
 	cloudflareModules?: boolean;
+
+	/**
+	 * By default, Astro will be configured to use Cloudflare KV to store session data. If you want to use sessions, 
+	 * you must create a KV namespace and declare it in your wrangler config file. You can do this with the wrangler command:
+	 * 
+	 * ```sh
+	 * npx wrangler kv namespace create SESSION
+	 * ```
+	 * 
+	 * This will log the id of the created namespace. You can then add it to your `wrangler.json` file like this:
+	 * 
+	 * ```json
+	 * {
+	 *   "kv_namespaces": [
+	 *     {
+	 *       "binding": "SESSION",
+	 *       "id": "<your kv namespace id here>"
+	 *     }
+	 *   ]
path: packages/integrations/cloudflare/src/index.ts
user: login: ematipico
body: Should we tell users what will happen if they forget to add this namespace? Since we can't catch the error at runtime, the least we can do tell users what will happen if they miss this step
created_at: 2025-03-28T17:12:00Z
author_association: MEMBER
diff_hunk: @@ -111,13 +141,46 @@ export default function createIntegration(args?: Options): AstroIntegration {
 				addWatchFile,
 				addMiddleware,
 			}) => {
+				let session = config.session;
+
+				const isBuild = command === 'build';
+
+				if (config.experimental.session && !session?.driver) {
+					const bindingName = args?.sessionKVBindingName ?? 'SESSION';
+					logger.info(
+						`Configuring experimental session support using ${isBuild ? 'Cloudflare KV' : 'filesystem storage'}`,
+					);
+					logger.info(`Be sure to define a KV binding named "${bindingName}".`);
+					session = isBuild
+						? {
+								...session,
+								driver: 'cloudflare-kv-binding',
+								options: {
+									// This is the name of the global variable where the KV namespace is stored
+									// in the worker. It is assigned to this inside the request handler.
+									binding: '__ASTRO_SESSION',
path: packages/integrations/cloudflare/src/index.ts
user: login: ematipico
body: Whether this is a nitpick or not, I foresee a use case where users might want to override this because they don't want to leak information relative to their framework. However, I think it's fine to keep it like this for now, and re-iterate if users need to overrde it
created_at: 2025-03-28T17:14:50Z
author_association: MEMBER
diff_hunk: @@ -111,13 +141,46 @@ export default function createIntegration(args?: Options): AstroIntegration {
 				addWatchFile,
 				addMiddleware,
 			}) => {
+				let session = config.session;
+
+				const isBuild = command === 'build';
+
+				if (config.experimental.session && !session?.driver) {
+					const bindingName = args?.sessionKVBindingName ?? 'SESSION';
+					logger.info(
+						`Configuring experimental session support using ${isBuild ? 'Cloudflare KV' : 'filesystem storage'}`,
+					);
+					logger.info(`Be sure to define a KV binding named "${bindingName}".`);
+					session = isBuild
+						? {
+								...session,
+								driver: 'cloudflare-kv-binding',
+								options: {
+									// This is the name of the global variable where the KV namespace is stored
+									// in the worker. It is assigned to this inside the request handler.
+									binding: '__ASTRO_SESSION',
+									...session?.options,
+								},
+							}
+						: {
+								...session,
+								driver: 'fs-lite',
+								options: {
+									base: fileURLToPath(new URL('sessions', config.cacheDir)),
path: packages/integrations/cloudflare/src/index.ts
user: login: florian-lefebvre
body: I think in dev it should live somewhere in .astro instead (you can use createCodegenDir)
created_at: 2025-03-28T17:18:36Z
author_association: MEMBER
diff_hunk: @@ -111,13 +141,46 @@ export default function createIntegration(args?: Options): AstroIntegration {
 				addWatchFile,
 				addMiddleware,
 			}) => {
+				let session = config.session;
+
+				const isBuild = command === 'build';
+
+				if (config.experimental.session && !session?.driver) {
+					const bindingName = args?.sessionKVBindingName ?? 'SESSION';
+					logger.info(
+						`Configuring experimental session support using ${isBuild ? 'Cloudflare KV' : 'filesystem storage'}`,
+					);
+					logger.info(`Be sure to define a KV binding named "${bindingName}".`);
+					session = isBuild
+						? {
+								...session,
+								driver: 'cloudflare-kv-binding',
+								options: {
+									// This is the name of the global variable where the KV namespace is stored
+									// in the worker. It is assigned to this inside the request handler.
+									binding: '__ASTRO_SESSION',
path: packages/integrations/cloudflare/src/index.ts
user: login: ascorbic
body: They can override the actual binding name: this is just the name of the variable we use to pass it to unstorage.
created_at: 2025-03-28T17:26:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -111,13 +141,46 @@ export default function createIntegration(args?: Options): AstroIntegration {
 				addWatchFile,
 				addMiddleware,
 			}) => {
+				let session = config.session;
+
+				const isBuild = command === 'build';
+
+				if (config.experimental.session && !session?.driver) {
+					const bindingName = args?.sessionKVBindingName ?? 'SESSION';
+					logger.info(
+						`Configuring experimental session support using ${isBuild ? 'Cloudflare KV' : 'filesystem storage'}`,
+					);
+					logger.info(`Be sure to define a KV binding named "${bindingName}".`);
+					session = isBuild
+						? {
+								...session,
+								driver: 'cloudflare-kv-binding',
+								options: {
+									// This is the name of the global variable where the KV namespace is stored
+									// in the worker. It is assigned to this inside the request handler.
+									binding: '__ASTRO_SESSION',
path: packages/integrations/cloudflare/src/index.ts
user: login: ascorbic
body: I've changed it to use the same name as the binding, because otherwise the error message from unstorage would be confusing.
created_at: 2025-03-28T17:52:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -71,6 +72,35 @@ export type Options = {
 	 * for reference on how these file types are exported
 	 */
 	cloudflareModules?: boolean;
+
+	/**
+	 * By default, Astro will be configured to use Cloudflare KV to store session data. If you want to use sessions, 
+	 * you must create a KV namespace and declare it in your wrangler config file. You can do this with the wrangler command:
+	 * 
+	 * ```sh
+	 * npx wrangler kv namespace create SESSION
+	 * ```
+	 * 
+	 * This will log the id of the created namespace. You can then add it to your `wrangler.json` file like this:
+	 * 
+	 * ```json
+	 * {
+	 *   "kv_namespaces": [
+	 *     {
+	 *       "binding": "SESSION",
+	 *       "id": "<your kv namespace id here>"
+	 *     }
+	 *   ]
path: packages/integrations/cloudflare/src/index.ts
user: login: ascorbic
body: I've added an explicit message saying what the error would be
created_at: 2025-03-28T17:53:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,6 +3,7 @@ import type {
 	CacheStorage as CLOUDFLARE_CACHESTORAGE,
 	Request as CLOUDFLARE_REQUEST,
 	ExecutionContext,
+	KVNamespace,
path: packages/integrations/cloudflare/src/entrypoints/server.ts
user: login: ematipico
body: Un used import
created_at: 2025-03-31T08:30:28Z
author_association: MEMBER

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-03-28T15:41:35Z
body: ###  🦋  Changeset detected

Latest commit: fd17d369b9497e3cae260381af7c7ff88403040f

**The changes in this PR will be included in the next version bump.**



Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/withastro/astro/new/cf-session?filename=.changeset/nasty-phones-sniff.md&value=---%0A%22%40fake-scope%2Ffake-pkg%22%3A%20patch%0A---%0A%0Afeat(cloudflare)%3A%20add%20KV%20session%20storage%20support%0A)


author_association: NONE
user: login: pkg-pr-new[bot]
created_at: 2025-03-28T15:42:24Z
body: 



<details><summary><b>astro</b></summary><p>

```
npm i https://pkg.pr.new/astro@13514
```

</p></details>
      
    

<details><summary><b>@astrojs/cloudflare</b></summary><p>

```
npm i https://pkg.pr.new/@astrojs/cloudflare@13514
```

</p></details>
      
    

<details><summary><b>@astrojs/netlify</b></summary><p>

```
npm i https://pkg.pr.new/@astrojs/netlify@13514
```

</p></details>
      
    

<details><summary><b>@astrojs/node</b></summary><p>

```
npm i https://pkg.pr.new/@astrojs/node@13514
```

</p></details>
      
    

<details><summary><b>@astrojs/vercel</b></summary><p>

```
npm i https://pkg.pr.new/@astrojs/vercel@13514
```

</p></details>
      
    

_commit: <a href="https://github.com/withastro/astro/runs/39596631243"><code>20237b8</code></a>_

author_association: NONE
user: login: ascorbic
created_at: 2025-03-31T08:50:33Z
body: I've added this to the 5.6 milestone just for the purposes of blogs, as it's an experimental feature
author_association: CONTRIBUTOR