diff --git a/.changeset/hip-toys-shake.md b/.changeset/hip-toys-shake.md
new file mode 100644
index 0000000000..dcbe9c89d4
--- /dev/null
+++ b/.changeset/hip-toys-shake.md
@@ -0,0 +1,9 @@
+---
+'@astrojs/node': patch
+---
+
+Move polyfills up before awaiting the env module in the Node.js adapter.
+
+Previously the env setting was happening before the polyfills were applied. This means that if the Astro env code (or any dependencies) depended on `crypto`, it would not be polyfilled in time.
+
+Polyfills should be applied ASAP to prevent races. This moves it to the top of the Node adapter.
diff --git a/.changeset/little-humans-act.md b/.changeset/little-humans-act.md
new file mode 100644
index 0000000000..a9dd54fcee
--- /dev/null
+++ b/.changeset/little-humans-act.md
@@ -0,0 +1,9 @@
+---
+'astro': patch
+---
+
+Encrypt server island props
+
+Server island props are not encrypted with a key generated at build-time. This is intended to prevent accidentally leaking secrets caused by exposing secrets through prop-passing. This is not intended to allow a server island to be trusted to skip authentication, or to protect against any other vulnerabilities other than secret leakage.
+
+See the RFC for an explanation: https://github.com/withastro/roadmap/blob/server-islands/proposals/server-islands.md#props-serialization
diff --git a/packages/astro/e2e/fixtures/server-islands/src/components/Island.astro b/packages/astro/e2e/fixtures/server-islands/src/components/Island.astro
index b7c376f517..5eab0dc4df 100644
--- a/packages/astro/e2e/fixtures/server-islands/src/components/Island.astro
+++ b/packages/astro/e2e/fixtures/server-islands/src/components/Island.astro
@@ -1,4 +1,6 @@
---
+const { secret } = Astro.props;
---
I am an island
+{secret}
diff --git a/packages/astro/e2e/fixtures/server-islands/src/pages/index.astro b/packages/astro/e2e/fixtures/server-islands/src/pages/index.astro
index 998d6c0740..de9a6c456f 100644
--- a/packages/astro/e2e/fixtures/server-islands/src/pages/index.astro
+++ b/packages/astro/e2e/fixtures/server-islands/src/pages/index.astro
@@ -8,7 +8,7 @@ import Self from '../components/Self.astro';
-
+
children
diff --git a/packages/astro/e2e/server-islands.test.js b/packages/astro/e2e/server-islands.test.js
index b036eaafa3..b37495b288 100644
--- a/packages/astro/e2e/server-islands.test.js
+++ b/packages/astro/e2e/server-islands.test.js
@@ -38,6 +38,12 @@ test.describe('Server islands', () => {
await expect(el, 'element rendered').toBeVisible();
});
+ test('Props are encrypted', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/base/'));
+ let el = page.locator('#secret');
+ await expect(el).toHaveText('test');
+ });
+
test('Self imported module can server defer', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/base/'));
let el = page.locator('.now');
@@ -69,5 +75,11 @@ test.describe('Server islands', () => {
await expect(el, 'element rendered').toBeVisible();
await expect(el, 'should have content').toHaveText('I am an island');
});
+
+ test('Props are encrypted', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+ let el = page.locator('#secret');
+ await expect(el).toHaveText('test');
+ });
});
});
diff --git a/packages/astro/package.json b/packages/astro/package.json
index 64ec709d59..602b1f7aed 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -132,6 +132,7 @@
"@babel/plugin-transform-react-jsx": "^7.25.2",
"@babel/traverse": "^7.25.3",
"@babel/types": "^7.25.2",
+ "@oslojs/encoding": "^0.4.1",
"@types/babel__core": "^7.20.5",
"@types/cookie": "^0.6.0",
"acorn": "^8.12.1",
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index aeef4f0edf..e9c42954a1 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -3283,6 +3283,7 @@ export interface SSRResult {
cookies: AstroCookies | undefined;
serverIslandNameMap: Map;
trailingSlash: AstroConfig['trailingSlash'];
+ key: Promise;
_metadata: SSRMetadata;
}
diff --git a/packages/astro/src/container/index.ts b/packages/astro/src/container/index.ts
index 292b49ece1..d9ad61f256 100644
--- a/packages/astro/src/container/index.ts
+++ b/packages/astro/src/container/index.ts
@@ -25,6 +25,7 @@ import { getParts, validateSegment } from '../core/routing/manifest/create.js';
import { getPattern } from '../core/routing/manifest/pattern.js';
import type { AstroComponentFactory } from '../runtime/server/index.js';
import { ContainerPipeline } from './pipeline.js';
+import { createKey } from '../core/encryption.js';
/**
* Options to be passed when rendering a route
@@ -130,6 +131,7 @@ function createManifest(
checkOrigin: false,
middleware: manifest?.middleware ?? middleware ?? defaultMiddleware,
experimentalEnvGetSecretEnabled: false,
+ key: createKey(),
};
}
diff --git a/packages/astro/src/core/app/common.ts b/packages/astro/src/core/app/common.ts
index 19bbee1954..7cfe1c5dd7 100644
--- a/packages/astro/src/core/app/common.ts
+++ b/packages/astro/src/core/app/common.ts
@@ -1,3 +1,4 @@
+import { decodeKey } from '../encryption.js';
import { deserializeRouteData } from '../routing/manifest/serialization.js';
import type { RouteInfo, SSRManifest, SerializedSSRManifest } from './types.js';
@@ -18,6 +19,7 @@ export function deserializeManifest(serializedManifest: SerializedSSRManifest):
const inlinedScripts = new Map(serializedManifest.inlinedScripts);
const clientDirectives = new Map(serializedManifest.clientDirectives);
const serverIslandNameMap = new Map(serializedManifest.serverIslandNameMap);
+ const key = decodeKey(serializedManifest.key);
return {
// in case user middleware exists, this no-op middleware will be reassigned (see plugin-ssr.ts)
@@ -31,5 +33,6 @@ export function deserializeManifest(serializedManifest: SerializedSSRManifest):
clientDirectives,
routes,
serverIslandNameMap,
+ key,
};
}
diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts
index d19a4da7d3..7ccf219c88 100644
--- a/packages/astro/src/core/app/index.ts
+++ b/packages/astro/src/core/app/index.ts
@@ -416,13 +416,15 @@ export class App {
`${this.#baseWithoutTrailingSlash}/${status}${maybeDotHtml}`,
url,
);
- const response = await fetch(statusURL.toString());
+ if(statusURL.toString() !== request.url) {
+ const response = await fetch(statusURL.toString());
- // response for /404.html and 500.html is 200, which is not meaningful
- // so we create an override
- const override = { status };
+ // response for /404.html and 500.html is 200, which is not meaningful
+ // so we create an override
+ const override = { status };
- return this.#mergeResponses(response, originalResponse, override);
+ return this.#mergeResponses(response, originalResponse, override);
+ }
}
const mod = await this.#pipeline.getModuleForRoute(errorRouteData);
try {
diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts
index 2e4e8d8057..00e37dacd9 100644
--- a/packages/astro/src/core/app/types.ts
+++ b/packages/astro/src/core/app/types.ts
@@ -66,6 +66,7 @@ export type SSRManifest = {
pageMap?: Map;
serverIslandMap?: Map Promise>;
serverIslandNameMap?: Map;
+ key: Promise;
i18n: SSRManifestI18n | undefined;
middleware: MiddlewareHandler;
checkOrigin: boolean;
@@ -90,6 +91,7 @@ export type SerializedSSRManifest = Omit<
| 'inlinedScripts'
| 'clientDirectives'
| 'serverIslandNameMap'
+ | 'key'
> & {
routes: SerializedRouteInfo[];
assets: string[];
@@ -97,4 +99,5 @@ export type SerializedSSRManifest = Omit<
inlinedScripts: [string, string][];
clientDirectives: [string, string][];
serverIslandNameMap: [string, string][];
+ key: string;
};
diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts
index 5897ba7e4d..329530154d 100644
--- a/packages/astro/src/core/build/generate.ts
+++ b/packages/astro/src/core/build/generate.ts
@@ -77,6 +77,7 @@ export async function generatePages(options: StaticBuildOptions, internals: Buil
internals,
renderers.renderers as SSRLoadedRenderer[],
middleware,
+ options.key,
);
}
const pipeline = BuildPipeline.create({ internals, manifest, options });
@@ -521,6 +522,7 @@ function createBuildManifest(
internals: BuildInternals,
renderers: SSRLoadedRenderer[],
middleware: MiddlewareHandler,
+ key: Promise
): SSRManifest {
let i18nManifest: SSRManifestI18n | undefined = undefined;
if (settings.config.i18n) {
@@ -551,6 +553,7 @@ function createBuildManifest(
buildFormat: settings.config.build.format,
middleware,
checkOrigin: settings.config.security?.checkOrigin ?? false,
+ key,
experimentalEnvGetSecretEnabled: false,
};
}
diff --git a/packages/astro/src/core/build/index.ts b/packages/astro/src/core/build/index.ts
index 8df72d8b22..ecc97161fc 100644
--- a/packages/astro/src/core/build/index.ts
+++ b/packages/astro/src/core/build/index.ts
@@ -33,6 +33,7 @@ import { collectPagesData } from './page-data.js';
import { staticBuild, viteBuild } from './static-build.js';
import type { StaticBuildOptions } from './types.js';
import { getTimeStat } from './util.js';
+import { createKey } from '../encryption.js';
export interface BuildOptions {
/**
@@ -201,6 +202,7 @@ class AstroBuilder {
pageNames,
teardownCompiler: this.teardownCompiler,
viteConfig,
+ key: createKey(),
};
const { internals, ssrOutputChunkNames, contentFileNames } = await viteBuild(opts);
diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts
index bb1add5b45..098c5528d0 100644
--- a/packages/astro/src/core/build/plugins/plugin-manifest.ts
+++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts
@@ -20,6 +20,7 @@ import { type BuildInternals, cssOrder, mergeInlineCss } from '../internal.js';
import type { AstroBuildPlugin } from '../plugin.js';
import type { StaticBuildOptions } from '../types.js';
import { makePageDataKey } from './util.js';
+import { encodeKey } from '../../encryption.js';
const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@';
const replaceExp = new RegExp(`['"]${manifestReplace}['"]`, 'g');
@@ -132,7 +133,8 @@ async function createManifest(
}
const staticFiles = internals.staticFiles;
- return buildManifest(buildOpts, internals, Array.from(staticFiles));
+ const encodedKey = await encodeKey(await buildOpts.key);
+ return buildManifest(buildOpts, internals, Array.from(staticFiles), encodedKey);
}
/**
@@ -150,6 +152,7 @@ function buildManifest(
opts: StaticBuildOptions,
internals: BuildInternals,
staticFiles: string[],
+ encodedKey: string,
): SerializedSSRManifest {
const { settings } = opts;
@@ -277,6 +280,7 @@ function buildManifest(
buildFormat: settings.config.build.format,
checkOrigin: settings.config.security?.checkOrigin ?? false,
serverIslandNameMap: Array.from(settings.serverIslandNameMap),
+ key: encodedKey,
experimentalEnvGetSecretEnabled:
settings.config.experimental.env !== undefined &&
(settings.adapter?.supportedAstroFeatures.envGetSecret ?? 'unsupported') !== 'unsupported',
diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts
index f395141a0f..201c48a5e9 100644
--- a/packages/astro/src/core/build/plugins/plugin-ssr.ts
+++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts
@@ -37,6 +37,11 @@ function vitePluginSSR(
inputs.add(getVirtualModulePageName(ASTRO_PAGE_MODULE_ID, pageData.component));
}
+ const adapterServerEntrypoint = options.settings.adapter?.serverEntrypoint;
+ if(adapterServerEntrypoint) {
+ inputs.add(adapterServerEntrypoint);
+ }
+
inputs.add(SSR_VIRTUAL_MODULE_ID);
return addRollupInput(opts, Array.from(inputs));
},
@@ -246,8 +251,8 @@ function generateSSRCode(settings: AstroSettings, adapter: AstroAdapter, middlew
const imports = [
`import { renderers } from '${RENDERERS_MODULE_ID}';`,
- `import { manifest as defaultManifest } from '${SSR_MANIFEST_VIRTUAL_MODULE_ID}';`,
`import * as serverEntrypointModule from '${adapter.serverEntrypoint}';`,
+ `import { manifest as defaultManifest } from '${SSR_MANIFEST_VIRTUAL_MODULE_ID}';`,
edgeMiddleware ? `` : `import { onRequest as middleware } from '${middlewareId}';`,
settings.config.experimental.serverIslands
? `import { serverIslandMap } from '${VIRTUAL_ISLAND_MAP_ID}';`
diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts
index 8626019562..7e2272dde6 100644
--- a/packages/astro/src/core/build/static-build.ts
+++ b/packages/astro/src/core/build/static-build.ts
@@ -255,6 +255,8 @@ async function ssrBuild(
return 'renderers.mjs';
} else if (chunkInfo.facadeModuleId === RESOLVED_SSR_MANIFEST_VIRTUAL_MODULE_ID) {
return 'manifest_[hash].mjs';
+ } else if (chunkInfo.facadeModuleId === settings.adapter?.serverEntrypoint) {
+ return 'adapter_[hash].mjs';
} else if (
settings.config.experimental.contentCollectionCache &&
chunkInfo.facadeModuleId &&
diff --git a/packages/astro/src/core/build/types.ts b/packages/astro/src/core/build/types.ts
index 11724b8244..572140ef66 100644
--- a/packages/astro/src/core/build/types.ts
+++ b/packages/astro/src/core/build/types.ts
@@ -42,6 +42,7 @@ export interface StaticBuildOptions {
pageNames: string[];
viteConfig: InlineConfig;
teardownCompiler: boolean;
+ key: Promise;
}
type ImportComponentInstance = () => Promise;
diff --git a/packages/astro/src/core/encryption.ts b/packages/astro/src/core/encryption.ts
new file mode 100644
index 0000000000..f849662972
--- /dev/null
+++ b/packages/astro/src/core/encryption.ts
@@ -0,0 +1,88 @@
+import { encodeBase64, decodeBase64, decodeHex, encodeHexUpperCase } from '@oslojs/encoding';
+
+// Chose this algorithm for no particular reason, can change.
+// This algo does check against text manipulation though. See
+// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/encrypt#aes-gcm
+const ALGORITHM = 'AES-GCM';
+
+/**
+ * Creates a CryptoKey object that can be used to encrypt any string.
+ */
+export async function createKey() {
+ const key = await crypto.subtle.generateKey(
+ {
+ name: ALGORITHM,
+ length: 256,
+ },
+ true,
+ ['encrypt', 'decrypt']
+ );
+ return key;
+}
+
+/**
+ * Takes a key that has been serialized to an array of bytes and returns a CryptoKey
+ */
+export async function importKey(bytes: Uint8Array): Promise {
+ const key = await crypto.subtle.importKey('raw', bytes, ALGORITHM, true, ['encrypt', 'decrypt']);
+ return key;
+}
+
+/**
+ * Encodes a CryptoKey to base64 string, so that it can be embedded in JSON / JavaScript
+ */
+export async function encodeKey(key: CryptoKey) {
+ const exported = await crypto.subtle.exportKey('raw', key);
+ const encodedKey = encodeBase64(new Uint8Array(exported));
+ return encodedKey;
+}
+
+/**
+ * Decodes a base64 string into bytes and then imports the key.
+ */
+export async function decodeKey(encoded: string): Promise {
+ const bytes = decodeBase64(encoded);
+ return crypto.subtle.importKey('raw', bytes, ALGORITHM, true, ['encrypt', 'decrypt']);
+}
+
+const encoder = new TextEncoder();
+const decoder = new TextDecoder();
+// The length of the initialization vector
+// See https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams
+const IV_LENGTH = 24;
+
+/**
+ * Using a CryptoKey, encrypt a string into a base64 string.
+ */
+export async function encryptString(key: CryptoKey, raw: string) {
+ const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH / 2));
+ const data = encoder.encode(raw);
+ const buffer = await crypto.subtle.encrypt(
+ {
+ name: ALGORITHM,
+ iv,
+ },
+ key,
+ data
+ );
+ // iv is 12, hex brings it to 24
+ return encodeHexUpperCase(iv) + encodeBase64(new Uint8Array(buffer));
+}
+
+/**
+ * Takes a base64 encoded string, decodes it and returns the decrypted text.
+ */
+export async function decryptString(key: CryptoKey, encoded: string) {
+ const iv = decodeHex(encoded.slice(0, IV_LENGTH));
+ const dataArray = decodeBase64(encoded.slice(IV_LENGTH));
+ const decryptedBuffer = await crypto.subtle.decrypt(
+ {
+ name: ALGORITHM,
+ iv,
+ },
+ key,
+ dataArray
+ );
+ const decryptedString = decoder.decode(decryptedBuffer);
+ return decryptedString;
+}
diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts
index a572215744..653d853c97 100644
--- a/packages/astro/src/core/render-context.ts
+++ b/packages/astro/src/core/render-context.ts
@@ -318,6 +318,7 @@ export class RenderContext {
? deserializeActionResult(this.locals._actionPayload.actionResult)
: undefined;
+
// Create the result object that will be passed into the renderPage 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.
@@ -344,6 +345,7 @@ export class RenderContext {
styles,
actionResult,
serverIslandNameMap: manifest.serverIslandNameMap ?? new Map(),
+ key: manifest.key,
trailingSlash: manifest.trailingSlash,
_metadata: {
hasHydrationScript: false,
diff --git a/packages/astro/src/core/server-islands/endpoint.ts b/packages/astro/src/core/server-islands/endpoint.ts
index 638e228829..fc87202ff3 100644
--- a/packages/astro/src/core/server-islands/endpoint.ts
+++ b/packages/astro/src/core/server-islands/endpoint.ts
@@ -11,6 +11,7 @@ import {
renderTemplate,
} from '../../runtime/server/index.js';
import { createSlotValueFromString } from '../../runtime/server/render/slot.js';
+import { decryptString } from '../encryption.js';
import { getPattern } from '../routing/manifest/pattern.js';
export const SERVER_ISLAND_ROUTE = '/_server-islands/[name]';
@@ -48,7 +49,7 @@ export function ensureServerIslandRoute(config: ConfigFields, routeManifest: Man
type RenderOptions = {
componentExport: string;
- props: Record;
+ encryptedProps: string;
slots: Record;
};
@@ -74,7 +75,11 @@ export function createEndpoint(manifest: SSRManifest) {
});
}
- const props = data.props;
+ const key = await manifest.key;
+ const encryptedProps = data.encryptedProps;
+ const propString = await decryptString(key, encryptedProps);
+ const props = JSON.parse(propString);
+
const componentModule = await imp();
const Component = (componentModule as any)[data.componentExport];
diff --git a/packages/astro/src/runtime/server/render/server-islands.ts b/packages/astro/src/runtime/server/render/server-islands.ts
index c2263addaa..46f3fd6b21 100644
--- a/packages/astro/src/runtime/server/render/server-islands.ts
+++ b/packages/astro/src/runtime/server/render/server-islands.ts
@@ -1,8 +1,10 @@
+import { encryptString } from '../../../core/encryption.js';
import type { SSRResult } from '../../../@types/astro.js';
import { renderChild } from './any.js';
import type { RenderInstance } from './common.js';
import { type ComponentSlots, renderSlotToString } from './slot.js';
+
const internalProps = new Set([
'server:component-path',
'server:component-export',
@@ -59,6 +61,9 @@ export function renderServerIsland(
}
}
+ const key = await result.key;
+ const propsEncrypted = await encryptString(key, JSON.stringify(props));
+
const hostId = crypto.randomUUID();
const serverIslandUrl = `${result.base}_server-islands/${componentId}${result.trailingSlash === 'always' ? '/' : ''}`;
@@ -68,7 +73,7 @@ let componentExport = ${safeJsonStringify(componentExport)};
let script = document.querySelector('script[data-island-id="${hostId}"]');
let data = {
componentExport,
- props: ${safeJsonStringify(props)},
+ encryptedProps: ${safeJsonStringify(propsEncrypted)},
slots: ${safeJsonStringify(renderedSlots)},
};
diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts
index 7d1e2fb6f8..238eb7c561 100644
--- a/packages/astro/src/vite-plugin-astro-server/plugin.ts
+++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts
@@ -18,6 +18,7 @@ import { recordServerError } from './error.js';
import { DevPipeline } from './pipeline.js';
import { handleRequest } from './request.js';
import { setRouteError } from './server-state.js';
+import { createKey } from '../core/encryption.js';
export interface AstroPluginOptions {
settings: AstroSettings;
@@ -129,6 +130,7 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest
domainLookupTable: {},
};
}
+
return {
hrefRoot: settings.config.root.toString(),
trailingSlash: settings.config.trailingSlash,
@@ -148,6 +150,7 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest
i18n: i18nManifest,
checkOrigin: settings.config.security?.checkOrigin ?? false,
experimentalEnvGetSecretEnabled: false,
+ key: createKey(),
middleware(_, next) {
return next();
},
diff --git a/packages/astro/test/server-islands.test.js b/packages/astro/test/server-islands.test.js
index 2b784276de..8806f35115 100644
--- a/packages/astro/test/server-islands.test.js
+++ b/packages/astro/test/server-islands.test.js
@@ -82,38 +82,6 @@ describe('Server islands', () => {
const serverIslandScript = $('script[data-island-id]');
assert.equal(serverIslandScript.length, 1, 'has the island script');
});
-
- describe('prod', () => {
- async function fetchIsland() {
- const app = await fixture.loadTestAdapterApp();
- const request = new Request('http://example.com/_server-islands/Island', {
- method: 'POST',
- body: JSON.stringify({
- componentExport: 'default',
- props: {},
- slots: {},
- }),
- });
- return app.render(request);
- }
-
- it('Island returns its HTML', async () => {
- const response = await fetchIsland();
- const html = await response.text();
- const $ = cheerio.load(html);
-
- const serverIslandEl = $('h2#island');
- assert.equal(serverIslandEl.length, 1);
- });
-
- it('Island does not include the doctype', async () => {
- const response = await fetchIsland();
- const html = await response.text();
- console.log(html);
-
- assert.ok(!/doctype/i.test(html), 'html does not include doctype');
- });
- });
});
});
});
diff --git a/packages/astro/test/test-adapter.js b/packages/astro/test/test-adapter.js
index 880b5fe646..8c4643367d 100644
--- a/packages/astro/test/test-adapter.js
+++ b/packages/astro/test/test-adapter.js
@@ -72,7 +72,7 @@ export default function ({
async render(request, { routeData, clientAddress, locals, addCookieHeader } = {}) {
const url = new URL(request.url);
if(this.#manifest.assets.has(url.pathname)) {
- const filePath = new URL('../client/' + this.removeBase(url.pathname), import.meta.url);
+ const filePath = new URL('../../client/' + this.removeBase(url.pathname), import.meta.url);
const data = await fs.promises.readFile(filePath);
return new Response(data);
}
diff --git a/packages/integrations/node/src/serve-static.ts b/packages/integrations/node/src/serve-static.ts
index 8256c588ec..e5cd73daf1 100644
--- a/packages/integrations/node/src/serve-static.ts
+++ b/packages/integrations/node/src/serve-static.ts
@@ -103,7 +103,14 @@ function resolveClientDir(options: Options) {
const clientURLRaw = new URL(options.client);
const serverURLRaw = new URL(options.server);
const rel = path.relative(url.fileURLToPath(serverURLRaw), url.fileURLToPath(clientURLRaw));
- const serverEntryURL = new URL(import.meta.url);
+
+ // walk up the parent folders until you find the one that is the root of the server entry folder. This is how we find the client folder relatively.
+ const serverFolder = path.basename(options.server);
+ let serverEntryFolderURL = path.dirname(import.meta.url);
+ while(!serverEntryFolderURL.endsWith(serverFolder)) {
+ serverEntryFolderURL = path.dirname(serverEntryFolderURL);
+ }
+ const serverEntryURL = serverEntryFolderURL + '/entry.mjs';
const clientURL = new URL(appendForwardSlash(rel), serverEntryURL);
const client = url.fileURLToPath(clientURL);
return client;
diff --git a/packages/integrations/node/src/server.ts b/packages/integrations/node/src/server.ts
index e5b503292d..1bb27e002f 100644
--- a/packages/integrations/node/src/server.ts
+++ b/packages/integrations/node/src/server.ts
@@ -4,6 +4,8 @@ import createMiddleware from './middleware.js';
import { createStandaloneHandler } from './standalone.js';
import startServer from './standalone.js';
import type { Options } from './types.js';
+// This needs to run first because some internals depend on `crypto`
+applyPolyfills();
// Won't throw if the virtual module is not available because it's not supported in
// the users's astro version or if astro:env is not enabled in the project
@@ -11,7 +13,6 @@ await import('astro/env/setup')
.then((mod) => mod.setGetEnv((key) => process.env[key]))
.catch(() => {});
-applyPolyfills();
export function createExports(manifest: SSRManifest, options: Options) {
const app = new NodeApp(manifest);
options.trailingSlash = manifest.trailingSlash;
diff --git a/packages/integrations/node/test/errors.test.js b/packages/integrations/node/test/errors.test.js
index d75155aa53..c785af586e 100644
--- a/packages/integrations/node/test/errors.test.js
+++ b/packages/integrations/node/test/errors.test.js
@@ -1,6 +1,7 @@
import assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import { Worker } from 'node:worker_threads';
+import { fileURLToPath } from 'node:url';
import * as cheerio from 'cheerio';
import nodejs from '../dist/index.js';
import { loadFixture } from './test-utils.js';
@@ -29,7 +30,8 @@ describe('Errors', () => {
it('stays alive after offshoot promise rejections', async () => {
// this test needs to happen in a worker because node test runner adds a listener for unhandled rejections in the main thread
- const worker = new Worker('./test/fixtures/errors/dist/server/entry.mjs', {
+ const url = new URL('./fixtures/errors/dist/server/entry.mjs', import.meta.url);
+ const worker = new Worker(fileURLToPath(url), {
type: 'module',
env: { ASTRO_NODE_LOGGING: 'enabled' },
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0e7edd4dc4..271f0472e8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -582,6 +582,9 @@ importers:
'@babel/types':
specifier: ^7.25.2
version: 7.25.2
+ '@oslojs/encoding':
+ specifier: ^0.4.1
+ version: 0.4.1
'@types/babel__core':
specifier: ^7.20.5
version: 7.20.5
@@ -7118,6 +7121,9 @@ packages:
'@octokit/types@13.5.0':
resolution: {integrity: sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==}
+ '@oslojs/encoding@0.4.1':
+ resolution: {integrity: sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q==}
+
'@parse5/tools@0.3.0':
resolution: {integrity: sha512-zxRyTHkqb7WQMV8kTNBKWb1BeOFUKXBXTBWuxg9H9hfvQB3IwP6Iw2U75Ia5eyRxPNltmY7E8YAlz6zWwUnjKg==}
@@ -8154,7 +8160,7 @@ packages:
resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==}
concat-map@0.0.1:
- resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+ resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=}
consola@3.2.3:
resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==}
@@ -9333,6 +9339,7 @@ packages:
libsql@0.3.12:
resolution: {integrity: sha512-to30hj8O3DjS97wpbKN6ERZ8k66MN1IaOfFLR6oHqd25GMiPJ/ZX0VaZ7w+TsPmxcFS3p71qArj/hiedCyvXCg==}
+ cpu: [x64, arm64, wasm32]
os: [darwin, linux, win32]
lilconfig@2.1.0:
@@ -12952,6 +12959,8 @@ snapshots:
dependencies:
'@octokit/openapi-types': 22.2.0
+ '@oslojs/encoding@0.4.1': {}
+
'@parse5/tools@0.3.0':
dependencies:
parse5: 7.1.2