diff --git a/.changeset/plenty-mirrors-clap.md b/.changeset/plenty-mirrors-clap.md new file mode 100644 index 000000000000..6701059de4a7 --- /dev/null +++ b/.changeset/plenty-mirrors-clap.md @@ -0,0 +1,8 @@ +--- +'@sveltejs/adapter-netlify': patch +'@sveltejs/adapter-node': patch +'@sveltejs/adapter-vercel': patch +'@sveltejs/kit': patch +--- + +[breaking] replace @sveltejs/kit/install-fetch with @sveltejs/kit/node/polyfills diff --git a/documentation/docs/01-web-standards.md b/documentation/docs/01-web-standards.md index 5f060daaf733..ecf44be5eb81 100644 --- a/documentation/docs/01-web-standards.md +++ b/documentation/docs/01-web-standards.md @@ -65,3 +65,11 @@ export {}; // ---cut--- const foo = url.searchParams.get('foo'); ``` + +### Web Crypto + +The [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) is made available via the `crypto` global. It's used internally for [Content Security Policy](/docs/configuration#csp) headers, but you can also use it for things like generating UUIDs: + +```js +const uuid = crypto.randomUUID(); +``` diff --git a/packages/adapter-netlify/package.json b/packages/adapter-netlify/package.json index c9e8568a9542..5aea793466e7 100644 --- a/packages/adapter-netlify/package.json +++ b/packages/adapter-netlify/package.json @@ -34,9 +34,11 @@ "dependencies": { "@iarna/toml": "^2.2.5", "esbuild": "^0.14.29", + "set-cookie-parser": "^2.4.8", "tiny-glob": "^0.2.9" }, "devDependencies": { + "@types/set-cookie-parser": "^2.4.2", "@netlify/functions": "^1.0.0", "@sveltejs/kit": "workspace:*" } diff --git a/packages/adapter-netlify/src/headers.js b/packages/adapter-netlify/src/headers.js index b317e854c27b..76770b46ac69 100644 --- a/packages/adapter-netlify/src/headers.js +++ b/packages/adapter-netlify/src/headers.js @@ -1,3 +1,5 @@ +import * as set_cookie_parser from 'set-cookie-parser'; + /** * Splits headers into two categories: single value and multi value * @param {Headers} headers @@ -15,8 +17,7 @@ export function split_headers(headers) { headers.forEach((value, key) => { if (key === 'set-cookie') { - // @ts-expect-error (headers.raw() is non-standard) - m[key] = headers.raw()[key]; + m[key] = set_cookie_parser.splitCookiesString(value); } else { h[key] = value; } diff --git a/packages/adapter-netlify/src/headers.spec.js b/packages/adapter-netlify/src/headers.spec.js index 3086cbb1e87f..8a254ae36447 100644 --- a/packages/adapter-netlify/src/headers.spec.js +++ b/packages/adapter-netlify/src/headers.spec.js @@ -1,7 +1,7 @@ -import '../src/shims.js'; +import './shims.js'; import { test } from 'uvu'; import * as assert from 'uvu/assert'; -import { split_headers } from '../src/headers.js'; +import { split_headers } from './headers.js'; test('empty headers', () => { const headers = new Headers(); diff --git a/packages/adapter-netlify/src/shims.js b/packages/adapter-netlify/src/shims.js index 8d2fe00acd85..2490311daa1e 100644 --- a/packages/adapter-netlify/src/shims.js +++ b/packages/adapter-netlify/src/shims.js @@ -1,2 +1,2 @@ -import { installFetch } from '@sveltejs/kit/install-fetch'; -installFetch(); +import { installPolyfills } from '@sveltejs/kit/node/polyfills'; +installPolyfills(); diff --git a/packages/adapter-node/src/shims.js b/packages/adapter-node/src/shims.js index 8d2fe00acd85..2490311daa1e 100644 --- a/packages/adapter-node/src/shims.js +++ b/packages/adapter-node/src/shims.js @@ -1,2 +1,2 @@ -import { installFetch } from '@sveltejs/kit/install-fetch'; -installFetch(); +import { installPolyfills } from '@sveltejs/kit/node/polyfills'; +installPolyfills(); diff --git a/packages/adapter-vercel/files/serverless.js b/packages/adapter-vercel/files/serverless.js index 9e7285f26e9a..551b731f6f6f 100644 --- a/packages/adapter-vercel/files/serverless.js +++ b/packages/adapter-vercel/files/serverless.js @@ -1,9 +1,9 @@ -import { installFetch } from '@sveltejs/kit/install-fetch'; +import { installPolyfills } from '@sveltejs/kit/node/polyfills'; import { getRequest, setResponse } from '@sveltejs/kit/node'; import { Server } from 'SERVER'; import { manifest } from 'MANIFEST'; -installFetch(); +installPolyfills(); const server = new Server(manifest); diff --git a/packages/kit/package.json b/packages/kit/package.json index 028953b24e24..85f2c12ba32f 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -68,11 +68,11 @@ "./node": { "import": "./dist/node.js" }, + "./node/polyfills": { + "import": "./dist/node/polyfills.js" + }, "./hooks": { "import": "./dist/hooks.js" - }, - "./install-fetch": { - "import": "./dist/install-fetch.js" } }, "types": "types/index.d.ts", diff --git a/packages/kit/rollup.config.js b/packages/kit/rollup.config.js index 6d4cf26498fb..feee1e87d5ad 100644 --- a/packages/kit/rollup.config.js +++ b/packages/kit/rollup.config.js @@ -61,9 +61,9 @@ export default [ { input: { cli: 'src/cli.js', - node: 'src/node.js', - hooks: 'src/hooks.js', - 'install-fetch': 'src/install-fetch.js' + node: 'src/node/index.js', + 'node/polyfills': 'src/node/polyfills.js', + hooks: 'src/hooks.js' }, output: { dir: 'dist', diff --git a/packages/kit/src/core/build/prerender/prerender.js b/packages/kit/src/core/build/prerender/prerender.js index 070180ece180..92f536c01baf 100644 --- a/packages/kit/src/core/build/prerender/prerender.js +++ b/packages/kit/src/core/build/prerender/prerender.js @@ -2,7 +2,7 @@ import { readFileSync, writeFileSync } from 'fs'; import { dirname, join } from 'path'; import { pathToFileURL, URL } from 'url'; import { mkdirp } from '../../../utils/filesystem.js'; -import { installFetch } from '../../../install-fetch.js'; +import { installPolyfills } from '../../../node/polyfills.js'; import { is_root_relative, normalize_path, resolve } from '../../../utils/url.js'; import { queue } from './queue.js'; import { crawl } from './crawl.js'; @@ -59,7 +59,7 @@ export async function prerender({ config, entries, files, log }) { return prerendered; } - installFetch(); + installPolyfills(); const server_root = join(config.kit.outDir, 'output'); diff --git a/packages/kit/src/core/dev/plugin.js b/packages/kit/src/core/dev/plugin.js index 02e3a6301ec4..01955a36e2fb 100644 --- a/packages/kit/src/core/dev/plugin.js +++ b/packages/kit/src/core/dev/plugin.js @@ -3,9 +3,9 @@ import path from 'path'; import { URL } from 'url'; import colors from 'kleur'; import sirv from 'sirv'; -import { installFetch } from '../../install-fetch.js'; +import { installPolyfills } from '../../node/polyfills.js'; import * as sync from '../sync/sync.js'; -import { getRequest, setResponse } from '../../node.js'; +import { getRequest, setResponse } from '../../node/index.js'; import { SVELTE_KIT_ASSETS } from '../constants.js'; import { get_mime_lookup, get_runtime_path, resolve_entry } from '../utils.js'; import { coalesce_to_error } from '../../utils/error.js'; @@ -34,7 +34,7 @@ export async function create_plugin(config, cwd) { name: 'vite-plugin-svelte-kit', configureServer(vite) { - installFetch(); + installPolyfills(); /** @type {import('types').SSRManifest} */ let manifest; diff --git a/packages/kit/src/core/preview/index.js b/packages/kit/src/core/preview/index.js index afbcb47d1118..b931202f18df 100644 --- a/packages/kit/src/core/preview/index.js +++ b/packages/kit/src/core/preview/index.js @@ -4,8 +4,8 @@ import https from 'https'; import { join } from 'path'; import sirv from 'sirv'; import { pathToFileURL } from 'url'; -import { getRequest, setResponse } from '../../node.js'; -import { installFetch } from '../../install-fetch.js'; +import { getRequest, setResponse } from '../../node/index.js'; +import { installPolyfills } from '../../node/polyfills.js'; import { SVELTE_KIT_ASSETS } from '../constants.js'; /** @typedef {import('http').IncomingMessage} Req */ @@ -34,7 +34,7 @@ const mutable = (dir) => * }} opts */ export async function preview({ port, host, config, https: use_https = false }) { - installFetch(); + installPolyfills(); const { paths } = config.kit; const base = paths.base; diff --git a/packages/kit/src/install-fetch.js b/packages/kit/src/install-fetch.js deleted file mode 100644 index 2e75d62ec560..000000000000 --- a/packages/kit/src/install-fetch.js +++ /dev/null @@ -1,27 +0,0 @@ -import fetch, { Response, Request, Headers } from 'node-fetch'; - -// exported for dev/preview and node environments -export function installFetch() { - Object.defineProperties(globalThis, { - fetch: { - enumerable: true, - configurable: true, - value: fetch - }, - Response: { - enumerable: true, - configurable: true, - value: Response - }, - Request: { - enumerable: true, - configurable: true, - value: Request - }, - Headers: { - enumerable: true, - configurable: true, - value: Headers - } - }); -} diff --git a/packages/kit/src/node.js b/packages/kit/src/node/index.js similarity index 85% rename from packages/kit/src/node.js rename to packages/kit/src/node/index.js index 666ebe918615..1600e05bb71c 100644 --- a/packages/kit/src/node.js +++ b/packages/kit/src/node/index.js @@ -1,4 +1,5 @@ import { Readable } from 'stream'; +import * as set_cookie_parser from 'set-cookie-parser'; /** @param {import('http').IncomingMessage} req */ function get_raw_body(req) { @@ -56,6 +57,7 @@ export async function getRequest(base, req) { if (req.httpVersionMajor === 2) { // we need to strip out the HTTP/2 pseudo-headers because node-fetch's // Request implementation doesn't like them + // TODO is this still true with Node 18 headers = Object.assign({}, headers); delete headers[':method']; delete headers[':path']; @@ -74,8 +76,11 @@ export async function setResponse(res, response) { const headers = Object.fromEntries(response.headers); if (response.headers.has('set-cookie')) { - // @ts-expect-error (headers.raw() is non-standard) - headers['set-cookie'] = response.headers.raw()['set-cookie']; + const header = /** @type {string} */ (response.headers.get('set-cookie')); + const split = set_cookie_parser.splitCookiesString(header); + + // @ts-expect-error + headers['set-cookie'] = split; } res.writeHead(response.status, headers); @@ -84,7 +89,7 @@ export async function setResponse(res, response) { response.body.pipe(res); } else { if (response.body) { - res.write(await response.arrayBuffer()); + res.write(new Uint8Array(await response.arrayBuffer())); } res.end(); diff --git a/packages/kit/src/node/polyfills.js b/packages/kit/src/node/polyfills.js new file mode 100644 index 000000000000..45502e66bedb --- /dev/null +++ b/packages/kit/src/node/polyfills.js @@ -0,0 +1,23 @@ +import fetch, { Response, Request, Headers } from 'node-fetch'; +import { webcrypto as crypto } from 'crypto'; + +/** @type {Record} */ +const globals = { + crypto, + fetch, + Response, + Request, + Headers +}; + +// exported for dev/preview and node environments +export function installPolyfills() { + for (const name in globals) { + // TODO use built-in fetch once https://github.com/nodejs/undici/issues/1262 is resolved + Object.defineProperty(globalThis, name, { + enumerable: true, + configurable: true, + value: globals[name] + }); + } +} diff --git a/packages/kit/src/runtime/server/page/crypto.spec.js b/packages/kit/src/runtime/server/page/crypto.spec.js index cf967e184c97..a17f1f16ea28 100644 --- a/packages/kit/src/runtime/server/page/crypto.spec.js +++ b/packages/kit/src/runtime/server/page/crypto.spec.js @@ -1,6 +1,6 @@ import { test } from 'uvu'; import * as assert from 'uvu/assert'; -import crypto from 'crypto'; +import { webcrypto } from 'crypto'; import { sha256 } from './crypto.js'; const inputs = [ @@ -12,9 +12,13 @@ const inputs = [ ].slice(0); inputs.forEach((input) => { - test(input, () => { - const expected_bytes = crypto.createHash('sha256').update(input, 'utf-8').digest(); - const expected = expected_bytes.toString('base64'); + test(input, async () => { + // @ts-expect-error typescript what are you doing you lunatic + const expected_bytes = await webcrypto.subtle.digest( + 'SHA-256', + new TextEncoder().encode(input) + ); + const expected = Buffer.from(expected_bytes).toString('base64'); const actual = sha256(input); assert.equal(actual, expected); diff --git a/packages/kit/src/runtime/server/page/csp.js b/packages/kit/src/runtime/server/page/csp.js index c3d3297347c1..c1baeba40bbe 100644 --- a/packages/kit/src/runtime/server/page/csp.js +++ b/packages/kit/src/runtime/server/page/csp.js @@ -4,33 +4,11 @@ import { sha256, base64 } from './crypto.js'; /** @type {Promise} */ export let csp_ready; -/** @type {() => string} */ -let generate_nonce; - -/** @type {(input: string) => string} */ -let generate_hash; - -if (typeof crypto !== 'undefined') { - const array = new Uint8Array(16); - - generate_nonce = () => { - crypto.getRandomValues(array); - return base64(array); - }; - - generate_hash = sha256; -} else { - // TODO: remove this in favor of web crypto API once we no longer support Node 14 - const name = 'crypto'; // store in a variable to fool esbuild when adapters bundle kit - csp_ready = import(name).then((crypto) => { - generate_nonce = () => { - return crypto.randomBytes(16).toString('base64'); - }; - - generate_hash = (input) => { - return crypto.createHash('sha256').update(input, 'utf-8').digest().toString('base64'); - }; - }); +const array = new Uint8Array(16); + +function generate_nonce() { + crypto.getRandomValues(array); + return base64(array); } const quoted = new Set([ @@ -133,7 +111,7 @@ export class Csp { add_script(content) { if (this.#script_needs_csp) { if (this.#use_hashes) { - this.#script_src.push(`sha256-${generate_hash(content)}`); + this.#script_src.push(`sha256-${sha256(content)}`); } else if (this.#script_src.length === 0) { this.#script_src.push(`nonce-${this.nonce}`); } @@ -144,7 +122,7 @@ export class Csp { add_style(content) { if (this.#style_needs_csp) { if (this.#use_hashes) { - this.#style_src.push(`sha256-${generate_hash(content)}`); + this.#style_src.push(`sha256-${sha256(content)}`); } else if (this.#style_src.length === 0) { this.#style_src.push(`nonce-${this.nonce}`); } diff --git a/packages/kit/src/runtime/server/page/csp.spec.js b/packages/kit/src/runtime/server/page/csp.spec.js index 69b038b840c3..0b3cfa591f63 100644 --- a/packages/kit/src/runtime/server/page/csp.spec.js +++ b/packages/kit/src/runtime/server/page/csp.spec.js @@ -1,7 +1,11 @@ import { test } from 'uvu'; import * as assert from 'uvu/assert'; +import { webcrypto } from 'crypto'; import { Csp } from './csp.js'; +// @ts-expect-error +globalThis.crypto = webcrypto; + test('generates blank CSP header', () => { const csp = new Csp( { diff --git a/packages/kit/types/ambient.d.ts b/packages/kit/types/ambient.d.ts index 89a0285f2d2d..a36c354c60c7 100644 --- a/packages/kit/types/ambient.d.ts +++ b/packages/kit/types/ambient.d.ts @@ -283,11 +283,16 @@ declare module '@sveltejs/kit/hooks' { /** * A polyfill for `fetch` and its related interfaces, used by adapters for environments that don't provide a native implementation. */ -declare module '@sveltejs/kit/install-fetch' { +declare module '@sveltejs/kit/node/polyfills' { /** - * Make `fetch`, `Headers`, `Request` and `Response` available as globals, via `node-fetch` + * Make various web APIs available as globals: + * - `crypto` + * - `fetch` + * - `Headers` + * - `Request` + * - `Response` */ - export function installFetch(): void; + export function installPolyfills(): void; } /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b5ce8b432a9..2a4f8ad5e94e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -103,15 +103,19 @@ importers: '@iarna/toml': ^2.2.5 '@netlify/functions': ^1.0.0 '@sveltejs/kit': workspace:* + '@types/set-cookie-parser': ^2.4.2 esbuild: ^0.14.29 + set-cookie-parser: ^2.4.8 tiny-glob: ^0.2.9 dependencies: '@iarna/toml': 2.2.5 esbuild: 0.14.29 + set-cookie-parser: 2.4.8 tiny-glob: 0.2.9 devDependencies: '@netlify/functions': 1.0.0 '@sveltejs/kit': link:../kit + '@types/set-cookie-parser': 2.4.2 packages/adapter-node: specifiers: @@ -4898,7 +4902,6 @@ packages: /set-cookie-parser/2.4.8: resolution: {integrity: sha512-edRH8mBKEWNVIVMKejNnuJxleqYE/ZSdcT8/Nem9/mmosx12pctd80s2Oy00KNZzrogMZS5mauK2/ymL1bvlvg==} - dev: true /sharp/0.29.3: resolution: {integrity: sha512-fKWUuOw77E4nhpyzCCJR1ayrttHoFHBT2U/kR/qEMRhvPEcluG4BKj324+SCO1e84+knXHwhJ1HHJGnUt4ElGA==}