@stacksjs/rpx 0.11.3 → 0.11.4
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 +1 -1
- package/dist/{chunk-8mnzvjyr.js → chunk-pbbtnqsx.js} +1 -1
- package/dist/src/index.js +1 -1
- package/package.json +3 -11
- package/src/colors.ts +13 -0
- package/src/config.ts +45 -0
- package/src/dns.ts +399 -0
- package/src/hosts.ts +257 -0
- package/src/https.ts +780 -0
- package/src/index.ts +33 -0
- package/src/logger.ts +19 -0
- package/src/port-manager.ts +183 -0
- package/src/process-manager.ts +164 -0
- package/src/start.ts +1361 -0
- package/src/types.ts +84 -0
- package/src/utils.ts +127 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { TlsConfig, TlsOption } from '@stacksjs/tlsx'
|
|
2
|
+
|
|
3
|
+
export interface StartOptions {
|
|
4
|
+
command: string
|
|
5
|
+
cwd?: string
|
|
6
|
+
env?: Record<string, string>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface PathRewrite {
|
|
10
|
+
/** Path prefix to match, e.g. '/api' */
|
|
11
|
+
from: string
|
|
12
|
+
/** Target backend to route to, e.g. 'localhost:3008' */
|
|
13
|
+
to: string
|
|
14
|
+
/** Strip the matched prefix before forwarding (default: true) */
|
|
15
|
+
stripPrefix?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface BaseProxyConfig {
|
|
19
|
+
from: string // localhost:5173
|
|
20
|
+
to: string // stacks.localhost
|
|
21
|
+
start?: StartOptions
|
|
22
|
+
pathRewrites?: PathRewrite[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type BaseProxyOptions = Partial<BaseProxyConfig>
|
|
26
|
+
|
|
27
|
+
export interface CleanupConfig {
|
|
28
|
+
domains: string[] // default: [], if only specific domain/s should be cleaned up
|
|
29
|
+
hosts: boolean // default: true, if hosts file should be cleaned up
|
|
30
|
+
certs: boolean // default: false, if certificates should be cleaned up
|
|
31
|
+
verbose: boolean // default: false
|
|
32
|
+
vitePluginUsage?: boolean // default: false, if cleanup was initiated by the Vite plugin
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type CleanupOptions = Partial<CleanupConfig>
|
|
36
|
+
|
|
37
|
+
export interface SharedProxyConfig {
|
|
38
|
+
https: boolean | TlsOption
|
|
39
|
+
cleanup: boolean | CleanupOptions
|
|
40
|
+
vitePluginUsage: boolean
|
|
41
|
+
verbose: boolean
|
|
42
|
+
_cachedSSLConfig?: SSLConfig | null
|
|
43
|
+
start?: StartOptions
|
|
44
|
+
cleanUrls: boolean
|
|
45
|
+
changeOrigin?: boolean // default: false - changes the origin of the host header to the target URL
|
|
46
|
+
regenerateUntrustedCerts?: boolean // If true, will regenerate and re-trust certs that exist but are not trusted by the system.
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type SharedProxyOptions = Partial<SharedProxyConfig>
|
|
50
|
+
|
|
51
|
+
export interface SingleProxyConfig extends BaseProxyConfig, SharedProxyConfig {}
|
|
52
|
+
|
|
53
|
+
export interface MultiProxyConfig extends SharedProxyConfig {
|
|
54
|
+
proxies: Array<BaseProxyConfig & { cleanUrls: boolean, pathRewrites?: PathRewrite[] }>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type ProxyConfig = SingleProxyConfig
|
|
58
|
+
export type ProxyConfigs = SingleProxyConfig | MultiProxyConfig
|
|
59
|
+
|
|
60
|
+
export type BaseProxyOption = Partial<BaseProxyConfig>
|
|
61
|
+
export type ProxyOption = Partial<SingleProxyConfig>
|
|
62
|
+
export type ProxyOptions = Partial<SingleProxyConfig> | Partial<MultiProxyConfig>
|
|
63
|
+
|
|
64
|
+
export interface SSLConfig {
|
|
65
|
+
key: string
|
|
66
|
+
cert: string
|
|
67
|
+
ca?: string | string[]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ProxySetupOptions extends Omit<ProxyOption, 'from'> {
|
|
71
|
+
fromPort: number
|
|
72
|
+
sourceUrl: Pick<URL, 'hostname' | 'host'>
|
|
73
|
+
ssl: SSLConfig | null
|
|
74
|
+
from: string
|
|
75
|
+
to: string
|
|
76
|
+
portManager?: PortManager
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface PortManager {
|
|
80
|
+
usedPorts: Set<number>
|
|
81
|
+
getNextAvailablePort: (startPort: number) => Promise<number>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type { TlsConfig, TlsOption }
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { MultiProxyConfig, ProxyConfigs, ProxyOption, ProxyOptions, SingleProxyConfig } from './types'
|
|
2
|
+
import { execSync } from 'node:child_process'
|
|
3
|
+
import * as fs from 'node:fs/promises'
|
|
4
|
+
import { Logger } from '@stacksjs/clarity'
|
|
5
|
+
|
|
6
|
+
const logger = new Logger('rpx', {
|
|
7
|
+
showTags: false,
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get sudo password from environment variable if set
|
|
12
|
+
*/
|
|
13
|
+
export function getSudoPassword(): string | undefined {
|
|
14
|
+
return process.env.SUDO_PASSWORD
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Execute a command with sudo, using SUDO_PASSWORD if available
|
|
19
|
+
*/
|
|
20
|
+
export function execSudoSync(command: string): string {
|
|
21
|
+
const sudoPassword = getSudoPassword()
|
|
22
|
+
const escaped = command.replace(/'/g, `'\\''`)
|
|
23
|
+
|
|
24
|
+
if (sudoPassword) {
|
|
25
|
+
return execSync(`echo '${sudoPassword}' | sudo -S sh -c '${escaped}' 2>/dev/null`, {
|
|
26
|
+
encoding: 'utf-8',
|
|
27
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return execSync(`sudo sh -c '${escaped}'`, { encoding: 'utf-8' })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function debugLog(category: string, message: string, verbose?: boolean): void {
|
|
35
|
+
if (verbose)
|
|
36
|
+
logger.debug(`[rpx:${category}] ${message}`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Extracts hostnames from proxy configuration
|
|
41
|
+
*/
|
|
42
|
+
export function extractHostname(options: ProxyOption | ProxyOptions): string[] {
|
|
43
|
+
if (isMultiProxyOptions(options)) {
|
|
44
|
+
return options.proxies.map((proxy) => {
|
|
45
|
+
const domain = proxy.to || 'stacks.localhost'
|
|
46
|
+
return domain.startsWith('http') ? new URL(domain).hostname : domain
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (isSingleProxyOptions(options)) {
|
|
51
|
+
const domain = options.to || 'stacks.localhost'
|
|
52
|
+
return [domain.startsWith('http') ? new URL(domain).hostname : domain]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return ['stacks.localhost']
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface RootCA {
|
|
59
|
+
certificate: string
|
|
60
|
+
privateKey: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function isValidRootCA(value: unknown): value is RootCA {
|
|
64
|
+
return (
|
|
65
|
+
typeof value === 'object'
|
|
66
|
+
&& value !== null
|
|
67
|
+
&& 'certificate' in value
|
|
68
|
+
&& 'privateKey' in value
|
|
69
|
+
&& typeof (value as RootCA).certificate === 'string'
|
|
70
|
+
&& typeof (value as RootCA).privateKey === 'string'
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getPrimaryDomain(options?: ProxyOption | ProxyOptions): string {
|
|
75
|
+
if (!options)
|
|
76
|
+
return 'stacks.localhost'
|
|
77
|
+
|
|
78
|
+
if (isMultiProxyOptions(options) && options.proxies.length > 0)
|
|
79
|
+
return options.proxies[0].to || 'stacks.localhost'
|
|
80
|
+
|
|
81
|
+
if (isSingleProxyOptions(options))
|
|
82
|
+
return options.to || 'stacks.localhost'
|
|
83
|
+
|
|
84
|
+
return 'stacks.localhost'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Type guard for multi-proxy configuration
|
|
89
|
+
*/
|
|
90
|
+
export function isMultiProxyConfig(options: ProxyConfigs | ProxyOptions): options is MultiProxyConfig {
|
|
91
|
+
return !!(options && 'proxies' in options && Array.isArray((options as MultiProxyConfig).proxies))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Type guard to check if options are for multi-proxy configuration
|
|
96
|
+
*/
|
|
97
|
+
export function isMultiProxyOptions(options: ProxyOption | ProxyOptions): options is MultiProxyConfig {
|
|
98
|
+
return 'proxies' in options && Array.isArray((options as MultiProxyConfig).proxies)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Type guard to check if options are for single-proxy configuration
|
|
103
|
+
*/
|
|
104
|
+
export function isSingleProxyOptions(options: ProxyOption | ProxyOptions): options is SingleProxyConfig {
|
|
105
|
+
return 'to' in options && typeof (options as SingleProxyConfig).to === 'string'
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function isSingleProxyConfig(options: ProxyConfigs | ProxyOptions): options is SingleProxyConfig {
|
|
109
|
+
return !!(options && 'to' in options && !('proxies' in options))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Safely delete a file if it exists
|
|
114
|
+
*/
|
|
115
|
+
export async function safeDeleteFile(filePath: string, verbose?: boolean): Promise<void> {
|
|
116
|
+
try {
|
|
117
|
+
// Try to delete the file directly without checking existence first
|
|
118
|
+
await fs.unlink(filePath)
|
|
119
|
+
debugLog('certificates', `Successfully deleted: ${filePath}`, verbose)
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
// Ignore errors where file doesn't exist
|
|
123
|
+
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
124
|
+
debugLog('certificates', `Warning: Could not delete ${filePath}: ${err}`, verbose)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|