@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/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
+ }