astro 4.13.2 → 4.13.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1842,25 +1842,29 @@ export interface AstroUserConfig {
1842
1842
  * @version 4.10.0
1843
1843
  * @description
1844
1844
  *
1845
- * Enables experimental `astro:env` features .
1845
+ * Enables experimental `astro:env` features.
1846
1846
  *
1847
1847
  * The `astro:env` API lets you configure a type-safe schema for your environment variables, and indicate whether they should be available on the server or the client. Import and use your defined variables from the appropriate `/client` or `/server` module:
1848
1848
  *
1849
1849
  * ```astro
1850
1850
  * ---
1851
- * import { APP_ID } from "astro:env/client"
1852
- * import { API_URL, API_TOKEN, getSecret } from "astro:env/server"
1853
- * const NODE_ENV = getSecret("NODE_ENV")
1851
+ * import { API_URL } from "astro:env/client"
1852
+ * import { API_SECRET_TOKEN } from "astro:env/server"
1854
1853
  *
1855
1854
  * const data = await fetch(`${API_URL}/users`, {
1856
- * method: "POST",
1855
+ * method: "GET",
1857
1856
  * headers: {
1858
1857
  * "Content-Type": "application/json",
1859
- * "Authorization": `Bearer ${API_TOKEN}`
1858
+ * "Authorization": `Bearer ${API_SECRET_TOKEN}`
1860
1859
  * },
1861
- * body: JSON.stringify({ appId: APP_ID, nodeEnv: NODE_ENV })
1862
1860
  * })
1863
1861
  * ---
1862
+ *
1863
+ * <script>
1864
+ * import { API_URL } from "astro:env/client"
1865
+ *
1866
+ * fetch(`${API_URL}/ping`)
1867
+ * </script>
1864
1868
  * ```
1865
1869
  *
1866
1870
  * To define the data type and properties of your environment variables, declare a schema in your Astro config in `experimental.env.schema`. The `envField` helper allows you define your variable as a string, number, or boolean and pass properties in an object:
@@ -1898,7 +1902,7 @@ export interface AstroUserConfig {
1898
1902
  * import { PORT } from "astro:env/server"
1899
1903
  * ```
1900
1904
  *
1901
- * - **Secret server variables**: These variables are not part of your final bundle and can be accessed on the server through the `astro:env/server` module. The `getSecret()` helper function can be used to retrieve secrets not specified in the schema:
1905
+ * - **Secret server variables**: These variables are not part of your final bundle and can be accessed on the server through the `astro:env/server` module. The `getSecret()` helper function can be used to retrieve secrets not specified in the schema. Its implementation is provided by your adapter and defaults to `process.env`:
1902
1906
  *
1903
1907
  * ```js
1904
1908
  * import { API_SECRET, getSecret } from "astro:env/server"
@@ -2163,7 +2167,7 @@ export type GetDataEntryInfoReturnType = {
2163
2167
  };
2164
2168
  export interface AstroAdapterFeatures {
2165
2169
  /**
2166
- * Creates and edge function that will communiate with the Astro middleware
2170
+ * Creates an edge function that will communiate with the Astro middleware
2167
2171
  */
2168
2172
  edgeMiddleware: boolean;
2169
2173
  /**
@@ -2956,6 +2960,7 @@ export interface SSRResult {
2956
2960
  cookies: AstroCookies | undefined;
2957
2961
  serverIslandNameMap: Map<string, string>;
2958
2962
  trailingSlash: AstroConfig['trailingSlash'];
2963
+ key: Promise<CryptoKey>;
2959
2964
  _metadata: SSRMetadata;
2960
2965
  }
2961
2966
  /**
@@ -4,3 +4,8 @@ export declare const ACTIONS_TYPES_FILE = "actions.d.ts";
4
4
  export declare const VIRTUAL_INTERNAL_MODULE_ID = "astro:internal-actions";
5
5
  export declare const RESOLVED_VIRTUAL_INTERNAL_MODULE_ID = "\0astro:internal-actions";
6
6
  export declare const NOOP_ACTIONS = "\0noop-actions";
7
+ export declare const ACTION_QUERY_PARAMS: {
8
+ actionName: string;
9
+ actionPayload: string;
10
+ actionRedirect: string;
11
+ };
@@ -4,8 +4,14 @@ const ACTIONS_TYPES_FILE = "actions.d.ts";
4
4
  const VIRTUAL_INTERNAL_MODULE_ID = "astro:internal-actions";
5
5
  const RESOLVED_VIRTUAL_INTERNAL_MODULE_ID = "\0astro:internal-actions";
6
6
  const NOOP_ACTIONS = "\0noop-actions";
7
+ const ACTION_QUERY_PARAMS = {
8
+ actionName: "_astroAction",
9
+ actionPayload: "_astroActionPayload",
10
+ actionRedirect: "_astroActionRedirect"
11
+ };
7
12
  export {
8
13
  ACTIONS_TYPES_FILE,
14
+ ACTION_QUERY_PARAMS,
9
15
  NOOP_ACTIONS,
10
16
  RESOLVED_VIRTUAL_INTERNAL_MODULE_ID,
11
17
  RESOLVED_VIRTUAL_MODULE_ID,
@@ -1,8 +1,9 @@
1
1
  import { type SerializedActionResult } from './virtual/shared.js';
2
+ export type ActionPayload = {
3
+ actionResult: SerializedActionResult;
4
+ actionName: string;
5
+ };
2
6
  export type Locals = {
3
- _actionsInternal: {
4
- actionResult: SerializedActionResult;
5
- actionName: string;
6
- };
7
+ _actionPayload: ActionPayload;
7
8
  };
8
9
  export declare const onRequest: import("../../@types/astro.js").MiddlewareHandler;
@@ -2,6 +2,7 @@ import { yellow } from "kleur/colors";
2
2
  import { ActionQueryStringInvalidError } from "../../core/errors/errors-data.js";
3
3
  import { AstroError } from "../../core/errors/errors.js";
4
4
  import { defineMiddleware } from "../../core/middleware/index.js";
5
+ import { ACTION_QUERY_PARAMS } from "../consts.js";
5
6
  import { formContentTypes, hasContentType } from "./utils.js";
6
7
  import { getAction } from "./virtual/get-action.js";
7
8
  import {
@@ -10,7 +11,14 @@ import {
10
11
  const onRequest = defineMiddleware(async (context, next) => {
11
12
  const locals = context.locals;
12
13
  const { request } = context;
13
- if (locals._actionsInternal) return next();
14
+ if (locals._actionPayload) return next();
15
+ const actionPayload = context.cookies.get(ACTION_QUERY_PARAMS.actionPayload)?.json();
16
+ if (actionPayload) {
17
+ if (!isActionPayload(actionPayload)) {
18
+ throw new Error("Internal: Invalid action payload in cookie.");
19
+ }
20
+ return renderResult({ context, next, ...actionPayload });
21
+ }
14
22
  if (import.meta.env.DEV && request.method === "POST" && request.body === null) {
15
23
  console.warn(
16
24
  yellow("[astro:actions]"),
@@ -18,7 +26,7 @@ const onRequest = defineMiddleware(async (context, next) => {
18
26
  );
19
27
  return next();
20
28
  }
21
- const actionName = context.url.searchParams.get("_astroAction");
29
+ const actionName = context.url.searchParams.get(ACTION_QUERY_PARAMS.actionName);
22
30
  if (context.request.method === "POST" && actionName) {
23
31
  return handlePost({ context, next, actionName });
24
32
  }
@@ -27,6 +35,25 @@ const onRequest = defineMiddleware(async (context, next) => {
27
35
  }
28
36
  return next();
29
37
  });
38
+ async function renderResult({
39
+ context,
40
+ next,
41
+ actionResult,
42
+ actionName
43
+ }) {
44
+ const locals = context.locals;
45
+ locals._actionPayload = { actionResult, actionName };
46
+ const response = await next();
47
+ context.cookies.delete(ACTION_QUERY_PARAMS.actionPayload);
48
+ if (actionResult.type === "error") {
49
+ return new Response(response.body, {
50
+ status: actionResult.status,
51
+ statusText: actionResult.type,
52
+ headers: response.headers
53
+ });
54
+ }
55
+ return response;
56
+ }
30
57
  async function handlePost({
31
58
  context,
32
59
  next,
@@ -47,28 +74,33 @@ async function handlePost({
47
74
  }
48
75
  const action = baseAction.bind(context);
49
76
  const actionResult = await action(formData);
50
- return handleResult({ context, next, actionName, actionResult });
77
+ if (context.url.searchParams.get(ACTION_QUERY_PARAMS.actionRedirect) === "false") {
78
+ return renderResult({
79
+ context,
80
+ next,
81
+ actionName,
82
+ actionResult: serializeActionResult(actionResult)
83
+ });
84
+ }
85
+ return redirectWithResult({ context, actionName, actionResult });
51
86
  }
52
- async function handleResult({
87
+ async function redirectWithResult({
53
88
  context,
54
- next,
55
89
  actionName,
56
90
  actionResult
57
91
  }) {
58
- const locals = context.locals;
59
- locals._actionsInternal = {
92
+ context.cookies.set(ACTION_QUERY_PARAMS.actionPayload, {
60
93
  actionName,
61
94
  actionResult: serializeActionResult(actionResult)
62
- };
63
- const response = await next();
95
+ });
64
96
  if (actionResult.error) {
65
- return new Response(response.body, {
66
- status: actionResult.error.status,
67
- statusText: actionResult.error.type,
68
- headers: response.headers
69
- });
97
+ const referer = context.request.headers.get("Referer");
98
+ if (!referer) {
99
+ throw new Error("Internal: Referer unexpectedly missing from Action POST request.");
100
+ }
101
+ return context.redirect(referer);
70
102
  }
71
- return response;
103
+ return context.redirect(context.url.pathname);
72
104
  }
73
105
  async function handlePostLegacy({ context, next }) {
74
106
  const { request } = context;
@@ -79,7 +111,7 @@ async function handlePostLegacy({ context, next }) {
79
111
  formData = await request.clone().formData();
80
112
  }
81
113
  if (!formData) return next();
82
- const actionName = formData.get("_astroAction");
114
+ const actionName = formData.get(ACTION_QUERY_PARAMS.actionName);
83
115
  if (!actionName) return next();
84
116
  const baseAction = await getAction(actionName);
85
117
  if (!baseAction) {
@@ -90,7 +122,13 @@ async function handlePostLegacy({ context, next }) {
90
122
  }
91
123
  const action = baseAction.bind(context);
92
124
  const actionResult = await action(formData);
93
- return handleResult({ context, next, actionName, actionResult });
125
+ return redirectWithResult({ context, actionName, actionResult });
126
+ }
127
+ function isActionPayload(json) {
128
+ if (typeof json !== "object" || json == null) return false;
129
+ if (!("actionResult" in json) || typeof json.actionResult !== "object") return false;
130
+ if (!("actionName" in json) || typeof json.actionName !== "string") return false;
131
+ return true;
94
132
  }
95
133
  export {
96
134
  onRequest
@@ -10,7 +10,7 @@ export type ActionReturnType<T extends ActionHandler<any, any>> = Awaited<Return
10
10
  export type ActionClient<TOutput, TAccept extends ActionAccept | undefined, TInputSchema extends ActionInputSchema<TAccept> | undefined> = TInputSchema extends z.ZodType ? ((input: TAccept extends 'form' ? FormData : z.input<TInputSchema>) => Promise<SafeResult<z.input<TInputSchema> extends ErrorInferenceObject ? z.input<TInputSchema> : ErrorInferenceObject, Awaited<TOutput>>>) & {
11
11
  queryString: string;
12
12
  orThrow: (input: TAccept extends 'form' ? FormData : z.input<TInputSchema>) => Promise<Awaited<TOutput>>;
13
- } : (input?: any) => Promise<SafeResult<never, Awaited<TOutput>>> & {
13
+ } : ((input?: any) => Promise<SafeResult<never, Awaited<TOutput>>>) & {
14
14
  orThrow: (input?: any) => Promise<Awaited<TOutput>>;
15
15
  };
16
16
  export declare function defineAction<TOutput, TAccept extends ActionAccept | undefined = undefined, TInputSchema extends ActionInputSchema<ActionAccept> | undefined = TAccept extends 'form' ? z.ZodType<FormData> : undefined>({ accept, input: inputSchema, handler, }: {
@@ -62,11 +62,17 @@ function formDataToObject(formData, schema) {
62
62
  const obj = {};
63
63
  for (const [key, baseValidator] of Object.entries(schema.shape)) {
64
64
  let validator = baseValidator;
65
- while (validator instanceof z.ZodOptional || validator instanceof z.ZodNullable) {
65
+ while (validator instanceof z.ZodOptional || validator instanceof z.ZodNullable || validator instanceof z.ZodDefault) {
66
+ if (validator instanceof z.ZodDefault && !formData.has(key)) {
67
+ obj[key] = validator._def.defaultValue();
68
+ }
66
69
  validator = validator._def.innerType;
67
70
  }
68
- if (validator instanceof z.ZodBoolean) {
69
- obj[key] = formData.has(key);
71
+ if (!formData.has(key) && key in obj) {
72
+ continue;
73
+ } else if (validator instanceof z.ZodBoolean) {
74
+ const val = formData.get(key);
75
+ obj[key] = val === "true" ? true : val === "false" ? false : formData.has(key);
70
76
  } else if (validator instanceof z.ZodArray) {
71
77
  obj[key] = handleFormDataGetAll(key, formData, validator);
72
78
  } else {
@@ -1,5 +1,10 @@
1
1
  import type { z } from 'zod';
2
2
  import type { ErrorInferenceObject, MaybePromise } from '../utils.js';
3
+ export declare const ACTION_QUERY_PARAMS: {
4
+ actionName: string;
5
+ actionPayload: string;
6
+ actionRedirect: string;
7
+ };
3
8
  export declare const ACTION_ERROR_CODES: readonly ["BAD_REQUEST", "UNAUTHORIZED", "FORBIDDEN", "NOT_FOUND", "TIMEOUT", "CONFLICT", "PRECONDITION_FAILED", "PAYLOAD_TOO_LARGE", "UNSUPPORTED_MEDIA_TYPE", "UNPROCESSABLE_CONTENT", "TOO_MANY_REQUESTS", "CLIENT_CLOSED_REQUEST", "INTERNAL_SERVER_ERROR"];
4
9
  export type ActionErrorCode = (typeof ACTION_ERROR_CODES)[number];
5
10
  export declare class ActionError<T extends ErrorInferenceObject = ErrorInferenceObject> extends Error {
@@ -1,4 +1,6 @@
1
1
  import { parse as devalueParse, stringify as devalueStringify } from "devalue";
2
+ import { ACTION_QUERY_PARAMS as _ACTION_QUERY_PARAMS } from "../../consts.js";
3
+ const ACTION_QUERY_PARAMS = _ACTION_QUERY_PARAMS;
2
4
  const ACTION_ERROR_CODES = [
3
5
  "BAD_REQUEST",
4
6
  "UNAUTHORIZED",
@@ -113,7 +115,7 @@ async function callSafely(handler) {
113
115
  }
114
116
  }
115
117
  function getActionQueryString(name) {
116
- const searchParams = new URLSearchParams({ _astroAction: name });
118
+ const searchParams = new URLSearchParams({ [_ACTION_QUERY_PARAMS.actionName]: name });
117
119
  return `?${searchParams.toString()}`;
118
120
  }
119
121
  function getActionProps(action) {
@@ -130,14 +132,16 @@ function getActionProps(action) {
130
132
  }
131
133
  function serializeActionResult(res) {
132
134
  if (res.error) {
135
+ if (import.meta.env?.DEV) {
136
+ actionResultErrorStack.set(res.error.stack);
137
+ }
133
138
  return {
134
139
  type: "error",
135
140
  status: res.error.status,
136
141
  contentType: "application/json",
137
142
  body: JSON.stringify({
138
143
  ...res.error,
139
- message: res.error.message,
140
- stack: import.meta.env.PROD ? void 0 : res.error.stack
144
+ message: res.error.message
141
145
  })
142
146
  };
143
147
  }
@@ -159,7 +163,16 @@ function serializeActionResult(res) {
159
163
  }
160
164
  function deserializeActionResult(res) {
161
165
  if (res.type === "error") {
162
- return { error: ActionError.fromJson(JSON.parse(res.body)), data: void 0 };
166
+ if (import.meta.env?.PROD) {
167
+ return { error: ActionError.fromJson(JSON.parse(res.body)), data: void 0 };
168
+ } else {
169
+ const error = ActionError.fromJson(JSON.parse(res.body));
170
+ error.stack = actionResultErrorStack.get();
171
+ return {
172
+ error,
173
+ data: void 0
174
+ };
175
+ }
163
176
  }
164
177
  if (res.type === "empty") {
165
178
  return { data: void 0, error: void 0 };
@@ -171,8 +184,20 @@ function deserializeActionResult(res) {
171
184
  error: void 0
172
185
  };
173
186
  }
187
+ const actionResultErrorStack = /* @__PURE__ */ function actionResultErrorStackFn() {
188
+ let errorStack;
189
+ return {
190
+ set(stack) {
191
+ errorStack = stack;
192
+ },
193
+ get() {
194
+ return errorStack;
195
+ }
196
+ };
197
+ }();
174
198
  export {
175
199
  ACTION_ERROR_CODES,
200
+ ACTION_QUERY_PARAMS,
176
201
  ActionError,
177
202
  ActionInputError,
178
203
  callSafely,
@@ -1,6 +1,6 @@
1
1
  import type { APIContext } from '../@types/astro.js';
2
2
  import type { Locals } from './runtime/middleware.js';
3
3
  import { type ActionAPIContext } from './runtime/utils.js';
4
- export declare function hasActionsInternal(locals: APIContext['locals']): locals is Locals;
4
+ export declare function hasActionPayload(locals: APIContext['locals']): locals is Locals;
5
5
  export declare function createGetActionResult(locals: APIContext['locals']): APIContext['getActionResult'];
6
6
  export declare function createCallAction(context: ActionAPIContext): APIContext['callAction'];
@@ -1,14 +1,14 @@
1
1
  import {} from "./runtime/utils.js";
2
2
  import { deserializeActionResult, getActionQueryString } from "./runtime/virtual/shared.js";
3
- function hasActionsInternal(locals) {
4
- return "_actionsInternal" in locals;
3
+ function hasActionPayload(locals) {
4
+ return "_actionPayload" in locals;
5
5
  }
6
6
  function createGetActionResult(locals) {
7
7
  return (actionFn) => {
8
- if (!hasActionsInternal(locals) || actionFn.toString() !== getActionQueryString(locals._actionsInternal.actionName)) {
8
+ if (!hasActionPayload(locals) || actionFn.toString() !== getActionQueryString(locals._actionPayload.actionName)) {
9
9
  return void 0;
10
10
  }
11
- return deserializeActionResult(locals._actionsInternal.actionResult);
11
+ return deserializeActionResult(locals._actionPayload.actionResult);
12
12
  };
13
13
  }
14
14
  function createCallAction(context) {
@@ -20,5 +20,5 @@ function createCallAction(context) {
20
20
  export {
21
21
  createCallAction,
22
22
  createGetActionResult,
23
- hasActionsInternal
23
+ hasActionPayload
24
24
  };
@@ -2,6 +2,7 @@ import { posix } from "node:path";
2
2
  import { getDefaultClientDirectives } from "../core/client-directive/index.js";
3
3
  import { ASTRO_CONFIG_DEFAULTS } from "../core/config/schema.js";
4
4
  import { validateConfig } from "../core/config/validate.js";
5
+ import { createKey } from "../core/encryption.js";
5
6
  import { Logger } from "../core/logger/core.js";
6
7
  import { nodeLogDestination } from "../core/logger/node.js";
7
8
  import { removeLeadingForwardSlash } from "../core/path.js";
@@ -31,7 +32,8 @@ function createManifest(manifest, renderers, middleware) {
31
32
  i18n: manifest?.i18n,
32
33
  checkOrigin: false,
33
34
  middleware: manifest?.middleware ?? middleware ?? defaultMiddleware,
34
- experimentalEnvGetSecretEnabled: false
35
+ experimentalEnvGetSecretEnabled: false,
36
+ key: createKey()
35
37
  };
36
38
  }
37
39
  class experimental_AstroContainer {
@@ -1,3 +1,4 @@
1
+ import { decodeKey } from "../encryption.js";
1
2
  import { deserializeRouteData } from "../routing/manifest/serialization.js";
2
3
  function deserializeManifest(serializedManifest) {
3
4
  const routes = [];
@@ -14,6 +15,7 @@ function deserializeManifest(serializedManifest) {
14
15
  const inlinedScripts = new Map(serializedManifest.inlinedScripts);
15
16
  const clientDirectives = new Map(serializedManifest.clientDirectives);
16
17
  const serverIslandNameMap = new Map(serializedManifest.serverIslandNameMap);
18
+ const key = decodeKey(serializedManifest.key);
17
19
  return {
18
20
  // in case user middleware exists, this no-op middleware will be reassigned (see plugin-ssr.ts)
19
21
  middleware(_, next) {
@@ -25,7 +27,8 @@ function deserializeManifest(serializedManifest) {
25
27
  inlinedScripts,
26
28
  clientDirectives,
27
29
  routes,
28
- serverIslandNameMap
30
+ serverIslandNameMap,
31
+ key
29
32
  };
30
33
  }
31
34
  export {
@@ -298,9 +298,11 @@ class App {
298
298
  `${this.#baseWithoutTrailingSlash}/${status}${maybeDotHtml}`,
299
299
  url
300
300
  );
301
- const response2 = await fetch(statusURL.toString());
302
- const override = { status };
303
- return this.#mergeResponses(response2, originalResponse, override);
301
+ if (statusURL.toString() !== request.url) {
302
+ const response2 = await fetch(statusURL.toString());
303
+ const override = { status };
304
+ return this.#mergeResponses(response2, originalResponse, override);
305
+ }
304
306
  }
305
307
  const mod = await this.#pipeline.getModuleForRoute(errorRouteData);
306
308
  try {
@@ -52,6 +52,7 @@ export type SSRManifest = {
52
52
  pageMap?: Map<ComponentPath, ImportComponentInstance>;
53
53
  serverIslandMap?: Map<string, () => Promise<ComponentInstance>>;
54
54
  serverIslandNameMap?: Map<string, string>;
55
+ key: Promise<CryptoKey>;
55
56
  i18n: SSRManifestI18n | undefined;
56
57
  middleware: MiddlewareHandler;
57
58
  checkOrigin: boolean;
@@ -64,11 +65,12 @@ export type SSRManifestI18n = {
64
65
  defaultLocale: string;
65
66
  domainLookupTable: Record<string, string>;
66
67
  };
67
- export type SerializedSSRManifest = Omit<SSRManifest, 'middleware' | 'routes' | 'assets' | 'componentMetadata' | 'inlinedScripts' | 'clientDirectives' | 'serverIslandNameMap'> & {
68
+ export type SerializedSSRManifest = Omit<SSRManifest, 'middleware' | 'routes' | 'assets' | 'componentMetadata' | 'inlinedScripts' | 'clientDirectives' | 'serverIslandNameMap' | 'key'> & {
68
69
  routes: SerializedRouteInfo[];
69
70
  assets: string[];
70
71
  componentMetadata: [string, SSRComponentMetadata][];
71
72
  inlinedScripts: [string, string][];
72
73
  clientDirectives: [string, string][];
73
74
  serverIslandNameMap: [string, string][];
75
+ key: string;
74
76
  };
@@ -54,7 +54,8 @@ async function generatePages(options, internals) {
54
54
  options.settings,
55
55
  internals,
56
56
  renderers.renderers,
57
- middleware
57
+ middleware,
58
+ options.key
58
59
  );
59
60
  }
60
61
  const pipeline = BuildPipeline.create({ internals, manifest, options });
@@ -358,7 +359,7 @@ function getPrettyRouteName(route) {
358
359
  return route.component;
359
360
  }
360
361
  }
361
- function createBuildManifest(settings, internals, renderers, middleware) {
362
+ function createBuildManifest(settings, internals, renderers, middleware, key) {
362
363
  let i18nManifest = void 0;
363
364
  if (settings.config.i18n) {
364
365
  i18nManifest = {
@@ -388,6 +389,7 @@ function createBuildManifest(settings, internals, renderers, middleware) {
388
389
  buildFormat: settings.config.build.format,
389
390
  middleware,
390
391
  checkOrigin: settings.config.security?.checkOrigin ?? false,
392
+ key,
391
393
  experimentalEnvGetSecretEnabled: false
392
394
  };
393
395
  }
@@ -15,6 +15,7 @@ import { resolveConfig } from "../config/config.js";
15
15
  import { createNodeLogger } from "../config/logging.js";
16
16
  import { createSettings } from "../config/settings.js";
17
17
  import { createVite } from "../create-vite.js";
18
+ import { createKey } from "../encryption.js";
18
19
  import { levels, timerMessage } from "../logger/core.js";
19
20
  import { apply as applyPolyfill } from "../polyfill.js";
20
21
  import { createRouteManifest } from "../routing/index.js";
@@ -135,7 +136,8 @@ class AstroBuilder {
135
136
  origin: this.origin,
136
137
  pageNames,
137
138
  teardownCompiler: this.teardownCompiler,
138
- viteConfig
139
+ viteConfig,
140
+ key: createKey()
139
141
  };
140
142
  const { internals, ssrOutputChunkNames, contentFileNames } = await viteBuild(opts);
141
143
  await staticBuild(opts, internals, ssrOutputChunkNames, contentFileNames);
@@ -5,6 +5,7 @@ import { normalizeTheLocale } from "../../../i18n/index.js";
5
5
  import { toRoutingStrategy } from "../../../i18n/utils.js";
6
6
  import { runHookBuildSsr } from "../../../integrations/hooks.js";
7
7
  import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from "../../../vite-plugin-scripts/index.js";
8
+ import { encodeKey } from "../../encryption.js";
8
9
  import { fileExtension, joinPaths, prependForwardSlash } from "../../path.js";
9
10
  import { serializeRouteData } from "../../routing/index.js";
10
11
  import { addRollupInput } from "../add-rollup-input.js";
@@ -103,7 +104,8 @@ async function createManifest(buildOpts, internals) {
103
104
  internals.staticFiles.add(file);
104
105
  }
105
106
  const staticFiles = internals.staticFiles;
106
- return buildManifest(buildOpts, internals, Array.from(staticFiles));
107
+ const encodedKey = await encodeKey(await buildOpts.key);
108
+ return buildManifest(buildOpts, internals, Array.from(staticFiles), encodedKey);
107
109
  }
108
110
  function injectManifest(manifest, chunk) {
109
111
  const code = chunk.code;
@@ -111,7 +113,7 @@ function injectManifest(manifest, chunk) {
111
113
  return JSON.stringify(manifest);
112
114
  });
113
115
  }
114
- function buildManifest(opts, internals, staticFiles) {
116
+ function buildManifest(opts, internals, staticFiles, encodedKey) {
115
117
  const { settings } = opts;
116
118
  const routes = [];
117
119
  const domainLookupTable = {};
@@ -214,6 +216,7 @@ function buildManifest(opts, internals, staticFiles) {
214
216
  buildFormat: settings.config.build.format,
215
217
  checkOrigin: settings.config.security?.checkOrigin ?? false,
216
218
  serverIslandNameMap: Array.from(settings.serverIslandNameMap),
219
+ key: encodedKey,
217
220
  experimentalEnvGetSecretEnabled: settings.config.experimental.env !== void 0 && (settings.adapter?.supportedAstroFeatures.envGetSecret ?? "unsupported") !== "unsupported"
218
221
  };
219
222
  }
@@ -24,6 +24,10 @@ function vitePluginSSR(internals, adapter, options) {
24
24
  }
25
25
  inputs.add(getVirtualModulePageName(ASTRO_PAGE_MODULE_ID, pageData.component));
26
26
  }
27
+ const adapterServerEntrypoint = options.settings.adapter?.serverEntrypoint;
28
+ if (adapterServerEntrypoint) {
29
+ inputs.add(adapterServerEntrypoint);
30
+ }
27
31
  inputs.add(SSR_VIRTUAL_MODULE_ID);
28
32
  return addRollupInput(opts, Array.from(inputs));
29
33
  },
@@ -195,8 +199,8 @@ function generateSSRCode(settings, adapter, middlewareId) {
195
199
  const pageMap = isFunctionPerRouteEnabled(adapter) ? "pageModule" : "pageMap";
196
200
  const imports = [
197
201
  `import { renderers } from '${RENDERERS_MODULE_ID}';`,
198
- `import { manifest as defaultManifest } from '${SSR_MANIFEST_VIRTUAL_MODULE_ID}';`,
199
202
  `import * as serverEntrypointModule from '${adapter.serverEntrypoint}';`,
203
+ `import { manifest as defaultManifest } from '${SSR_MANIFEST_VIRTUAL_MODULE_ID}';`,
200
204
  edgeMiddleware ? `` : `import { onRequest as middleware } from '${middlewareId}';`,
201
205
  settings.config.experimental.serverIslands ? `import { serverIslandMap } from '${VIRTUAL_ISLAND_MAP_ID}';` : ""
202
206
  ];
@@ -199,6 +199,8 @@ async function ssrBuild(opts, internals, input, container, logger) {
199
199
  return "renderers.mjs";
200
200
  } else if (chunkInfo.facadeModuleId === RESOLVED_SSR_MANIFEST_VIRTUAL_MODULE_ID) {
201
201
  return "manifest_[hash].mjs";
202
+ } else if (chunkInfo.facadeModuleId === settings.adapter?.serverEntrypoint) {
203
+ return "adapter_[hash].mjs";
202
204
  } else if (settings.config.experimental.contentCollectionCache && chunkInfo.facadeModuleId && hasAnyContentFlag(chunkInfo.facadeModuleId)) {
203
205
  const moduleId = reverseSymlink({
204
206
  symlinks,
@@ -39,6 +39,7 @@ export interface StaticBuildOptions {
39
39
  pageNames: string[];
40
40
  viteConfig: InlineConfig;
41
41
  teardownCompiler: boolean;
42
+ key: Promise<CryptoKey>;
42
43
  }
43
44
  type ImportComponentInstance = () => Promise<ComponentInstance>;
44
45
  export interface SinglePageBuiltModule {
@@ -1,4 +1,4 @@
1
- const ASTRO_VERSION = "4.13.2";
1
+ const ASTRO_VERSION = "4.13.4";
2
2
  const REROUTE_DIRECTIVE_HEADER = "X-Astro-Reroute";
3
3
  const REWRITE_DIRECTIVE_HEADER_KEY = "X-Astro-Rewrite";
4
4
  const REWRITE_DIRECTIVE_HEADER_VALUE = "yes";
@@ -19,7 +19,7 @@ async function dev(inlineConfig) {
19
19
  await telemetry.record([]);
20
20
  const restart = await createContainerWithAutomaticRestart({ inlineConfig, fs });
21
21
  const logger = restart.container.logger;
22
- const currentVersion = "4.13.2";
22
+ const currentVersion = "4.13.4";
23
23
  const isPrerelease = currentVersion.includes("-");
24
24
  if (!isPrerelease) {
25
25
  try {
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Creates a CryptoKey object that can be used to encrypt any string.
3
+ */
4
+ export declare function createKey(): Promise<CryptoKey>;
5
+ /**
6
+ * Takes a key that has been serialized to an array of bytes and returns a CryptoKey
7
+ */
8
+ export declare function importKey(bytes: Uint8Array): Promise<CryptoKey>;
9
+ /**
10
+ * Encodes a CryptoKey to base64 string, so that it can be embedded in JSON / JavaScript
11
+ */
12
+ export declare function encodeKey(key: CryptoKey): Promise<string>;
13
+ /**
14
+ * Decodes a base64 string into bytes and then imports the key.
15
+ */
16
+ export declare function decodeKey(encoded: string): Promise<CryptoKey>;
17
+ /**
18
+ * Using a CryptoKey, encrypt a string into a base64 string.
19
+ */
20
+ export declare function encryptString(key: CryptoKey, raw: string): Promise<string>;
21
+ /**
22
+ * Takes a base64 encoded string, decodes it and returns the decrypted text.
23
+ */
24
+ export declare function decryptString(key: CryptoKey, encoded: string): Promise<string>;
@@ -0,0 +1,64 @@
1
+ import { decodeBase64, decodeHex, encodeBase64, encodeHexUpperCase } from "@oslojs/encoding";
2
+ const ALGORITHM = "AES-GCM";
3
+ async function createKey() {
4
+ const key = await crypto.subtle.generateKey(
5
+ {
6
+ name: ALGORITHM,
7
+ length: 256
8
+ },
9
+ true,
10
+ ["encrypt", "decrypt"]
11
+ );
12
+ return key;
13
+ }
14
+ async function importKey(bytes) {
15
+ const key = await crypto.subtle.importKey("raw", bytes, ALGORITHM, true, ["encrypt", "decrypt"]);
16
+ return key;
17
+ }
18
+ async function encodeKey(key) {
19
+ const exported = await crypto.subtle.exportKey("raw", key);
20
+ const encodedKey = encodeBase64(new Uint8Array(exported));
21
+ return encodedKey;
22
+ }
23
+ async function decodeKey(encoded) {
24
+ const bytes = decodeBase64(encoded);
25
+ return crypto.subtle.importKey("raw", bytes, ALGORITHM, true, ["encrypt", "decrypt"]);
26
+ }
27
+ const encoder = new TextEncoder();
28
+ const decoder = new TextDecoder();
29
+ const IV_LENGTH = 24;
30
+ async function encryptString(key, raw) {
31
+ const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH / 2));
32
+ const data = encoder.encode(raw);
33
+ const buffer = await crypto.subtle.encrypt(
34
+ {
35
+ name: ALGORITHM,
36
+ iv
37
+ },
38
+ key,
39
+ data
40
+ );
41
+ return encodeHexUpperCase(iv) + encodeBase64(new Uint8Array(buffer));
42
+ }
43
+ async function decryptString(key, encoded) {
44
+ const iv = decodeHex(encoded.slice(0, IV_LENGTH));
45
+ const dataArray = decodeBase64(encoded.slice(IV_LENGTH));
46
+ const decryptedBuffer = await crypto.subtle.decrypt(
47
+ {
48
+ name: ALGORITHM,
49
+ iv
50
+ },
51
+ key,
52
+ dataArray
53
+ );
54
+ const decryptedString = decoder.decode(decryptedBuffer);
55
+ return decryptedString;
56
+ }
57
+ export {
58
+ createKey,
59
+ decodeKey,
60
+ decryptString,
61
+ encodeKey,
62
+ encryptString,
63
+ importKey
64
+ };
@@ -38,7 +38,7 @@ function serverStart({
38
38
  host,
39
39
  base
40
40
  }) {
41
- const version = "4.13.2";
41
+ const version = "4.13.4";
42
42
  const localPrefix = `${dim("\u2503")} Local `;
43
43
  const networkPrefix = `${dim("\u2503")} Network `;
44
44
  const emptyPrefix = " ".repeat(11);
@@ -270,7 +270,7 @@ function printHelp({
270
270
  message.push(
271
271
  linebreak(),
272
272
  ` ${bgGreen(black(` ${commandName} `))} ${green(
273
- `v${"4.13.2"}`
273
+ `v${"4.13.4"}`
274
274
  )} ${headline}`
275
275
  );
276
276
  }
@@ -1,5 +1,5 @@
1
1
  import { deserializeActionResult } from "../actions/runtime/virtual/shared.js";
2
- import { createCallAction, createGetActionResult, hasActionsInternal } from "../actions/utils.js";
2
+ import { createCallAction, createGetActionResult, hasActionPayload } from "../actions/utils.js";
3
3
  import {
4
4
  computeCurrentLocale,
5
5
  computePreferredLocale,
@@ -256,7 +256,7 @@ class RenderContext {
256
256
  throw new AstroError(AstroErrorData.AstroResponseHeadersReassigned);
257
257
  }
258
258
  };
259
- const actionResult = hasActionsInternal(this.locals) ? deserializeActionResult(this.locals._actionsInternal.actionResult) : void 0;
259
+ const actionResult = hasActionPayload(this.locals) ? deserializeActionResult(this.locals._actionPayload.actionResult) : void 0;
260
260
  const result = {
261
261
  base: manifest.base,
262
262
  cancelled: false,
@@ -279,6 +279,7 @@ class RenderContext {
279
279
  styles,
280
280
  actionResult,
281
281
  serverIslandNameMap: manifest.serverIslandNameMap ?? /* @__PURE__ */ new Map(),
282
+ key: manifest.key,
282
283
  trailingSlash: manifest.trailingSlash,
283
284
  _metadata: {
284
285
  hasHydrationScript: false,
@@ -3,6 +3,7 @@ import {
3
3
  renderTemplate
4
4
  } from "../../runtime/server/index.js";
5
5
  import { createSlotValueFromString } from "../../runtime/server/render/slot.js";
6
+ import { decryptString } from "../encryption.js";
6
7
  import { getPattern } from "../routing/manifest/pattern.js";
7
8
  const SERVER_ISLAND_ROUTE = "/_server-islands/[name]";
8
9
  const SERVER_ISLAND_COMPONENT = "_server-islands.astro";
@@ -51,7 +52,10 @@ function createEndpoint(manifest) {
51
52
  statusText: "Not found"
52
53
  });
53
54
  }
54
- const props = data.props;
55
+ const key = await manifest.key;
56
+ const encryptedProps = data.encryptedProps;
57
+ const propString = await decryptString(key, encryptedProps);
58
+ const props = JSON.parse(propString);
55
59
  const componentModule = await imp();
56
60
  const Component = componentModule[data.componentExport];
57
61
  const slots = {};
@@ -1,3 +1,4 @@
1
+ import { encryptString } from "../../../core/encryption.js";
1
2
  import { renderChild } from "./any.js";
2
3
  import { renderSlotToString } from "./slot.js";
3
4
  const internalProps = /* @__PURE__ */ new Set([
@@ -21,9 +22,9 @@ function renderServerIsland(result, _displayName, props, slots) {
21
22
  if (!componentId) {
22
23
  throw new Error(`Could not find server component name`);
23
24
  }
24
- for (const key of Object.keys(props)) {
25
- if (internalProps.has(key)) {
26
- delete props[key];
25
+ for (const key2 of Object.keys(props)) {
26
+ if (internalProps.has(key2)) {
27
+ delete props[key2];
27
28
  }
28
29
  }
29
30
  destination.write("<!--server-island-start-->");
@@ -36,6 +37,8 @@ function renderServerIsland(result, _displayName, props, slots) {
36
37
  await renderChild(destination, slots.fallback(result));
37
38
  }
38
39
  }
40
+ const key = await result.key;
41
+ const propsEncrypted = await encryptString(key, JSON.stringify(props));
39
42
  const hostId = crypto.randomUUID();
40
43
  const serverIslandUrl = `${result.base}_server-islands/${componentId}${result.trailingSlash === "always" ? "/" : ""}`;
41
44
  destination.write(`<script async type="module" data-island-id="${hostId}">
@@ -44,7 +47,7 @@ let componentExport = ${safeJsonStringify(componentExport)};
44
47
  let script = document.querySelector('script[data-island-id="${hostId}"]');
45
48
  let data = {
46
49
  componentExport,
47
- props: ${safeJsonStringify(props)},
50
+ encryptedProps: ${safeJsonStringify(propsEncrypted)},
48
51
  slots: ${safeJsonStringify(renderedSlots)},
49
52
  };
50
53
 
@@ -1,5 +1,6 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { IncomingMessage } from "node:http";
3
+ import { createKey } from "../core/encryption.js";
3
4
  import { getViteErrorPayload } from "../core/errors/dev/index.js";
4
5
  import { AstroError, AstroErrorData } from "../core/errors/index.js";
5
6
  import { patchOverlay } from "../core/errors/overlay.js";
@@ -117,6 +118,7 @@ function createDevelopmentManifest(settings) {
117
118
  i18n: i18nManifest,
118
119
  checkOrigin: settings.config.security?.checkOrigin ?? false,
119
120
  experimentalEnvGetSecretEnabled: false,
121
+ key: createKey(),
120
122
  middleware(_, next) {
121
123
  return next();
122
124
  }
@@ -35,7 +35,7 @@ async function scan(code, id, settings) {
35
35
  const { n: name, le: endOfLocalName } = _export;
36
36
  if (BOOLEAN_EXPORTS.has(name)) {
37
37
  const prefix = code.slice(0, endOfLocalName).split("export").pop().trim().replace("prerender", "").trim();
38
- const suffix = code.slice(endOfLocalName).trim().replace(/=/, "").trim().split(/[;\n]/)[0];
38
+ const suffix = code.slice(endOfLocalName).trim().replace(/=/, "").trim().split(/[;\n\r]/)[0].trim();
39
39
  if (prefix !== "const" || !(isTruthy(suffix) || isFalsy(suffix))) {
40
40
  throw new AstroError({
41
41
  ...AstroErrorData.InvalidPrerenderExport,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro",
3
- "version": "4.13.2",
3
+ "version": "4.13.4",
4
4
  "description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.",
5
5
  "type": "module",
6
6
  "author": "withastro",
@@ -107,13 +107,14 @@
107
107
  "vendor"
108
108
  ],
109
109
  "dependencies": {
110
- "@astrojs/compiler": "^2.10.1",
110
+ "@astrojs/compiler": "^2.10.2",
111
111
  "@babel/core": "^7.25.2",
112
112
  "@babel/generator": "^7.25.0",
113
113
  "@babel/parser": "^7.25.3",
114
114
  "@babel/plugin-transform-react-jsx": "^7.25.2",
115
115
  "@babel/traverse": "^7.25.3",
116
116
  "@babel/types": "^7.25.2",
117
+ "@oslojs/encoding": "^0.4.1",
117
118
  "@types/babel__core": "^7.20.5",
118
119
  "@types/cookie": "^0.6.0",
119
120
  "acorn": "^8.12.1",
@@ -159,22 +160,22 @@
159
160
  "tsconfck": "^3.1.1",
160
161
  "unist-util-visit": "^5.0.0",
161
162
  "vfile": "^6.0.2",
162
- "vite": "^5.3.5",
163
+ "vite": "^5.4.0",
163
164
  "vitefu": "^0.2.5",
164
165
  "which-pm": "^3.0.0",
165
166
  "yargs-parser": "^21.1.1",
166
167
  "zod": "^3.23.8",
167
168
  "zod-to-json-schema": "^3.23.2",
169
+ "@astrojs/markdown-remark": "5.2.0",
168
170
  "@astrojs/internal-helpers": "0.4.1",
169
- "@astrojs/telemetry": "3.1.0",
170
- "@astrojs/markdown-remark": "5.2.0"
171
+ "@astrojs/telemetry": "3.1.0"
171
172
  },
172
173
  "optionalDependencies": {
173
174
  "sharp": "^0.33.3"
174
175
  },
175
176
  "devDependencies": {
176
- "@astrojs/check": "^0.9.1",
177
- "@playwright/test": "^1.45.3",
177
+ "@astrojs/check": "^0.9.2",
178
+ "@playwright/test": "^1.46.0",
178
179
  "@types/aria-query": "^5.0.4",
179
180
  "@types/babel__generator": "^7.6.8",
180
181
  "@types/babel__traverse": "^7.20.6",
@@ -191,7 +192,7 @@
191
192
  "@types/prompts": "^2.4.9",
192
193
  "@types/semver": "^7.5.8",
193
194
  "@types/yargs-parser": "^21.0.3",
194
- "cheerio": "1.0.0-rc.12",
195
+ "cheerio": "1.0.0",
195
196
  "eol": "^0.9.1",
196
197
  "expect-type": "^0.19.0",
197
198
  "mdast-util-mdx": "^3.0.0",
@@ -205,7 +206,7 @@
205
206
  "remark-code-titles": "^0.1.2",
206
207
  "rollup": "^4.20.0",
207
208
  "sass": "^1.77.8",
208
- "undici": "^6.19.5",
209
+ "undici": "^6.19.7",
209
210
  "unified": "^11.0.5",
210
211
  "astro-scripts": "0.0.14"
211
212
  },
@@ -1,4 +1,9 @@
1
- import { ActionError, deserializeActionResult, getActionQueryString } from 'astro:actions';
1
+ import {
2
+ ACTION_QUERY_PARAMS,
3
+ ActionError,
4
+ deserializeActionResult,
5
+ getActionQueryString,
6
+ } from 'astro:actions';
2
7
 
3
8
  function toActionProxy(actionCallback = {}, aggregatedPath = '') {
4
9
  return new Proxy(actionCallback, {
@@ -16,13 +21,18 @@ function toActionProxy(actionCallback = {}, aggregatedPath = '') {
16
21
  toString: () => action.queryString,
17
22
  // Progressive enhancement info for React.
18
23
  $$FORM_ACTION: function () {
24
+ const searchParams = new URLSearchParams(action.toString());
25
+ // Astro will redirect with a GET request by default.
26
+ // Disable this behavior to preserve form state
27
+ // for React's progressive enhancement.
28
+ searchParams.set(ACTION_QUERY_PARAMS.actionRedirect, 'false');
19
29
  return {
20
30
  method: 'POST',
21
31
  // `name` creates a hidden input.
22
32
  // It's unused by Astro, but we can't turn this off.
23
33
  // At least use a name that won't conflict with a user's formData.
24
34
  name: '_astroAction',
25
- action: action.toString(),
35
+ action: '?' + searchParams.toString(),
26
36
  };
27
37
  },
28
38
  // Note: `orThrow` does not have progressive enhancement info.