@stacksjs/rpx 0.11.19 → 0.11.21
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/acme-challenge.d.ts +13 -0
- package/dist/auth.d.ts +21 -0
- package/dist/bin/cli.js +252 -200
- package/dist/cert-inspect.d.ts +5 -3
- package/dist/{chunk-83dqq28c.js → chunk-1108y1dk.js} +1 -1
- package/dist/{chunk-0f32jmrb.js → chunk-c9brkawa.js} +1 -1
- package/dist/{chunk-w888yhnp.js → chunk-cqkz06bg.js} +1 -1
- package/dist/chunk-qs36t2rf.js +224 -0
- package/dist/daemon.d.ts +4 -1
- package/dist/https.d.ts +1 -0
- package/dist/index.d.ts +33 -1
- package/dist/index.js +7 -7
- package/dist/proxy-handler.d.ts +18 -1
- package/dist/proxy-pool.d.ts +5 -0
- package/dist/redirect.d.ts +40 -0
- package/dist/registry.d.ts +2 -1
- package/dist/site-resolver.d.ts +106 -0
- package/dist/site-splash.d.ts +19 -0
- package/dist/site-supervisor.d.ts +56 -0
- package/dist/start.d.ts +31 -8
- package/dist/types.d.ts +64 -0
- package/package.json +6 -7
- package/dist/chunk-rs8gqpax.js +0 -174
- package/src/cert-inspect.ts +0 -69
- package/src/colors.ts +0 -13
- package/src/config.ts +0 -45
- package/src/daemon-runner.ts +0 -180
- package/src/daemon.ts +0 -1209
- package/src/dns-state.ts +0 -116
- package/src/dns.ts +0 -568
- package/src/host-match.ts +0 -52
- package/src/host-routes.ts +0 -147
- package/src/hosts.ts +0 -283
- package/src/https.ts +0 -905
- package/src/index.ts +0 -161
- package/src/logger.ts +0 -19
- package/src/macos-trust.ts +0 -175
- package/src/on-demand.ts +0 -264
- package/src/origin-guard.ts +0 -127
- package/src/port-manager.ts +0 -183
- package/src/process-manager.ts +0 -164
- package/src/proxy-handler.ts +0 -387
- package/src/proxy-pool.ts +0 -1003
- package/src/registry.ts +0 -366
- package/src/sni.ts +0 -93
- package/src/start.ts +0 -1421
- package/src/static-files.ts +0 -201
- package/src/types.ts +0 -267
- package/src/utils.ts +0 -243
package/src/utils.ts
DELETED
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
import type { MultiProxyConfig, PathRewrite, 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
|
-
/** 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
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Execute a command with sudo, using SUDO_PASSWORD if available
|
|
31
|
-
*/
|
|
32
|
-
export function execSudoSync(command: string): string {
|
|
33
|
-
if (isProcessElevated()) {
|
|
34
|
-
return execSync(command, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] })
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const sudoPassword = getSudoPassword()
|
|
38
|
-
const escaped = command.replace(/'/g, `'\\''`)
|
|
39
|
-
|
|
40
|
-
if (sudoPassword) {
|
|
41
|
-
return execSync(`echo '${sudoPassword}' | sudo -S sh -c '${escaped}' 2>/dev/null`, {
|
|
42
|
-
encoding: 'utf-8',
|
|
43
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
44
|
-
})
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Never block on an interactive password prompt — dev servers must start headlessly.
|
|
48
|
-
try {
|
|
49
|
-
return execSync(`sudo -n sh -c '${escaped}'`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] })
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
throw new Error('sudo required but no cached credentials (set SUDO_PASSWORD in .env or run sudo -v)')
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function debugLog(category: string, message: string, verbose?: boolean): void {
|
|
57
|
-
if (verbose)
|
|
58
|
-
logger.debug(`[rpx:${category}] ${message}`)
|
|
59
|
-
}
|
|
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
|
-
|
|
78
|
-
const REDACTED = '[redacted]'
|
|
79
|
-
const SENSITIVE_KEYS = new Set([
|
|
80
|
-
'certificate',
|
|
81
|
-
'privatekey',
|
|
82
|
-
'key',
|
|
83
|
-
'cert',
|
|
84
|
-
'ca',
|
|
85
|
-
'rootca',
|
|
86
|
-
'password',
|
|
87
|
-
'sudo_password',
|
|
88
|
-
])
|
|
89
|
-
const PEM_BLOCK_RE = /-----BEGIN [A-Z ]+-----[\s\S]*?-----END [A-Z ]+-----/
|
|
90
|
-
|
|
91
|
-
function isSensitiveKey(key: string): boolean {
|
|
92
|
-
const normalized = key.toLowerCase()
|
|
93
|
-
return SENSITIVE_KEYS.has(normalized)
|
|
94
|
-
|| normalized.endsWith('password')
|
|
95
|
-
|| normalized.includes('secret')
|
|
96
|
-
|| normalized.includes('token')
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function redactSensitive(value: unknown): unknown {
|
|
100
|
-
if (Array.isArray(value))
|
|
101
|
-
return value.map(item => redactSensitive(item))
|
|
102
|
-
|
|
103
|
-
if (typeof value === 'string')
|
|
104
|
-
return PEM_BLOCK_RE.test(value) ? REDACTED : value
|
|
105
|
-
|
|
106
|
-
if (!value || typeof value !== 'object')
|
|
107
|
-
return value
|
|
108
|
-
|
|
109
|
-
const output: Record<string, unknown> = {}
|
|
110
|
-
for (const [key, nested] of Object.entries(value)) {
|
|
111
|
-
if (isSensitiveKey(key)) {
|
|
112
|
-
output[key] = REDACTED
|
|
113
|
-
continue
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
output[key] = redactSensitive(nested)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return output
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function safeStringify(value: unknown, space?: number): string {
|
|
123
|
-
return JSON.stringify(redactSensitive(value), null, space)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Extracts hostnames from proxy configuration
|
|
128
|
-
*/
|
|
129
|
-
export function extractHostname(options: ProxyOption | ProxyOptions): string[] {
|
|
130
|
-
if (isMultiProxyOptions(options)) {
|
|
131
|
-
return options.proxies.map((proxy) => {
|
|
132
|
-
const domain = proxy.to || 'stacks.localhost'
|
|
133
|
-
return domain.startsWith('http') ? new URL(domain).hostname : domain
|
|
134
|
-
})
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (isSingleProxyOptions(options)) {
|
|
138
|
-
const domain = options.to || 'stacks.localhost'
|
|
139
|
-
return [domain.startsWith('http') ? new URL(domain).hostname : domain]
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return ['stacks.localhost']
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
interface RootCA {
|
|
146
|
-
certificate: string
|
|
147
|
-
privateKey: string
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export function isValidRootCA(value: unknown): value is RootCA {
|
|
151
|
-
return (
|
|
152
|
-
typeof value === 'object'
|
|
153
|
-
&& value !== null
|
|
154
|
-
&& 'certificate' in value
|
|
155
|
-
&& 'privateKey' in value
|
|
156
|
-
&& typeof (value as RootCA).certificate === 'string'
|
|
157
|
-
&& typeof (value as RootCA).privateKey === 'string'
|
|
158
|
-
)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function getPrimaryDomain(options?: ProxyOption | ProxyOptions): string {
|
|
162
|
-
if (!options)
|
|
163
|
-
return 'stacks.localhost'
|
|
164
|
-
|
|
165
|
-
if (isMultiProxyOptions(options) && options.proxies.length > 0)
|
|
166
|
-
return options.proxies[0].to || 'stacks.localhost'
|
|
167
|
-
|
|
168
|
-
if (isSingleProxyOptions(options))
|
|
169
|
-
return options.to || 'stacks.localhost'
|
|
170
|
-
|
|
171
|
-
return 'stacks.localhost'
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Type guard for multi-proxy configuration
|
|
176
|
-
*/
|
|
177
|
-
export function isMultiProxyConfig(options: ProxyConfigs | ProxyOptions): options is MultiProxyConfig {
|
|
178
|
-
return !!(options && 'proxies' in options && Array.isArray((options as MultiProxyConfig).proxies))
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Type guard to check if options are for multi-proxy configuration
|
|
183
|
-
*/
|
|
184
|
-
export function isMultiProxyOptions(options: ProxyOption | ProxyOptions): options is MultiProxyConfig {
|
|
185
|
-
return 'proxies' in options && Array.isArray((options as MultiProxyConfig).proxies)
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Type guard to check if options are for single-proxy configuration
|
|
190
|
-
*/
|
|
191
|
-
export function isSingleProxyOptions(options: ProxyOption | ProxyOptions): options is SingleProxyConfig {
|
|
192
|
-
return 'to' in options && typeof (options as SingleProxyConfig).to === 'string'
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export function isSingleProxyConfig(options: ProxyConfigs | ProxyOptions): options is SingleProxyConfig {
|
|
196
|
-
return !!(options && 'to' in options && !('proxies' in options))
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Resolve a path against a list of `pathRewrites`.
|
|
201
|
-
*
|
|
202
|
-
* Returns `null` if no rewrite matches; otherwise returns `{ targetHost, targetPath }`
|
|
203
|
-
* with the prefix preserved by default (or stripped when `stripPrefix === true`).
|
|
204
|
-
*
|
|
205
|
-
* Matching rule: rewrite matches if `pathname` is exactly `from` OR starts with
|
|
206
|
-
* `from + '/'`. So `/api` matches `/api`, `/api/`, `/api/cart` — but not `/apidocs`.
|
|
207
|
-
*/
|
|
208
|
-
export function resolvePathRewrite(
|
|
209
|
-
pathname: string,
|
|
210
|
-
rewrites: PathRewrite[] | undefined,
|
|
211
|
-
): { targetHost: string, targetPath: string } | null {
|
|
212
|
-
if (!rewrites || rewrites.length === 0)
|
|
213
|
-
return null
|
|
214
|
-
|
|
215
|
-
for (const rewrite of rewrites) {
|
|
216
|
-
if (pathname === rewrite.from || pathname.startsWith(`${rewrite.from}/`)) {
|
|
217
|
-
const targetHost = rewrite.to.startsWith('http') ? new URL(rewrite.to).host : rewrite.to
|
|
218
|
-
const targetPath = rewrite.stripPrefix === true
|
|
219
|
-
? (pathname.slice(rewrite.from.length) || '/')
|
|
220
|
-
: pathname
|
|
221
|
-
return { targetHost, targetPath }
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return null
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Safely delete a file if it exists
|
|
230
|
-
*/
|
|
231
|
-
export async function safeDeleteFile(filePath: string, verbose?: boolean): Promise<void> {
|
|
232
|
-
try {
|
|
233
|
-
// Try to delete the file directly without checking existence first
|
|
234
|
-
await fs.unlink(filePath)
|
|
235
|
-
debugLog('certificates', `Successfully deleted: ${filePath}`, verbose)
|
|
236
|
-
}
|
|
237
|
-
catch (err) {
|
|
238
|
-
// Ignore errors where file doesn't exist
|
|
239
|
-
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
240
|
-
debugLog('certificates', `Warning: Could not delete ${filePath}: ${err}`, verbose)
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|