@stacksjs/rpx 0.11.4 → 0.11.7
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/README.md +82 -0
- package/dist/bin/cli.js +243 -8
- package/dist/chunk-6z1nzq0x.js +1 -0
- package/dist/chunk-jpf41gb9.js +49 -0
- package/dist/chunk-qcdcnadb.js +1 -0
- package/dist/daemon-runner.d.ts +29 -0
- package/dist/daemon.d.ts +99 -0
- package/dist/https.d.ts +8 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +155 -0
- package/dist/process-manager.d.ts +1 -0
- package/dist/proxy-handler.d.ts +15 -0
- package/dist/registry.d.ts +74 -0
- package/dist/start.d.ts +3 -0
- package/dist/types.d.ts +2 -0
- package/dist/utils.d.ts +13 -1
- package/package.json +11 -9
- package/src/daemon-runner.ts +148 -0
- package/src/daemon.ts +496 -0
- package/src/https.ts +105 -64
- package/src/index.ts +42 -0
- package/src/process-manager.ts +2 -2
- package/src/proxy-handler.ts +99 -0
- package/src/registry.ts +346 -0
- package/src/start.ts +66 -84
- package/src/types.ts +21 -1
- package/src/utils.ts +78 -1
- package/dist/chunk-3y886wa5.js +0 -1
- package/dist/chunk-61re8msk.js +0 -1
- package/dist/chunk-94pvxvt5.js +0 -1
- package/dist/chunk-dz3837t8.js +0 -45
- package/dist/chunk-g5db14m7.js +0 -19
- package/dist/chunk-gbny098p.js +0 -2
- package/dist/chunk-pbbtnqsx.js +0 -123
- package/dist/dns.d.ts +0 -21
- package/dist/src/index.js +0 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PathRewrite } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Build a Bun.serve-compatible `fetch` handler that routes requests based on
|
|
4
|
+
* the `Host` header. Returns 404 when no route matches and 502 on upstream
|
|
5
|
+
* failures.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createProxyFetchHandler(getRoute: GetRoute, verbose?: boolean): ProxyFetchHandler;
|
|
8
|
+
export declare interface ProxyRoute {
|
|
9
|
+
sourceHost: string
|
|
10
|
+
cleanUrls?: boolean
|
|
11
|
+
changeOrigin?: boolean
|
|
12
|
+
pathRewrites?: PathRewrite[]
|
|
13
|
+
}
|
|
14
|
+
export type GetRoute = (hostname: string) => ProxyRoute | undefined;
|
|
15
|
+
export type ProxyFetchHandler = (req: Request) => Promise<Response>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { PathRewrite } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Default location for the registry directory. The daemon's PID file and log
|
|
4
|
+
* sit alongside it under `~/.stacks/rpx/`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function getRegistryDir(): string;
|
|
7
|
+
/**
|
|
8
|
+
* Validate an entry id. Rejects anything that could escape the registry dir
|
|
9
|
+
* (path traversal, slashes) or that would round-trip oddly through a filename.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isValidId(id: string): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Check whether a PID is alive. `kill(pid, 0)` returns without sending a
|
|
14
|
+
* signal but throws ESRCH if the process is gone — exactly the probe we need.
|
|
15
|
+
* EPERM means the process exists but we don't own it; treat as alive.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isPidAlive(pid: number): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Atomically write an entry to disk.
|
|
20
|
+
*
|
|
21
|
+
* Writes to a temp file in the same directory, then renames into place. POSIX
|
|
22
|
+
* rename within the same filesystem is atomic, so a concurrent reader either
|
|
23
|
+
* sees the old file or the new file — never a half-written one.
|
|
24
|
+
*/
|
|
25
|
+
export declare function writeEntry(entry: RegistryEntry, dir?: string, verbose?: boolean): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Remove an entry by id. No-op if the file is already gone.
|
|
28
|
+
*/
|
|
29
|
+
export declare function removeEntry(id: string, dir?: string, verbose?: boolean): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Read a single entry by id. Returns `null` if missing or malformed (malformed
|
|
32
|
+
* files are deleted so they don't keep poisoning subsequent reads).
|
|
33
|
+
*/
|
|
34
|
+
export declare function readEntry(id: string, dir?: string, verbose?: boolean): Promise<RegistryEntry | null>;
|
|
35
|
+
/**
|
|
36
|
+
* Read all entries from the registry directory. Malformed files are pruned.
|
|
37
|
+
* This does NOT GC stale PIDs — call `gcStaleEntries` for that explicitly.
|
|
38
|
+
*/
|
|
39
|
+
export declare function readAll(dir?: string, verbose?: boolean): Promise<RegistryEntry[]>;
|
|
40
|
+
/**
|
|
41
|
+
* Remove entries whose writer PID is no longer alive. Returns the count of
|
|
42
|
+
* entries removed. Safe to call repeatedly; intended to run on daemon startup
|
|
43
|
+
* and on a slow timer (e.g. every 5s) while the daemon is up.
|
|
44
|
+
*/
|
|
45
|
+
export declare function gcStaleEntries(dir?: string, verbose?: boolean): Promise<number>;
|
|
46
|
+
/**
|
|
47
|
+
* Watch the registry directory and invoke `onChange` with the full current
|
|
48
|
+
* entry list whenever something changes. Events are debounced so a flurry of
|
|
49
|
+
* rapid writes (e.g. several `./buddy dev` invocations starting in parallel)
|
|
50
|
+
* triggers at most one rebuild.
|
|
51
|
+
*
|
|
52
|
+
* The watcher tolerates a missing directory at startup — it creates the dir
|
|
53
|
+
* before opening the watch, so the first `writeEntry` doesn't race the daemon.
|
|
54
|
+
*/
|
|
55
|
+
export declare function watchRegistry(onChange: (entries: RegistryEntry[]) => void | Promise<void>, opts?: WatchOptions & { dir?: string }): WatchHandle;
|
|
56
|
+
export declare interface RegistryEntry {
|
|
57
|
+
id: string
|
|
58
|
+
from: string
|
|
59
|
+
to: string
|
|
60
|
+
pid?: number
|
|
61
|
+
cwd?: string
|
|
62
|
+
createdAt: string
|
|
63
|
+
pathRewrites?: PathRewrite[]
|
|
64
|
+
cleanUrls?: boolean
|
|
65
|
+
changeOrigin?: boolean
|
|
66
|
+
}
|
|
67
|
+
export declare interface WatchHandle {
|
|
68
|
+
close: () => void
|
|
69
|
+
}
|
|
70
|
+
export declare interface WatchOptions {
|
|
71
|
+
debounceMs?: number
|
|
72
|
+
pollMs?: number
|
|
73
|
+
verbose?: boolean
|
|
74
|
+
}
|
package/dist/start.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import * as http from 'node:http';
|
|
2
|
+
import * as http2 from 'node:http2';
|
|
3
|
+
import * as https from 'node:https';
|
|
1
4
|
import type { CleanupOptions, ProxyOption, ProxyOptions, ProxySetupOptions, SingleProxyConfig } from './types';
|
|
2
5
|
export declare function cleanup(options?: CleanupOptions): Promise<void>;
|
|
3
6
|
export declare function startServer(options: SingleProxyConfig): Promise<void>;
|
package/dist/types.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export declare interface BaseProxyConfig {
|
|
|
15
15
|
to: string
|
|
16
16
|
start?: StartOptions
|
|
17
17
|
pathRewrites?: PathRewrite[]
|
|
18
|
+
id?: string
|
|
18
19
|
}
|
|
19
20
|
export declare interface CleanupConfig {
|
|
20
21
|
domains: string[]
|
|
@@ -33,6 +34,7 @@ export declare interface SharedProxyConfig {
|
|
|
33
34
|
cleanUrls: boolean
|
|
34
35
|
changeOrigin?: boolean
|
|
35
36
|
regenerateUntrustedCerts?: boolean
|
|
37
|
+
viaDaemon?: boolean
|
|
36
38
|
}
|
|
37
39
|
export declare interface SingleProxyConfig extends BaseProxyConfig, SharedProxyConfig {}
|
|
38
40
|
export declare interface MultiProxyConfig extends SharedProxyConfig {
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MultiProxyConfig, ProxyConfigs, ProxyOption, ProxyOptions, SingleProxyConfig } from './types';
|
|
1
|
+
import type { MultiProxyConfig, PathRewrite, ProxyConfigs, ProxyOption, ProxyOptions, SingleProxyConfig } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* Get sudo password from environment variable if set
|
|
4
4
|
*/
|
|
@@ -8,6 +8,8 @@ export declare function getSudoPassword(): string | undefined;
|
|
|
8
8
|
*/
|
|
9
9
|
export declare function execSudoSync(command: string): string;
|
|
10
10
|
export declare function debugLog(category: string, message: string, verbose?: boolean): void;
|
|
11
|
+
export declare function redactSensitive(value: unknown): unknown;
|
|
12
|
+
export declare function safeStringify(value: unknown, space?: number): string;
|
|
11
13
|
/**
|
|
12
14
|
* Extracts hostnames from proxy configuration
|
|
13
15
|
*/
|
|
@@ -27,6 +29,16 @@ export declare function isMultiProxyOptions(options: ProxyOption | ProxyOptions)
|
|
|
27
29
|
*/
|
|
28
30
|
export declare function isSingleProxyOptions(options: ProxyOption | ProxyOptions): options is SingleProxyConfig;
|
|
29
31
|
export declare function isSingleProxyConfig(options: ProxyConfigs | ProxyOptions): options is SingleProxyConfig;
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a path against a list of `pathRewrites`.
|
|
34
|
+
*
|
|
35
|
+
* Returns `null` if no rewrite matches; otherwise returns `{ targetHost, targetPath }`
|
|
36
|
+
* with the prefix preserved by default (or stripped when `stripPrefix === true`).
|
|
37
|
+
*
|
|
38
|
+
* Matching rule: rewrite matches if `pathname` is exactly `from` OR starts with
|
|
39
|
+
* `from + '/'`. So `/api` matches `/api`, `/api/`, `/api/cart` — but not `/apidocs`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolvePathRewrite(pathname: string, rewrites: PathRewrite[] | undefined): { targetHost: string, targetPath: string } | null;
|
|
30
42
|
/**
|
|
31
43
|
* Safely delete a file if it exists
|
|
32
44
|
*/
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/rpx",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.11.
|
|
4
|
+
"version": "0.11.7",
|
|
5
5
|
"description": "A modern and smart reverse proxy.",
|
|
6
6
|
"author": "Chris Breuer <chris@stacksjs.org>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"src"
|
|
44
44
|
],
|
|
45
45
|
"scripts": {
|
|
46
|
-
"build": "bun build.ts && bun
|
|
46
|
+
"build": "bun build.ts && bun build ./bin/cli.ts --compile --minify --outfile bin/rpx",
|
|
47
47
|
"compile": "bun build ./bin/cli.ts --compile --minify --outfile bin/rpx",
|
|
48
48
|
"compile:all": "bun run compile:linux-x64 && bun run compile:linux-arm64 && bun run compile:windows-x64 && bun run compile:darwin-x64 && bun run compile:darwin-arm64",
|
|
49
49
|
"compile:linux-x64": "bun build ./bin/cli.ts --compile --minify --target=bun-linux-x64 --outfile bin/rpx-linux-x64",
|
|
@@ -51,11 +51,12 @@
|
|
|
51
51
|
"compile:windows-x64": "bun build ./bin/cli.ts --compile --minify --target=bun-windows-x64 --outfile bin/rpx-windows-x64.exe",
|
|
52
52
|
"compile:darwin-x64": "bun build ./bin/cli.ts --compile --minify --target=bun-darwin-x64 --outfile bin/rpx-darwin-x64",
|
|
53
53
|
"compile:darwin-arm64": "bun build ./bin/cli.ts --compile --minify --target=bun-darwin-arm64 --outfile bin/rpx-darwin-arm64",
|
|
54
|
-
"lint": "bunx --bun
|
|
55
|
-
"lint:fix": "bunx --bun
|
|
54
|
+
"lint": "bunx --bun pickier .",
|
|
55
|
+
"lint:fix": "bunx --bun pickier . --fix",
|
|
56
56
|
"fresh": "bunx rimraf node_modules/ bun.lock && bun i",
|
|
57
57
|
"changelog": "changelogen --output CHANGELOG.md",
|
|
58
|
-
"prepublishOnly": "bun
|
|
58
|
+
"prepublishOnly": "bun build.ts",
|
|
59
|
+
"release:binaries": "bun run compile:all && bun run zip",
|
|
59
60
|
"test": "bun test",
|
|
60
61
|
"typecheck": "bunx tsc --noEmit",
|
|
61
62
|
"zip": "bun run zip:all",
|
|
@@ -66,10 +67,11 @@
|
|
|
66
67
|
"zip:darwin-x64": "zip -j bin/rpx-darwin-x64.zip bin/rpx-darwin-x64",
|
|
67
68
|
"zip:darwin-arm64": "zip -j bin/rpx-darwin-arm64.zip bin/rpx-darwin-arm64"
|
|
68
69
|
},
|
|
70
|
+
"dependencies": {
|
|
71
|
+
"@stacksjs/clapp": "^0.2.10",
|
|
72
|
+
"@stacksjs/tlsx": "^0.13.6"
|
|
73
|
+
},
|
|
69
74
|
"devDependencies": {
|
|
70
|
-
"@stacksjs/clapp": "^0.2.0",
|
|
71
|
-
"@stacksjs/tlsx": "^0.13.0",
|
|
72
|
-
"bun-plugin-dtsx": "^0.21.17",
|
|
73
75
|
"bunfig": "^0.15.6",
|
|
74
76
|
"typescript": "^5.9.3"
|
|
75
77
|
},
|
|
@@ -77,6 +79,6 @@
|
|
|
77
79
|
"pre-commit": "bunx lint-staged"
|
|
78
80
|
},
|
|
79
81
|
"lint-staged": {
|
|
80
|
-
"*.{js,ts}": "bunx
|
|
82
|
+
"*.{js,ts}": "bunx --bun pickier . --fix"
|
|
81
83
|
}
|
|
82
84
|
}
|
|
@@ -0,0 +1,148 @@
|
|
|
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 } 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
|
+
from: string
|
|
25
|
+
to: string
|
|
26
|
+
cleanUrls?: boolean
|
|
27
|
+
changeOrigin?: boolean
|
|
28
|
+
pathRewrites?: PathRewrite[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DaemonRunnerOptions {
|
|
32
|
+
proxies: DaemonRunnerProxy[]
|
|
33
|
+
verbose?: boolean
|
|
34
|
+
/** Override the registry dir (tests). Defaults to `~/.stacks/rpx/registry.d`. */
|
|
35
|
+
registryDir?: string
|
|
36
|
+
/** Override the rpx state dir (tests). Defaults to `~/.stacks/rpx`. */
|
|
37
|
+
rpxDir?: string
|
|
38
|
+
/**
|
|
39
|
+
* Skip the blocking await + signal handlers. Tests use this to register
|
|
40
|
+
* entries, verify, and tear down without keeping the test runner alive.
|
|
41
|
+
*/
|
|
42
|
+
detached?: boolean
|
|
43
|
+
/** Override the daemon spawn command (tests). */
|
|
44
|
+
spawnCommand?: string[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Sanitize an arbitrary `to` host into a valid registry id. Drops anything
|
|
49
|
+
* that isn't `[a-zA-Z0-9._-]`, collapses runs to a single dash, and trims
|
|
50
|
+
* leading/trailing dashes. Falls back to `'rpx'` if nothing's left.
|
|
51
|
+
*/
|
|
52
|
+
export function deriveIdFromTarget(to: string): string {
|
|
53
|
+
const cleaned = to.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 128)
|
|
54
|
+
return cleaned.length > 0 ? cleaned : 'rpx'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Register every proxy with the daemon and (unless `detached`) block until a
|
|
59
|
+
* shutdown signal arrives. Throws if any id collides or the daemon fails to
|
|
60
|
+
* spawn.
|
|
61
|
+
*/
|
|
62
|
+
export async function runViaDaemon(opts: DaemonRunnerOptions): Promise<void> {
|
|
63
|
+
if (opts.proxies.length === 0)
|
|
64
|
+
throw new Error('runViaDaemon: no proxies provided')
|
|
65
|
+
|
|
66
|
+
const verbose = opts.verbose ?? false
|
|
67
|
+
const registryDir = opts.registryDir
|
|
68
|
+
const ids = new Set<string>()
|
|
69
|
+
|
|
70
|
+
// Resolve and validate all ids up front so we don't half-register and bail.
|
|
71
|
+
const resolved = opts.proxies.map((p) => {
|
|
72
|
+
const id = p.id ?? deriveIdFromTarget(p.to)
|
|
73
|
+
if (!isValidId(id))
|
|
74
|
+
throw new Error(`invalid registry id "${id}" derived from to="${p.to}"`)
|
|
75
|
+
if (ids.has(id))
|
|
76
|
+
throw new Error(`duplicate registry id "${id}" — set an explicit \`id\` on one of the proxies`)
|
|
77
|
+
ids.add(id)
|
|
78
|
+
return { ...p, id }
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
for (const p of resolved) {
|
|
82
|
+
await writeEntry({
|
|
83
|
+
id: p.id,
|
|
84
|
+
from: p.from,
|
|
85
|
+
to: p.to,
|
|
86
|
+
pid: process.pid,
|
|
87
|
+
cwd: process.cwd(),
|
|
88
|
+
createdAt: new Date().toISOString(),
|
|
89
|
+
cleanUrls: p.cleanUrls,
|
|
90
|
+
changeOrigin: p.changeOrigin,
|
|
91
|
+
pathRewrites: p.pathRewrites,
|
|
92
|
+
}, registryDir, verbose)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const result = await ensureDaemonRunning({
|
|
96
|
+
rpxDir: opts.rpxDir,
|
|
97
|
+
verbose,
|
|
98
|
+
spawnCommand: opts.spawnCommand,
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
for (const p of resolved)
|
|
102
|
+
log.success(`https://${p.to} → ${p.from}`)
|
|
103
|
+
log.info(`(via rpx daemon pid=${result.pid}; \`rpx daemon:status\` to inspect)`)
|
|
104
|
+
|
|
105
|
+
if (opts.detached)
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
// Cleanup registry entries on shutdown so the daemon's routing table reflects
|
|
109
|
+
// reality immediately (its PID-GC would catch us eventually, but this is
|
|
110
|
+
// faster and avoids a stale-route window).
|
|
111
|
+
let cleaned = false
|
|
112
|
+
const dirForCleanup = registryDir ?? getRegistryDir()
|
|
113
|
+
const idsForCleanup = resolved.map(p => p.id)
|
|
114
|
+
|
|
115
|
+
const cleanup = async (): Promise<void> => {
|
|
116
|
+
if (cleaned)
|
|
117
|
+
return
|
|
118
|
+
cleaned = true
|
|
119
|
+
for (const id of idsForCleanup) {
|
|
120
|
+
await removeEntry(id, registryDir, verbose).catch((err) => {
|
|
121
|
+
debugLog('runner', `removeEntry(${id}) failed: ${err}`, verbose)
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const onSignal = (sig: NodeJS.Signals): void => {
|
|
127
|
+
debugLog('runner', `received ${sig}, unregistering ${idsForCleanup.length} entries`, verbose)
|
|
128
|
+
cleanup().finally(() => process.exit(0))
|
|
129
|
+
}
|
|
130
|
+
process.once('SIGINT', onSignal)
|
|
131
|
+
process.once('SIGTERM', onSignal)
|
|
132
|
+
|
|
133
|
+
// Last-resort sync cleanup if the process exits without our signal handlers
|
|
134
|
+
// running (e.g. a thrown uncaught exception or `process.exit()` from elsewhere).
|
|
135
|
+
process.once('exit', () => {
|
|
136
|
+
if (cleaned)
|
|
137
|
+
return
|
|
138
|
+
for (const id of idsForCleanup) {
|
|
139
|
+
try {
|
|
140
|
+
fs.unlinkSync(path.join(dirForCleanup, `${id}.json`))
|
|
141
|
+
}
|
|
142
|
+
catch {}
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// Park forever; signal handlers do the actual exiting.
|
|
147
|
+
await new Promise<void>(() => {})
|
|
148
|
+
}
|