astro 6.0.0-beta.10 → 6.0.0-beta.11

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.
@@ -64,7 +64,7 @@ function verifyOptions(options) {
64
64
  if (options.widths && options.densities) {
65
65
  throw new AstroError(AstroErrorData.IncompatibleDescriptorOptions);
66
66
  }
67
- if (options.src.format === "svg" && options.format !== "svg" || options.src.format !== "svg" && options.format === "svg") {
67
+ if (options.src.format !== "svg" && options.format === "svg") {
68
68
  throw new AstroError(AstroErrorData.UnsupportedImageConversion);
69
69
  }
70
70
  }
@@ -72,12 +72,13 @@ function verifyOptions(options) {
72
72
  const baseService = {
73
73
  propertiesToHash: DEFAULT_HASH_PROPS,
74
74
  validateOptions(options) {
75
- if (isESMImportedImage(options.src) && options.src.format === "svg") {
76
- options.format = "svg";
77
- }
78
75
  verifyOptions(options);
79
76
  if (!options.format) {
80
- options.format = DEFAULT_OUTPUT_FORMAT;
77
+ if (isESMImportedImage(options.src) && options.src.format === "svg") {
78
+ options.format = "svg";
79
+ } else {
80
+ options.format = DEFAULT_OUTPUT_FORMAT;
81
+ }
81
82
  }
82
83
  if (options.width) options.width = Math.round(options.width);
83
84
  if (options.height) options.height = Math.round(options.height);
@@ -1,6 +1,6 @@
1
1
  class BuildTimeAstroVersionProvider {
2
2
  // Injected during the build through esbuild define
3
- version = "6.0.0-beta.10";
3
+ version = "6.0.0-beta.11";
4
4
  }
5
5
  export {
6
6
  BuildTimeAstroVersionProvider
@@ -181,7 +181,7 @@ ${contentConfig.error.message}`
181
181
  logger.info("Content config changed");
182
182
  shouldClear = true;
183
183
  }
184
- if (previousAstroVersion && previousAstroVersion !== "6.0.0-beta.10") {
184
+ if (previousAstroVersion && previousAstroVersion !== "6.0.0-beta.11") {
185
185
  logger.info("Astro version changed");
186
186
  shouldClear = true;
187
187
  }
@@ -189,8 +189,8 @@ ${contentConfig.error.message}`
189
189
  logger.info("Clearing content store");
190
190
  this.#store.clearAll();
191
191
  }
192
- if ("6.0.0-beta.10") {
193
- await this.#store.metaStore().set("astro-version", "6.0.0-beta.10");
192
+ if ("6.0.0-beta.11") {
193
+ await this.#store.metaStore().set("astro-version", "6.0.0-beta.11");
194
194
  }
195
195
  if (currentConfigDigest) {
196
196
  await this.#store.metaStore().set("content-config-digest", currentConfigDigest);
@@ -22,6 +22,8 @@ class MutableDataStore extends ImmutableDataStore {
22
22
  #modulesDirty = false;
23
23
  #assetImports = /* @__PURE__ */ new Set();
24
24
  #moduleImports = /* @__PURE__ */ new Map();
25
+ #writeInProgress = false;
26
+ #writeQueued = false;
25
27
  set(collectionName, key, value) {
26
28
  const collection = this._collections.get(collectionName) ?? /* @__PURE__ */ new Map();
27
29
  collection.set(String(key), value);
@@ -121,7 +123,7 @@ ${lines.join(",\n")}]);
121
123
  this.#modulesDirty = false;
122
124
  }
123
125
  #maybeResolveSavePromise() {
124
- if (!this.#saveTimeout && !this.#assetsSaveTimeout && !this.#modulesSaveTimeout && this.#savePromiseResolve) {
126
+ if (!this.#saveTimeout && !this.#assetsSaveTimeout && !this.#modulesSaveTimeout && !this.#writeQueued && !this.#writeInProgress && this.#savePromiseResolve) {
125
127
  this.#savePromiseResolve();
126
128
  this.#savePromiseResolve = void 0;
127
129
  this.#savePromise = void 0;
@@ -317,11 +319,22 @@ ${lines.join(",\n")}]);
317
319
  if (!this.#file) {
318
320
  throw new AstroError(AstroErrorData.UnknownFilesystemError);
319
321
  }
322
+ if (this.#writeInProgress) {
323
+ this.#writeQueued = true;
324
+ return;
325
+ }
320
326
  try {
321
327
  this.#dirty = false;
328
+ this.#writeInProgress = true;
322
329
  await this.#writeFileAtomic(this.#file, this.toString());
323
330
  } catch (err) {
324
331
  throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err });
332
+ } finally {
333
+ this.#writeInProgress = false;
334
+ if (this.#writeQueued) {
335
+ this.#writeQueued = false;
336
+ await this.writeToDisk();
337
+ }
325
338
  }
326
339
  }
327
340
  /**
@@ -4,7 +4,7 @@ import { clientAddressSymbol, nodeRequestAbortControllerCleanupSymbol } from "..
4
4
  import { deserializeManifest } from "./manifest.js";
5
5
  import { createOutgoingHttpHeaders } from "./createOutgoingHttpHeaders.js";
6
6
  import { App } from "./index.js";
7
- import { sanitizeHost, validateForwardedHeaders } from "./validate-forwarded-headers.js";
7
+ import { validateForwardedHeaders, validateHost } from "./validate-headers.js";
8
8
  class NodeApp extends App {
9
9
  headersMap = void 0;
10
10
  setHeadersMap(headers) {
@@ -50,7 +50,7 @@ class NodeApp extends App {
50
50
  return multiValueHeader?.toString()?.split(",").map((e) => e.trim())?.[0];
51
51
  };
52
52
  const providedProtocol = isEncrypted ? "https" : "http";
53
- const providedHostname = req.headers.host ?? req.headers[":authority"];
53
+ const untrustedHostname = req.headers.host ?? req.headers[":authority"];
54
54
  const validated = validateForwardedHeaders(
55
55
  getFirstForwardedValue(req.headers["x-forwarded-proto"]),
56
56
  getFirstForwardedValue(req.headers["x-forwarded-host"]),
@@ -58,18 +58,20 @@ class NodeApp extends App {
58
58
  allowedDomains
59
59
  );
60
60
  const protocol = validated.protocol ?? providedProtocol;
61
- const sanitizedProvidedHostname = sanitizeHost(
62
- typeof providedHostname === "string" ? providedHostname : void 0
61
+ const validatedHostname = validateHost(
62
+ typeof untrustedHostname === "string" ? untrustedHostname : void 0,
63
+ protocol,
64
+ allowedDomains
63
65
  );
64
- const hostname = validated.host ?? sanitizedProvidedHostname;
66
+ const hostname = validated.host ?? validatedHostname ?? "localhost";
65
67
  const port = validated.port;
66
68
  let url;
67
69
  try {
68
70
  const hostnamePort = getHostnamePort(hostname, port);
69
71
  url = new URL(`${protocol}://${hostnamePort}${req.url}`);
70
72
  } catch {
71
- const hostnamePort = getHostnamePort(providedHostname, port);
72
- url = new URL(`${providedProtocol}://${hostnamePort}`);
73
+ const hostnamePort = getHostnamePort(hostname, port);
74
+ url = new URL(`${protocol}://${hostnamePort}`);
73
75
  }
74
76
  const options = {
75
77
  method: req.method || "GET",
@@ -1,9 +1,10 @@
1
1
  import { type RemotePattern } from '@astrojs/internal-helpers/remote';
2
2
  /**
3
- * Validate a hostname by rejecting any with path separators.
4
- * Prevents path injection attacks. Invalid hostnames return undefined.
3
+ * Validate a host against allowedDomains.
4
+ * Returns the host only if it matches an allowed pattern, otherwise undefined.
5
+ * This prevents SSRF attacks by ensuring the Host header is trusted.
5
6
  */
6
- export declare function sanitizeHost(hostname: string | undefined): string | undefined;
7
+ export declare function validateHost(host: string | undefined, protocol: string, allowedDomains?: Partial<RemotePattern>[]): string | undefined;
7
8
  /**
8
9
  * Validate forwarded headers (proto, host, port) against allowedDomains.
9
10
  * Returns validated values or undefined for rejected headers.
@@ -4,6 +4,33 @@ function sanitizeHost(hostname) {
4
4
  if (/[/\\]/.test(hostname)) return void 0;
5
5
  return hostname;
6
6
  }
7
+ function parseHost(host) {
8
+ const parts = host.split(":");
9
+ return {
10
+ hostname: parts[0],
11
+ port: parts[1]
12
+ };
13
+ }
14
+ function matchesAllowedDomains(hostname, protocol, port, allowedDomains) {
15
+ const hostWithPort = port ? `${hostname}:${port}` : hostname;
16
+ const urlString = `${protocol}://${hostWithPort}`;
17
+ if (!URL.canParse(urlString)) {
18
+ return false;
19
+ }
20
+ const testUrl = new URL(urlString);
21
+ return allowedDomains.some((pattern) => matchPattern(testUrl, pattern));
22
+ }
23
+ function validateHost(host, protocol, allowedDomains) {
24
+ if (!host || host.length === 0) return void 0;
25
+ if (!allowedDomains || allowedDomains.length === 0) return void 0;
26
+ const sanitized = sanitizeHost(host);
27
+ if (!sanitized) return void 0;
28
+ const { hostname, port } = parseHost(sanitized);
29
+ if (matchesAllowedDomains(hostname, protocol, port, allowedDomains)) {
30
+ return sanitized;
31
+ }
32
+ return void 0;
33
+ }
7
34
  function validateForwardedHeaders(forwardedProtocol, forwardedHost, forwardedPort, allowedDomains) {
8
35
  const result = {};
9
36
  if (forwardedProtocol) {
@@ -38,23 +65,16 @@ function validateForwardedHeaders(forwardedProtocol, forwardedHost, forwardedPor
38
65
  const protoForValidation = result.protocol || "https";
39
66
  const sanitized = sanitizeHost(forwardedHost);
40
67
  if (sanitized) {
41
- try {
42
- const hostnameOnly = sanitized.split(":")[0];
43
- const portFromHost = sanitized.includes(":") ? sanitized.split(":")[1] : void 0;
44
- const portForValidation = result.port || portFromHost;
45
- const hostWithPort = portForValidation ? `${hostnameOnly}:${portForValidation}` : hostnameOnly;
46
- const testUrl = new URL(`${protoForValidation}://${hostWithPort}`);
47
- const isAllowed = allowedDomains.some((pattern) => matchPattern(testUrl, pattern));
48
- if (isAllowed) {
49
- result.host = sanitized;
50
- }
51
- } catch {
68
+ const { hostname, port: portFromHost } = parseHost(sanitized);
69
+ const portForValidation = result.port || portFromHost;
70
+ if (matchesAllowedDomains(hostname, protoForValidation, portForValidation, allowedDomains)) {
71
+ result.host = sanitized;
52
72
  }
53
73
  }
54
74
  }
55
75
  return result;
56
76
  }
57
77
  export {
58
- sanitizeHost,
59
- validateForwardedHeaders
78
+ validateForwardedHeaders,
79
+ validateHost
60
80
  };
@@ -84,6 +84,19 @@ async function buildEnvironments(opts, internals) {
84
84
  let currentRollupInput = void 0;
85
85
  plugins.push({
86
86
  name: "astro:resolve-input",
87
+ // When the rollup input is safe to update, we normalize it to always be an object
88
+ // so we can reliably identify which entrypoint corresponds to the adapter
89
+ enforce: "post",
90
+ config(config) {
91
+ if (typeof config.build?.rollupOptions?.input === "string") {
92
+ config.build.rollupOptions.input = { index: config.build.rollupOptions.input };
93
+ } else if (Array.isArray(config.build?.rollupOptions?.input)) {
94
+ config.build.rollupOptions.input = Object.fromEntries(
95
+ config.build.rollupOptions.input.map((v, i) => [`index_${i}`, v])
96
+ );
97
+ }
98
+ },
99
+ // We save the rollup input to be able to check later on
87
100
  configResolved(config) {
88
101
  currentRollupInput = config.build.rollupOptions.input;
89
102
  }
@@ -113,10 +126,7 @@ async function buildEnvironments(opts, internals) {
113
126
  }
114
127
  });
115
128
  function isRollupInput(moduleName) {
116
- if (!currentRollupInput) {
117
- return false;
118
- }
119
- if (!moduleName) {
129
+ if (!currentRollupInput || !moduleName) {
120
130
  return false;
121
131
  }
122
132
  if (typeof currentRollupInput === "string") {
@@ -1,4 +1,4 @@
1
- const ASTRO_VERSION = "6.0.0-beta.10";
1
+ const ASTRO_VERSION = "6.0.0-beta.11";
2
2
  const ASTRO_GENERATOR = `Astro v${ASTRO_VERSION}`;
3
3
  const REROUTE_DIRECTIVE_HEADER = "X-Astro-Reroute";
4
4
  const REWRITE_DIRECTIVE_HEADER_KEY = "X-Astro-Rewrite";
@@ -22,7 +22,7 @@ async function dev(inlineConfig) {
22
22
  await telemetry.record([]);
23
23
  const restart = await createContainerWithAutomaticRestart({ inlineConfig, fs });
24
24
  const logger = restart.container.logger;
25
- const currentVersion = "6.0.0-beta.10";
25
+ const currentVersion = "6.0.0-beta.11";
26
26
  const isPrerelease = currentVersion.includes("-");
27
27
  if (!isPrerelease) {
28
28
  try {
@@ -28,7 +28,7 @@ function serverStart({
28
28
  host,
29
29
  base
30
30
  }) {
31
- const version = "6.0.0-beta.10";
31
+ const version = "6.0.0-beta.11";
32
32
  const localPrefix = `${dim("\u2503")} Local `;
33
33
  const networkPrefix = `${dim("\u2503")} Network `;
34
34
  const emptyPrefix = " ".repeat(11);
@@ -265,7 +265,7 @@ function printHelp({
265
265
  message.push(
266
266
  linebreak(),
267
267
  ` ${bgGreen(black(` ${commandName} `))} ${green(
268
- `v${"6.0.0-beta.10"}`
268
+ `v${"6.0.0-beta.11"}`
269
269
  )} ${headline}`
270
270
  );
271
271
  }
@@ -103,6 +103,7 @@ interface AdapterSelfProperties {
103
103
  * @default 'legacy-dynamic'
104
104
  */
105
105
  entryType: 'self';
106
+ serverEntrypoint?: string | URL;
106
107
  }
107
108
  export type AstroAdapter = {
108
109
  name: string;
@@ -1,9 +1,24 @@
1
1
  import { isAstroServerEnvironment } from "../environments.js";
2
+ import { fileURLToPath } from "node:url";
2
3
  const VIRTUAL_CLIENT_ID = "virtual:astro:adapter-config/client";
3
4
  const RESOLVED_VIRTUAL_CLIENT_ID = "\0" + VIRTUAL_CLIENT_ID;
4
5
  function vitePluginAdapterConfig(settings) {
5
6
  return {
6
7
  name: "astro:adapter-config",
8
+ config() {
9
+ const { adapter } = settings;
10
+ if (adapter && adapter.entryType === "self" && adapter.serverEntrypoint) {
11
+ return {
12
+ build: {
13
+ rollupOptions: {
14
+ input: {
15
+ index: typeof adapter.serverEntrypoint === "string" ? adapter.serverEntrypoint : fileURLToPath(adapter.serverEntrypoint)
16
+ }
17
+ }
18
+ }
19
+ };
20
+ }
21
+ },
7
22
  resolveId: {
8
23
  filter: {
9
24
  id: new RegExp(`^${VIRTUAL_CLIENT_ID}$`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro",
3
- "version": "6.0.0-beta.10",
3
+ "version": "6.0.0-beta.11",
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",
@@ -156,8 +156,8 @@
156
156
  "yocto-spinner": "^1.0.0",
157
157
  "zod": "^4.3.6",
158
158
  "@astrojs/internal-helpers": "0.8.0-beta.1",
159
- "@astrojs/markdown-remark": "7.0.0-beta.6",
160
- "@astrojs/telemetry": "3.3.0"
159
+ "@astrojs/telemetry": "3.3.0",
160
+ "@astrojs/markdown-remark": "7.0.0-beta.6"
161
161
  },
162
162
  "optionalDependencies": {
163
163
  "sharp": "^0.34.0"