@stacksjs/rpx 0.11.18 → 0.11.19
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/bin/cli.js +162 -149
- package/dist/chunk-0f32jmrb.js +1 -0
- package/dist/chunk-83dqq28c.js +1 -0
- package/dist/chunk-rs8gqpax.js +174 -0
- package/dist/{chunk-rbgb5fyg.js → chunk-w888yhnp.js} +12 -12
- package/dist/daemon.d.ts +17 -8
- package/dist/dns.d.ts +2 -0
- package/dist/https.d.ts +9 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -7
- package/dist/logger.d.ts +8 -8
- package/dist/proxy-handler.d.ts +1 -1
- package/dist/proxy-pool.d.ts +77 -0
- package/dist/types.d.ts +10 -0
- package/dist/utils.d.ts +15 -0
- package/package.json +6 -1
- package/src/daemon.ts +471 -48
- package/src/dns.ts +66 -7
- package/src/hosts.ts +6 -1
- package/src/https.ts +46 -3
- package/src/index.ts +3 -0
- package/src/logger.ts +16 -16
- package/src/origin-guard.ts +27 -5
- package/src/proxy-handler.ts +122 -50
- package/src/proxy-pool.ts +1003 -0
- package/src/start.ts +32 -32
- package/src/types.ts +11 -0
- package/src/utils.ts +33 -0
- package/dist/chunk-9njrhbd4.js +0 -1
- package/dist/chunk-a0b9f9fs.js +0 -1
- package/dist/chunk-s4tdtq37.js +0 -161
package/src/start.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
2
|
import type { IncomingHttpHeaders, SecureServerOptions } from 'node:http2'
|
|
3
3
|
import type { ServerOptions } from 'node:https'
|
|
4
|
-
import type { BaseProxyConfig, CleanupOptions, ProxyConfig, ProxyOption, ProxyOptions, ProxySetupOptions, SingleProxyConfig, SSLConfig, StartOptions } from './types'
|
|
4
|
+
import type { BaseProxyConfig, CleanupOptions, ProxyConfig, ProxyOption, ProxyOptions, ProxySetupOptions, ResolvedProxyOptions, SingleProxyConfig, SSLConfig, StartOptions } from './types'
|
|
5
5
|
import { exec, execSync } from 'node:child_process'
|
|
6
6
|
import * as fs from 'node:fs'
|
|
7
7
|
import * as http from 'node:http'
|
|
@@ -26,7 +26,7 @@ import type { ProxyRoute, ProxyServer as ProxyServerLike } from './proxy-handler
|
|
|
26
26
|
import { isWildcardPattern } from './host-match'
|
|
27
27
|
import { buildHostRoutes, matchHostRoute, normalizePathPrefix } from './host-routes'
|
|
28
28
|
import { resolveStaticRoute } from './static-files'
|
|
29
|
-
import { debugLog, getSudoPassword, safeStringify } from './utils'
|
|
29
|
+
import { debugLog, getSudoPassword, safeStringify, shouldReusePort } from './utils'
|
|
30
30
|
|
|
31
31
|
const processManager = new ProcessManager()
|
|
32
32
|
const version = '0.12.0'
|
|
@@ -237,7 +237,8 @@ async function testConnection(hostname: string, port: number, verbose?: boolean,
|
|
|
237
237
|
try {
|
|
238
238
|
await tryConnect()
|
|
239
239
|
}
|
|
240
|
-
catch (err:
|
|
240
|
+
catch (err: unknown) {
|
|
241
|
+
const e = err as NodeJS.ErrnoException
|
|
241
242
|
// Check if we've exceeded the maximum test duration
|
|
242
243
|
if (Date.now() - startTime > maxTestDuration) {
|
|
243
244
|
debugLog('connection', `Connection test timed out after ${maxTestDuration}ms, but continuing anyway`, verbose)
|
|
@@ -246,7 +247,7 @@ async function testConnection(hostname: string, port: number, verbose?: boolean,
|
|
|
246
247
|
}
|
|
247
248
|
|
|
248
249
|
// If we're dealing with a server that takes time to start up
|
|
249
|
-
if (
|
|
250
|
+
if (e.code === 'ECONNREFUSED' && retries > 0) {
|
|
250
251
|
debugLog('connection', `Connection refused, server might be starting up. Retrying in 2 seconds... (${retries} retries left)`, verbose)
|
|
251
252
|
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
252
253
|
return testConnection(hostname, port, verbose, retries - 1)
|
|
@@ -294,7 +295,7 @@ async function testConnection(hostname: string, port: number, verbose?: boolean,
|
|
|
294
295
|
|
|
295
296
|
// For production environments, we might want to be more strict
|
|
296
297
|
// But for typical usage, let's be permissive and just warn
|
|
297
|
-
const errorMessage = `Failed to connect to ${hostname}:${port} after ${5 - retries} attempts: ${
|
|
298
|
+
const errorMessage = `Failed to connect to ${hostname}:${port} after ${5 - retries} attempts: ${e.message}`
|
|
298
299
|
debugLog('connection', `${errorMessage}. To bypass this check set RPX_BYPASS_CONNECTION_TEST=true`, verbose)
|
|
299
300
|
log.warn(errorMessage)
|
|
300
301
|
log.warn(`RPX will try to continue anyway. If you're sure this is correct, you can set RPX_BYPASS_CONNECTION_TEST=true to skip this check.`)
|
|
@@ -585,6 +586,7 @@ async function createProxyServer(
|
|
|
585
586
|
const bunServer = Bun.serve({
|
|
586
587
|
port: listenPort,
|
|
587
588
|
hostname,
|
|
589
|
+
reusePort: shouldReusePort(),
|
|
588
590
|
tls: {
|
|
589
591
|
key: ssl.key,
|
|
590
592
|
cert: ssl.cert,
|
|
@@ -597,9 +599,19 @@ async function createProxyServer(
|
|
|
597
599
|
const url = new URL(req.url)
|
|
598
600
|
debugLog('request', `Bun.serve received: ${req.method} ${url.pathname}`, verbose)
|
|
599
601
|
|
|
600
|
-
//
|
|
602
|
+
// Handle clean URLs redirect before any upstream work — the
|
|
603
|
+
// redirect doesn't depend on the origin response.
|
|
604
|
+
if (cleanUrls && url.pathname.endsWith('.html')) {
|
|
605
|
+
const cleanPath = url.pathname.replace(/\.html$/, '')
|
|
606
|
+
return new Response(null, {
|
|
607
|
+
status: 301,
|
|
608
|
+
headers: { Location: cleanPath },
|
|
609
|
+
})
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Build target URL by string concat (avoids an extra URL parse).
|
|
601
613
|
const baseUrl = `http://${sourceUrl.host}`
|
|
602
|
-
const targetUrl =
|
|
614
|
+
const targetUrl = `${baseUrl}${url.pathname}${url.search}`
|
|
603
615
|
|
|
604
616
|
// Forward the request
|
|
605
617
|
try {
|
|
@@ -612,30 +624,15 @@ async function createProxyServer(
|
|
|
612
624
|
headers.set('x-forwarded-proto', 'https')
|
|
613
625
|
headers.set('x-forwarded-host', to)
|
|
614
626
|
|
|
615
|
-
|
|
627
|
+
// Return the upstream response directly — Bun streams the body and
|
|
628
|
+
// forwards status/headers verbatim, so re-wrapping it in a fresh
|
|
629
|
+
// Response would just copy every header and allocate twice.
|
|
630
|
+
return await fetch(targetUrl, {
|
|
616
631
|
method: req.method,
|
|
617
632
|
headers,
|
|
618
633
|
body: req.body,
|
|
619
634
|
redirect: 'manual',
|
|
620
635
|
})
|
|
621
|
-
|
|
622
|
-
// Clone response with modified headers if needed
|
|
623
|
-
const responseHeaders = new Headers(response.headers)
|
|
624
|
-
|
|
625
|
-
// Handle clean URLs redirect
|
|
626
|
-
if (cleanUrls && url.pathname.endsWith('.html')) {
|
|
627
|
-
const cleanPath = url.pathname.replace(/\.html$/, '')
|
|
628
|
-
return new Response(null, {
|
|
629
|
-
status: 301,
|
|
630
|
-
headers: { Location: cleanPath },
|
|
631
|
-
})
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
return new Response(response.body, {
|
|
635
|
-
status: response.status,
|
|
636
|
-
statusText: response.statusText,
|
|
637
|
-
headers: responseHeaders,
|
|
638
|
-
})
|
|
639
636
|
}
|
|
640
637
|
catch (err) {
|
|
641
638
|
debugLog('request', `Proxy error: ${err}`, verbose)
|
|
@@ -934,7 +931,7 @@ export function startProxy(options: ProxyOption): void {
|
|
|
934
931
|
}
|
|
935
932
|
|
|
936
933
|
// Helper function to safely get verbose flag from different config types
|
|
937
|
-
function getVerbose(options:
|
|
934
|
+
function getVerbose(options: ProxyOptions): boolean {
|
|
938
935
|
return options?.verbose || false
|
|
939
936
|
}
|
|
940
937
|
|
|
@@ -944,7 +941,7 @@ function getVerbose(options: any): boolean {
|
|
|
944
941
|
* `false`). Real-server deployments with real DNS should set
|
|
945
942
|
* `hostsManagement: false` so rpx never touches `/etc/hosts`.
|
|
946
943
|
*/
|
|
947
|
-
function isHostsManagementEnabled(options:
|
|
944
|
+
function isHostsManagementEnabled(options: ProxyOptions): boolean {
|
|
948
945
|
if (options?.hostsManagement === false)
|
|
949
946
|
return false
|
|
950
947
|
const cleanup = options?.cleanup
|
|
@@ -971,7 +968,7 @@ export async function startProxies(options?: ProxyOptions): Promise<void> {
|
|
|
971
968
|
cleanUrls: false,
|
|
972
969
|
changeOrigin: false,
|
|
973
970
|
regenerateUntrustedCerts: true,
|
|
974
|
-
} as
|
|
971
|
+
} as ResolvedProxyOptions
|
|
975
972
|
|
|
976
973
|
if (options) {
|
|
977
974
|
mergedOptions = {
|
|
@@ -1007,7 +1004,7 @@ export async function startProxies(options?: ProxyOptions): Promise<void> {
|
|
|
1007
1004
|
: [{
|
|
1008
1005
|
id: mergedOptions.id,
|
|
1009
1006
|
from: mergedOptions.from,
|
|
1010
|
-
to: mergedOptions.to,
|
|
1007
|
+
to: mergedOptions.to ?? 'rpx.localhost',
|
|
1011
1008
|
path: mergedOptions.path,
|
|
1012
1009
|
cleanUrls: mergedOptions.cleanUrls,
|
|
1013
1010
|
changeOrigin: mergedOptions.changeOrigin,
|
|
@@ -1030,7 +1027,7 @@ export async function startProxies(options?: ProxyOptions): Promise<void> {
|
|
|
1030
1027
|
await processManager.startProcess(proxyId, proxy.start, verbose)
|
|
1031
1028
|
|
|
1032
1029
|
// Parse the URL to get hostname and port
|
|
1033
|
-
const fromUrl = new URL(proxy.from
|
|
1030
|
+
const fromUrl = new URL(proxy.from?.startsWith('http') ? proxy.from : `http://${proxy.from}`)
|
|
1034
1031
|
const hostname = fromUrl.hostname || 'localhost'
|
|
1035
1032
|
const port = Number(fromUrl.port) || 80
|
|
1036
1033
|
|
|
@@ -1136,7 +1133,7 @@ export async function startProxies(options?: ProxyOptions): Promise<void> {
|
|
|
1136
1133
|
|
|
1137
1134
|
// Prepare proxy configurations
|
|
1138
1135
|
const proxyOptions = 'proxies' in mergedOptions && Array.isArray(mergedOptions.proxies)
|
|
1139
|
-
? mergedOptions.proxies.map(
|
|
1136
|
+
? mergedOptions.proxies.map(proxy => ({
|
|
1140
1137
|
...proxy,
|
|
1141
1138
|
https: mergedOptions.https,
|
|
1142
1139
|
cleanup: mergedOptions.cleanup,
|
|
@@ -1340,6 +1337,9 @@ export async function startProxies(options?: ProxyOptions): Promise<void> {
|
|
|
1340
1337
|
const bunServer = Bun.serve({
|
|
1341
1338
|
port: listenPort,
|
|
1342
1339
|
hostname: '0.0.0.0',
|
|
1340
|
+
// Opt-in (RPX_REUSE_PORT): lets multiple rpx instances share :443 for
|
|
1341
|
+
// multi-core scaling on Linux. Off by default — see shouldReusePort().
|
|
1342
|
+
reusePort: shouldReusePort(),
|
|
1343
1343
|
tls: {
|
|
1344
1344
|
key: sslConfig.key,
|
|
1345
1345
|
cert: sslConfig.cert,
|
package/src/types.ts
CHANGED
|
@@ -233,6 +233,17 @@ export type BaseProxyOption = Partial<BaseProxyConfig>
|
|
|
233
233
|
export type ProxyOption = Partial<SingleProxyConfig>
|
|
234
234
|
export type ProxyOptions = Partial<SingleProxyConfig> | Partial<MultiProxyConfig>
|
|
235
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Internal shape used by `startProxies` after merging the built-in defaults with
|
|
238
|
+
* the caller's single- or multi-proxy options. Every field is optional, and the
|
|
239
|
+
* `proxies` array elements tolerate the per-proxy `cleanUrls`/`changeOrigin`
|
|
240
|
+
* overrides the runtime reads — so the merged object can be accessed across both
|
|
241
|
+
* single and multi shapes without falling back to `any`.
|
|
242
|
+
*/
|
|
243
|
+
export type ResolvedProxyOptions = Partial<SingleProxyConfig> & {
|
|
244
|
+
proxies?: Array<BaseProxyConfig & { cleanUrls?: boolean, changeOrigin?: boolean, pathRewrites?: PathRewrite[] }>
|
|
245
|
+
}
|
|
246
|
+
|
|
236
247
|
export interface SSLConfig {
|
|
237
248
|
key: string
|
|
238
249
|
cert: string
|
package/src/utils.ts
CHANGED
|
@@ -14,10 +14,26 @@ export function getSudoPassword(): string | undefined {
|
|
|
14
14
|
return process.env.SUDO_PASSWORD
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/** True when the process can write system paths without `sudo` (e.g. rpx daemon started via sudo). */
|
|
18
|
+
export function isProcessElevated(): boolean {
|
|
19
|
+
if (process.platform === 'win32')
|
|
20
|
+
return false
|
|
21
|
+
try {
|
|
22
|
+
return typeof process.getuid === 'function' && process.getuid() === 0
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
17
29
|
/**
|
|
18
30
|
* Execute a command with sudo, using SUDO_PASSWORD if available
|
|
19
31
|
*/
|
|
20
32
|
export function execSudoSync(command: string): string {
|
|
33
|
+
if (isProcessElevated()) {
|
|
34
|
+
return execSync(command, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] })
|
|
35
|
+
}
|
|
36
|
+
|
|
21
37
|
const sudoPassword = getSudoPassword()
|
|
22
38
|
const escaped = command.replace(/'/g, `'\\''`)
|
|
23
39
|
|
|
@@ -42,6 +58,23 @@ export function debugLog(category: string, message: string, verbose?: boolean):
|
|
|
42
58
|
logger.debug(`[rpx:${category}] ${message}`)
|
|
43
59
|
}
|
|
44
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Whether the proxy's listeners should set `reusePort`. Off by default: rpx is a
|
|
63
|
+
* single-instance local-dev proxy, and one instance gains nothing from
|
|
64
|
+
* `SO_REUSEPORT` while losing the helpful "port 443 already in use" guard.
|
|
65
|
+
*
|
|
66
|
+
* Opt in with `RPX_REUSE_PORT=1` when running **multiple** rpx instances behind a
|
|
67
|
+
* process manager (systemd/pm2/k8s) on **Linux**, where the kernel load-balances
|
|
68
|
+
* accepted connections across them for multi-core scaling. It is a no-op on
|
|
69
|
+
* macOS/BSD, whose `SO_REUSEPORT` does not load-balance across processes — so
|
|
70
|
+
* rpx never spawns a worker cluster itself; scaling is left to the OS + your
|
|
71
|
+
* supervisor, which is where it belongs for a dev proxy.
|
|
72
|
+
*/
|
|
73
|
+
export function shouldReusePort(): boolean {
|
|
74
|
+
const v = process.env.RPX_REUSE_PORT
|
|
75
|
+
return v === '1' || v === 'true'
|
|
76
|
+
}
|
|
77
|
+
|
|
45
78
|
const REDACTED = '[redacted]'
|
|
46
79
|
const SENSITIVE_KEYS = new Set([
|
|
47
80
|
'certificate',
|
package/dist/chunk-9njrhbd4.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{ka as a,la as b,ma as c,na as d,oa as e,pa as f,qa as g,ra as h,sa as i,ta as j,ua as k,va as l,wa as m,xa as n}from"./chunk-s4tdtq37.js";import"./chunk-rbgb5fyg.js";export{l as tearDownDevelopmentDns,k as syncDevelopmentDnsFromRegistry,d as stopDnsServer,c as startDnsServer,h as setupResolver,j as setupDevelopmentDns,f as resolverFilePath,m as removeResolver,i as removeLegacyTldResolvers,n as reconcileStaleDevelopmentDns,e as isDnsServerRunning,g as contentLooksLikeRpxResolver,b as RPX_RESOLVER_MARKER,a as DNS_PORT};
|
package/dist/chunk-a0b9f9fs.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{Ka as a,La as b,Ma as c,Na as d,Oa as e,Pa as f,Qa as g,Ra as h,Sa as i,Ta as j,Ua as k,Va as l,Wa as m,Xa as n}from"./chunk-rbgb5fyg.js";export{e as safeStringify,n as safeDeleteFile,m as resolvePathRewrite,d as redactSensitive,g as isValidRootCA,k as isSingleProxyOptions,l as isSingleProxyConfig,j as isMultiProxyOptions,i as isMultiProxyConfig,a as getSudoPassword,h as getPrimaryDomain,f as extractHostname,b as execSudoSync,c as debugLog};
|