diff options
18 files changed, 356 insertions, 215 deletions
diff --git a/.changeset/all-readers-jump.md b/.changeset/all-readers-jump.md new file mode 100644 index 000000000..f6dbe24dd --- /dev/null +++ b/.changeset/all-readers-jump.md @@ -0,0 +1,89 @@ +--- +'@astrojs/cloudflare': minor +'@astrojs/netlify': minor +'@astrojs/node': minor +'astro': minor +--- + +The experimental session API introduced in Astro 5.1 is now stable and ready for production use. + +Sessions are used to store user state between requests for [on-demand rendered pages](https://astro.build/en/guides/on-demand-rendering/). You can use them to store user data, such as authentication tokens, shopping cart contents, or any other data that needs to persist across requests: + +```astro +--- +export const prerender = false; // Not needed with 'server' output +const cart = await Astro.session.get('cart'); +--- + +<a href="/checkout">🛒 {cart?.length ?? 0} items</a> +``` + +## Configuring session storage + +Sessions require a storage driver to store the data. The Node, Cloudflare and Netlify adapters automatically configure a default driver for you, but other adapters currently require you to specify a custom storage driver in your configuration. + +If you are using an adapter that doesn't have a default driver, or if you want to choose a different driver, you can configure it using the `session` configuration option: + +```js +import { defineConfig } from 'astro/config'; +import vercel from '@astrojs/vercel'; + +export default defineConfig({ + adapter: vercel(), + session: { + driver: 'upstash', + }, +}); +``` + +## Using sessions + +Sessions are available in on-demand rendered pages, API endpoints, actions and middleware. + +In pages and components, you can access the session using `Astro.session`: + +```astro +--- +const cart = await Astro.session.get('cart'); +--- + +<a href="/checkout">🛒 {cart?.length ?? 0} items</a> +``` + +In endpoints, actions, and middleware, you can access the session using `context.session`: + +```js +export async function GET(context) { + const cart = await context.session.get('cart'); + return Response.json({ cart }); +} +``` + +If you attempt to access the session when there is no storage driver configured, or in a prerendered page, the session object will be `undefined` and an error will be logged in the console: + +```astro +--- +export const prerender = true; +const cart = await Astro.session?.get('cart'); // Logs an error. Astro.session is undefined +--- +``` + +## Upgrading from Experimental to Stable + +If you were previously using the experimental API, please remove the `experimental.session` flag from your configuration: + +```diff +import { defineConfig } from 'astro/config'; +import node from '@astrojs/node'; + +export default defineConfig({ + adapter: node({ + mode: "standalone", + }), +- experimental: { +- session: true, +- }, +}); +``` + +See [the sessions guide](https://docs.astro.build/en/guides/sessions/) for more information. diff --git a/packages/astro/src/core/config/schemas/base.ts b/packages/astro/src/core/config/schemas/base.ts index 1776785b9..2bfbf9e5c 100644 --- a/packages/astro/src/core/config/schemas/base.ts +++ b/packages/astro/src/core/config/schemas/base.ts @@ -96,7 +96,6 @@ export const ASTRO_CONFIG_DEFAULTS = { responsiveImages: false, svg: false, serializeConfig: false, - session: false, headingIdCompat: false, preserveScriptOrder: false, }, @@ -464,7 +463,6 @@ export const AstroConfigSchema = z.object({ .boolean() .optional() .default(ASTRO_CONFIG_DEFAULTS.experimental.responsiveImages), - session: z.boolean().optional(), svg: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.svg), serializeConfig: z .boolean() diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index db2a841b8..687c0ab90 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -1827,24 +1827,9 @@ export const ActionsCantBeLoaded = { // Session Errors /** * @docs - * @see - * - [Server output adapter feature](https://docs.astro.build/en/reference/adapter-reference/#building-an-adapter) - * @description - * Your adapter must support server output to use sessions. - */ -export const SessionWithoutSupportedAdapterOutputError = { - name: 'SessionWithoutSupportedAdapterOutputError', - title: "Sessions cannot be used with an adapter that doesn't support server output.", - message: - 'Sessions require an adapter that supports server output. The adapter must set `"server"` in the `buildOutput` adapter feature.', - hint: 'Ensure your adapter supports `buildOutput: "server"`: https://docs.astro.build/en/reference/adapter-reference/#building-an-adapter', -} satisfies ErrorData; - -/** - * @docs * @message Error when initializing session storage with driver `DRIVER`. `ERROR` * @see - * - [experimental.session](https://docs.astro.build/en/reference/experimental-flags/sessions/) + * - [Sessions](https://docs.astro.build/en/guides/sessions/) * @description * Thrown when the session storage could not be initialized. */ @@ -1853,14 +1838,14 @@ export const SessionStorageInitError = { title: 'Session storage could not be initialized.', message: (error: string, driver?: string) => `Error when initializing session storage${driver ? ` with driver \`${driver}\`` : ''}. \`${error ?? ''}\``, - hint: 'For more information, see https://docs.astro.build/en/reference/experimental-flags/sessions/', + hint: 'For more information, see https://docs.astro.build/en/guides/sessions/', } satisfies ErrorData; /** * @docs * @message Error when saving session data with driver `DRIVER`. `ERROR` * @see - * - [experimental.session](https://docs.astro.build/en/reference/experimental-flags/sessions/) + * - [Sessions](https://docs.astro.build/en/guides/sessions/) * @description * Thrown when the session data could not be saved. */ @@ -1869,14 +1854,30 @@ export const SessionStorageSaveError = { title: 'Session data could not be saved.', message: (error: string, driver?: string) => `Error when saving session data${driver ? ` with driver \`${driver}\`` : ''}. \`${error ?? ''}\``, - hint: 'For more information, see https://docs.astro.build/en/reference/experimental-flags/sessions/', + hint: 'For more information, see https://docs.astro.build/en/guides/sessions/', } satisfies ErrorData; /** * @docs - * @message The `experimental.session` flag was set to `true`, but no storage was configured. Either configure the storage manually or use an adapter that provides session storage * @see - * - [experimental.session](https://docs.astro.build/en/reference/experimental-flags/sessions/) + * - [Sessions](https://docs.astro.build/en/guides/sessions/) + * @deprecated This error was removed in Astro 5.7, when the Sessions feature stopped being experimental. + * @description + * Your adapter must support server output to use sessions. + */ +export const SessionWithoutSupportedAdapterOutputError = { + name: 'SessionWithoutSupportedAdapterOutputError', + title: "Sessions cannot be used with an adapter that doesn't support server output.", + message: + 'Sessions require an adapter that supports server output. The adapter must set `"server"` in the `buildOutput` adapter feature.', + hint: 'Ensure your adapter supports `buildOutput: "server"`: https://docs.astro.build/en/reference/adapter-reference/#building-an-adapter', +} satisfies ErrorData; +/** + * @docs + * @message The `experimental.session` flag was set to `true`, but no storage was configured. Either configure the storage manually or use an adapter that provides session storage. + * @deprecated This error was removed in Astro 5.7, when the Sessions feature stopped being experimental. + * @see + * - [Sessions](https://docs.astro.build/en/guides/sessions/) * @description * Thrown when session storage is enabled but not configured. */ @@ -1885,14 +1886,15 @@ export const SessionConfigMissingError = { title: 'Session storage was enabled but not configured.', message: 'The `experimental.session` flag was set to `true`, but no storage was configured. Either configure the storage manually or use an adapter that provides session storage', - hint: 'See https://docs.astro.build/en/reference/experimental-flags/sessions/', -} satisfies ErrorData; + hint: 'For more information, see https://docs.astro.build/en/guides/sessions/', + } satisfies ErrorData; /** * @docs * @message Session config was provided without enabling the `experimental.session` flag + * @deprecated This error was removed in Astro 5.7, when the Sessions feature stopped being experimental. * @see - * - [experimental.session](https://docs.astro.build/en/reference/experimental-flags/sessions/) + * - [Sessions](https://docs.astro.build/en/guides/sessions/) * @description * Thrown when session storage is configured but the `experimental.session` flag is not enabled. */ @@ -1900,7 +1902,7 @@ export const SessionConfigWithoutFlagError = { name: 'SessionConfigWithoutFlagError', title: 'Session flag not set', message: 'Session config was provided without enabling the `experimental.session` flag', - hint: 'See https://docs.astro.build/en/reference/experimental-flags/sessions/', + hint: 'For more information, see https://docs.astro.build/en/guides/sessions/', } satisfies ErrorData; /* diff --git a/packages/astro/src/core/logger/core.ts b/packages/astro/src/core/logger/core.ts index e5c91f653..9d8c7e357 100644 --- a/packages/astro/src/core/logger/core.ts +++ b/packages/astro/src/core/logger/core.ts @@ -28,6 +28,7 @@ export type LoggerLabel = | 'preferences' | 'redirects' | 'sync' + | 'session' | 'toolbar' | 'assets' | 'env' diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts index 828090fa5..07462db56 100644 --- a/packages/astro/src/core/render-context.ts +++ b/packages/astro/src/core/render-context.ts @@ -1,3 +1,4 @@ +import { green } from 'kleur/colors'; import type { ActionAPIContext } from '../actions/runtime/utils.js'; import { getActionContext } from '../actions/runtime/virtual/server.js'; import { deserializeActionResult } from '../actions/runtime/virtual/shared.js'; @@ -59,7 +60,7 @@ export class RenderContext { public props: Props = {}, public partial: undefined | boolean = undefined, public session: AstroSession | undefined = pipeline.manifest.sessionConfig - ? new AstroSession(cookies, pipeline.manifest.sessionConfig) + ? new AstroSession(cookies, pipeline.manifest.sessionConfig, pipeline.runtimeMode) : undefined, ) {} @@ -335,7 +336,7 @@ export class RenderContext { createActionAPIContext(): ActionAPIContext { const renderContext = this; - const { cookies, params, pipeline, url, session } = this; + const { cookies, params, pipeline, url } = this; const generator = `Astro v${ASTRO_VERSION}`; const rewrite = async (reroutePayload: RewritePayload) => { @@ -373,7 +374,23 @@ export class RenderContext { get originPathname() { return getOriginPathname(renderContext.request); }, - session, + get session() { + if (this.isPrerendered) { + pipeline.logger.warn( + 'session', + `context.session was used when rendering the route ${green(this.routePattern)}, but it is not available on prerendered routes. If you need access to sessions, make sure that the route is server-rendered using \`export const prerender = false;\` or by setting \`output\` to \`"server"\` in your Astro config to make all your routes server-rendered by default. For more information, see https://docs.astro.build/en/guides/sessions/`, + ); + return undefined; + } + if (!renderContext.session) { + pipeline.logger.warn( + 'session', + `context.session was used when rendering the route ${green(this.routePattern)}, but no storage configuration was provided. Either configure the storage manually or use an adapter that provides session storage. For more information, see https://docs.astro.build/en/guides/sessions/`, + ); + return undefined; + } + return renderContext.session; + }, }; } @@ -512,7 +529,7 @@ export class RenderContext { apiContext: ActionAPIContext, ): Omit<AstroGlobal, 'props' | 'self' | 'slots'> { const renderContext = this; - const { cookies, locals, params, pipeline, url, session } = this; + const { cookies, locals, params, pipeline, url } = this; const { response } = result; const redirect = (path: string, status = 302) => { // If the response is already sent, error as we cannot proceed with the redirect. @@ -536,7 +553,23 @@ export class RenderContext { routePattern: this.routeData.route, isPrerendered: this.routeData.prerender, cookies, - session, + get session() { + if (this.isPrerendered) { + pipeline.logger.warn( + 'session', + `Astro.session was used when rendering the route ${green(this.routePattern)}, but it is not available on prerendered pages. If you need access to sessions, make sure that the page is server-rendered using \`export const prerender = false;\` or by setting \`output\` to \`"server"\` in your Astro config to make all your pages server-rendered by default. For more information, see https://docs.astro.build/en/guides/sessions/`, + ); + return undefined; + } + if (!renderContext.session) { + pipeline.logger.warn( + 'session', + `Astro.session was used when rendering the route ${green(this.routePattern)}, but no storage configuration was provided. Either configure the storage manually or use an adapter that provides session storage. For more information, see https://docs.astro.build/en/guides/sessions/`, + ); + return undefined; + } + return renderContext.session; + }, get clientAddress() { return renderContext.getClientAddress(); }, diff --git a/packages/astro/src/core/session.ts b/packages/astro/src/core/session.ts index f86fa536b..653514889 100644 --- a/packages/astro/src/core/session.ts +++ b/packages/astro/src/core/session.ts @@ -6,21 +6,15 @@ import { builtinDrivers, createStorage, } from 'unstorage'; -import type { AstroSettings } from '../types/astro.js'; import type { ResolvedSessionConfig, + RuntimeMode, SessionConfig, SessionDriverName, } from '../types/public/config.js'; import type { AstroCookies } from './cookies/cookies.js'; import type { AstroCookieSetOptions } from './cookies/cookies.js'; -import { - SessionConfigMissingError, - SessionConfigWithoutFlagError, - SessionStorageInitError, - SessionStorageSaveError, - SessionWithoutSupportedAdapterOutputError, -} from './errors/errors-data.js'; +import { SessionStorageInitError, SessionStorageSaveError } from './errors/errors-data.js'; import { AstroError } from './errors/index.js'; export const PERSIST_SYMBOL = Symbol(); @@ -84,6 +78,7 @@ export class AstroSession<TDriver extends SessionDriverName = any> { cookie: cookieConfig = DEFAULT_COOKIE_NAME, ...config }: Exclude<ResolvedSessionConfig<TDriver>, undefined>, + runtimeMode?: RuntimeMode ) { this.#cookies = cookies; let cookieConfigObject: AstroCookieSetOptions | undefined; @@ -96,7 +91,7 @@ export class AstroSession<TDriver extends SessionDriverName = any> { } this.#cookieConfig = { sameSite: 'lax', - secure: true, + secure: runtimeMode === 'production', path: '/', ...cookieConfigObject, httpOnly: true, @@ -523,22 +518,3 @@ export async function resolveSessionDriver(driver: string | undefined): Promise< return driver; } - -export function validateSessionConfig(settings: AstroSettings): void { - const { experimental, session } = settings.config; - const { buildOutput } = settings; - let error: AstroError | undefined; - if (experimental.session) { - if (!session?.driver) { - error = new AstroError(SessionConfigMissingError); - } else if (buildOutput === 'static') { - error = new AstroError(SessionWithoutSupportedAdapterOutputError); - } - } else if (session?.driver) { - error = new AstroError(SessionConfigWithoutFlagError); - } - if (error) { - error.stack = undefined; - throw error; - } -} diff --git a/packages/astro/src/integrations/hooks.ts b/packages/astro/src/integrations/hooks.ts index cd0315db8..cdf4eb80b 100644 --- a/packages/astro/src/integrations/hooks.ts +++ b/packages/astro/src/integrations/hooks.ts @@ -16,7 +16,6 @@ import { mergeConfig } from '../core/config/index.js'; import { validateConfigRefined } from '../core/config/validate.js'; import { validateSetAdapter } from '../core/dev/adapter-validation.js'; import type { AstroIntegrationLogger, Logger } from '../core/logger/core.js'; -import { validateSessionConfig } from '../core/session.js'; import type { AstroSettings } from '../types/astro.js'; import type { AstroConfig } from '../types/public/config.js'; import type { @@ -416,11 +415,6 @@ export async function runHookConfigDone({ }), }); } - // Session config is validated after all integrations have had a chance to - // register a default session driver, and we know the output type. - // This can't happen in the Zod schema because it that happens before adapters run - // and also doesn't know whether it's a server build or static build. - validateSessionConfig(settings); } export async function runHookServerSetup({ diff --git a/packages/astro/src/types/public/config.ts b/packages/astro/src/types/public/config.ts index bcfd0e43f..6bd9c6b99 100644 --- a/packages/astro/src/types/public/config.ts +++ b/packages/astro/src/types/public/config.ts @@ -297,7 +297,7 @@ export interface ViteUserConfig extends OriginalViteUserConfig { * '/news': { * status: 302, * destination: 'https://example.com/news' - * }, + * }, * // '/product1/', '/product1' // Note, this is not supported * } * }) @@ -579,36 +579,6 @@ export interface ViteUserConfig extends OriginalViteUserConfig { /** * @docs - * @name session - * @type {SessionConfig} - * @version 5.3.0 - * @description - * - * Configures experimental session support by specifying a storage `driver` as well as any associated `options`. - * You must enable the `experimental.session` flag to use this feature. - * Some adapters may provide a default session driver, but you can override it with your own configuration. - * - * You can specify [any driver from Unstorage](https://unstorage.unjs.io/drivers) or provide a custom config which will override your adapter's default. - * - * See [the experimental session guide](https://docs.astro.build/en/reference/experimental-flags/sessions/) for more information. - * - * ```js title="astro.config.mjs" - * { - * session: { - * // Required: the name of the Unstorage driver - * driver: 'redis', - * // The required options depend on the driver - * options: { - * url: process.env.REDIS_URL, - * }, - * } - * } - * ``` - */ - session?: SessionConfig<TSession>; - - /** - * @docs * @name vite * @typeraw {ViteUserConfig} * @description @@ -964,6 +934,151 @@ export interface ViteUserConfig extends OriginalViteUserConfig { /** * @docs * @kind heading + * @version 5.7.0 + * @name Session Options + * @description + * + * Configures session storage for your Astro project. This is used to store session data in a persistent way, so that it can be accessed across different requests. + * Some adapters may provide a default session driver, but you can override it with your own configuration. + * + * See [the sessions guide](https://docs.astro.build/en/guides/sessions/) for more information. + * + * ```js title="astro.config.mjs" + * { + * session: { + * // The name of the Unstorage driver + * driver: 'redis', + * // The required options depend on the driver + * options: { + * url: process.env.REDIS_URL, + * }, + * ttl: 3600, // 1 hour + * } + * } + * ``` + */ + session?: SessionConfig<TSession>; + + /** + * @docs + * @name session.driver + * @type {string | undefined} + * @version 5.7.0 + * @description + * + * The Unstorage driver to use for session storage. The [Node](https://docs.astro.build/en/guides/integrations-guide/node/#sessions), + * [Cloudflare](https://docs.astro.build/en/guides/integrations-guide/cloudflare/#sessions), and + * [Netlify](/en/guides/integrations-guide/netlify/#sessions) adapters automatically configure a default driver for you, + * but you can specify your own if you would prefer or if you are using an adapter that does not provide one. + * + * The value is the "Driver name" from the [Unstorage driver documentation](https://unstorage.unjs.io/drivers). + * + * ```js title="astro.config.mjs" ins={4} + * { + * adapter: vercel(), + * session: { + * driver: "redis", + * }, + * } + * ``` + * :::note + * Some drivers may need extra packages to be installed. Some drivers may also require environment variables or credentials to be set. See the [Unstorage documentation](https://unstorage.unjs.io/drivers) for more information. + * ::: + * + */ + + /** + * @docs + * @name session.options + * @type {Record<string, unknown> | undefined} + * @version 5.7.0 + * @default `{}` + * @description + * + * The driver-specific options to use for session storage. The options depend on the driver you are using. See the [Unstorage documentation](https://unstorage.unjs.io/drivers) + * for more information on the options available for each driver. + * + * ```js title="astro.config.mjs" ins={4-6} + * { + * session: { + * driver: "redis", + * options: { + * url: process.env.REDIS_URL + * }, + * } + * } + * ``` + */ + + /** + * @docs + * @name session.cookie + * @type {string | AstroCookieSetOptions | undefined} + * @version 5.7.0 + * @default `{ name: "astro-session", sameSite: "lax", httpOnly: true, secure: true }` + * @description + * + * The session cookie configuration. If set to a string, it will be used as the cookie name. + * Alternatively, you can pass an object with additional options. These will be merged with the defaults. + * + * ```js title="astro.config.mjs" ins={3-4} + * { + * session: { + * // If set to a string, it will be used as the cookie name. + * cookie: "my-session-cookie", + * } + * } + * + * ``` + * + * ```js title="astro.config.mjs" ins={4-8} + * { + * session: { + * // If set to an object, it will be used as the cookie options. + * cookie: { + * name: "my-session-cookie", + * sameSite: "lax", + * secure: true, + * } + * } + * } + * ``` + */ + + /** + * @docs + * @name session.ttl + * @version 5.7.0 + * @type {number | undefined} + * @default {Infinity} + * @description + * + * An optional default time-to-live expiration period for session values, in seconds. + * + * By default, session values persist until they are deleted or the session is destroyed, and do not automatically expire because a particular amount of time has passed. + * Set `session.ttl` to add a default expiration period for your session values. Passing a `ttl` option to [`session.set()`](https://docs.astro.build/en/reference/api-reference/#set) will override the global default + * for that individual entry. + * + * ```js title="astro.config.mjs" ins={3-4} + * { + * session: { + * // Set a default expiration period of 1 hour (3600 seconds) + * ttl: 3600, + * } + * } + * ``` + * :::note + * Setting a value for `ttl` does not automatically delete the value from storage after the time limit has passed. + * + * Values from storage will only be deleted when there is an attempt to access them after the `ttl` period has expired. At this time, the session value will be undefined and only then will the value be deleted. + * + * Individual drivers may also support a `ttl` option that will automatically delete sessions after the specified time. See your chosen driver's documentation for more information. + * ::: + */ + + /** + * @docs + * @kind heading * @name Dev Toolbar Options */ devToolbar?: { @@ -1950,7 +2065,7 @@ export interface ViteUserConfig extends OriginalViteUserConfig { * * ```js title=astro.config.mjs * { - * experimental: { + * experimental: { * responsiveImages: true, * }, * } @@ -2058,33 +2173,6 @@ export interface ViteUserConfig extends OriginalViteUserConfig { /** * - * @name experimental.session - * @type {boolean} - * @default `false` - * @version 5.0.0 - * @description - * - * Enables support for sessions in Astro. Sessions are used to store user data across requests, such as user authentication state. - * - * When enabled you can access the `Astro.session` object to read and write data that persists across requests. You can configure the session driver using the [`session` option](#session), or use the default provided by your adapter. - * - * ```astro title=src/components/CartButton.astro - * --- - * export const prerender = false; // Not needed in 'server' mode - * const cart = await Astro.session.get('cart'); - * --- - * - * <a href="/checkout">🛒 {cart?.length ?? 0} items</a> - * - * ``` - * - * For more details, see [the experimental session guide](https://docs.astro.build/en/reference/experimental-flags/sessions/). - * - */ - - session?: boolean; - /** - * * @name experimental.svg * @type {boolean} * @default `undefined` diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts index bed0069b6..88b8b0a81 100644 --- a/packages/astro/src/vite-plugin-astro-server/plugin.ts +++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts @@ -206,6 +206,6 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest onRequest: NOOP_MIDDLEWARE_FN, }; }, - sessionConfig: settings.config.experimental.session ? settings.config.session : undefined, + sessionConfig: settings.config.session, }; } diff --git a/packages/astro/test/sessions.test.js b/packages/astro/test/sessions.test.js index 9ac94c944..3fe385fb4 100644 --- a/packages/astro/test/sessions.test.js +++ b/packages/astro/test/sessions.test.js @@ -18,9 +18,6 @@ describe('Astro.session', () => { driver: 'fs', ttl: 20, }, - experimental: { - session: true, - }, }); }); @@ -38,7 +35,7 @@ describe('Astro.session', () => { } it('can regenerate session cookies upon request', async () => { - const firstResponse = await fetchResponse('/regenerate', { method: 'GET' }); + const firstResponse = await fetchResponse('/regenerate'); const firstHeaders = Array.from(app.setCookieHeaders(firstResponse)); const firstSessionId = firstHeaders[0].split(';')[0].split('=')[1]; @@ -53,8 +50,21 @@ describe('Astro.session', () => { assert.notEqual(firstSessionId, secondSessionId); }); + it('defaults to secure cookies in production', async () => { + const firstResponse = await fetchResponse('/regenerate'); + const firstHeaders = Array.from(app.setCookieHeaders(firstResponse)); + assert.ok( + firstHeaders[0].includes('Secure'), + 'Secure cookie not set in production', + ); + assert.ok( + firstHeaders[0].includes('HttpOnly'), + 'HttpOnly cookie not set in production', + ); + }); + it('can save session data by value', async () => { - const firstResponse = await fetchResponse('/update', { method: 'GET' }); + const firstResponse = await fetchResponse('/update'); const firstValue = await firstResponse.json(); assert.equal(firstValue.previousValue, 'none'); @@ -141,9 +151,6 @@ describe('Astro.session', () => { driver: 'fs', ttl: 20, }, - experimental: { - session: true, - }, }); devServer = await fixture.startDevServer(); }); @@ -170,6 +177,13 @@ describe('Astro.session', () => { assert.notEqual(firstSessionId, secondSessionId); }); + + it('defaults to non-secure cookies in development', async () => { + const response = await fixture.fetch('/regenerate'); + const setCookieHeader = response.headers.get('set-cookie'); + assert.ok(!setCookieHeader.includes('Secure')); + }); + it('can save session data by value', async () => { const firstResponse = await fixture.fetch('/update'); const firstValue = await firstResponse.json(); @@ -219,60 +233,4 @@ describe('Astro.session', () => { ); }); }); - - describe('Configuration', () => { - it('throws if flag is enabled but driver is not set', async () => { - const fixture = await loadFixture({ - root: './fixtures/sessions/', - output: 'server', - adapter: testAdapter(), - experimental: { - session: true, - }, - }); - await assert.rejects( - fixture.build({}), - /Error: The `experimental.session` flag was set to `true`, but no storage was configured/, - ); - }); - - it('throws if session is configured but flag is not enabled', async () => { - const fixture = await loadFixture({ - root: './fixtures/sessions/', - output: 'server', - adapter: testAdapter(), - session: { - driver: 'fs', - }, - experimental: { - session: false, - }, - }); - await assert.rejects( - fixture.build({}), - /Error: Session config was provided without enabling the `experimental.session` flag/, - ); - }); - - it('throws if output is static', async () => { - const fixture = await loadFixture({ - root: './fixtures/sessions/', - output: 'static', - session: { - driver: 'fs', - ttl: 20, - }, - experimental: { - session: true, - }, - }); - // Disable actions so we can do a static build - await fixture.editFile('src/actions/index.ts', () => ''); - await assert.rejects( - fixture.build({}), - /Sessions require an adapter that supports server output/, - ); - await fixture.resetAllFiles(); - }); - }); }); diff --git a/packages/astro/test/units/sessions/astro-session.test.js b/packages/astro/test/units/sessions/astro-session.test.js index 3fa1b9de1..a7d65f215 100644 --- a/packages/astro/test/units/sessions/astro-session.test.js +++ b/packages/astro/test/units/sessions/astro-session.test.js @@ -18,13 +18,13 @@ const defaultConfig = { }; // Helper to create a new session instance with mocked dependencies -function createSession(config = defaultConfig, cookies = defaultMockCookies, mockStorage) { +function createSession(config = defaultConfig, cookies = defaultMockCookies, mockStorage, runtimeMode = 'production') { if (mockStorage) { config.driver = 'test'; config.options ??= {}; config.options.mockStorage = mockStorage; } - return new AstroSession(cookies, config); + return new AstroSession(cookies, config, runtimeMode); } test('AstroSession - Basic Operations', async (t) => { @@ -380,7 +380,7 @@ test('AstroSession - Cookie Security', async (t) => { assert.equal(cookieOptions.httpOnly, true); }); - await t.test('should set secure and sameSite by default', async () => { + await t.test('should set secure and sameSite by default in production', async () => { let cookieOptions; const mockCookies = { ...defaultMockCookies, @@ -395,6 +395,23 @@ test('AstroSession - Cookie Security', async (t) => { assert.equal(cookieOptions.secure, true); assert.equal(cookieOptions.sameSite, 'lax'); }); + + await t.test('should set secure to false in development', async () => { + let cookieOptions; + const mockCookies = { + ...defaultMockCookies, + set: (_name, _value, options) => { + cookieOptions = options; + }, + }; + + const session = createSession(defaultConfig, mockCookies, undefined, 'development'); + + session.set('key', 'value'); + assert.equal(cookieOptions.secure, false); + assert.equal(cookieOptions.sameSite, 'lax'); + }) + }); test('AstroSession - Storage Errors', async (t) => { diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts index cb275eae0..d5c2e0be9 100644 --- a/packages/integrations/cloudflare/src/index.ts +++ b/packages/integrations/cloudflare/src/index.ts @@ -145,11 +145,11 @@ export default function createIntegration(args?: Options): AstroIntegration { const isBuild = command === 'build'; - if (config.experimental.session && !session?.driver) { + if (!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}".`, + `Enabling sessions with ${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.`, diff --git a/packages/integrations/cloudflare/test/fixtures/sessions/astro.config.mjs b/packages/integrations/cloudflare/test/fixtures/sessions/astro.config.mjs index 7d37e9762..b5f6fc107 100644 --- a/packages/integrations/cloudflare/test/fixtures/sessions/astro.config.mjs +++ b/packages/integrations/cloudflare/test/fixtures/sessions/astro.config.mjs @@ -9,7 +9,4 @@ export default defineConfig({ enabled: true, }, }), - experimental: { - session: true, - } }); diff --git a/packages/integrations/netlify/src/index.ts b/packages/integrations/netlify/src/index.ts index 3a447f5fd..b2c020e70 100644 --- a/packages/integrations/netlify/src/index.ts +++ b/packages/integrations/netlify/src/index.ts @@ -521,9 +521,9 @@ export default function netlifyIntegration( let session = config.session; - if (config.experimental.session && !session?.driver) { + if (!session?.driver) { logger.info( - `Configuring experimental session support using ${isRunningInNetlify ? 'Netlify Blobs' : 'filesystem storage'}`, + `Enabling sessions with ${isRunningInNetlify ? 'Netlify Blobs' : 'filesystem storage'}`, ); session = isRunningInNetlify ? { diff --git a/packages/integrations/netlify/test/functions/fixtures/sessions/astro.config.mjs b/packages/integrations/netlify/test/functions/fixtures/sessions/astro.config.mjs index 23d03b44f..f7ac423c9 100644 --- a/packages/integrations/netlify/test/functions/fixtures/sessions/astro.config.mjs +++ b/packages/integrations/netlify/test/functions/fixtures/sessions/astro.config.mjs @@ -4,8 +4,5 @@ import { defineConfig } from 'astro/config'; export default defineConfig({ output: 'server', adapter: netlify(), - site: `http://example.com`, - experimental: { - session: true, - } + site: `http://example.com` }); diff --git a/packages/integrations/netlify/test/functions/sessions.test.js b/packages/integrations/netlify/test/functions/sessions.test.js index 767895e5c..107e32190 100644 --- a/packages/integrations/netlify/test/functions/sessions.test.js +++ b/packages/integrations/netlify/test/functions/sessions.test.js @@ -40,9 +40,6 @@ describe('Astro.session', () => { root: new URL('./fixtures/sessions/', import.meta.url), output: 'server', adapter: netlify(), - experimental: { - session: true, - }, // @ts-ignore session: { driver: '', options }, }); diff --git a/packages/integrations/node/src/index.ts b/packages/integrations/node/src/index.ts index e1cf5e8b5..26ffd2ad0 100644 --- a/packages/integrations/node/src/index.ts +++ b/packages/integrations/node/src/index.ts @@ -37,8 +37,8 @@ export default function createIntegration(userOptions: UserOptions): AstroIntegr 'astro:config:setup': async ({ updateConfig, config, logger }) => { let session = config.session; - if (config.experimental.session && !session?.driver) { - logger.info('Configuring experimental session support using filesystem storage'); + if (!session?.driver) { + logger.info('Enabling sessions with filesystem storage'); session = { ...session, driver: 'fs-lite', diff --git a/packages/integrations/node/test/sessions.test.js b/packages/integrations/node/test/sessions.test.js index e3cac3f11..431363744 100644 --- a/packages/integrations/node/test/sessions.test.js +++ b/packages/integrations/node/test/sessions.test.js @@ -15,9 +15,6 @@ describe('Astro.session', () => { root: './fixtures/sessions/', output: 'server', adapter: nodejs({ mode: 'middleware' }), - experimental: { - session: true, - }, }); }); @@ -109,9 +106,6 @@ describe('Astro.session', () => { root: './fixtures/sessions/', output: 'server', adapter: nodejs({ mode: 'middleware' }), - experimental: { - session: true, - }, }); devServer = await fixture.startDevServer(); }); |