@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/cert-inspect.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'node:child_process'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Normalize openssl / security fingerprint output to uppercase hex without separators.
|
|
5
|
-
*/
|
|
6
|
-
export function normalizeSha256Fingerprint(raw: string): string {
|
|
7
|
-
const value = raw.includes('=') ? raw.split('=').pop()! : raw
|
|
8
|
-
return value.replace(/SHA-256\s+hash:\s*/gi, '').replace(/:/g, '').trim().toUpperCase()
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function readCertSha256Fingerprint(certPath: string): string | null {
|
|
12
|
-
try {
|
|
13
|
-
const out = execSync(`openssl x509 -noout -fingerprint -sha256 -in "${certPath}"`, { encoding: 'utf8' })
|
|
14
|
-
return normalizeSha256Fingerprint(out)
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
return null
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function readCertCommonName(certPath: string): string | null {
|
|
22
|
-
try {
|
|
23
|
-
const subject = execSync(`openssl x509 -in "${certPath}" -noout -subject -nameopt RFC2253`, { encoding: 'utf8' })
|
|
24
|
-
const match = subject.match(/CN=([^,/]+)/)
|
|
25
|
-
return match?.[1]?.trim() ?? null
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
return null
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function certIncludesSanHostnames(certPath: string, hostnames: string[]): boolean {
|
|
33
|
-
try {
|
|
34
|
-
const text = execSync(`openssl x509 -in "${certPath}" -noout -text`, { encoding: 'utf8' })
|
|
35
|
-
return hostnames.every(host => text.includes(`DNS:${host}`))
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
return false
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* True when :443 (or `port`) presents a chain trusted by `caPath` for `domain`.
|
|
44
|
-
*/
|
|
45
|
-
export function verifyHttpsChain(domain: string, caPath: string, port = 443): boolean {
|
|
46
|
-
try {
|
|
47
|
-
const out = execSync(
|
|
48
|
-
`echo | openssl s_client -connect ${domain}:${port} -servername ${domain} -CAfile "${caPath}" 2>/dev/null | grep "Verify return code"`,
|
|
49
|
-
{ encoding: 'utf8', timeout: 4000 },
|
|
50
|
-
)
|
|
51
|
-
return out.includes(': 0 (ok)')
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
return false
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Parse `security find-certificate -Z` listing lines into SHA-256 hashes.
|
|
60
|
-
*/
|
|
61
|
-
export function parseSha256HashesFromSecurityListing(listing: string): string[] {
|
|
62
|
-
const hashes: string[] = []
|
|
63
|
-
for (const line of listing.split('\n')) {
|
|
64
|
-
const match = line.match(/SHA-256 hash:\s*([A-F0-9]+)/i)
|
|
65
|
-
if (match)
|
|
66
|
-
hashes.push(match[1]!.toUpperCase())
|
|
67
|
-
}
|
|
68
|
-
return hashes
|
|
69
|
-
}
|
package/src/colors.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
const c = (open: number, close: number): (str: string) => string => (str: string): string => `\x1B[${open}m${str}\x1B[${close}m`
|
|
2
|
-
|
|
3
|
-
export const colors: {
|
|
4
|
-
bold: (str: string) => string
|
|
5
|
-
dim: (str: string) => string
|
|
6
|
-
green: (str: string) => string
|
|
7
|
-
cyan: (str: string) => string
|
|
8
|
-
} = {
|
|
9
|
-
bold: c(1, 22),
|
|
10
|
-
dim: c(2, 22),
|
|
11
|
-
green: c(32, 39),
|
|
12
|
-
cyan: c(36, 39),
|
|
13
|
-
}
|
package/src/config.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import type { ProxyConfig } from './types'
|
|
2
|
-
import { homedir } from 'node:os'
|
|
3
|
-
import { join, resolve } from 'node:path'
|
|
4
|
-
import { loadConfig } from 'bunfig'
|
|
5
|
-
|
|
6
|
-
export const defaultConfig: ProxyConfig = {
|
|
7
|
-
from: 'localhost:5173',
|
|
8
|
-
to: 'stacks.localhost',
|
|
9
|
-
cleanUrls: false,
|
|
10
|
-
https: {
|
|
11
|
-
basePath: '',
|
|
12
|
-
caCertPath: join(homedir(), '.stacks', 'ssl', `stacks.localhost.ca.crt`),
|
|
13
|
-
certPath: join(homedir(), '.stacks', 'ssl', `stacks.localhost.crt`),
|
|
14
|
-
keyPath: join(homedir(), '.stacks', 'ssl', `stacks.localhost.crt.key`),
|
|
15
|
-
},
|
|
16
|
-
cleanup: {
|
|
17
|
-
certs: false,
|
|
18
|
-
hosts: false,
|
|
19
|
-
},
|
|
20
|
-
vitePluginUsage: false,
|
|
21
|
-
verbose: true,
|
|
22
|
-
changeOrigin: false,
|
|
23
|
-
/**
|
|
24
|
-
* If true, will regenerate and re-trust certs that exist but are not trusted by the system.
|
|
25
|
-
* If false, will use the existing cert even if not trusted (may result in browser warnings).
|
|
26
|
-
*/
|
|
27
|
-
regenerateUntrustedCerts: true,
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Lazy-loaded config to avoid top-level await (enables bun --compile)
|
|
31
|
-
let _config: ProxyConfig | null = null
|
|
32
|
-
|
|
33
|
-
export async function getConfig(): Promise<ProxyConfig> {
|
|
34
|
-
if (!_config) {
|
|
35
|
-
_config = await loadConfig({
|
|
36
|
-
name: 'rpx',
|
|
37
|
-
cwd: resolve(__dirname, '..'),
|
|
38
|
-
defaultConfig,
|
|
39
|
-
})
|
|
40
|
-
}
|
|
41
|
-
return _config
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// For backwards compatibility - synchronous access with default fallback
|
|
45
|
-
export const config: ProxyConfig = defaultConfig
|
package/src/daemon-runner.ts
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bridges the `startProxy` / `startProxies` entrypoints to the long-running
|
|
3
|
-
* rpx daemon. When `viaDaemon: true` is set on a proxy config (or
|
|
4
|
-
* `--via-daemon` is passed to the CLI), we don't bind our own `:443` —
|
|
5
|
-
* instead we:
|
|
6
|
-
*
|
|
7
|
-
* 1. Write a registry entry per proxy under `~/.stacks/rpx/registry.d`.
|
|
8
|
-
* 2. Ensure the daemon is running (lazy-spawn if needed).
|
|
9
|
-
* 3. Block until SIGINT/SIGTERM, then unregister our entries.
|
|
10
|
-
*
|
|
11
|
-
* The daemon's PID-GC reaps anything we miss if this process dies `kill -9`.
|
|
12
|
-
*/
|
|
13
|
-
import type { PathRewrite, StaticRouteConfig } from './types'
|
|
14
|
-
import * as fs from 'node:fs'
|
|
15
|
-
import * as path from 'node:path'
|
|
16
|
-
import * as process from 'node:process'
|
|
17
|
-
import { ensureDaemonRunning } from './daemon'
|
|
18
|
-
import { log } from './logger'
|
|
19
|
-
import { getRegistryDir, isValidId, removeEntry, writeEntry } from './registry'
|
|
20
|
-
import { debugLog } from './utils'
|
|
21
|
-
|
|
22
|
-
export interface DaemonRunnerProxy {
|
|
23
|
-
id?: string
|
|
24
|
-
/** Upstream `host:port`. Optional when `static` is set. */
|
|
25
|
-
from?: string
|
|
26
|
-
to: string
|
|
27
|
-
/**
|
|
28
|
-
* Optional path prefix this route owns under the host `to` (e.g. `'/api'`).
|
|
29
|
-
* Lets several routes share one host on different paths. When two proxies
|
|
30
|
-
* share `to` but differ by `path`, give each an explicit `id` (or rely on the
|
|
31
|
-
* path being folded into the derived id).
|
|
32
|
-
*/
|
|
33
|
-
path?: string
|
|
34
|
-
cleanUrls?: boolean
|
|
35
|
-
changeOrigin?: boolean
|
|
36
|
-
pathRewrites?: PathRewrite[]
|
|
37
|
-
/** Serve a local directory for this route instead of proxying. */
|
|
38
|
-
static?: string | StaticRouteConfig
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface DaemonRunnerOptions {
|
|
42
|
-
proxies: DaemonRunnerProxy[]
|
|
43
|
-
verbose?: boolean
|
|
44
|
-
/** Override the registry dir (tests). Defaults to `~/.stacks/rpx/registry.d`. */
|
|
45
|
-
registryDir?: string
|
|
46
|
-
/** Override the rpx state dir (tests). Defaults to `~/.stacks/rpx`. */
|
|
47
|
-
rpxDir?: string
|
|
48
|
-
/**
|
|
49
|
-
* Skip the blocking await + signal handlers. Tests use this to register
|
|
50
|
-
* entries, verify, and tear down without keeping the test runner alive.
|
|
51
|
-
*/
|
|
52
|
-
detached?: boolean
|
|
53
|
-
/** Override the daemon spawn command (tests). */
|
|
54
|
-
spawnCommand?: string[]
|
|
55
|
-
/** Passed through to `ensureDaemonRunning` (default 5000ms). */
|
|
56
|
-
startupTimeoutMs?: number
|
|
57
|
-
/** Extra env for the daemon child (e.g. `SUDO_PASSWORD`). */
|
|
58
|
-
spawnEnv?: Record<string, string>
|
|
59
|
-
/**
|
|
60
|
-
* When true, registry entries omit `pid` so they persist until
|
|
61
|
-
* `rpx unregister` — avoids PID-GC dropping routes when the registering
|
|
62
|
-
* process is short-lived (e.g. a CLI wrapper).
|
|
63
|
-
*/
|
|
64
|
-
persistent?: boolean
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Sanitize an arbitrary `to` host into a valid registry id. Drops anything
|
|
69
|
-
* that isn't `[a-zA-Z0-9._-]`, collapses runs to a single dash, and trims
|
|
70
|
-
* leading/trailing dashes. Falls back to `'rpx'` if nothing's left.
|
|
71
|
-
*/
|
|
72
|
-
export function deriveIdFromTarget(to: string, routePath?: string): string {
|
|
73
|
-
// Fold the path into the id so several routes on the same host don't collide
|
|
74
|
-
// (e.g. `stacksjs.com` + `/api` and `stacksjs.com` + `/docs`).
|
|
75
|
-
const base = routePath && routePath !== '/' ? `${to}${routePath}` : to
|
|
76
|
-
const cleaned = base.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 128)
|
|
77
|
-
return cleaned.length > 0 ? cleaned : 'rpx'
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Register every proxy with the daemon and (unless `detached`) block until a
|
|
82
|
-
* shutdown signal arrives. Throws if any id collides or the daemon fails to
|
|
83
|
-
* spawn.
|
|
84
|
-
*/
|
|
85
|
-
export async function runViaDaemon(opts: DaemonRunnerOptions): Promise<void> {
|
|
86
|
-
if (opts.proxies.length === 0)
|
|
87
|
-
throw new Error('runViaDaemon: no proxies provided')
|
|
88
|
-
|
|
89
|
-
const verbose = opts.verbose ?? false
|
|
90
|
-
const registryDir = opts.registryDir
|
|
91
|
-
const ids = new Set<string>()
|
|
92
|
-
|
|
93
|
-
// Resolve and validate all ids up front so we don't half-register and bail.
|
|
94
|
-
const resolved = opts.proxies.map((p) => {
|
|
95
|
-
const id = p.id ?? deriveIdFromTarget(p.to, p.path)
|
|
96
|
-
if (!isValidId(id))
|
|
97
|
-
throw new Error(`invalid registry id "${id}" derived from to="${p.to}"`)
|
|
98
|
-
if (ids.has(id))
|
|
99
|
-
throw new Error(`duplicate registry id "${id}" — set an explicit \`id\` on one of the proxies`)
|
|
100
|
-
ids.add(id)
|
|
101
|
-
return { ...p, id }
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
const createdAt = new Date().toISOString()
|
|
105
|
-
for (const p of resolved) {
|
|
106
|
-
await writeEntry({
|
|
107
|
-
id: p.id,
|
|
108
|
-
from: p.from,
|
|
109
|
-
to: p.to,
|
|
110
|
-
path: p.path,
|
|
111
|
-
pid: opts.persistent ? undefined : process.pid,
|
|
112
|
-
cwd: process.cwd(),
|
|
113
|
-
createdAt,
|
|
114
|
-
cleanUrls: p.cleanUrls,
|
|
115
|
-
changeOrigin: p.changeOrigin,
|
|
116
|
-
pathRewrites: p.pathRewrites,
|
|
117
|
-
static: p.static,
|
|
118
|
-
}, registryDir, verbose)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const result = await ensureDaemonRunning({
|
|
122
|
-
rpxDir: opts.rpxDir,
|
|
123
|
-
verbose,
|
|
124
|
-
spawnCommand: opts.spawnCommand,
|
|
125
|
-
startupTimeoutMs: opts.startupTimeoutMs,
|
|
126
|
-
spawnEnv: opts.spawnEnv,
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
for (const p of resolved) {
|
|
130
|
-
const target = p.static
|
|
131
|
-
? `static ${typeof p.static === 'string' ? p.static : p.static.dir}`
|
|
132
|
-
: p.from
|
|
133
|
-
log.success(`https://${p.to} → ${target}`)
|
|
134
|
-
}
|
|
135
|
-
log.info(`(via rpx daemon pid=${result.pid}; \`rpx daemon:status\` to inspect)`)
|
|
136
|
-
|
|
137
|
-
if (opts.detached)
|
|
138
|
-
return
|
|
139
|
-
|
|
140
|
-
// Cleanup registry entries on shutdown so the daemon's routing table reflects
|
|
141
|
-
// reality immediately (its PID-GC would catch us eventually, but this is
|
|
142
|
-
// faster and avoids a stale-route window).
|
|
143
|
-
let cleaned = false
|
|
144
|
-
const dirForCleanup = registryDir ?? getRegistryDir()
|
|
145
|
-
const idsForCleanup = resolved.map(p => p.id)
|
|
146
|
-
|
|
147
|
-
const cleanup = async (): Promise<void> => {
|
|
148
|
-
if (cleaned)
|
|
149
|
-
return
|
|
150
|
-
cleaned = true
|
|
151
|
-
for (const id of idsForCleanup) {
|
|
152
|
-
await removeEntry(id, registryDir, verbose).catch((err) => {
|
|
153
|
-
debugLog('runner', `removeEntry(${id}) failed: ${err}`, verbose)
|
|
154
|
-
})
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const onSignal = (sig: NodeJS.Signals): void => {
|
|
159
|
-
debugLog('runner', `received ${sig}, unregistering ${idsForCleanup.length} entries`, verbose)
|
|
160
|
-
cleanup().finally(() => process.exit(0))
|
|
161
|
-
}
|
|
162
|
-
process.once('SIGINT', onSignal)
|
|
163
|
-
process.once('SIGTERM', onSignal)
|
|
164
|
-
|
|
165
|
-
// Last-resort sync cleanup if the process exits without our signal handlers
|
|
166
|
-
// running (e.g. a thrown uncaught exception or `process.exit()` from elsewhere).
|
|
167
|
-
process.once('exit', () => {
|
|
168
|
-
if (cleaned)
|
|
169
|
-
return
|
|
170
|
-
for (const id of idsForCleanup) {
|
|
171
|
-
try {
|
|
172
|
-
fs.unlinkSync(path.join(dirForCleanup, `${id}.json`))
|
|
173
|
-
}
|
|
174
|
-
catch {}
|
|
175
|
-
}
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
// Park forever; signal handlers do the actual exiting.
|
|
179
|
-
await new Promise<void>(() => {})
|
|
180
|
-
}
|