@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.
Files changed (49) hide show
  1. package/dist/acme-challenge.d.ts +13 -0
  2. package/dist/auth.d.ts +21 -0
  3. package/dist/bin/cli.js +252 -200
  4. package/dist/cert-inspect.d.ts +5 -3
  5. package/dist/{chunk-83dqq28c.js → chunk-1108y1dk.js} +1 -1
  6. package/dist/{chunk-0f32jmrb.js → chunk-c9brkawa.js} +1 -1
  7. package/dist/{chunk-w888yhnp.js → chunk-cqkz06bg.js} +1 -1
  8. package/dist/chunk-qs36t2rf.js +224 -0
  9. package/dist/daemon.d.ts +4 -1
  10. package/dist/https.d.ts +1 -0
  11. package/dist/index.d.ts +33 -1
  12. package/dist/index.js +7 -7
  13. package/dist/proxy-handler.d.ts +18 -1
  14. package/dist/proxy-pool.d.ts +5 -0
  15. package/dist/redirect.d.ts +40 -0
  16. package/dist/registry.d.ts +2 -1
  17. package/dist/site-resolver.d.ts +106 -0
  18. package/dist/site-splash.d.ts +19 -0
  19. package/dist/site-supervisor.d.ts +56 -0
  20. package/dist/start.d.ts +31 -8
  21. package/dist/types.d.ts +64 -0
  22. package/package.json +6 -7
  23. package/dist/chunk-rs8gqpax.js +0 -174
  24. package/src/cert-inspect.ts +0 -69
  25. package/src/colors.ts +0 -13
  26. package/src/config.ts +0 -45
  27. package/src/daemon-runner.ts +0 -180
  28. package/src/daemon.ts +0 -1209
  29. package/src/dns-state.ts +0 -116
  30. package/src/dns.ts +0 -568
  31. package/src/host-match.ts +0 -52
  32. package/src/host-routes.ts +0 -147
  33. package/src/hosts.ts +0 -283
  34. package/src/https.ts +0 -905
  35. package/src/index.ts +0 -161
  36. package/src/logger.ts +0 -19
  37. package/src/macos-trust.ts +0 -175
  38. package/src/on-demand.ts +0 -264
  39. package/src/origin-guard.ts +0 -127
  40. package/src/port-manager.ts +0 -183
  41. package/src/process-manager.ts +0 -164
  42. package/src/proxy-handler.ts +0 -387
  43. package/src/proxy-pool.ts +0 -1003
  44. package/src/registry.ts +0 -366
  45. package/src/sni.ts +0 -93
  46. package/src/start.ts +0 -1421
  47. package/src/static-files.ts +0 -201
  48. package/src/types.ts +0 -267
  49. package/src/utils.ts +0 -243
@@ -1,127 +0,0 @@
1
- /**
2
- * Origin verification guard for "CDN in front of rpx" topologies.
3
- *
4
- * When rpx is the origin behind a CDN (e.g. CloudFront → rpx), the origin host
5
- * is publicly resolvable, so a client could resolve it and hit rpx directly,
6
- * bypassing the CDN's caching/WAF. The standard mitigation is a shared secret:
7
- * the CDN injects a secret request header on the origin fetch, and the origin
8
- * rejects any request to the protected hosts that lacks it.
9
- *
10
- * `createOriginGuard` returns a tiny pre-router gate you place in front of your
11
- * fetch handler. It only guards the listed hosts (exact or `*.wildcard`) — every
12
- * other host (e.g. apps served directly, not via the CDN) passes through
13
- * untouched. ACME HTTP-01 challenge paths are exempt by default so cert renewal
14
- * keeps working on the open `:80` listener.
15
- *
16
- * @example
17
- * const guard = createOriginGuard({
18
- * header: 'x-origin-verify',
19
- * value: process.env.ORIGIN_SECRET!,
20
- * hosts: ['stacksjs.com', 'www.stacksjs.com', 'origin.stacksjs.com'],
21
- * })
22
- * Bun.serve({ fetch: req => guard(req) ?? handler(req, server) })
23
- */
24
- import { timingSafeEqual } from 'node:crypto'
25
- import { matchesWildcard } from './host-match'
26
-
27
- /**
28
- * Compare a request-supplied secret against the expected value in constant time,
29
- * so the shared secret can't be recovered byte-by-byte via response timing. The
30
- * length check leaks only the secret's length (standard and acceptable); the
31
- * byte comparison itself is timing-safe.
32
- */
33
- function secretMatches(provided: string | null, expected: string): boolean {
34
- if (provided == null)
35
- return false
36
- const a = Buffer.from(provided)
37
- const b = Buffer.from(expected)
38
- if (a.length !== b.length)
39
- return false
40
- return timingSafeEqual(a, b)
41
- }
42
-
43
- /** Lowercase and strip a single trailing dot so `Host: example.com.` ≡ `example.com`. */
44
- function normalizeHost(host: string): string {
45
- return host.toLowerCase().replace(/\.$/, '')
46
- }
47
-
48
- export interface OriginGuardOptions {
49
- /** Header the CDN injects on the origin hop (case-insensitive), e.g. `x-origin-verify`. */
50
- header: string
51
- /** Expected secret value. Requests to protected hosts must carry `header: value`. */
52
- value: string
53
- /** Hosts to protect — exact (`stacksjs.com`) or wildcard (`*.stacksjs.com`). Others pass through. */
54
- hosts: string[]
55
- /**
56
- * Request paths exempt from the check (prefix match). Defaults to the ACME
57
- * HTTP-01 challenge prefix so cert issuance/renewal is never blocked.
58
- */
59
- exemptPaths?: string[]
60
- /** Body returned on rejection. Defaults to a short plain-text message. */
61
- forbiddenMessage?: string
62
- }
63
-
64
- export interface OriginGuard {
65
- /** Returns a 403 Response to short-circuit, or `undefined` to let the request proceed. */
66
- (req: Request): Response | undefined
67
- /** Whether a given hostname is in the protected set. */
68
- protects: (hostname: string) => boolean
69
- }
70
-
71
- const DEFAULT_EXEMPT = ['/.well-known/acme-challenge/']
72
-
73
- function hostnameOf(req: Request): string {
74
- const hostHeader = req.headers.get('host')
75
- if (hostHeader)
76
- return normalizeHost(hostHeader.split(':')[0])
77
- try {
78
- return normalizeHost(new URL(req.url).hostname)
79
- }
80
- catch {
81
- return ''
82
- }
83
- }
84
-
85
- export function createOriginGuard(options: OriginGuardOptions): OriginGuard {
86
- const header = options.header.toLowerCase()
87
- const exact = new Set<string>()
88
- const wildcards: string[] = []
89
- for (const h of options.hosts) {
90
- const host = normalizeHost(h)
91
- if (host.startsWith('*.'))
92
- wildcards.push(host)
93
- else
94
- exact.add(host)
95
- }
96
- const exemptPaths = options.exemptPaths ?? DEFAULT_EXEMPT
97
- const forbidden = options.forbiddenMessage
98
- ?? 'Forbidden: direct origin access is not allowed; requests must arrive via the CDN.\n'
99
-
100
- const protects = (hostname: string): boolean => {
101
- const h = normalizeHost(hostname)
102
- return exact.has(h) || wildcards.some(w => matchesWildcard(h, w))
103
- }
104
-
105
- const guard: OriginGuard = ((req: Request): Response | undefined => {
106
- const host = hostnameOf(req)
107
- if (!protects(host))
108
- return undefined
109
-
110
- let pathname = '/'
111
- try {
112
- pathname = new URL(req.url).pathname
113
- }
114
- catch {
115
- // fall through with '/'
116
- }
117
- if (exemptPaths.some(p => pathname.startsWith(p)))
118
- return undefined
119
-
120
- if (secretMatches(req.headers.get(header), options.value))
121
- return undefined
122
-
123
- return new Response(forbidden, { status: 403, headers: { 'content-type': 'text/plain' } })
124
- }) as OriginGuard
125
- guard.protects = protects
126
- return guard
127
- }
@@ -1,183 +0,0 @@
1
- import type { PortManager } from './types'
2
- import * as net from 'node:net'
3
- import { debugLog } from './utils'
4
-
5
- /**
6
- * Check if a port is in use
7
- */
8
- export function isPortInUse(port: number, hostname: string, verbose?: boolean): Promise<boolean> {
9
- debugLog('port', `Checking if port ${port} is in use on ${hostname}`, verbose)
10
- return new Promise((resolve) => {
11
- const server = net.createServer()
12
-
13
- // Add a timeout to ensure we don't hang indefinitely
14
- const timeout = setTimeout(() => {
15
- debugLog('port', `Checking port ${port} timed out, assuming it's in use`, verbose)
16
- server.close()
17
- resolve(true)
18
- }, 3000) // 3 second timeout
19
-
20
- server.once('error', (err: NodeJS.ErrnoException) => {
21
- clearTimeout(timeout)
22
- if (err.code === 'EADDRINUSE') {
23
- debugLog('port', `Port ${port} is in use`, verbose)
24
- resolve(true)
25
- }
26
- else {
27
- // Other errors should also be treated as port unavailable
28
- debugLog('port', `Error checking port ${port}: ${err.message}`, verbose)
29
- resolve(true)
30
- }
31
- })
32
-
33
- server.once('listening', () => {
34
- clearTimeout(timeout)
35
- debugLog('port', `Port ${port} is available`, verbose)
36
- server.close()
37
- resolve(false)
38
- })
39
-
40
- try {
41
- server.listen(port, hostname)
42
- }
43
- catch (err) {
44
- clearTimeout(timeout)
45
- debugLog('port', `Exception checking port ${port}: ${err}`, verbose)
46
- resolve(true)
47
- }
48
- })
49
- }
50
-
51
- /**
52
- * Find next available port
53
- */
54
- export async function findAvailablePort(
55
- startPort: number,
56
- hostname: string,
57
- verbose?: boolean,
58
- maxAttempts = 50,
59
- ): Promise<number> {
60
- debugLog('port', `Finding available port starting from ${startPort} (max attempts: ${maxAttempts})`, verbose)
61
- let port = startPort
62
- let attempts = 0
63
-
64
- while (attempts < maxAttempts) {
65
- attempts++
66
- const isInUse = await isPortInUse(port, hostname, verbose)
67
-
68
- if (!isInUse) {
69
- debugLog('port', `Found available port: ${port} after ${attempts} attempts`, verbose)
70
- return port
71
- }
72
-
73
- debugLog('port', `Port ${port} is in use, trying ${port + 1} (attempt ${attempts}/${maxAttempts})`, verbose)
74
- port++
75
- }
76
-
77
- throw new Error(`Unable to find available port after ${maxAttempts} attempts starting from ${startPort}`)
78
- }
79
-
80
- /**
81
- * Test if a port is actually connectable
82
- */
83
- export function testPortConnectivity(
84
- port: number,
85
- hostname: string,
86
- timeout = 5000,
87
- verbose?: boolean,
88
- ): Promise<boolean> {
89
- debugLog('port', `Testing connection to ${hostname}:${port}`, verbose)
90
- return new Promise((resolve) => {
91
- const socket = net.connect({
92
- host: hostname,
93
- port,
94
- timeout,
95
- })
96
-
97
- socket.once('connect', () => {
98
- debugLog('port', `Successfully connected to ${hostname}:${port}`, verbose)
99
- socket.end()
100
- resolve(true)
101
- })
102
-
103
- socket.once('timeout', () => {
104
- debugLog('port', `Connection to ${hostname}:${port} timed out`, verbose)
105
- socket.destroy()
106
- resolve(false)
107
- })
108
-
109
- socket.once('error', (err) => {
110
- debugLog('port', `Failed to connect to ${hostname}:${port}: ${err.message}`, verbose)
111
- socket.destroy()
112
- resolve(false)
113
- })
114
- })
115
- }
116
-
117
- export class DefaultPortManager implements PortManager {
118
- usedPorts: Set<number> = new Set()
119
- private hostname: string
120
- private verbose?: boolean
121
- private maxRetries: number
122
-
123
- constructor(hostname: string = '0.0.0.0', verbose?: boolean, maxRetries = 50) {
124
- this.hostname = hostname
125
- this.verbose = verbose
126
- this.maxRetries = maxRetries
127
- }
128
-
129
- async getNextAvailablePort(startPort: number, testConnectivity = false): Promise<number> {
130
- if (this.usedPorts.has(startPort)) {
131
- // If we already have this port registered as used, find another one
132
- return this.findNextAvailablePort(startPort + 1, testConnectivity)
133
- }
134
-
135
- const isInUse = await isPortInUse(startPort, this.hostname, this.verbose)
136
-
137
- if (isInUse) {
138
- return this.findNextAvailablePort(startPort + 1, testConnectivity)
139
- }
140
-
141
- // If requested, test that we can actually connect to this port
142
- if (testConnectivity) {
143
- const isConnectable = await testPortConnectivity(startPort, this.hostname, 3000, this.verbose)
144
- if (!isConnectable) {
145
- debugLog('port', `Port ${startPort} is available but not connectable, trying next port`, this.verbose)
146
- return this.findNextAvailablePort(startPort + 1, testConnectivity)
147
- }
148
- }
149
-
150
- // Port is available, register it
151
- this.usedPorts.add(startPort)
152
- return startPort
153
- }
154
-
155
- private async findNextAvailablePort(startPort: number, testConnectivity = false): Promise<number> {
156
- const port = await findAvailablePort(startPort, this.hostname, this.verbose, this.maxRetries)
157
-
158
- // If requested, test that we can actually connect to this port
159
- if (testConnectivity) {
160
- const isConnectable = await testPortConnectivity(port, this.hostname, 3000, this.verbose)
161
- if (!isConnectable) {
162
- // If the port isn't connectable, try the next one
163
- if (port < startPort + this.maxRetries) {
164
- return this.findNextAvailablePort(port + 1, testConnectivity)
165
- }
166
- else {
167
- throw new Error(`Unable to find a connectable port after ${this.maxRetries} attempts`)
168
- }
169
- }
170
- }
171
-
172
- this.usedPorts.add(port)
173
- return port
174
- }
175
-
176
- releasePort(port: number): void {
177
- debugLog('port', `Releasing port ${port}`, this.verbose)
178
- this.usedPorts.delete(port)
179
- }
180
- }
181
-
182
- // Global port manager instance
183
- export const portManager: DefaultPortManager = new DefaultPortManager()
@@ -1,164 +0,0 @@
1
- import type { ChildProcess } from 'node:child_process'
2
- import type { StartOptions } from './types'
3
- import { spawn } from 'node:child_process'
4
- import * as process from 'node:process'
5
- import { log } from './logger'
6
- import { debugLog, safeStringify } from './utils'
7
-
8
- export interface ManagedProcess {
9
- command: string
10
- cwd: string
11
- process: ChildProcess | null
12
- env?: Record<string, string>
13
- }
14
-
15
- export class ProcessManager {
16
- private processes: Map<string, ManagedProcess> = new Map()
17
- private isShuttingDown = false
18
-
19
- async startProcess(id: string, options: StartOptions, verbose?: boolean): Promise<void> {
20
- if (this.processes.has(id)) {
21
- debugLog('start', `Process ${id} is already running`, verbose)
22
- return
23
- }
24
-
25
- const [cmd, ...args] = options.command.split(' ')
26
- const cwd = options.cwd || process.cwd()
27
-
28
- debugLog('start', `Starting process ${id}:`, verbose)
29
- debugLog('start', ` Command: ${cmd} ${args.join(' ')}`, verbose)
30
- debugLog('start', ` Working directory: ${cwd}`, verbose)
31
- debugLog('start', ` Environment variables: ${safeStringify(options.env)}`, verbose)
32
-
33
- const childProcess = spawn(cmd, args, {
34
- cwd,
35
- env: { ...process.env, ...options.env },
36
- shell: true,
37
- stdio: 'inherit',
38
- })
39
-
40
- this.processes.set(id, {
41
- command: options.command,
42
- cwd,
43
- process: childProcess,
44
- env: options.env,
45
- })
46
-
47
- return new Promise((resolve, reject) => {
48
- childProcess.on('error', (err) => {
49
- if (!this.isShuttingDown) {
50
- debugLog('start', `Process ${id} failed to start: ${err}`, verbose)
51
- this.processes.delete(id)
52
- reject(err)
53
- // Trigger cleanup on process error
54
- process.emit('SIGINT')
55
- }
56
- })
57
-
58
- childProcess.on('exit', (code) => {
59
- if (!this.isShuttingDown && code !== null && code !== 0) {
60
- debugLog('start', `Process ${id} exited with code ${code}`, verbose)
61
- this.processes.delete(id)
62
- reject(new Error(`Process ${id} exited with code ${code}`))
63
- // Trigger cleanup on non-zero exit
64
- process.emit('SIGINT')
65
- }
66
- })
67
-
68
- if (verbose) {
69
- childProcess.stdout?.on('data', (data) => {
70
- debugLog('process', `[${id}] ${data.toString().trim()}`, true)
71
- })
72
-
73
- childProcess.stderr?.on('data', (data) => {
74
- debugLog('process', `[${id}] ERR: ${data.toString().trim()}`, true)
75
- })
76
- }
77
-
78
- // Resolve after a delay to allow the process to start
79
- setTimeout(() => {
80
- if (!this.isShuttingDown && childProcess.killed) {
81
- this.processes.delete(id)
82
- reject(new Error(`Process ${id} was killed during startup`))
83
- }
84
- else {
85
- debugLog('start', `Process ${id} started successfully`, verbose)
86
- resolve()
87
- }
88
- }, 1000)
89
- })
90
- }
91
-
92
- async stopProcess(id: string, verbose?: boolean): Promise<void> {
93
- const managed = this.processes.get(id)
94
- if (!managed?.process) {
95
- debugLog('start', `No process found for ${id}`, verbose)
96
- return
97
- }
98
-
99
- debugLog('start', `Stopping process ${id}`, verbose)
100
-
101
- return new Promise((resolve) => {
102
- if (!managed.process) {
103
- resolve()
104
- return
105
- }
106
-
107
- managed.process.once('exit', () => {
108
- this.processes.delete(id)
109
- debugLog('start', `Process ${id} stopped`, verbose)
110
- resolve()
111
- })
112
-
113
- try {
114
- managed.process.kill('SIGTERM')
115
-
116
- // Force kill after 3 seconds if process hasn't exited
117
- setTimeout(() => {
118
- if (managed.process) {
119
- debugLog('start', `Force killing process ${id}`, verbose)
120
- try {
121
- managed.process.kill('SIGKILL')
122
- }
123
- // eslint-disable-next-line unused-imports/no-unused-vars
124
- catch (err) {
125
- // Ignore errors during force kill
126
- }
127
- }
128
- }, 3000)
129
- }
130
- catch (err) {
131
- debugLog('start', `Error stopping process ${id}: ${err}`, verbose)
132
- this.processes.delete(id)
133
- resolve()
134
- }
135
- })
136
- }
137
-
138
- async stopAll(verbose?: boolean): Promise<void> {
139
- if (this.isShuttingDown) {
140
- debugLog('start', 'Already shutting down, skipping duplicate stopAll call', verbose)
141
- return
142
- }
143
-
144
- this.isShuttingDown = true
145
- debugLog('start', 'Stopping all processes', verbose)
146
-
147
- const promises = Array.from(this.processes.keys()).map(id =>
148
- this.stopProcess(id, verbose).catch((err) => {
149
- log.error(`Failed to stop process ${id}:`, err)
150
- }),
151
- )
152
-
153
- await Promise.allSettled(promises)
154
- this.processes.clear()
155
- this.isShuttingDown = false
156
- }
157
-
158
- isRunning(id: string): boolean {
159
- const managed = this.processes.get(id)
160
- return !!managed?.process && !managed.process.killed
161
- }
162
- }
163
-
164
- export const processManager: ProcessManager = new ProcessManager()