mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 14:03:33 +09:00
Merge main into Advanced styles branch
This commit is contained in:
commit
13e4f35443
46 files changed
+479
-30
No files matched your search
@@ -354,6 +354,8 @@ Resource actions need a server. Webstudio Cloud provides this; a static export c
|
||||
|
||||
Self-hosted sites need a supported server setup and an email integration to send Email Resources. See [self-hosting](../self-hosting/README.md).
|
||||
|
||||
For a Docker or other Node export, an HTTP Resource action can send to a public API on a sibling subdomain, such as `cms.example.com` from `site.example.com`. The export blocks known site hostnames and rejects marked requests that return through another alias. Rebuild and redeploy the export with an updated CLI; installing a newer SDK in an existing container does not change the generated destination policy.
|
||||
|
||||
## Related
|
||||
|
||||
- [Variables](../foundations/variables.md) — Resources, scope, and bindings.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type ComponentProps, memo, useMemo } from "react";
|
||||
import {
|
||||
type MetaFunction,
|
||||
@@ -98,6 +99,11 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
// Node exports have no Worker entrypoint guard; stop requests through aliases here.
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProjectRequest(
|
||||
arg.request,
|
||||
authRoutes,
|
||||
@@ -263,6 +269,10 @@ export const action = async ({
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] } | ManagedFormResponse
|
||||
> => {
|
||||
if (isProtectedResourceReentry(request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
authenticateProjectRequest(request, authRoutes, projectDomain);
|
||||
|
||||
let isManagedFormRequest = false;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type ComponentProps, memo, useMemo } from "react";
|
||||
import {
|
||||
type MetaFunction,
|
||||
@@ -98,6 +99,11 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
// Node exports have no Worker entrypoint guard; stop requests through aliases here.
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProjectRequest(
|
||||
arg.request,
|
||||
authRoutes,
|
||||
@@ -263,6 +269,10 @@ export const action = async ({
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] } | ManagedFormResponse
|
||||
> => {
|
||||
if (isProtectedResourceReentry(request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
authenticateProjectRequest(request, authRoutes, projectDomain);
|
||||
|
||||
let isManagedFormRequest = false;
|
||||
|
||||
+5
-3
@@ -1,13 +1,15 @@
|
||||
import { getDeniedResourceHostnames } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { createNodeProtectedResourceFetch } from "@webstudio-is/sdk/protected-resource-fetch-node";
|
||||
export const createManagedFormEmailSender = (_input: { context: unknown; formData: FormData; projectId: string }) => undefined;
|
||||
export const validateManagedFormEmail = (_request: unknown, _formData: FormData) => undefined;
|
||||
export const createManagedFormResourceFetch = ({ request, context, projectDomain }: { request: Request; context: unknown; projectDomain?: string }) => {
|
||||
void context;
|
||||
return createNodeProtectedResourceFetch({
|
||||
deniedHostnames: getDeniedResourceHostnames([
|
||||
projectId: "d845c167-ea07-4875-b08d-83e97c09dcce",
|
||||
matchSubdomains: false,
|
||||
deniedHostnames: [
|
||||
new URL(request.url).hostname,
|
||||
projectDomain,
|
||||
]),
|
||||
...["cli-basic-test-d0osr"],
|
||||
].filter((hostname): hostname is string => hostname !== undefined),
|
||||
});
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type ComponentProps, memo, useMemo } from "react";
|
||||
import {
|
||||
type MetaFunction,
|
||||
@@ -98,6 +99,11 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
// Node exports have no Worker entrypoint guard; stop requests through aliases here.
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProjectRequest(
|
||||
arg.request,
|
||||
authRoutes,
|
||||
@@ -263,6 +269,10 @@ export const action = async ({
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] } | ManagedFormResponse
|
||||
> => {
|
||||
if (isProtectedResourceReentry(request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
authenticateProjectRequest(request, authRoutes, projectDomain);
|
||||
|
||||
let isManagedFormRequest = false;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type ComponentProps, memo, useMemo } from "react";
|
||||
import {
|
||||
type MetaFunction,
|
||||
@@ -98,6 +99,11 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
// Node exports have no Worker entrypoint guard; stop requests through aliases here.
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProjectRequest(
|
||||
arg.request,
|
||||
authRoutes,
|
||||
@@ -263,6 +269,10 @@ export const action = async ({
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] } | ManagedFormResponse
|
||||
> => {
|
||||
if (isProtectedResourceReentry(request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
authenticateProjectRequest(request, authRoutes, projectDomain);
|
||||
|
||||
let isManagedFormRequest = false;
|
||||
|
||||
+5
-3
@@ -1,13 +1,15 @@
|
||||
import { getDeniedResourceHostnames } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { createNodeProtectedResourceFetch } from "@webstudio-is/sdk/protected-resource-fetch-node";
|
||||
export const createManagedFormEmailSender = (_input: { context: unknown; formData: FormData; projectId: string }) => undefined;
|
||||
export const validateManagedFormEmail = (_request: unknown, _formData: FormData) => undefined;
|
||||
export const createManagedFormResourceFetch = ({ request, context, projectDomain }: { request: Request; context: unknown; projectDomain?: string }) => {
|
||||
void context;
|
||||
return createNodeProtectedResourceFetch({
|
||||
deniedHostnames: getDeniedResourceHostnames([
|
||||
projectId: "d845c167-ea07-4875-b08d-83e97c09dcce",
|
||||
matchSubdomains: false,
|
||||
deniedHostnames: [
|
||||
new URL(request.url).hostname,
|
||||
projectDomain,
|
||||
]),
|
||||
...["cli-basic-test-d0osr"],
|
||||
].filter((hostname): hostname is string => hostname !== undefined),
|
||||
});
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type ComponentProps, memo, useMemo } from "react";
|
||||
import {
|
||||
type MetaFunction,
|
||||
@@ -98,6 +99,11 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
// Node exports have no Worker entrypoint guard; stop requests through aliases here.
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProjectRequest(
|
||||
arg.request,
|
||||
authRoutes,
|
||||
@@ -263,6 +269,10 @@ export const action = async ({
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] } | ManagedFormResponse
|
||||
> => {
|
||||
if (isProtectedResourceReentry(request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
authenticateProjectRequest(request, authRoutes, projectDomain);
|
||||
|
||||
let isManagedFormRequest = false;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type ComponentProps, memo, useMemo } from "react";
|
||||
import {
|
||||
type MetaFunction,
|
||||
@@ -98,6 +99,11 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
// Node exports have no Worker entrypoint guard; stop requests through aliases here.
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProjectRequest(
|
||||
arg.request,
|
||||
authRoutes,
|
||||
@@ -263,6 +269,10 @@ export const action = async ({
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] } | ManagedFormResponse
|
||||
> => {
|
||||
if (isProtectedResourceReentry(request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
authenticateProjectRequest(request, authRoutes, projectDomain);
|
||||
|
||||
let isManagedFormRequest = false;
|
||||
|
||||
+5
-3
@@ -1,13 +1,15 @@
|
||||
import { getDeniedResourceHostnames } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { createNodeProtectedResourceFetch } from "@webstudio-is/sdk/protected-resource-fetch-node";
|
||||
export const createManagedFormEmailSender = (_input: { context: unknown; formData: FormData; projectId: string }) => undefined;
|
||||
export const validateManagedFormEmail = (_request: unknown, _formData: FormData) => undefined;
|
||||
export const createManagedFormResourceFetch = ({ request, context, projectDomain }: { request: Request; context: unknown; projectDomain?: string }) => {
|
||||
void context;
|
||||
return createNodeProtectedResourceFetch({
|
||||
deniedHostnames: getDeniedResourceHostnames([
|
||||
projectId: "d845c167-ea07-4875-b08d-83e97c09dcce",
|
||||
matchSubdomains: false,
|
||||
deniedHostnames: [
|
||||
new URL(request.url).hostname,
|
||||
projectDomain,
|
||||
]),
|
||||
...["cli-basic-test-d0osr"],
|
||||
].filter((hostname): hostname is string => hostname !== undefined),
|
||||
});
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type ComponentProps, memo, useMemo } from "react";
|
||||
import {
|
||||
type MetaFunction,
|
||||
@@ -98,6 +99,11 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
// Node exports have no Worker entrypoint guard; stop requests through aliases here.
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProjectRequest(
|
||||
arg.request,
|
||||
authRoutes,
|
||||
@@ -263,6 +269,10 @@ export const action = async ({
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] } | ManagedFormResponse
|
||||
> => {
|
||||
if (isProtectedResourceReentry(request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
authenticateProjectRequest(request, authRoutes, projectDomain);
|
||||
|
||||
let isManagedFormRequest = false;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type ComponentProps, memo, useMemo } from "react";
|
||||
import {
|
||||
type MetaFunction,
|
||||
@@ -98,6 +99,11 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
// Node exports have no Worker entrypoint guard; stop requests through aliases here.
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProjectRequest(
|
||||
arg.request,
|
||||
authRoutes,
|
||||
@@ -263,6 +269,10 @@ export const action = async ({
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] } | ManagedFormResponse
|
||||
> => {
|
||||
if (isProtectedResourceReentry(request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
authenticateProjectRequest(request, authRoutes, projectDomain);
|
||||
|
||||
let isManagedFormRequest = false;
|
||||
|
||||
+5
-3
@@ -1,13 +1,15 @@
|
||||
import { getDeniedResourceHostnames } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { createNodeProtectedResourceFetch } from "@webstudio-is/sdk/protected-resource-fetch-node";
|
||||
export const createManagedFormEmailSender = (_input: { context: unknown; formData: FormData; projectId: string }) => undefined;
|
||||
export const validateManagedFormEmail = (_request: unknown, _formData: FormData) => undefined;
|
||||
export const createManagedFormResourceFetch = ({ request, context, projectDomain }: { request: Request; context: unknown; projectDomain?: string }) => {
|
||||
void context;
|
||||
return createNodeProtectedResourceFetch({
|
||||
deniedHostnames: getDeniedResourceHostnames([
|
||||
projectId: "d845c167-ea07-4875-b08d-83e97c09dcce",
|
||||
matchSubdomains: false,
|
||||
deniedHostnames: [
|
||||
new URL(request.url).hostname,
|
||||
projectDomain,
|
||||
]),
|
||||
...[],
|
||||
].filter((hostname): hostname is string => hostname !== undefined),
|
||||
});
|
||||
};
|
||||
Generated
+5
-3
@@ -1,13 +1,15 @@
|
||||
import { getDeniedResourceHostnames } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { createNodeProtectedResourceFetch } from "@webstudio-is/sdk/protected-resource-fetch-node";
|
||||
export const createManagedFormEmailSender = (_input: { context: unknown; formData: FormData; projectId: string }) => undefined;
|
||||
export const validateManagedFormEmail = (_request: unknown, _formData: FormData) => undefined;
|
||||
export const createManagedFormResourceFetch = ({ request, context, projectDomain }: { request: Request; context: unknown; projectDomain?: string }) => {
|
||||
void context;
|
||||
return createNodeProtectedResourceFetch({
|
||||
deniedHostnames: getDeniedResourceHostnames([
|
||||
projectId: "8a7358b1-7de3-459d-b7b1-56dddfb6ce1e",
|
||||
matchSubdomains: false,
|
||||
deniedHostnames: [
|
||||
new URL(request.url).hostname,
|
||||
projectDomain,
|
||||
]),
|
||||
...[],
|
||||
].filter((hostname): hostname is string => hostname !== undefined),
|
||||
});
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type ComponentProps, memo, useMemo } from "react";
|
||||
import {
|
||||
type ServerRuntimeMetaFunction as MetaFunction,
|
||||
@@ -117,6 +118,11 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
// Node exports have no Worker entrypoint guard; stop requests through aliases here.
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProductionRequest(arg.request);
|
||||
|
||||
const url = new URL(arg.request.url);
|
||||
@@ -278,6 +284,10 @@ export const action = async ({
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] } | ManagedFormResponse
|
||||
> => {
|
||||
if (isProtectedResourceReentry(request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
authenticateProductionRequest(request);
|
||||
|
||||
let isManagedFormRequest = false;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type ComponentProps, memo, useMemo } from "react";
|
||||
import {
|
||||
type ServerRuntimeMetaFunction as MetaFunction,
|
||||
@@ -117,6 +118,11 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
// Node exports have no Worker entrypoint guard; stop requests through aliases here.
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProductionRequest(arg.request);
|
||||
|
||||
const url = new URL(arg.request.url);
|
||||
@@ -278,6 +284,10 @@ export const action = async ({
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] } | ManagedFormResponse
|
||||
> => {
|
||||
if (isProtectedResourceReentry(request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
authenticateProductionRequest(request);
|
||||
|
||||
let isManagedFormRequest = false;
|
||||
|
||||
+5
-3
@@ -1,13 +1,15 @@
|
||||
import { getDeniedResourceHostnames } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { createNodeProtectedResourceFetch } from "@webstudio-is/sdk/protected-resource-fetch-node";
|
||||
export const createManagedFormEmailSender = (_input: { context: unknown; formData: FormData; projectId: string }) => undefined;
|
||||
export const validateManagedFormEmail = (_request: unknown, _formData: FormData) => undefined;
|
||||
export const createManagedFormResourceFetch = ({ request, context, projectDomain }: { request: Request; context: unknown; projectDomain?: string }) => {
|
||||
void context;
|
||||
return createNodeProtectedResourceFetch({
|
||||
deniedHostnames: getDeniedResourceHostnames([
|
||||
projectId: "cddc1d44-af37-4cb6-a430-d300cf6f932d",
|
||||
matchSubdomains: false,
|
||||
deniedHostnames: [
|
||||
new URL(request.url).hostname,
|
||||
projectDomain,
|
||||
]),
|
||||
...["webstudio-fixture-project-a-0su3o"],
|
||||
].filter((hostname): hostname is string => hostname !== undefined),
|
||||
});
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type ComponentProps, memo, useMemo } from "react";
|
||||
import {
|
||||
type MetaFunction,
|
||||
@@ -98,6 +99,11 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
// Node exports have no Worker entrypoint guard; stop requests through aliases here.
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProjectRequest(
|
||||
arg.request,
|
||||
authRoutes,
|
||||
@@ -263,6 +269,10 @@ export const action = async ({
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] } | ManagedFormResponse
|
||||
> => {
|
||||
if (isProtectedResourceReentry(request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
authenticateProjectRequest(request, authRoutes, projectDomain);
|
||||
|
||||
let isManagedFormRequest = false;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type ComponentProps, memo, useMemo } from "react";
|
||||
import {
|
||||
type MetaFunction,
|
||||
@@ -98,6 +99,11 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
// Node exports have no Worker entrypoint guard; stop requests through aliases here.
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProjectRequest(
|
||||
arg.request,
|
||||
authRoutes,
|
||||
@@ -263,6 +269,10 @@ export const action = async ({
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] } | ManagedFormResponse
|
||||
> => {
|
||||
if (isProtectedResourceReentry(request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
authenticateProjectRequest(request, authRoutes, projectDomain);
|
||||
|
||||
let isManagedFormRequest = false;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type ComponentProps, memo, useMemo } from "react";
|
||||
import {
|
||||
type MetaFunction,
|
||||
@@ -98,6 +99,11 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
// Node exports have no Worker entrypoint guard; stop requests through aliases here.
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProjectRequest(
|
||||
arg.request,
|
||||
authRoutes,
|
||||
@@ -263,6 +269,10 @@ export const action = async ({
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] } | ManagedFormResponse
|
||||
> => {
|
||||
if (isProtectedResourceReentry(request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
authenticateProjectRequest(request, authRoutes, projectDomain);
|
||||
|
||||
let isManagedFormRequest = false;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type ComponentProps, memo, useMemo } from "react";
|
||||
import {
|
||||
type MetaFunction,
|
||||
@@ -98,6 +99,11 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
// Node exports have no Worker entrypoint guard; stop requests through aliases here.
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProjectRequest(
|
||||
arg.request,
|
||||
authRoutes,
|
||||
@@ -263,6 +269,10 @@ export const action = async ({
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] } | ManagedFormResponse
|
||||
> => {
|
||||
if (isProtectedResourceReentry(request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
authenticateProjectRequest(request, authRoutes, projectDomain);
|
||||
|
||||
let isManagedFormRequest = false;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type ComponentProps, memo, useMemo } from "react";
|
||||
import {
|
||||
type MetaFunction,
|
||||
@@ -98,6 +99,11 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
// Node exports have no Worker entrypoint guard; stop requests through aliases here.
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProjectRequest(
|
||||
arg.request,
|
||||
authRoutes,
|
||||
@@ -263,6 +269,10 @@ export const action = async ({
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] } | ManagedFormResponse
|
||||
> => {
|
||||
if (isProtectedResourceReentry(request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
authenticateProjectRequest(request, authRoutes, projectDomain);
|
||||
|
||||
let isManagedFormRequest = false;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type ComponentProps, memo, useMemo } from "react";
|
||||
import {
|
||||
type MetaFunction,
|
||||
@@ -98,6 +99,11 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
// Node exports have no Worker entrypoint guard; stop requests through aliases here.
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProjectRequest(
|
||||
arg.request,
|
||||
authRoutes,
|
||||
@@ -263,6 +269,10 @@ export const action = async ({
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] } | ManagedFormResponse
|
||||
> => {
|
||||
if (isProtectedResourceReentry(request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
authenticateProjectRequest(request, authRoutes, projectDomain);
|
||||
|
||||
let isManagedFormRequest = false;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type ComponentProps, memo, useMemo } from "react";
|
||||
import {
|
||||
type MetaFunction,
|
||||
@@ -98,6 +99,11 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
// Node exports have no Worker entrypoint guard; stop requests through aliases here.
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProjectRequest(
|
||||
arg.request,
|
||||
authRoutes,
|
||||
@@ -263,6 +269,10 @@ export const action = async ({
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] } | ManagedFormResponse
|
||||
> => {
|
||||
if (isProtectedResourceReentry(request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
authenticateProjectRequest(request, authRoutes, projectDomain);
|
||||
|
||||
let isManagedFormRequest = false;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type ComponentProps, memo, useMemo } from "react";
|
||||
import {
|
||||
type MetaFunction,
|
||||
@@ -98,6 +99,11 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
// Node exports have no Worker entrypoint guard; stop requests through aliases here.
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProjectRequest(
|
||||
arg.request,
|
||||
authRoutes,
|
||||
@@ -263,6 +269,10 @@ export const action = async ({
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] } | ManagedFormResponse
|
||||
> => {
|
||||
if (isProtectedResourceReentry(request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
authenticateProjectRequest(request, authRoutes, projectDomain);
|
||||
|
||||
let isManagedFormRequest = false;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type ComponentProps, memo, useMemo } from "react";
|
||||
import {
|
||||
type MetaFunction,
|
||||
@@ -98,6 +99,11 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
// Node exports have no Worker entrypoint guard; stop requests through aliases here.
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProjectRequest(
|
||||
arg.request,
|
||||
authRoutes,
|
||||
@@ -263,6 +269,10 @@ export const action = async ({
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] } | ManagedFormResponse
|
||||
> => {
|
||||
if (isProtectedResourceReentry(request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
authenticateProjectRequest(request, authRoutes, projectDomain);
|
||||
|
||||
let isManagedFormRequest = false;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type ComponentProps, memo, useMemo } from "react";
|
||||
import {
|
||||
type MetaFunction,
|
||||
@@ -98,6 +99,11 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
// Node exports have no Worker entrypoint guard; stop requests through aliases here.
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProjectRequest(
|
||||
arg.request,
|
||||
authRoutes,
|
||||
@@ -263,6 +269,10 @@ export const action = async ({
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] } | ManagedFormResponse
|
||||
> => {
|
||||
if (isProtectedResourceReentry(request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
authenticateProjectRequest(request, authRoutes, projectDomain);
|
||||
|
||||
let isManagedFormRequest = false;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type ComponentProps, memo, useMemo } from "react";
|
||||
import {
|
||||
type MetaFunction,
|
||||
@@ -98,6 +99,11 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
// Node exports have no Worker entrypoint guard; stop requests through aliases here.
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProjectRequest(
|
||||
arg.request,
|
||||
authRoutes,
|
||||
@@ -263,6 +269,10 @@ export const action = async ({
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] } | ManagedFormResponse
|
||||
> => {
|
||||
if (isProtectedResourceReentry(request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
authenticateProjectRequest(request, authRoutes, projectDomain);
|
||||
|
||||
let isManagedFormRequest = false;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type ComponentProps, memo, useMemo } from "react";
|
||||
import {
|
||||
type MetaFunction,
|
||||
@@ -98,6 +99,11 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
// Node exports have no Worker entrypoint guard; stop requests through aliases here.
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProjectRequest(
|
||||
arg.request,
|
||||
authRoutes,
|
||||
@@ -263,6 +269,10 @@ export const action = async ({
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] } | ManagedFormResponse
|
||||
> => {
|
||||
if (isProtectedResourceReentry(request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
authenticateProjectRequest(request, authRoutes, projectDomain);
|
||||
|
||||
let isManagedFormRequest = false;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type ComponentProps, memo, useMemo } from "react";
|
||||
import {
|
||||
type MetaFunction,
|
||||
@@ -98,6 +99,11 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
// Node exports have no Worker entrypoint guard; stop requests through aliases here.
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProjectRequest(
|
||||
arg.request,
|
||||
authRoutes,
|
||||
@@ -263,6 +269,10 @@ export const action = async ({
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] } | ManagedFormResponse
|
||||
> => {
|
||||
if (isProtectedResourceReentry(request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
authenticateProjectRequest(request, authRoutes, projectDomain);
|
||||
|
||||
let isManagedFormRequest = false;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { renderToString } from "react-dom/server";
|
||||
import { type LoaderFunctionArgs, redirect } from "react-router";
|
||||
import {
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
import {
|
||||
Page,
|
||||
breakpoints,
|
||||
projectId,
|
||||
projectDomain,
|
||||
} from "../__generated__/[sitemap.xml]._index";
|
||||
import {
|
||||
@@ -59,6 +61,10 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProjectRequest(
|
||||
arg.request,
|
||||
authRoutes,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type ComponentProps, memo, useMemo } from "react";
|
||||
import {
|
||||
type MetaFunction,
|
||||
@@ -98,6 +99,11 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
// Node exports have no Worker entrypoint guard; stop requests through aliases here.
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProjectRequest(
|
||||
arg.request,
|
||||
authRoutes,
|
||||
@@ -263,6 +269,10 @@ export const action = async ({
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] } | ManagedFormResponse
|
||||
> => {
|
||||
if (isProtectedResourceReentry(request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
authenticateProjectRequest(request, authRoutes, projectDomain);
|
||||
|
||||
let isManagedFormRequest = false;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type ComponentProps, memo, useMemo } from "react";
|
||||
import {
|
||||
type MetaFunction,
|
||||
@@ -98,6 +99,11 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
// Node exports have no Worker entrypoint guard; stop requests through aliases here.
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProjectRequest(
|
||||
arg.request,
|
||||
authRoutes,
|
||||
@@ -263,6 +269,10 @@ export const action = async ({
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] } | ManagedFormResponse
|
||||
> => {
|
||||
if (isProtectedResourceReentry(request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
authenticateProjectRequest(request, authRoutes, projectDomain);
|
||||
|
||||
let isManagedFormRequest = false;
|
||||
|
||||
@@ -1131,6 +1131,68 @@ describe("prebuild", () => {
|
||||
}
|
||||
);
|
||||
|
||||
test("allows a sibling API host but blocks the site's hosts in a Node export", async () => {
|
||||
const siteData = createSiteData();
|
||||
siteData.projectDomain = "project.wstd.io";
|
||||
await writeFile(
|
||||
".webstudio/data.json",
|
||||
JSON.stringify({
|
||||
...siteData,
|
||||
build: {
|
||||
...siteData.build,
|
||||
deployment: {
|
||||
destination: undefined,
|
||||
domains: ["webstudio-demo-john.stauferth.de"],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
await prebuild({ assets: false, template: ["react-router"] });
|
||||
await symlink(join(originalCwd, "node_modules"), "node_modules", "dir");
|
||||
const bundle = await build({
|
||||
entryPoints: [
|
||||
join(
|
||||
tempDir,
|
||||
"app/__generated__/$resources.managed-form-fetch.server.ts"
|
||||
),
|
||||
],
|
||||
bundle: true,
|
||||
format: "esm",
|
||||
platform: "node",
|
||||
conditions: ["webstudio"],
|
||||
banner: {
|
||||
js: `import { createRequire } from "node:module"; const require = createRequire(${JSON.stringify(pathToFileURL(join(originalCwd, "package.json")).href)});`,
|
||||
},
|
||||
write: false,
|
||||
});
|
||||
const adapter = (await import(
|
||||
/* @vite-ignore */
|
||||
`data:text/javascript;base64,${Buffer.from(bundle.outputFiles[0].text).toString("base64")}`
|
||||
)) as {
|
||||
createManagedFormResourceFetch: (input: {
|
||||
request: Request;
|
||||
context: unknown;
|
||||
projectDomain: string;
|
||||
}) => typeof fetch & { validateDestination: (url: URL) => void };
|
||||
};
|
||||
const resourceFetch = adapter.createManagedFormResourceFetch({
|
||||
request: new Request("https://webstudio-demo-john.stauferth.de"),
|
||||
context: {},
|
||||
projectDomain: siteData.projectDomain,
|
||||
});
|
||||
expect(() =>
|
||||
resourceFetch.validateDestination(new URL("https://cms.stauferth.de"))
|
||||
).not.toThrow();
|
||||
for (const hostname of [
|
||||
"webstudio-demo-john.stauferth.de",
|
||||
"project.wstd.io",
|
||||
]) {
|
||||
expect(() =>
|
||||
resourceFetch.validateDestination(new URL(`https://${hostname}`))
|
||||
).toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
test("publishes configured file-input attributes", async () => {
|
||||
await writeSiteData(
|
||||
createSiteData({
|
||||
|
||||
@@ -1106,8 +1106,7 @@ export const createManagedFormResourceFetch = ({ request, context, projectDomain
|
||||
});
|
||||
};
|
||||
`
|
||||
: `import { getDeniedResourceHostnames } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { createNodeProtectedResourceFetch } from "@webstudio-is/sdk/protected-resource-fetch-node";
|
||||
: `${evaluationTransport === undefined ? "" : 'import { getDeniedResourceHostnames } from "@webstudio-is/sdk/protected-resource-fetch";\n'}import { createNodeProtectedResourceFetch } from "@webstudio-is/sdk/protected-resource-fetch-node";
|
||||
export const createManagedFormEmailSender = (_input: { context: unknown; formData: FormData; projectId: string }) => undefined;
|
||||
export const validateManagedFormEmail = (_request: unknown, _formData: FormData) => undefined;
|
||||
export const createManagedFormResourceFetch = ({ request, context, projectDomain }: { request: Request; context: unknown; projectDomain?: string }) => {
|
||||
@@ -1115,10 +1114,18 @@ export const createManagedFormResourceFetch = ({ request, context, projectDomain
|
||||
${
|
||||
evaluationTransport ??
|
||||
`return createNodeProtectedResourceFetch({
|
||||
deniedHostnames: getDeniedResourceHostnames([
|
||||
projectId: ${JSON.stringify(siteData.build.projectId)},
|
||||
matchSubdomains: false,
|
||||
deniedHostnames: [
|
||||
new URL(request.url).hostname,
|
||||
projectDomain,
|
||||
]),
|
||||
...${JSON.stringify(
|
||||
siteData.build.deployment !== undefined &&
|
||||
"domains" in siteData.build.deployment
|
||||
? siteData.build.deployment.domains
|
||||
: []
|
||||
)},
|
||||
].filter((hostname): hostname is string => hostname !== undefined),
|
||||
});`
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type ComponentProps, memo, useMemo } from "react";
|
||||
import {
|
||||
type ServerRuntimeMetaFunction as MetaFunction,
|
||||
@@ -117,6 +118,11 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
// Node exports have no Worker entrypoint guard; stop requests through aliases here.
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProductionRequest(arg.request);
|
||||
|
||||
const url = new URL(arg.request.url);
|
||||
@@ -278,6 +284,10 @@ export const action = async ({
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] } | ManagedFormResponse
|
||||
> => {
|
||||
if (isProtectedResourceReentry(request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
authenticateProductionRequest(request);
|
||||
|
||||
let isManagedFormRequest = false;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
|
||||
import { isLocalResource, loadResources, getSystemSearch } from "@webstudio-is/sdk/runtime";
|
||||
import { authenticateRequest } from "@webstudio-is/wsauth";
|
||||
import { projectDomain } from "__CLIENT__";
|
||||
import { projectId, projectDomain } from "__CLIENT__";
|
||||
import { getPageMeta, getRemixParams, getResources } from "__SERVER__";
|
||||
import { sitemap } from "__SITEMAP__";
|
||||
import { authRoutes } from "__AUTH__";
|
||||
@@ -57,6 +58,10 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProductionRequest(arg.request);
|
||||
|
||||
const url = new URL(arg.request.url);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { renderToString } from "react-dom/server";
|
||||
import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
|
||||
import { isLocalResource, loadResources, getSystemSearch } from "@webstudio-is/sdk/runtime";
|
||||
@@ -6,7 +7,7 @@ import {
|
||||
ReactSdkContext,
|
||||
xmlNodeTagSuffix,
|
||||
} from "@webstudio-is/react-sdk/runtime";
|
||||
import { Page, breakpoints, projectDomain } from "__CLIENT__";
|
||||
import { Page, breakpoints, projectId, projectDomain } from "__CLIENT__";
|
||||
import { getPageMeta, getRemixParams, getResources } from "__SERVER__";
|
||||
import { assetBaseUrl, imageLoader } from "__CONSTANTS__";
|
||||
import { sitemap } from "__SITEMAP__";
|
||||
@@ -65,6 +66,10 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProductionRequest(arg.request);
|
||||
|
||||
const url = new URL(arg.request.url);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type ComponentProps, memo, useMemo } from "react";
|
||||
import {
|
||||
type MetaFunction,
|
||||
@@ -98,6 +99,11 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
// Node exports have no Worker entrypoint guard; stop requests through aliases here.
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProjectRequest(
|
||||
arg.request,
|
||||
authRoutes,
|
||||
@@ -263,6 +269,10 @@ export const action = async ({
|
||||
}: ActionFunctionArgs): Promise<
|
||||
{ success: true } | { success: false; errors: string[] } | ManagedFormResponse
|
||||
> => {
|
||||
if (isProtectedResourceReentry(request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
authenticateProjectRequest(request, authRoutes, projectDomain);
|
||||
|
||||
let isManagedFormRequest = false;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { type LoaderFunctionArgs, redirect } from "react-router";
|
||||
import { isLocalResource, loadResources, getSystemSearch } from "@webstudio-is/sdk/runtime";
|
||||
import { authenticateProjectRequest } from "@webstudio-is/wsauth";
|
||||
import { projectDomain } from "__CLIENT__";
|
||||
import { projectId, projectDomain } from "__CLIENT__";
|
||||
import { getPageMeta, getRemixParams, getResources } from "__SERVER__";
|
||||
import { sitemap } from "__SITEMAP__";
|
||||
import { authRoutes } from "__AUTH__";
|
||||
@@ -39,6 +40,10 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProjectRequest(
|
||||
arg.request,
|
||||
authRoutes,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isProtectedResourceReentry } from "@webstudio-is/sdk/protected-resource-fetch";
|
||||
import { renderToString } from "react-dom/server";
|
||||
import { type LoaderFunctionArgs, redirect } from "react-router";
|
||||
import { isLocalResource, loadResources, getSystemSearch } from "@webstudio-is/sdk/runtime";
|
||||
@@ -6,7 +7,7 @@ import {
|
||||
ReactSdkContext,
|
||||
xmlNodeTagSuffix,
|
||||
} from "@webstudio-is/react-sdk/runtime";
|
||||
import { Page, breakpoints, projectDomain } from "__CLIENT__";
|
||||
import { Page, breakpoints, projectId, projectDomain } from "__CLIENT__";
|
||||
import { getPageMeta, getRemixParams, getResources } from "__SERVER__";
|
||||
import { assetBaseUrl, imageLoader } from "__CONSTANTS__";
|
||||
import { sitemap } from "__SITEMAP__";
|
||||
@@ -47,6 +48,10 @@ const customFetch: typeof fetch = (input, init) => {
|
||||
};
|
||||
|
||||
export const loader = async (arg: LoaderFunctionArgs) => {
|
||||
if (isProtectedResourceReentry(arg.request, projectId)) {
|
||||
throw new Response(null, { status: 508 });
|
||||
}
|
||||
|
||||
const authRoute = authenticateProjectRequest(
|
||||
arg.request,
|
||||
authRoutes,
|
||||
|
||||
@@ -33,6 +33,15 @@ const readColor = (color: string) => {
|
||||
return Array.from(context.getImageData(0, 0, 1, 1).data);
|
||||
};
|
||||
|
||||
const readTokenColor = (name: string) => {
|
||||
const sample = document.createElement("span");
|
||||
sample.style.color = `var(${name})`;
|
||||
document.body.append(sample);
|
||||
const color = readColor(getComputedStyle(sample).color);
|
||||
sample.remove();
|
||||
return color;
|
||||
};
|
||||
|
||||
test("compact form controls use their intended resting treatment", () => {
|
||||
const container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
@@ -55,6 +64,22 @@ test("compact form controls use their intended resting treatment", () => {
|
||||
{ "data-switch": true },
|
||||
createElement(Switch, { "aria-label": "Switch" })
|
||||
),
|
||||
createElement(
|
||||
"div",
|
||||
{ "data-disabled-switch": true },
|
||||
createElement(Switch, {
|
||||
"aria-label": "Disabled switch",
|
||||
disabled: true,
|
||||
})
|
||||
),
|
||||
createElement(
|
||||
"div",
|
||||
{ "data-checked-switch": true },
|
||||
createElement(Switch, {
|
||||
"aria-label": "Checked switch",
|
||||
checked: true,
|
||||
})
|
||||
),
|
||||
createElement(
|
||||
"div",
|
||||
{ "data-neutral-control": true },
|
||||
@@ -86,6 +111,23 @@ test("compact form controls use their intended resting treatment", () => {
|
||||
expect(
|
||||
readColor(getComputedStyle(switchControl, "::before").backgroundColor),
|
||||
`${mode} switch resting track`
|
||||
).toEqual(readTokenColor("--foreground-disabled"));
|
||||
const disabledSwitch = container.querySelector(
|
||||
"[data-disabled-switch] > button"
|
||||
);
|
||||
const checkedSwitch = container.querySelector(
|
||||
"[data-checked-switch] > button"
|
||||
);
|
||||
if (disabledSwitch === null || checkedSwitch === null) {
|
||||
throw new Error("Expected disabled and checked switches");
|
||||
}
|
||||
expect(
|
||||
readColor(getComputedStyle(disabledSwitch, "::before").backgroundColor),
|
||||
`${mode} disabled switch track`
|
||||
).toEqual(readColor(getComputedStyle(neutralControl).backgroundColor));
|
||||
expect(
|
||||
readColor(getComputedStyle(checkedSwitch, "::before").backgroundColor),
|
||||
`${mode} checked switch track`
|
||||
).toEqual(readTokenColor("--background-accent"));
|
||||
}
|
||||
});
|
||||
@@ -29,7 +29,7 @@ const switchStyle = css({
|
||||
position: "absolute",
|
||||
inset: padding,
|
||||
borderRadius: theme.borderRadius.pill,
|
||||
backgroundColor: neutralControlBackground,
|
||||
backgroundColor: cssVar("--foreground-disabled"),
|
||||
},
|
||||
|
||||
"&[data-state=checked]:not([data-disabled]):before, &[aria-checked=true]:not([data-disabled]):before":
|
||||
@@ -38,7 +38,7 @@ const switchStyle = css({
|
||||
},
|
||||
|
||||
"&[data-disabled]:before": {
|
||||
backgroundColor: cssVar("--foreground-disabled"),
|
||||
backgroundColor: neutralControlBackground,
|
||||
},
|
||||
|
||||
"&:focus": {
|
||||
|
||||
@@ -25,6 +25,7 @@ vi.mock("undici", () => ({
|
||||
}));
|
||||
|
||||
import { createNodeProtectedResourceFetch } from "./protected-resource-fetch-node";
|
||||
import { isProtectedResourceReentry } from "./protected-resource-fetch";
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.lookup.mockReset();
|
||||
@@ -103,6 +104,23 @@ test("pins the vetted address in the connector and retains the URL hostname", as
|
||||
expect(mocks.agents[0].close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("marks Node Resource hops with the originating project", async () => {
|
||||
mocks.lookup.mockResolvedValue([{ address: "8.8.8.8", family: 4 }]);
|
||||
mocks.fetch.mockResolvedValue(new Response("ok"));
|
||||
const resourceFetch = createNodeProtectedResourceFetch({
|
||||
projectId: "project-a",
|
||||
deniedHostnames: ["site.example"],
|
||||
matchSubdomains: false,
|
||||
});
|
||||
|
||||
await resourceFetch("https://api.site.example/data");
|
||||
const [, init] = mocks.fetch.mock.calls[0];
|
||||
const outgoing = new Request("https://api.site.example/data", init);
|
||||
expect(isProtectedResourceReentry(outgoing, "project-a")).toBe(true);
|
||||
expect(isProtectedResourceReentry(outgoing, "project-b")).toBe(false);
|
||||
expect(mocks.agents[0].close).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
test("resolves and rejects private DNS answers on a cross-origin redirect", async () => {
|
||||
mocks.lookup
|
||||
.mockResolvedValueOnce([{ address: "8.8.8.8", family: 4 }])
|
||||
|
||||
@@ -4,6 +4,7 @@ import ipaddr from "ipaddr.js";
|
||||
import { Agent, fetch as undiciFetch } from "undici";
|
||||
import {
|
||||
createProtectedResourceFetch,
|
||||
protectedResourceRequestHeader,
|
||||
type ProtectedResourceFetch,
|
||||
} from "./protected-resource-fetch";
|
||||
|
||||
@@ -60,12 +61,21 @@ const resolvePublicAddress = async (hostname: string) => {
|
||||
*/
|
||||
export const createNodeProtectedResourceFetch = ({
|
||||
deniedHostnames = [],
|
||||
matchSubdomains = true,
|
||||
projectId,
|
||||
}: {
|
||||
deniedHostnames?: readonly string[];
|
||||
matchSubdomains?: boolean;
|
||||
projectId?: string;
|
||||
} = {}): ProtectedResourceFetch =>
|
||||
createProtectedResourceFetch({
|
||||
deniedHostnames,
|
||||
matchSubdomains,
|
||||
transport: async ({ url, method, headers, body, signal }) => {
|
||||
if (projectId !== undefined) {
|
||||
// The generated route checks this ID before loading Resources again.
|
||||
headers.set(protectedResourceRequestHeader, projectId);
|
||||
}
|
||||
const resolved = await resolvePublicAddress(url.hostname);
|
||||
const agent = new Agent({
|
||||
connect: {
|
||||
|
||||
@@ -22,7 +22,7 @@ const redirectStatuses = new Set([301, 302, 303, 307, 308]);
|
||||
const defaultMaxRedirects = 3;
|
||||
const maxRequestBytes = 25 * 1024 * 1024;
|
||||
|
||||
const protectedResourceRequestHeader = "x-webstudio-protected-resource";
|
||||
export const protectedResourceRequestHeader = "x-webstudio-protected-resource";
|
||||
|
||||
/** Project identity survives aliases while allowing calls to other projects.
|
||||
* This cooperative marker is not a sandbox for untrusted code or proxies that strip it.
|
||||
@@ -34,7 +34,7 @@ export const isProtectedResourceReentry = (
|
||||
projectId !== "" &&
|
||||
request.headers.get(protectedResourceRequestHeader) === projectId;
|
||||
|
||||
/** Preserve the domain-wide policy for Node and Workers without public routing. */
|
||||
/** Domain-wide policy for callers that intentionally restrict an entire zone. */
|
||||
export const getDeniedResourceHostnames = (
|
||||
hostnames: readonly (string | undefined)[]
|
||||
) =>
|
||||
|
||||
Reference in new issue
Block a user