astro 6.2.2 → 6.3.1
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.
- package/dist/actions/handler.d.ts +32 -0
- package/dist/actions/handler.js +45 -0
- package/dist/actions/runtime/server.js +1 -1
- package/dist/assets/build/generate.js +1 -1
- package/dist/assets/build/remote.d.ts +3 -2
- package/dist/assets/build/remote.js +16 -9
- package/dist/assets/endpoint/generic.js +8 -20
- package/dist/assets/endpoint/loadImage.d.ts +11 -0
- package/dist/assets/endpoint/loadImage.js +19 -0
- package/dist/assets/endpoint/shared.js +7 -2
- package/dist/assets/index.d.ts +1 -0
- package/dist/assets/index.js +2 -0
- package/dist/assets/services/sharp.js +7 -0
- package/dist/assets/utils/index.d.ts +1 -0
- package/dist/assets/utils/index.js +2 -0
- package/dist/assets/utils/redirectValidation.d.ts +48 -0
- package/dist/assets/utils/redirectValidation.js +48 -0
- package/dist/assets/utils/remoteProbe.js +25 -2
- package/dist/cli/infra/build-time-astro-version-provider.js +1 -1
- package/dist/container/index.js +18 -14
- package/dist/content/content-layer.js +3 -4
- package/dist/content/server-listeners.js +0 -4
- package/dist/content/vite-plugin-content-virtual-mod.js +9 -1
- package/dist/core/app/base.d.ts +33 -15
- package/dist/core/app/base.js +120 -324
- package/dist/core/app/dev/app.d.ts +3 -2
- package/dist/core/app/dev/app.js +4 -60
- package/dist/core/app/entrypoints/virtual/dev.js +2 -0
- package/dist/core/app/entrypoints/virtual/prod.js +4 -1
- package/dist/core/app/prepare-response.d.ts +11 -0
- package/dist/core/app/prepare-response.js +18 -0
- package/dist/core/app/render-options.d.ts +11 -0
- package/dist/core/app/render-options.js +11 -0
- package/dist/core/base-pipeline.d.ts +38 -1
- package/dist/core/base-pipeline.js +50 -7
- package/dist/core/build/app.d.ts +3 -4
- package/dist/core/build/app.js +3 -17
- package/dist/core/cache/handler.d.ts +29 -0
- package/dist/core/cache/handler.js +81 -0
- package/dist/core/config/schemas/base.d.ts +4 -0
- package/dist/core/config/schemas/base.js +4 -0
- package/dist/core/config/schemas/relative.d.ts +6 -0
- package/dist/core/constants.d.ts +27 -1
- package/dist/core/constants.js +14 -1
- package/dist/core/cookies/cookies.d.ts +7 -2
- package/dist/core/cookies/cookies.js +11 -4
- package/dist/core/cookies/response.d.ts +1 -1
- package/dist/core/cookies/response.js +1 -2
- package/dist/core/create-vite.js +15 -0
- package/dist/core/csp/runtime.js +6 -4
- package/dist/core/dev/dev.js +1 -1
- package/dist/core/errors/build-handler.d.ts +17 -0
- package/dist/core/errors/build-handler.js +22 -0
- package/dist/core/errors/default-handler.d.ts +14 -0
- package/dist/core/errors/default-handler.js +144 -0
- package/dist/core/errors/dev-handler.d.ts +21 -0
- package/dist/core/errors/dev-handler.js +82 -0
- package/dist/core/errors/handler.d.ts +9 -0
- package/dist/core/errors/handler.js +0 -0
- package/dist/core/fetch/default-handler.d.ts +17 -0
- package/dist/core/fetch/default-handler.js +45 -0
- package/dist/core/fetch/fetch-state.d.ts +244 -0
- package/dist/core/fetch/fetch-state.js +779 -0
- package/dist/core/fetch/index.d.ts +61 -0
- package/dist/core/fetch/index.js +121 -0
- package/dist/core/fetch/types.d.ts +6 -0
- package/dist/core/fetch/types.js +0 -0
- package/dist/core/fetch/vite-plugin.d.ts +5 -0
- package/dist/core/fetch/vite-plugin.js +69 -0
- package/dist/core/hono/index.d.ts +21 -0
- package/dist/core/hono/index.js +98 -0
- package/dist/core/i18n/handler.d.ts +18 -0
- package/dist/core/i18n/handler.js +119 -0
- package/dist/core/logger/core.d.ts +8 -0
- package/dist/core/logger/core.js +16 -0
- package/dist/core/messages/runtime.js +1 -1
- package/dist/core/middleware/astro-middleware.d.ts +27 -0
- package/dist/core/middleware/astro-middleware.js +53 -0
- package/dist/core/pages/handler.d.ts +20 -0
- package/dist/core/pages/handler.js +74 -0
- package/dist/core/preview/static-preview-server.js +3 -1
- package/dist/core/redirects/render.d.ts +2 -2
- package/dist/core/redirects/render.js +7 -8
- package/dist/core/rewrites/handler.d.ts +37 -0
- package/dist/core/rewrites/handler.js +67 -0
- package/dist/core/routing/3xx.js +8 -4
- package/dist/core/routing/handler.d.ts +17 -0
- package/dist/core/routing/handler.js +172 -0
- package/dist/core/routing/match.d.ts +0 -7
- package/dist/core/routing/match.js +0 -5
- package/dist/core/routing/trailing-slash-handler.d.ts +18 -0
- package/dist/core/routing/trailing-slash-handler.js +67 -0
- package/dist/core/session/drivers.d.ts +1 -1
- package/dist/core/session/handler.d.ts +11 -0
- package/dist/core/session/handler.js +33 -0
- package/dist/core/util/normalized-url.d.ts +10 -0
- package/dist/core/util/normalized-url.js +21 -0
- package/dist/i18n/middleware.d.ts +10 -0
- package/dist/i18n/middleware.js +4 -88
- package/dist/i18n/utils.js +2 -2
- package/dist/runtime/server/astro-island.js +57 -20
- package/dist/runtime/server/astro-island.prebuilt-dev.d.ts +1 -1
- package/dist/runtime/server/astro-island.prebuilt-dev.js +1 -1
- package/dist/runtime/server/astro-island.prebuilt.d.ts +1 -1
- package/dist/runtime/server/astro-island.prebuilt.js +1 -1
- package/dist/runtime/server/render/server-islands.js +2 -1
- package/dist/types/public/config.d.ts +46 -12
- package/dist/types/public/internal.d.ts +1 -1
- package/dist/vite-plugin-app/app.d.ts +4 -5
- package/dist/vite-plugin-app/app.js +20 -65
- package/package.json +11 -7
- package/dist/core/render-context.d.ts +0 -77
- package/dist/core/render-context.js +0 -826
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { APIContext } from '../types/public/context.js';
|
|
2
|
+
import type { FetchState } from '../core/fetch/fetch-state.js';
|
|
3
|
+
/**
|
|
4
|
+
* Handles Astro Action requests in both modes:
|
|
5
|
+
*
|
|
6
|
+
* - **RPC**: POST requests to `/_actions/<name>` (originating from the
|
|
7
|
+
* generated JS client). Runs the action and returns the serialized result
|
|
8
|
+
* as the response, so the caller can short-circuit rendering.
|
|
9
|
+
* - **Form**: POST requests with `?_action=<name>` targeting a page route
|
|
10
|
+
* (originating from an HTML `<form action={actions.foo}>`). Runs the
|
|
11
|
+
* action, stashes the result into `locals._actionPayload`, and returns
|
|
12
|
+
* `undefined` so the caller continues to render the page.
|
|
13
|
+
*
|
|
14
|
+
* Non-action requests are a no-op (`undefined`).
|
|
15
|
+
*
|
|
16
|
+
* This handler is invoked at the bottom of the middleware chain, before
|
|
17
|
+
* page dispatch. That placement preserves the existing behavior where
|
|
18
|
+
* user middleware sees action requests and response finalization (cookies,
|
|
19
|
+
* sessions, etc.) runs around the action response.
|
|
20
|
+
*/
|
|
21
|
+
export declare class ActionHandler {
|
|
22
|
+
#private;
|
|
23
|
+
/**
|
|
24
|
+
* Run action handling for the current request. Expects the APIContext
|
|
25
|
+
* that is already being used by the render pipeline.
|
|
26
|
+
*
|
|
27
|
+
* Returns a `Response` when the action fully handles the request (RPC),
|
|
28
|
+
* or `undefined` when the caller should continue processing the
|
|
29
|
+
* request (form actions or non-action requests).
|
|
30
|
+
*/
|
|
31
|
+
handle(apiContext: APIContext, state: FetchState): Promise<Response | undefined> | undefined;
|
|
32
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { PipelineFeatures } from "../core/base-pipeline.js";
|
|
2
|
+
import { getActionContext, serializeActionResult } from "./runtime/server.js";
|
|
3
|
+
class ActionHandler {
|
|
4
|
+
/**
|
|
5
|
+
* Run action handling for the current request. Expects the APIContext
|
|
6
|
+
* that is already being used by the render pipeline.
|
|
7
|
+
*
|
|
8
|
+
* Returns a `Response` when the action fully handles the request (RPC),
|
|
9
|
+
* or `undefined` when the caller should continue processing the
|
|
10
|
+
* request (form actions or non-action requests).
|
|
11
|
+
*/
|
|
12
|
+
handle(apiContext, state) {
|
|
13
|
+
state.pipeline.usedFeatures |= PipelineFeatures.actions;
|
|
14
|
+
if (apiContext.isPrerendered) {
|
|
15
|
+
return void 0;
|
|
16
|
+
}
|
|
17
|
+
const { action, setActionResult } = getActionContext(apiContext);
|
|
18
|
+
if (!action) {
|
|
19
|
+
return void 0;
|
|
20
|
+
}
|
|
21
|
+
return this.#executeAction(action, setActionResult);
|
|
22
|
+
}
|
|
23
|
+
async #executeAction(action, setActionResult) {
|
|
24
|
+
const actionResult = await action.handler();
|
|
25
|
+
const serialized = serializeActionResult(actionResult);
|
|
26
|
+
if (action.calledFrom === "rpc") {
|
|
27
|
+
if (serialized.type === "empty") {
|
|
28
|
+
return new Response(null, {
|
|
29
|
+
status: serialized.status
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return new Response(serialized.body, {
|
|
33
|
+
status: serialized.status,
|
|
34
|
+
headers: {
|
|
35
|
+
"Content-Type": serialized.contentType
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
setActionResult(action.name, serialized);
|
|
40
|
+
return void 0;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export {
|
|
44
|
+
ActionHandler
|
|
45
|
+
};
|
|
@@ -266,7 +266,7 @@ function handleFormDataGetAll(key, formData, validator) {
|
|
|
266
266
|
if (elementValidator instanceof z.$ZodNumber) {
|
|
267
267
|
return entries.map(Number);
|
|
268
268
|
} else if (elementValidator instanceof z.$ZodBoolean) {
|
|
269
|
-
return entries.map(Boolean);
|
|
269
|
+
return entries.map((v) => v === "true" ? true : v === "false" ? false : Boolean(v));
|
|
270
270
|
}
|
|
271
271
|
return entries;
|
|
272
272
|
}
|
|
@@ -234,7 +234,7 @@ function getStaticImageList() {
|
|
|
234
234
|
}
|
|
235
235
|
async function loadImage(path, env) {
|
|
236
236
|
if (isRemotePath(path)) {
|
|
237
|
-
return await loadRemoteImage(path);
|
|
237
|
+
return await loadRemoteImage(path, void 0, env.imageConfig);
|
|
238
238
|
}
|
|
239
239
|
return {
|
|
240
240
|
data: await fs.promises.readFile(getFullImagePath(path, env)),
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { type RemoteImageConfig } from '../utils/redirectValidation.js';
|
|
1
2
|
export type RemoteCacheEntry = {
|
|
2
3
|
data?: string;
|
|
3
4
|
expires: number;
|
|
4
5
|
etag?: string;
|
|
5
6
|
lastModified?: string;
|
|
6
7
|
};
|
|
7
|
-
export declare function loadRemoteImage(src: string, fetchFn?: typeof fetch): Promise<{
|
|
8
|
+
export declare function loadRemoteImage(src: string, fetchFn?: typeof fetch, imageConfig?: RemoteImageConfig): Promise<{
|
|
8
9
|
data: Buffer<ArrayBuffer>;
|
|
9
10
|
expires: number;
|
|
10
11
|
etag: string | undefined;
|
|
@@ -22,7 +23,7 @@ export declare function loadRemoteImage(src: string, fetchFn?: typeof fetch): Pr
|
|
|
22
23
|
export declare function revalidateRemoteImage(src: string, revalidationData: {
|
|
23
24
|
etag?: string;
|
|
24
25
|
lastModified?: string;
|
|
25
|
-
}, fetchFn?: typeof fetch): Promise<{
|
|
26
|
+
}, fetchFn?: typeof fetch, imageConfig?: RemoteImageConfig): Promise<{
|
|
26
27
|
data: Buffer<ArrayBuffer> | null;
|
|
27
28
|
expires: number;
|
|
28
29
|
etag: string | undefined;
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import CachePolicy from "http-cache-semantics";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const res = await
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import { fetchWithRedirects } from "../utils/redirectValidation.js";
|
|
3
|
+
async function loadRemoteImage(src, fetchFn = globalThis.fetch, imageConfig = { remotePatterns: [], domains: [] }) {
|
|
4
|
+
const res = await fetchWithRedirects({
|
|
5
|
+
url: src,
|
|
6
|
+
fetchFn,
|
|
7
|
+
imageConfig
|
|
8
|
+
});
|
|
8
9
|
if (!res.ok) {
|
|
9
10
|
throw new Error(
|
|
10
11
|
`Failed to load remote image ${src}. The request did not return a 200 OK response. (received ${res.status}))`
|
|
11
12
|
);
|
|
12
13
|
}
|
|
14
|
+
const req = new Request(src);
|
|
13
15
|
const policy = new CachePolicy(webToCachePolicyRequest(req), webToCachePolicyResponse(res));
|
|
14
16
|
const expires = policy.storable() ? policy.timeToLive() : 0;
|
|
15
17
|
return {
|
|
@@ -19,13 +21,18 @@ async function loadRemoteImage(src, fetchFn = globalThis.fetch) {
|
|
|
19
21
|
lastModified: res.headers.get("Last-Modified") ?? void 0
|
|
20
22
|
};
|
|
21
23
|
}
|
|
22
|
-
async function revalidateRemoteImage(src, revalidationData, fetchFn = globalThis.fetch) {
|
|
24
|
+
async function revalidateRemoteImage(src, revalidationData, fetchFn = globalThis.fetch, imageConfig = { remotePatterns: [], domains: [] }) {
|
|
23
25
|
const headers = {
|
|
24
26
|
...revalidationData.etag && { "If-None-Match": revalidationData.etag },
|
|
25
27
|
...revalidationData.lastModified && { "If-Modified-Since": revalidationData.lastModified }
|
|
26
28
|
};
|
|
27
29
|
const req = new Request(src, { headers, cache: "no-cache" });
|
|
28
|
-
const res = await
|
|
30
|
+
const res = await fetchWithRedirects({
|
|
31
|
+
url: src,
|
|
32
|
+
headers: new Headers(headers),
|
|
33
|
+
imageConfig,
|
|
34
|
+
fetchFn
|
|
35
|
+
});
|
|
29
36
|
if (!res.ok && res.status !== 304) {
|
|
30
37
|
if (res.status >= 300 && res.status < 400) {
|
|
31
38
|
throw new Error(
|
|
@@ -38,7 +45,7 @@ async function revalidateRemoteImage(src, revalidationData, fetchFn = globalThis
|
|
|
38
45
|
}
|
|
39
46
|
const data = Buffer.from(await res.arrayBuffer());
|
|
40
47
|
if (res.ok && !data.length) {
|
|
41
|
-
return await loadRemoteImage(src, fetchFn);
|
|
48
|
+
return await loadRemoteImage(src, fetchFn, imageConfig);
|
|
42
49
|
}
|
|
43
50
|
const policy = new CachePolicy(
|
|
44
51
|
webToCachePolicyRequest(req),
|
|
@@ -4,24 +4,7 @@ import { isRemoteAllowed } from "@astrojs/internal-helpers/remote";
|
|
|
4
4
|
import * as mime from "mrmime";
|
|
5
5
|
import { getConfiguredImageService } from "../internal.js";
|
|
6
6
|
import { etag } from "../utils/etag.js";
|
|
7
|
-
|
|
8
|
-
try {
|
|
9
|
-
const res = await fetch(src, {
|
|
10
|
-
// Forward all headers from the original request
|
|
11
|
-
headers,
|
|
12
|
-
redirect: "manual"
|
|
13
|
-
});
|
|
14
|
-
if (res.status >= 300 && res.status < 400) {
|
|
15
|
-
return void 0;
|
|
16
|
-
}
|
|
17
|
-
if (!res.ok) {
|
|
18
|
-
return void 0;
|
|
19
|
-
}
|
|
20
|
-
return await res.arrayBuffer();
|
|
21
|
-
} catch {
|
|
22
|
-
return void 0;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
7
|
+
import { loadImage } from "./loadImage.js";
|
|
25
8
|
const GET = async ({ request }) => {
|
|
26
9
|
try {
|
|
27
10
|
const imageService = await getConfiguredImageService();
|
|
@@ -42,7 +25,12 @@ const GET = async ({ request }) => {
|
|
|
42
25
|
if (!isRemoteImage && sourceUrl.origin !== url.origin) {
|
|
43
26
|
return new Response("Forbidden", { status: 403 });
|
|
44
27
|
}
|
|
45
|
-
inputBuffer = await
|
|
28
|
+
inputBuffer = await loadImage(
|
|
29
|
+
sourceUrl,
|
|
30
|
+
isRemoteImage ? new Headers() : request.headers,
|
|
31
|
+
imageConfig,
|
|
32
|
+
isRemoteImage
|
|
33
|
+
);
|
|
46
34
|
if (!inputBuffer) {
|
|
47
35
|
return new Response("Not Found", { status: 404 });
|
|
48
36
|
}
|
|
@@ -62,7 +50,7 @@ const GET = async ({ request }) => {
|
|
|
62
50
|
});
|
|
63
51
|
} catch (err) {
|
|
64
52
|
console.error("Could not process image request:", err);
|
|
65
|
-
return new Response(
|
|
53
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
66
54
|
}
|
|
67
55
|
};
|
|
68
56
|
export {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RemoteImageConfig } from '../utils/redirectValidation.js';
|
|
2
|
+
/**
|
|
3
|
+
* Fetches an image by URL. Used by the generic image endpoint for both
|
|
4
|
+
* remote images and local images (self-fetched from the same origin).
|
|
5
|
+
*
|
|
6
|
+
* For remote images, the final URL (after any redirects) is validated
|
|
7
|
+
* against `imageConfig.domains` and `imageConfig.remotePatterns`.
|
|
8
|
+
* Local images skip this check — they are already guarded by the
|
|
9
|
+
* same-origin check in the caller.
|
|
10
|
+
*/
|
|
11
|
+
export declare function loadImage(src: URL, headers: Headers, imageConfig: RemoteImageConfig, isRemote: boolean, fetchFn?: typeof globalThis.fetch): Promise<ArrayBuffer | undefined>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { isRemoteAllowed } from "@astrojs/internal-helpers/remote";
|
|
2
|
+
import { fetchWithRedirects } from "../utils/redirectValidation.js";
|
|
3
|
+
async function loadImage(src, headers, imageConfig, isRemote, fetchFn) {
|
|
4
|
+
try {
|
|
5
|
+
const res = await fetchWithRedirects({ url: src, headers, imageConfig, fetchFn });
|
|
6
|
+
if (isRemote && !isRemoteAllowed(res.url, imageConfig)) {
|
|
7
|
+
return void 0;
|
|
8
|
+
}
|
|
9
|
+
if (!res.ok) {
|
|
10
|
+
return void 0;
|
|
11
|
+
}
|
|
12
|
+
return await res.arrayBuffer();
|
|
13
|
+
} catch {
|
|
14
|
+
return void 0;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
loadImage
|
|
19
|
+
};
|
|
@@ -5,10 +5,15 @@ import * as mime from "mrmime";
|
|
|
5
5
|
import { getConfiguredImageService } from "../internal.js";
|
|
6
6
|
import { etag } from "../utils/etag.js";
|
|
7
7
|
import { inferSourceFormat } from "../utils/inferSourceFormat.js";
|
|
8
|
+
import { fetchWithRedirects } from "../utils/redirectValidation.js";
|
|
9
|
+
const isLocal = (url) => {
|
|
10
|
+
const hostname = new URL(url).hostname;
|
|
11
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
12
|
+
};
|
|
8
13
|
async function loadRemoteImage(src) {
|
|
9
14
|
try {
|
|
10
|
-
const res = await
|
|
11
|
-
if (res.
|
|
15
|
+
const res = await fetchWithRedirects({ url: src, imageConfig });
|
|
16
|
+
if (!isRemoteAllowed(res.url, imageConfig) && !isLocal(res.url)) {
|
|
12
17
|
return void 0;
|
|
13
18
|
}
|
|
14
19
|
if (!res.ok) {
|
package/dist/assets/index.d.ts
CHANGED
|
@@ -2,3 +2,4 @@ export { getConfiguredImageService, getImage, verifyOptions } from './internal.j
|
|
|
2
2
|
export { baseService, isLocalService } from './services/service.js';
|
|
3
3
|
export { hashTransform, propsToFilename } from './utils/hash.js';
|
|
4
4
|
export type { LocalImageProps, RemoteImageProps } from './types.js';
|
|
5
|
+
export { fetchWithRedirects } from './utils/redirectValidation.js';
|
package/dist/assets/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { getConfiguredImageService, getImage, verifyOptions } from "./internal.js";
|
|
2
2
|
import { baseService, isLocalService } from "./services/service.js";
|
|
3
3
|
import { hashTransform, propsToFilename } from "./utils/hash.js";
|
|
4
|
+
import { fetchWithRedirects } from "./utils/redirectValidation.js";
|
|
4
5
|
export {
|
|
5
6
|
baseService,
|
|
7
|
+
fetchWithRedirects,
|
|
6
8
|
getConfiguredImageService,
|
|
7
9
|
getImage,
|
|
8
10
|
hashTransform,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AstroError, AstroErrorData } from "../../core/errors/index.js";
|
|
2
|
+
import { detector } from "../utils/vendor/image-size/detector.js";
|
|
2
3
|
import {
|
|
3
4
|
baseService,
|
|
4
5
|
parseQuality
|
|
@@ -82,6 +83,12 @@ const sharpService = {
|
|
|
82
83
|
const transform = transformOptions;
|
|
83
84
|
const kernel = config.service.config.kernel;
|
|
84
85
|
if (transform.format === "svg") return { data: inputBuffer, format: "svg" };
|
|
86
|
+
if (detector(inputBuffer) === "svg" && !config.dangerouslyProcessSVG) {
|
|
87
|
+
throw new AstroError({
|
|
88
|
+
...AstroErrorData.UnsupportedImageFormat,
|
|
89
|
+
message: "SVG image processing is disabled. Set `image.dangerouslyProcessSVG: true` to allow processing of SVG sources."
|
|
90
|
+
});
|
|
91
|
+
}
|
|
85
92
|
const result = sharp(inputBuffer, {
|
|
86
93
|
failOnError: false,
|
|
87
94
|
pages: -1,
|
|
@@ -10,3 +10,4 @@ export { isESMImportedImage, isRemoteImage, resolveSrc } from './imageKind.js';
|
|
|
10
10
|
export { imageMetadata } from './metadata.js';
|
|
11
11
|
export { getOrigQueryParams } from './queryParams.js';
|
|
12
12
|
export { inferRemoteSize } from './remoteProbe.js';
|
|
13
|
+
export { fetchWithRedirects } from './redirectValidation.js';
|
|
@@ -7,8 +7,10 @@ import { isESMImportedImage, isRemoteImage, resolveSrc } from "./imageKind.js";
|
|
|
7
7
|
import { imageMetadata } from "./metadata.js";
|
|
8
8
|
import { getOrigQueryParams } from "./queryParams.js";
|
|
9
9
|
import { inferRemoteSize } from "./remoteProbe.js";
|
|
10
|
+
import { fetchWithRedirects } from "./redirectValidation.js";
|
|
10
11
|
export {
|
|
11
12
|
emitClientAsset,
|
|
13
|
+
fetchWithRedirects,
|
|
12
14
|
getOrigQueryParams,
|
|
13
15
|
imageMetadata,
|
|
14
16
|
inferRemoteSize,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for handling HTTP redirects with validation
|
|
3
|
+
*/
|
|
4
|
+
import type { AstroConfig } from '../../types/public/config.js';
|
|
5
|
+
export type RemoteImageConfig = Pick<AstroConfig['image'], 'remotePatterns' | 'domains'>;
|
|
6
|
+
export type FetchRedirectOptions = {
|
|
7
|
+
/**
|
|
8
|
+
* URL to fetch (either string or URL object)
|
|
9
|
+
*/
|
|
10
|
+
url: string | URL;
|
|
11
|
+
/**
|
|
12
|
+
* Headers to include in the request (optional)
|
|
13
|
+
*/
|
|
14
|
+
headers?: Headers;
|
|
15
|
+
/**
|
|
16
|
+
* Image config for validating redirect destinations (optional)
|
|
17
|
+
*/
|
|
18
|
+
imageConfig: RemoteImageConfig;
|
|
19
|
+
/**
|
|
20
|
+
* Fetch function to use (default: globalThis.fetch)
|
|
21
|
+
*/
|
|
22
|
+
fetchFn?: typeof fetch;
|
|
23
|
+
/**
|
|
24
|
+
* Maximum number of redirects to follow (default: 10)
|
|
25
|
+
*/
|
|
26
|
+
redirectLimit?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Error handler for redirect depth exceeded (default: generic Error)
|
|
29
|
+
*/
|
|
30
|
+
onMaxRedirectsExceeded?: (url: string) => Error;
|
|
31
|
+
/**
|
|
32
|
+
* Error handler for missing Location header (default: generic Error)
|
|
33
|
+
*/
|
|
34
|
+
onMissingLocationHeader?: (status: number, url: string) => Error;
|
|
35
|
+
/**
|
|
36
|
+
* Error handler for disallowed redirect (default: generic Error)
|
|
37
|
+
*/
|
|
38
|
+
onDisallowedRedirect?: (currentUrl: string, targetUrl: string) => Error;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Recursively follows HTTP redirects with validation according to the image configuration.
|
|
42
|
+
*
|
|
43
|
+
* If any of the domains in the redirect chain are not allowed by either `image.remotePatterns`
|
|
44
|
+
* or `image.domains`, this function will throw an error for a disallowed redirect.
|
|
45
|
+
*
|
|
46
|
+
* @param options The options for this fetch call.
|
|
47
|
+
*/
|
|
48
|
+
export declare function fetchWithRedirects(options: FetchRedirectOptions): Promise<Response>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { isRemoteAllowed } from "@astrojs/internal-helpers/remote";
|
|
2
|
+
async function fetchWithRedirects(options) {
|
|
3
|
+
const {
|
|
4
|
+
url,
|
|
5
|
+
headers,
|
|
6
|
+
imageConfig,
|
|
7
|
+
fetchFn = globalThis.fetch,
|
|
8
|
+
redirectLimit = 10,
|
|
9
|
+
onMaxRedirectsExceeded = (_u) => new Error("Maximum redirect depth exceeded"),
|
|
10
|
+
onMissingLocationHeader = (_s, _u) => new Error(`Redirect response ${_s} missing Location header`),
|
|
11
|
+
onDisallowedRedirect = (_current, _target) => new Error(
|
|
12
|
+
`The image at ${_current} redirected to ${_target}, which is not an allowed remote location.`
|
|
13
|
+
)
|
|
14
|
+
} = options;
|
|
15
|
+
if (redirectLimit <= 0) {
|
|
16
|
+
throw onMaxRedirectsExceeded(typeof url === "string" ? url : url.toString());
|
|
17
|
+
}
|
|
18
|
+
const urlString = typeof url === "string" ? url : url.toString();
|
|
19
|
+
const req = new Request(url, { headers });
|
|
20
|
+
const res = await fetchFn(req, { redirect: "manual" });
|
|
21
|
+
if ([301, 302, 303, 307, 308].includes(res.status)) {
|
|
22
|
+
const location = res.headers.get("Location");
|
|
23
|
+
if (!location) {
|
|
24
|
+
throw onMissingLocationHeader(res.status, urlString);
|
|
25
|
+
}
|
|
26
|
+
const redirectUrl = new URL(location, urlString).toString();
|
|
27
|
+
if (!isRemoteAllowed(redirectUrl, {
|
|
28
|
+
domains: imageConfig.domains ?? [],
|
|
29
|
+
remotePatterns: imageConfig.remotePatterns ?? []
|
|
30
|
+
})) {
|
|
31
|
+
throw onDisallowedRedirect(urlString, redirectUrl);
|
|
32
|
+
}
|
|
33
|
+
return fetchWithRedirects({
|
|
34
|
+
url: redirectUrl,
|
|
35
|
+
headers,
|
|
36
|
+
imageConfig,
|
|
37
|
+
fetchFn,
|
|
38
|
+
redirectLimit: redirectLimit - 1,
|
|
39
|
+
onMaxRedirectsExceeded,
|
|
40
|
+
onMissingLocationHeader,
|
|
41
|
+
onDisallowedRedirect
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return res;
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
fetchWithRedirects
|
|
48
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isRemoteAllowed } from "@astrojs/internal-helpers/remote";
|
|
2
2
|
import { AstroError, AstroErrorData } from "../../core/errors/index.js";
|
|
3
3
|
import { imageMetadata } from "./metadata.js";
|
|
4
|
+
import { fetchWithRedirects } from "./redirectValidation.js";
|
|
4
5
|
async function inferRemoteSize(url, imageConfig) {
|
|
5
6
|
if (!URL.canParse(url)) {
|
|
6
7
|
throw new AstroError({
|
|
@@ -27,13 +28,35 @@ async function inferRemoteSize(url, imageConfig) {
|
|
|
27
28
|
message: AstroErrorData.RemoteImageNotAllowed.message(url)
|
|
28
29
|
});
|
|
29
30
|
}
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
let response;
|
|
32
|
+
try {
|
|
33
|
+
response = await fetchWithRedirects({
|
|
34
|
+
url,
|
|
35
|
+
onMaxRedirectsExceeded: (u) => new AstroError({
|
|
36
|
+
...AstroErrorData.FailedToFetchRemoteImageDimensions,
|
|
37
|
+
message: AstroErrorData.FailedToFetchRemoteImageDimensions.message(u)
|
|
38
|
+
}),
|
|
39
|
+
onMissingLocationHeader: (_status, u) => new AstroError({
|
|
40
|
+
...AstroErrorData.FailedToFetchRemoteImageDimensions,
|
|
41
|
+
message: AstroErrorData.FailedToFetchRemoteImageDimensions.message(u)
|
|
42
|
+
}),
|
|
43
|
+
imageConfig: imageConfig ?? {
|
|
44
|
+
remotePatterns: [],
|
|
45
|
+
domains: []
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
} catch (_err) {
|
|
32
49
|
throw new AstroError({
|
|
33
50
|
...AstroErrorData.FailedToFetchRemoteImageDimensions,
|
|
34
51
|
message: AstroErrorData.FailedToFetchRemoteImageDimensions.message(url)
|
|
35
52
|
});
|
|
36
53
|
}
|
|
54
|
+
if (allowlistConfig && !isRemoteAllowed(response.url, allowlistConfig)) {
|
|
55
|
+
throw new AstroError({
|
|
56
|
+
...AstroErrorData.RemoteImageNotAllowed,
|
|
57
|
+
message: AstroErrorData.RemoteImageNotAllowed.message(url)
|
|
58
|
+
});
|
|
59
|
+
}
|
|
37
60
|
if (!response.body || !response.ok) {
|
|
38
61
|
throw new AstroError({
|
|
39
62
|
...AstroErrorData.FailedToFetchRemoteImageDimensions,
|
package/dist/container/index.js
CHANGED
|
@@ -3,9 +3,11 @@ import { getDefaultClientDirectives } from "../core/client-directive/index.js";
|
|
|
3
3
|
import { ASTRO_CONFIG_DEFAULTS } from "../core/config/schemas/index.js";
|
|
4
4
|
import { validateConfig } from "../core/config/validate.js";
|
|
5
5
|
import { createKey } from "../core/encryption.js";
|
|
6
|
+
import { FetchState } from "../core/fetch/fetch-state.js";
|
|
7
|
+
import { AstroMiddleware } from "../core/middleware/astro-middleware.js";
|
|
6
8
|
import { NOOP_MIDDLEWARE_FN } from "../core/middleware/noop-middleware.js";
|
|
9
|
+
import { PagesHandler } from "../core/pages/handler.js";
|
|
7
10
|
import { removeLeadingForwardSlash } from "../core/path.js";
|
|
8
|
-
import { RenderContext } from "../core/render-context.js";
|
|
9
11
|
import { getParts } from "../core/routing/parts.js";
|
|
10
12
|
import { getPattern } from "../core/routing/pattern.js";
|
|
11
13
|
import { validateSegment } from "../core/routing/segment.js";
|
|
@@ -69,6 +71,8 @@ function createManifest(manifest, renderers, middleware) {
|
|
|
69
71
|
}
|
|
70
72
|
class experimental_AstroContainer {
|
|
71
73
|
#pipeline;
|
|
74
|
+
#astroMiddleware;
|
|
75
|
+
#pagesHandler;
|
|
72
76
|
/**
|
|
73
77
|
* Internally used to check if the container was created with a manifest.
|
|
74
78
|
* @private
|
|
@@ -95,6 +99,8 @@ class experimental_AstroContainer {
|
|
|
95
99
|
return specifier;
|
|
96
100
|
}
|
|
97
101
|
});
|
|
102
|
+
this.#astroMiddleware = new AstroMiddleware(this.#pipeline);
|
|
103
|
+
this.#pagesHandler = new PagesHandler(this.#pipeline);
|
|
98
104
|
}
|
|
99
105
|
async #containerResolve(specifier, astroConfig) {
|
|
100
106
|
const found = this.#pipeline.manifest.entryModules[specifier];
|
|
@@ -285,23 +291,21 @@ class experimental_AstroContainer {
|
|
|
285
291
|
params: options.params,
|
|
286
292
|
type: routeType
|
|
287
293
|
});
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
partial: options?.partial ?? true,
|
|
296
|
-
clientAddress: ""
|
|
297
|
-
});
|
|
294
|
+
const state = new FetchState(this.#pipeline, request);
|
|
295
|
+
state.routeData = routeData;
|
|
296
|
+
state.pathname = url.pathname;
|
|
297
|
+
state.clientAddress = "";
|
|
298
|
+
state.partial = options?.partial ?? true;
|
|
299
|
+
state.componentInstance = componentInstance;
|
|
300
|
+
state.slots = slots ?? {};
|
|
298
301
|
if (options.params) {
|
|
299
|
-
|
|
302
|
+
state.params = options.params;
|
|
300
303
|
}
|
|
304
|
+
state.locals = options?.locals ?? {};
|
|
301
305
|
if (options.props) {
|
|
302
|
-
|
|
306
|
+
state.initialProps = options.props;
|
|
303
307
|
}
|
|
304
|
-
return
|
|
308
|
+
return this.#astroMiddleware.handle(state, this.#pagesHandler.handle.bind(this.#pagesHandler));
|
|
305
309
|
}
|
|
306
310
|
/**
|
|
307
311
|
* It stores an Astro **page** route. The first argument, `route`, gets associated to the `component`.
|
|
@@ -39,7 +39,6 @@ class ContentLayer {
|
|
|
39
39
|
watcher,
|
|
40
40
|
contentConfigObserver = globalContentConfigObserver
|
|
41
41
|
}) {
|
|
42
|
-
watcher?.setMaxListeners(50);
|
|
43
42
|
this.#logger = logger;
|
|
44
43
|
this.#store = store;
|
|
45
44
|
this.#settings = settings;
|
|
@@ -192,7 +191,7 @@ ${contentConfig.error.message}`
|
|
|
192
191
|
logger.info("Content config changed");
|
|
193
192
|
shouldClear = true;
|
|
194
193
|
}
|
|
195
|
-
if (previousAstroVersion && previousAstroVersion !== "6.
|
|
194
|
+
if (previousAstroVersion && previousAstroVersion !== "6.3.1") {
|
|
196
195
|
logger.info("Astro version changed");
|
|
197
196
|
shouldClear = true;
|
|
198
197
|
}
|
|
@@ -200,8 +199,8 @@ ${contentConfig.error.message}`
|
|
|
200
199
|
logger.info("Clearing content store");
|
|
201
200
|
this.#store.clearAll();
|
|
202
201
|
}
|
|
203
|
-
if ("6.
|
|
204
|
-
this.#store.metaStore().set("astro-version", "6.
|
|
202
|
+
if ("6.3.1") {
|
|
203
|
+
this.#store.metaStore().set("astro-version", "6.3.1");
|
|
205
204
|
}
|
|
206
205
|
if (currentConfigDigest) {
|
|
207
206
|
this.#store.metaStore().set("content-config-digest", currentConfigDigest);
|
|
@@ -6,10 +6,6 @@ async function attachContentServerListeners({
|
|
|
6
6
|
logger,
|
|
7
7
|
settings
|
|
8
8
|
}) {
|
|
9
|
-
const maxListeners = viteServer.watcher.getMaxListeners();
|
|
10
|
-
if (maxListeners !== 0 && maxListeners < 50) {
|
|
11
|
-
viteServer.watcher.setMaxListeners(50);
|
|
12
|
-
}
|
|
13
9
|
const contentGenerator = await createContentTypesGenerator({
|
|
14
10
|
fs,
|
|
15
11
|
settings,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import nodeFs from "node:fs";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { dataToEsm } from "@rollup/pluginutils";
|
|
4
|
-
import { normalizePath } from "vite";
|
|
4
|
+
import { isRunnableDevEnvironment, normalizePath } from "vite";
|
|
5
5
|
import { ASTRO_VITE_ENVIRONMENT_NAMES } from "../core/constants.js";
|
|
6
6
|
import { AstroError, AstroErrorData } from "../core/errors/index.js";
|
|
7
7
|
import { rootRelativePath } from "../core/viteUtils.js";
|
|
@@ -30,6 +30,14 @@ function invalidateDataStore(viteServer) {
|
|
|
30
30
|
const timestamp = Date.now();
|
|
31
31
|
environment.moduleGraph.invalidateModule(module, void 0, timestamp, true);
|
|
32
32
|
}
|
|
33
|
+
if (isRunnableDevEnvironment(environment)) {
|
|
34
|
+
const runnerModule = environment.runner.evaluatedModules.getModuleById(
|
|
35
|
+
RESOLVED_DATA_STORE_VIRTUAL_ID
|
|
36
|
+
);
|
|
37
|
+
if (runnerModule) {
|
|
38
|
+
environment.runner.evaluatedModules.invalidateModule(runnerModule);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
33
41
|
environment.hot.send("astro:content-changed", {});
|
|
34
42
|
viteServer.environments.client.hot.send({
|
|
35
43
|
type: "full-reload",
|