From f89a89580e7efe1d6078388abbe36eb69806eab8 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Mon, 21 Jul 2025 15:49:30 -0700 Subject: [PATCH 1/3] Allow external remote functions to be whitelisted --- packages/kit/src/core/config/options.js | 4 ++++ .../core/sync/create_manifest_data/index.js | 5 ++-- packages/kit/src/exports/public.d.ts | 9 +++++++ packages/kit/src/exports/vite/dev/index.js | 5 ++-- packages/kit/src/exports/vite/index.js | 24 +++++++++++++++++-- packages/kit/src/runtime/server/cookie.js | 9 +------ packages/kit/src/utils/array.js | 9 +++++++ .../external.remote.js | 3 +++ .../src/external-remotes/external.remote.js | 3 +++ .../basics/src/routes/remote/+page.svelte | 16 +++++++++++++ .../kit/test/apps/basics/svelte.config.js | 5 ++++ .../kit/test/apps/basics/test/client.test.js | 7 ++++++ packages/kit/types/index.d.ts | 9 +++++++ 13 files changed, 94 insertions(+), 14 deletions(-) create mode 100644 packages/kit/test/apps/basics/src/external-not-accessible/external.remote.js create mode 100644 packages/kit/test/apps/basics/src/external-remotes/external.remote.js diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js index 577ca4c9445d..948032370c41 100644 --- a/packages/kit/src/core/config/options.js +++ b/packages/kit/src/core/config/options.js @@ -264,6 +264,10 @@ const options = object( }) }), + remoteFunctions: object({ + allowedPaths: string_array([]) + }), + router: object({ type: list(['pathname', 'hash']), resolution: list(['client', 'server']) diff --git a/packages/kit/src/core/sync/create_manifest_data/index.js b/packages/kit/src/core/sync/create_manifest_data/index.js index 57bd98951768..452065eca5cf 100644 --- a/packages/kit/src/core/sync/create_manifest_data/index.js +++ b/packages/kit/src/core/sync/create_manifest_data/index.js @@ -475,8 +475,9 @@ function create_remotes(config) { const extensions = config.kit.moduleExtensions.map((ext) => `.remote${ext}`); - // TODO could files live in other directories, including node_modules? - return [config.kit.files.lib, config.kit.files.routes].flatMap((dir) => + const externals = config.kit.remoteFunctions.allowedPaths.map((dir) => path.resolve(dir)); + + return [config.kit.files.lib, config.kit.files.routes, ...externals].flatMap((dir) => fs.existsSync(dir) ? walk(dir) .filter((file) => extensions.some((ext) => file.endsWith(ext))) diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 048a14835772..30080f60eb9d 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -651,6 +651,15 @@ export interface KitConfig { */ origin?: string; }; + remoteFunctions?: { + /** + * A list of external paths that are allowed to provide remote functions. + * By default, remote functions are only allowed inside the `routes` and `lib` folders. + * + * Accepts absolute paths or paths relative to the project root. + */ + allowedPaths?: string[]; + }; router?: { /** * What type of client-side router to use. diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index cffaa3fa3716..53bad7d84398 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -30,9 +30,10 @@ const vite_css_query_regex = /(?:\?|&)(?:raw|url|inline)(?:&|$)/; * @param {import('vite').ViteDevServer} vite * @param {import('vite').ResolvedConfig} vite_config * @param {import('types').ValidatedConfig} svelte_config + * @param {(manifest_data: import('types').ManifestData) => void} manifest_cb * @return {Promise void>>} */ -export async function dev(vite, vite_config, svelte_config) { +export async function dev(vite, vite_config, svelte_config, manifest_cb) { installPolyfills(); const async_local_storage = new AsyncLocalStorage(); @@ -109,7 +110,7 @@ export async function dev(vite, vite_config, svelte_config) { function update_manifest() { try { ({ manifest_data } = sync.create(svelte_config)); - + manifest_cb(manifest_data); if (manifest_error) { manifest_error = null; vite.ws.send({ type: 'full-reload' }); diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index aad905da10fe..d862191188f4 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -36,7 +36,7 @@ import { sveltekit_remotes } from './module_ids.js'; import { import_peer } from '../../utils/import.js'; -import { compact } from '../../utils/array.js'; +import { compact, conjoin } from '../../utils/array.js'; import { build_remotes, enhance_remotes, @@ -656,6 +656,24 @@ Tips: ); } + if (!manifest_data.remotes.includes(id)) { + const relative_path = path.relative(dev_server.config.root, id); + const fn_names = [...remotes.values()].flat().map((name) => `"${name}"`); + const has_multiple = fn_names.length !== 1; + console.warn( + colors + .bold() + .yellow( + `Remote function${has_multiple ? 's' : ''} ${conjoin(fn_names)} from ${relative_path} ${has_multiple ? 'are' : 'is'} not accessible by default.` + ) + ); + console.warn( + colors.yellow( + `To whitelist them, add "${path.dirname(relative_path)}" to \`kit.remoteFunctions.allowedPaths\` in \`svelte.config.js\`.` + ) + ); + } + const exports = []; const specifiers = []; @@ -847,7 +865,9 @@ Tips: * @see https://vitejs.dev/guide/api-plugin.html#configureserver */ async configureServer(vite) { - return await dev(vite, vite_config, svelte_config); + return await dev(vite, vite_config, svelte_config, (_manifest_data) => { + manifest_data = _manifest_data; + }); }, /** diff --git a/packages/kit/src/runtime/server/cookie.js b/packages/kit/src/runtime/server/cookie.js index 2e683543a534..9019c86f0517 100644 --- a/packages/kit/src/runtime/server/cookie.js +++ b/packages/kit/src/runtime/server/cookie.js @@ -1,4 +1,5 @@ import { parse, serialize } from 'cookie'; +import { conjoin } from '../../utils/array.js'; import { normalize_path, resolve } from '../../utils/url.js'; import { add_data_suffix } from '../pathname.js'; @@ -286,11 +287,3 @@ export function add_cookies_to_headers(headers, cookies) { } } } - -/** - * @param {string[]} array - */ -function conjoin(array) { - if (array.length <= 2) return array.join(' and '); - return `${array.slice(0, -1).join(', ')} and ${array.at(-1)}`; -} diff --git a/packages/kit/src/utils/array.js b/packages/kit/src/utils/array.js index 08f93845149b..1d6dc4b6abce 100644 --- a/packages/kit/src/utils/array.js +++ b/packages/kit/src/utils/array.js @@ -7,3 +7,12 @@ export function compact(arr) { return arr.filter(/** @returns {val is NonNullable} */ (val) => val != null); } + +/** + * Joins an array of strings with commas and 'and'. + * @param {string[]} array + */ +export function conjoin(array) { + if (array.length <= 2) return array.join(' and '); + return `${array.slice(0, -1).join(', ')} and ${array.at(-1)}`; +} diff --git a/packages/kit/test/apps/basics/src/external-not-accessible/external.remote.js b/packages/kit/test/apps/basics/src/external-not-accessible/external.remote.js new file mode 100644 index 000000000000..5c883ceee850 --- /dev/null +++ b/packages/kit/test/apps/basics/src/external-not-accessible/external.remote.js @@ -0,0 +1,3 @@ +import { query } from '$app/server'; + +export const external_not_accessible = query(async () => 'external failure'); diff --git a/packages/kit/test/apps/basics/src/external-remotes/external.remote.js b/packages/kit/test/apps/basics/src/external-remotes/external.remote.js new file mode 100644 index 000000000000..ea5b438b138c --- /dev/null +++ b/packages/kit/test/apps/basics/src/external-remotes/external.remote.js @@ -0,0 +1,3 @@ +import { query } from '$app/server'; + +export const external = query(async () => 'external success'); diff --git a/packages/kit/test/apps/basics/src/routes/remote/+page.svelte b/packages/kit/test/apps/basics/src/routes/remote/+page.svelte index 37e23243f0c8..0c6b3789e389 100644 --- a/packages/kit/test/apps/basics/src/routes/remote/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/remote/+page.svelte @@ -1,6 +1,8 @@

{data.echo_result}

@@ -74,3 +78,15 @@ + +

+ {#await external_result then result}{result}{/await} +

+ +

+ {#await external_failure then result} + {result} + {:catch error} + {error} + {/await} +

diff --git a/packages/kit/test/apps/basics/svelte.config.js b/packages/kit/test/apps/basics/svelte.config.js index 2410ff83d57f..ac4aeaf51b7c 100644 --- a/packages/kit/test/apps/basics/svelte.config.js +++ b/packages/kit/test/apps/basics/svelte.config.js @@ -41,8 +41,13 @@ const config = { version: { name: 'TEST_VERSION' }, + router: { resolution: /** @type {'client' | 'server'} */ (process.env.ROUTER_RESOLUTION) || 'client' + }, + + remoteFunctions: { + allowedPaths: ['src/external-remotes'] } } }; diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js index 467b26e33179..4a9cc922ed19 100644 --- a/packages/kit/test/apps/basics/test/client.test.js +++ b/packages/kit/test/apps/basics/test/client.test.js @@ -1861,4 +1861,11 @@ test.describe('remote functions', () => { await page.click('button:nth-of-type(4)'); await expect(page.locator('p')).toHaveText('success'); }); + + test('external remotes work', async ({ page }) => { + await page.goto('/remote'); + await expect(page.locator('#external-success')).toHaveText('external success'); + await expect(page.locator('#external-failure')).not.toHaveText('external failure'); + await expect(page.locator('#external-failure')).toHaveText('Failed to execute remote function'); + }); }); diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index d84a82d39852..f1abd2a28d54 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -633,6 +633,15 @@ declare module '@sveltejs/kit' { */ origin?: string; }; + remoteFunctions?: { + /** + * A list of external paths that are allowed to provide remote functions. + * By default, remote functions are only allowed inside the `routes` and `lib` folders. + * + * Accepts absolute paths or paths relative to the project root. + */ + allowedPaths?: string[]; + }; router?: { /** * What type of client-side router to use. From 2d2de7bd48a1facce4bd1b517b83d44da2ad85af Mon Sep 17 00:00:00 2001 From: Ottomated Date: Mon, 21 Jul 2025 15:53:49 -0700 Subject: [PATCH 2/3] pluralize --- packages/kit/src/exports/vite/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index d862191188f4..fbb06b41084d 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -669,7 +669,7 @@ Tips: ); console.warn( colors.yellow( - `To whitelist them, add "${path.dirname(relative_path)}" to \`kit.remoteFunctions.allowedPaths\` in \`svelte.config.js\`.` + `To whitelist ${has_multiple ? 'them' : 'it'}, add "${path.dirname(relative_path)}" to \`kit.remoteFunctions.allowedPaths\` in \`svelte.config.js\`.` ) ); } From aea7ebf02140f643571b1a2eb661e2224be3d1a5 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Mon, 21 Jul 2025 17:03:10 -0700 Subject: [PATCH 3/3] fix test --- packages/kit/src/core/config/index.spec.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js index 90da427483d7..ed0c5c47732a 100644 --- a/packages/kit/src/core/config/index.spec.js +++ b/packages/kit/src/core/config/index.spec.js @@ -97,6 +97,9 @@ const get_defaults = (prefix = '') => ({ moduleExtensions: ['.js', '.ts'], output: { preloadStrategy: 'modulepreload', bundleStrategy: 'split' }, outDir: join(prefix, '.svelte-kit'), + remoteFunctions: { + allowedPaths: [] + }, router: { type: 'pathname', resolution: 'client'