summaryrefslogtreecommitdiff
path: root/packages/astro/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/astro/src')
-rw-r--r--packages/astro/src/@types/astro.ts7
-rw-r--r--packages/astro/src/core/app/index.ts5
-rw-r--r--packages/astro/src/core/cookies/cookies.ts202
-rw-r--r--packages/astro/src/core/cookies/index.ts9
-rw-r--r--packages/astro/src/core/cookies/response.ts26
-rw-r--r--packages/astro/src/core/endpoint/index.ts18
-rw-r--r--packages/astro/src/core/render/core.ts11
-rw-r--r--packages/astro/src/core/render/result.ts13
-rw-r--r--packages/astro/src/runtime/server/endpoint.ts13
-rw-r--r--packages/astro/src/vite-plugin-astro-server/index.ts6
10 files changed, 295 insertions, 15 deletions
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index c423a1abf..7c19dcca6 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -14,6 +14,7 @@ import type * as vite from 'vite';
import type { z } from 'zod';
import type { SerializedSSRManifest } from '../core/app/types';
import type { PageBuildData } from '../core/build/types';
+import type { AstroCookies } from '../core/cookies';
import type { AstroConfigSchema } from '../core/config';
import type { ViteConfigWithSSR } from '../core/create-vite';
import type { AstroComponentFactory, Metadata } from '../runtime/server';
@@ -116,6 +117,10 @@ export interface AstroGlobal extends AstroGlobalPartial {
*
* [Astro reference](https://docs.astro.build/en/reference/api-reference/#url)
*/
+ /**
+ * Utility for getting and setting cookies values.
+ */
+ cookies: AstroCookies,
url: URL;
/** Parameters passed to a dynamic page generated using [getStaticPaths](https://docs.astro.build/en/reference/api-reference/#getstaticpaths)
*
@@ -1083,6 +1088,7 @@ export interface AstroAdapter {
type Body = string;
export interface APIContext {
+ cookies: AstroCookies;
params: Params;
request: Request;
}
@@ -1219,6 +1225,7 @@ export interface SSRResult {
styles: Set<SSRElement>;
scripts: Set<SSRElement>;
links: Set<SSRElement>;
+ cookies: AstroCookies | undefined;
createAstro(
Astro: AstroGlobalPartial,
props: Record<string, any>,
diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts
index 413dba20c..ce3422738 100644
--- a/packages/astro/src/core/app/index.ts
+++ b/packages/astro/src/core/app/index.ts
@@ -21,6 +21,7 @@ import {
} from '../render/ssr-element.js';
import { matchRoute } from '../routing/match.js';
export { deserializeManifest } from './common.js';
+import { getSetCookiesFromResponse } from '../cookies/index.js';
export const pagesVirtualModuleId = '@astrojs-pages-virtual-entry';
export const resolvedPagesVirtualModuleId = '\0' + pagesVirtualModuleId;
@@ -116,6 +117,10 @@ export class App {
}
}
+ setCookieHeaders(response: Response) {
+ return getSetCookiesFromResponse(response);
+ }
+
async #renderPage(
request: Request,
routeData: RouteData,
diff --git a/packages/astro/src/core/cookies/cookies.ts b/packages/astro/src/core/cookies/cookies.ts
new file mode 100644
index 000000000..7f530ce85
--- /dev/null
+++ b/packages/astro/src/core/cookies/cookies.ts
@@ -0,0 +1,202 @@
+import type { CookieSerializeOptions } from 'cookie';
+import { parse, serialize } from 'cookie';
+
+interface AstroCookieSetOptions {
+ domain?: string;
+ expires?: Date;
+ httpOnly?: boolean;
+ maxAge?: number;
+ path?: string;
+ sameSite?: boolean | 'lax' | 'none' | 'strict';
+ secure?: boolean;
+}
+
+interface AstroCookieDeleteOptions {
+ path?: string;
+}
+
+interface AstroCookieInterface {
+ value: string | undefined;
+ json(): Record<string, any>;
+ number(): number;
+ boolean(): boolean;
+}
+
+interface AstroCookiesInterface {
+ get(key: string): AstroCookieInterface;
+ has(key: string): boolean;
+ set(key: string, value: string | Record<string, any>, options?: AstroCookieSetOptions): void;
+ delete(key: string, options?: AstroCookieDeleteOptions): void;
+}
+
+const DELETED_EXPIRATION = new Date(0);
+const DELETED_VALUE = 'deleted';
+
+class AstroCookie implements AstroCookieInterface {
+ constructor(public value: string | undefined) {}
+ json() {
+ if(this.value === undefined) {
+ throw new Error(`Cannot convert undefined to an object.`);
+ }
+ return JSON.parse(this.value);
+ }
+ number() {
+ return Number(this.value);
+ }
+ boolean() {
+ if(this.value === 'false') return false;
+ if(this.value === '0') return false;
+ return Boolean(this.value);
+ }
+}
+
+class AstroCookies implements AstroCookiesInterface {
+ #request: Request;
+ #requestValues: Record<string, string> | null;
+ #outgoing: Map<string, [string, string, boolean]> | null;
+ constructor(request: Request) {
+ this.#request = request;
+ this.#requestValues = null;
+ this.#outgoing = null;
+ }
+
+ /**
+ * Astro.cookies.delete(key) is used to delete a cookie. Using this method will result
+ * in a Set-Cookie header added to the response.
+ * @param key The cookie to delete
+ * @param options Options related to this deletion, such as the path of the cookie.
+ */
+ delete(key: string, options?: AstroCookieDeleteOptions): void {
+ const serializeOptions: CookieSerializeOptions = {
+ expires: DELETED_EXPIRATION
+ };
+
+ if(options?.path) {
+ serializeOptions.path = options.path;
+ }
+
+ // Set-Cookie: token=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT
+ this.#ensureOutgoingMap().set(key, [
+ DELETED_VALUE,
+ serialize(key, DELETED_VALUE, serializeOptions),
+ false
+ ]);
+ }
+
+ /**
+ * Astro.cookies.get(key) is used to get a cookie value. The cookie value is read from the
+ * request. If you have set a cookie via Astro.cookies.set(key, value), the value will be taken
+ * from that set call, overriding any values already part of the request.
+ * @param key The cookie to get.
+ * @returns An object containing the cookie value as well as convenience methods for converting its value.
+ */
+ get(key: string): AstroCookie {
+ // Check for outgoing Set-Cookie values first
+ if(this.#outgoing !== null && this.#outgoing.has(key)) {
+ let [serializedValue,, isSetValue] = this.#outgoing.get(key)!;
+ if(isSetValue) {
+ return new AstroCookie(serializedValue);
+ } else {
+ return new AstroCookie(undefined);
+ }
+ }
+
+ const values = this.#ensureParsed();
+ const value = values[key];
+ return new AstroCookie(value);
+ }
+
+ /**
+ * Astro.cookies.has(key) returns a boolean indicating whether this cookie is either
+ * part of the initial request or set via Astro.cookies.set(key)
+ * @param key The cookie to check for.
+ * @returns
+ */
+ has(key: string): boolean {
+ if(this.#outgoing !== null && this.#outgoing.has(key)) {
+ let [,,isSetValue] = this.#outgoing.get(key)!;
+ return isSetValue;
+ }
+ const values = this.#ensureParsed();
+ return !!values[key];
+ }
+
+ /**
+ * Astro.cookies.set(key, value) is used to set a cookie's value. If provided
+ * an object it will be stringified via JSON.stringify(value). Additionally you
+ * can provide options customizing how this cookie will be set, such as setting httpOnly
+ * in order to prevent the cookie from being read in client-side JavaScript.
+ * @param key The name of the cookie to set.
+ * @param value A value, either a string or other primitive or an object.
+ * @param options Options for the cookie, such as the path and security settings.
+ */
+ set(key: string, value: string | Record<string, any>, options?: AstroCookieSetOptions): void {
+ let serializedValue: string;
+ if(typeof value === 'string') {
+ serializedValue = value;
+ } else {
+ // Support stringifying JSON objects for convenience. First check that this is
+ // a plain object and if it is, stringify. If not, allow support for toString() overrides.
+ let toStringValue = value.toString();
+ if(toStringValue === Object.prototype.toString.call(value)) {
+ serializedValue = JSON.stringify(value);
+ } else {
+ serializedValue = toStringValue;
+ }
+ }
+
+ const serializeOptions: CookieSerializeOptions = {};
+ if(options) {
+ Object.assign(serializeOptions, options);
+ }
+
+ this.#ensureOutgoingMap().set(key, [
+ serializedValue,
+ serialize(key, serializedValue, serializeOptions),
+ true
+ ]);
+ }
+
+ /**
+ * Astro.cookies.header() returns an iterator for the cookies that have previously
+ * been set by either Astro.cookies.set() or Astro.cookies.delete().
+ * This method is primarily used by adapters to set the header on outgoing responses.
+ * @returns
+ */
+ *headers(): Generator<string, void, unknown> {
+ if(this.#outgoing == null) return;
+ for(const [,value] of this.#outgoing) {
+ yield value[1];
+ }
+ }
+
+ #ensureParsed(): Record<string, string> {
+ if(!this.#requestValues) {
+ this.#parse();
+ }
+ if(!this.#requestValues) {
+ this.#requestValues = {};
+ }
+ return this.#requestValues;
+ }
+
+ #ensureOutgoingMap(): Map<string, [string, string, boolean]> {
+ if(!this.#outgoing) {
+ this.#outgoing = new Map();
+ }
+ return this.#outgoing;
+ }
+
+ #parse() {
+ const raw = this.#request.headers.get('cookie');
+ if(!raw) {
+ return;
+ }
+
+ this.#requestValues = parse(raw);
+ }
+}
+
+export {
+ AstroCookies
+};
diff --git a/packages/astro/src/core/cookies/index.ts b/packages/astro/src/core/cookies/index.ts
new file mode 100644
index 000000000..18dc3ebca
--- /dev/null
+++ b/packages/astro/src/core/cookies/index.ts
@@ -0,0 +1,9 @@
+
+export {
+ AstroCookies
+} from './cookies.js';
+
+export {
+ attachToResponse,
+ getSetCookiesFromResponse
+} from './response.js';
diff --git a/packages/astro/src/core/cookies/response.ts b/packages/astro/src/core/cookies/response.ts
new file mode 100644
index 000000000..0e52ac8cb
--- /dev/null
+++ b/packages/astro/src/core/cookies/response.ts
@@ -0,0 +1,26 @@
+import type { AstroCookies } from './cookies';
+
+const astroCookiesSymbol = Symbol.for('astro.cookies');
+
+export function attachToResponse(response: Response, cookies: AstroCookies) {
+ Reflect.set(response, astroCookiesSymbol, cookies);
+}
+
+function getFromResponse(response: Response): AstroCookies | undefined {
+ let cookies = Reflect.get(response, astroCookiesSymbol);
+ if(cookies != null) {
+ return cookies as AstroCookies;
+ } else {
+ return undefined;
+ }
+}
+
+export function * getSetCookiesFromResponse(response: Response): Generator<string, void, unknown> {
+ const cookies = getFromResponse(response);
+ if(!cookies) {
+ return;
+ }
+ for(const headerValue of cookies.headers()) {
+ yield headerValue;
+ }
+}
diff --git a/packages/astro/src/core/endpoint/index.ts b/packages/astro/src/core/endpoint/index.ts
index 7fee0c428..75e451e6f 100644
--- a/packages/astro/src/core/endpoint/index.ts
+++ b/packages/astro/src/core/endpoint/index.ts
@@ -1,6 +1,8 @@
-import type { EndpointHandler } from '../../@types/astro';
-import { renderEndpoint } from '../../runtime/server/index.js';
+import type { APIContext, EndpointHandler, Params } from '../../@types/astro';
import type { RenderOptions } from '../render/core';
+
+import { AstroCookies, attachToResponse } from '../cookies/index.js';
+import { renderEndpoint } from '../../runtime/server/index.js';
import { getParamsAndProps, GetParamsAndPropsError } from '../render/core.js';
export type EndpointOptions = Pick<
@@ -28,6 +30,14 @@ type EndpointCallResult =
response: Response;
};
+function createAPIContext(request: Request, params: Params): APIContext {
+ return {
+ cookies: new AstroCookies(request),
+ request,
+ params
+ };
+}
+
export async function call(
mod: EndpointHandler,
opts: EndpointOptions
@@ -41,9 +51,11 @@ export async function call(
}
const [params] = paramsAndPropsResp;
- const response = await renderEndpoint(mod, opts.request, params, opts.ssr);
+ const context = createAPIContext(opts.request, params);
+ const response = await renderEndpoint(mod, context, opts.ssr);
if (response instanceof Response) {
+ attachToResponse(response, context.cookies);
return {
type: 'response',
response,
diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts
index c9efe02de..7e5fe1f96 100644
--- a/packages/astro/src/core/render/core.ts
+++ b/packages/astro/src/core/render/core.ts
@@ -10,6 +10,7 @@ import type {
} from '../../@types/astro';
import type { LogOptions } from '../logger/core.js';
+import { attachToResponse } from '../cookies/index.js';
import { Fragment, renderPage } from '../../runtime/server/index.js';
import { getParams } from '../routing/params.js';
import { createResult } from './result.js';
@@ -164,5 +165,13 @@ export async function render(opts: RenderOptions): Promise<Response> {
});
}
- return await renderPage(result, Component, pageProps, null, streaming);
+ const response = await renderPage(result, Component, pageProps, null, streaming);
+
+ // If there is an Astro.cookies instance, attach it to the response so that
+ // adapters can grab the Set-Cookie headers.
+ if(result.cookies) {
+ attachToResponse(response, result.cookies);
+ }
+
+ return response;
}
diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts
index d4704ca1f..c0e650a8f 100644
--- a/packages/astro/src/core/render/result.ts
+++ b/packages/astro/src/core/render/result.ts
@@ -11,6 +11,7 @@ import type {
SSRResult,
} from '../../@types/astro';
import { renderSlot } from '../../runtime/server/index.js';
+import { AstroCookies } from '../cookies/index.js';
import { LogOptions, warn } from '../logger/core.js';
import { isScriptRequest } from './script.js';
import { isCSSRequest } from './util.js';
@@ -139,6 +140,9 @@ export function createResult(args: CreateResultArgs): SSRResult {
writable: false,
});
+ // Astro.cookies is defined lazily to avoid the cost on pages that do not use it.
+ let cookies: AstroCookies | undefined = undefined;
+
// Create the result object that will be passed into the render function.
// This object starts here as an empty shell (not yet the result) but then
// calling the render() function will populate the object with scripts, styles, etc.
@@ -146,6 +150,7 @@ export function createResult(args: CreateResultArgs): SSRResult {
styles: args.styles ?? new Set<SSRElement>(),
scripts: args.scripts ?? new Set<SSRElement>(),
links: args.links ?? new Set<SSRElement>(),
+ cookies,
/** This function returns the `Astro` faux-global */
createAstro(
astroGlobal: AstroGlobalPartial,
@@ -171,6 +176,14 @@ export function createResult(args: CreateResultArgs): SSRResult {
return Reflect.get(request, clientAddressSymbol);
},
+ get cookies() {
+ if(cookies) {
+ return cookies;
+ }
+ cookies = new AstroCookies(request);
+ result.cookies = cookies;
+ return cookies;
+ },
params,
props,
request,
diff --git a/packages/astro/src/runtime/server/endpoint.ts b/packages/astro/src/runtime/server/endpoint.ts
index 31a0069db..a93d02adb 100644
--- a/packages/astro/src/runtime/server/endpoint.ts
+++ b/packages/astro/src/runtime/server/endpoint.ts
@@ -18,12 +18,8 @@ function getHandlerFromModule(mod: EndpointHandler, method: string) {
}
/** Renders an endpoint request to completion, returning the body. */
-export async function renderEndpoint(
- mod: EndpointHandler,
- request: Request,
- params: Params,
- ssr?: boolean
-) {
+export async function renderEndpoint(mod: EndpointHandler, context: APIContext, ssr: boolean) {
+ const { request, params } = context;
const chosenMethod = request.method?.toLowerCase();
const handler = getHandlerFromModule(mod, chosenMethod);
if (!ssr && ssr === false && chosenMethod && chosenMethod !== 'get') {
@@ -56,11 +52,6 @@ export function get({ params, request }) {
Update your code to remove this warning.`);
}
- const context = {
- request,
- params,
- };
-
const proxy = new Proxy(context, {
get(target, prop) {
if (prop in target) {
diff --git a/packages/astro/src/vite-plugin-astro-server/index.ts b/packages/astro/src/vite-plugin-astro-server/index.ts
index 72f2ce9ba..e976280c8 100644
--- a/packages/astro/src/vite-plugin-astro-server/index.ts
+++ b/packages/astro/src/vite-plugin-astro-server/index.ts
@@ -6,6 +6,7 @@ import type { SSROptions } from '../core/render/dev/index';
import { Readable } from 'stream';
import { call as callEndpoint } from '../core/endpoint/dev/index.js';
+import { getSetCookiesFromResponse } from '../core/cookies/index.js';
import {
collectErrorMetadata,
ErrorWithMetadata,
@@ -62,6 +63,11 @@ async function writeWebResponse(res: http.ServerResponse, webResponse: Response)
_headers = Object.fromEntries(headers.entries());
}
+ // Attach any set-cookie headers added via Astro.cookies.set()
+ const setCookieHeaders = Array.from(getSetCookiesFromResponse(webResponse));
+ if(setCookieHeaders.length) {
+ res.setHeader('Set-Cookie', setCookieHeaders);
+ }
res.writeHead(status, _headers);
if (body) {
if (Symbol.for('astro.responseBody') in webResponse) {