@megabudino/stack-utils 2.1.0 → 2.3.0

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.
@@ -1,6 +1,7 @@
1
1
  import { validateIslandsConfigAtStartup } from './islands.js';
2
2
  import { injectIntoSitemapIndex, normalizeSitemapPath, removeExcludedUrlsFromSitemap, rewriteHtml } from './rewrite.js';
3
3
  import { transformProxiedHtml } from './transform-html.js';
4
+ import { fetchViaTransport } from './upstream-fetch.js';
4
5
  /**
5
6
  * Create a SvelteKit `Handle` hook that reverse-proxies requests to an origin.
6
7
  *
@@ -10,6 +11,11 @@ import { transformProxiedHtml } from './transform-html.js';
10
11
  export function createProxyHandle(config) {
11
12
  assertNoRemovedConfig(config);
12
13
  const { originUrl, siteUrl } = config;
14
+ const logicalOrigin = new URL(originUrl);
15
+ const upstreamUrlTrimmed = config.upstreamUrl?.trim();
16
+ const useTransport = Boolean(upstreamUrlTrimmed);
17
+ const transport = new URL(upstreamUrlTrimmed || originUrl);
18
+ const forwardedProto = new URL(siteUrl).protocol.replace(':', '');
13
19
  const sitemap = config.sitemap ?? {};
14
20
  const sitemapIndexPath = sitemap.indexPath ? normalizeSitemapPath(sitemap.indexPath) : undefined;
15
21
  const sitemapIncludeFiles = sitemap.includeFiles ?? [];
@@ -34,21 +40,38 @@ export function createProxyHandle(config) {
34
40
  }
35
41
  const headers = new Headers(event.request.headers);
36
42
  headers.delete('accept-encoding');
37
- headers.set('host', siteHost);
43
+ if (useTransport) {
44
+ headers.set('host', logicalOrigin.host);
45
+ headers.set('x-forwarded-proto', forwardedProto);
46
+ if (!headers.has('x-forwarded-host')) {
47
+ headers.set('x-forwarded-host', siteHost);
48
+ }
49
+ }
50
+ else {
51
+ headers.set('host', siteHost);
52
+ }
38
53
  headers.delete('content-length');
54
+ const connectionBase = useTransport ? transport.origin : originUrl;
55
+ const requestBody = event.request.method === 'GET' || event.request.method === 'HEAD'
56
+ ? undefined
57
+ : event.request.body;
39
58
  let currentPath = pathname + search;
40
59
  for (let hops = 0;; hops++) {
41
- const upstream = new URL(currentPath, originUrl);
60
+ const upstream = new URL(currentPath, connectionBase);
42
61
  let upstreamResponse;
43
62
  try {
44
- upstreamResponse = await fetch(upstream, {
45
- method: event.request.method,
46
- headers,
47
- redirect: 'manual',
48
- body: event.request.method === 'GET' || event.request.method === 'HEAD'
49
- ? undefined
50
- : event.request.body
51
- });
63
+ upstreamResponse = useTransport
64
+ ? await fetchViaTransport(upstream, {
65
+ method: event.request.method,
66
+ headers,
67
+ body: requestBody
68
+ })
69
+ : await fetch(upstream, {
70
+ method: event.request.method,
71
+ headers,
72
+ redirect: 'manual',
73
+ body: requestBody
74
+ });
52
75
  }
53
76
  catch {
54
77
  return new Response('Bad Gateway', { status: 502 });
@@ -56,7 +79,7 @@ export function createProxyHandle(config) {
56
79
  if (upstreamResponse.status >= 300 && upstreamResponse.status < 400) {
57
80
  const location = upstreamResponse.headers.get('location');
58
81
  if (location) {
59
- const nextUrl = new URL(location, upstream);
82
+ const nextUrl = new URL(location, useTransport ? logicalOrigin : upstream);
60
83
  const isOrigin = originHosts.has(nextUrl.hostname);
61
84
  const isSite = nextUrl.hostname === siteHost;
62
85
  if ((isOrigin || isSite) && hops < maxRedirects) {
@@ -39,6 +39,11 @@ export function rewriteHtml(html, originUrl, siteUrl, proxyAssets = false, addit
39
39
  html = html.replace(originPattern(origin), '');
40
40
  }
41
41
  }
42
+ // SvelteKit origins emit relative `./_app/...` assets. Browsers resolve those to
43
+ // `/_app/...` on the proxy host, which collides with the local SvelteKit build.
44
+ // Point them at the origin so CSS/JS load from the upstream app (CORS permitting).
45
+ const originBase = originUrl.replace(/\/$/, '');
46
+ html = html.replace(/(["'(])\.\/_app\//g, `$1${originBase}/_app/`);
42
47
  return html;
43
48
  }
44
49
  /** Normalize URL-like values to comparable sitemap pathnames. */
@@ -133,8 +133,31 @@ export interface SitemapConfig {
133
133
  }
134
134
  /** Configuration for the reverse proxy. */
135
135
  export interface ProxyConfig {
136
- /** The origin URL (not publicly exposed). */
136
+ /**
137
+ * Logical origin URL: the host and scheme WordPress (or the upstream CMS) should
138
+ * believe it is serving. Used for the `Host` header when `upstreamUrl` is set,
139
+ * and as the source domain for response rewriting (`originUrl` → `siteUrl`).
140
+ *
141
+ * When `upstreamUrl` is omitted, this URL is also the connection target.
142
+ */
137
143
  originUrl: string;
144
+ /**
145
+ * Optional transport URL for the actual TCP/HTTP connection to the origin.
146
+ *
147
+ * Use this to reach an internal service name (e.g. a Docker overlay hostname)
148
+ * while still presenting the public `originUrl` identity to the CMS. When
149
+ * absent or blank, the proxy connects to `originUrl` and behavior is unchanged.
150
+ *
151
+ * @example
152
+ * ```ts
153
+ * createProxyHandle({
154
+ * originUrl: 'https://wordpress.example.com',
155
+ * upstreamUrl: process.env.WP_UPSTREAM, // e.g. http://wordpress-service:80
156
+ * siteUrl: 'https://www.example.com'
157
+ * });
158
+ * ```
159
+ */
160
+ upstreamUrl?: string;
138
161
  /** The public site URL (used in sitemaps and link rewriting). */
139
162
  siteUrl: string;
140
163
  /** Explicit sitemap rewriting configuration for proxied XML responses. */
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Forward a request to the origin using undici so a custom `Host` header is
3
+ * honored. Node's built-in `fetch` derives `Host` from the destination URL and
4
+ * ignores `headers.set('host', ...)`, which breaks transport/logical-origin splits.
5
+ */
6
+ export declare function fetchViaTransport(url: URL, init: {
7
+ method: string;
8
+ headers: Headers;
9
+ body?: BodyInit | null;
10
+ }): Promise<Response>;
@@ -0,0 +1,62 @@
1
+ import { request as undiciRequest } from 'undici';
2
+ /**
3
+ * Forward a request to the origin using undici so a custom `Host` header is
4
+ * honored. Node's built-in `fetch` derives `Host` from the destination URL and
5
+ * ignores `headers.set('host', ...)`, which breaks transport/logical-origin splits.
6
+ */
7
+ export async function fetchViaTransport(url, init) {
8
+ const { statusCode, headers, body } = await undiciRequest(url, {
9
+ method: init.method,
10
+ headers: headersRecord(init.headers),
11
+ body: init.method === 'GET' || init.method === 'HEAD'
12
+ ? undefined
13
+ : init.body
14
+ });
15
+ const responseHeaders = new Headers();
16
+ for (const [key, value] of Object.entries(headers)) {
17
+ if (value === undefined) {
18
+ continue;
19
+ }
20
+ if (Array.isArray(value)) {
21
+ for (const entry of value) {
22
+ responseHeaders.append(key, entry);
23
+ }
24
+ continue;
25
+ }
26
+ responseHeaders.set(key, value);
27
+ }
28
+ return new Response(body, {
29
+ status: statusCode,
30
+ headers: responseHeaders
31
+ });
32
+ }
33
+ /** Hop-by-hop and other headers undici rejects when forwarding a client request. */
34
+ const UNDICI_STRIPPED_HEADERS = new Set([
35
+ 'connection',
36
+ 'keep-alive',
37
+ 'proxy-authenticate',
38
+ 'proxy-authorization',
39
+ 'proxy-connection',
40
+ 'te',
41
+ 'trailer',
42
+ 'transfer-encoding',
43
+ 'upgrade',
44
+ 'http2-settings'
45
+ ]);
46
+ function headersRecord(headers) {
47
+ const skip = new Set(UNDICI_STRIPPED_HEADERS);
48
+ const connection = headers.get('connection');
49
+ if (connection) {
50
+ for (const token of connection.split(',')) {
51
+ skip.add(token.trim().toLowerCase());
52
+ }
53
+ }
54
+ const record = {};
55
+ for (const [key, value] of headers.entries()) {
56
+ if (skip.has(key.toLowerCase())) {
57
+ continue;
58
+ }
59
+ record[key] = value;
60
+ }
61
+ return record;
62
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@megabudino/stack-utils",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "Reusable utilities for a SvelteKit stack, including a reverse proxy and static image pipeline",
5
5
  "type": "module",
6
6
  "exports": {
@@ -41,6 +41,7 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "cheerio": "^1.2.0",
44
+ "undici": "^7.24.0",
44
45
  "node-addon-api": "^8.8.0",
45
46
  "node-gyp": "^12.3.0",
46
47
  "sharp": "^0.34.5"