@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,201 +0,0 @@
1
- /**
2
- * Static-file serving for proxy routes.
3
- *
4
- * A route configured with `static` serves files from a local directory instead
5
- * of forwarding to an upstream. Path resolution is split into a pure function
6
- * (`resolveStaticFile`) so it's trivially unit-testable, and a thin `Bun.file`
7
- * wrapper (`serveStaticFile`) that does the actual I/O.
8
- */
9
- import type { PathRewriteStyle, StaticRouteConfig } from './types'
10
- import * as path from 'node:path'
11
-
12
- /** Normalized static-route config (shorthand string already expanded). */
13
- export interface ResolvedStaticRoute {
14
- dir: string
15
- spa: boolean
16
- pathRewriteStyle: PathRewriteStyle
17
- maxAge: number
18
- cleanUrls: boolean
19
- }
20
-
21
- export function resolveStaticRoute(
22
- cfg: string | StaticRouteConfig,
23
- cleanUrls: boolean,
24
- ): ResolvedStaticRoute {
25
- if (typeof cfg === 'string')
26
- return { dir: cfg, spa: false, pathRewriteStyle: 'directory', maxAge: 0, cleanUrls }
27
- return {
28
- dir: cfg.dir,
29
- spa: cfg.spa ?? false,
30
- pathRewriteStyle: cfg.pathRewriteStyle ?? 'directory',
31
- maxAge: cfg.maxAge ?? 0,
32
- cleanUrls,
33
- }
34
- }
35
-
36
- /** A minimal extension → MIME map covering the common web asset types. */
37
- const MIME_TYPES: Record<string, string> = {
38
- '.html': 'text/html; charset=utf-8',
39
- '.htm': 'text/html; charset=utf-8',
40
- '.css': 'text/css; charset=utf-8',
41
- '.js': 'text/javascript; charset=utf-8',
42
- '.mjs': 'text/javascript; charset=utf-8',
43
- '.json': 'application/json; charset=utf-8',
44
- '.map': 'application/json; charset=utf-8',
45
- '.svg': 'image/svg+xml',
46
- '.png': 'image/png',
47
- '.jpg': 'image/jpeg',
48
- '.jpeg': 'image/jpeg',
49
- '.gif': 'image/gif',
50
- '.webp': 'image/webp',
51
- '.avif': 'image/avif',
52
- '.ico': 'image/x-icon',
53
- '.woff': 'font/woff',
54
- '.woff2': 'font/woff2',
55
- '.ttf': 'font/ttf',
56
- '.otf': 'font/otf',
57
- '.eot': 'application/vnd.ms-fontobject',
58
- '.txt': 'text/plain; charset=utf-8',
59
- '.xml': 'application/xml; charset=utf-8',
60
- '.pdf': 'application/pdf',
61
- '.wasm': 'application/wasm',
62
- '.mp4': 'video/mp4',
63
- '.webm': 'video/webm',
64
- '.mp3': 'audio/mpeg',
65
- '.wav': 'audio/wav',
66
- }
67
-
68
- export function contentTypeFor(filePath: string): string {
69
- const ext = path.extname(filePath).toLowerCase()
70
- return MIME_TYPES[ext] ?? 'application/octet-stream'
71
- }
72
-
73
- /**
74
- * Decode + normalize a URL pathname into a safe relative path.
75
- *
76
- * Traversal safety: normalizing against a leading `/` collapses every `..`
77
- * segment and clamps at the root, so the returned relative path never contains
78
- * `..` and `path.join(root, rel)` can't escape `root`. Backslash, NUL and
79
- * malformed percent-encoding are rejected outright (return `null`); the
80
- * residual `..` guard is belt-and-suspenders.
81
- */
82
- export function safeRelativePath(pathname: string): string | null {
83
- let decoded: string
84
- try {
85
- decoded = decodeURIComponent(pathname)
86
- }
87
- catch {
88
- return null
89
- }
90
- // Reject NUL and backslash (Windows-style) escapes outright.
91
- if (decoded.includes('\0') || decoded.includes('\\'))
92
- return null
93
-
94
- // `path.posix.normalize` collapses `..`/`.`; a leading `/` keeps it rooted so
95
- // a normalized result that still contains `..` means traversal above root.
96
- const normalized = path.posix.normalize(`/${decoded}`)
97
- if (normalized.includes('..'))
98
- return null
99
- // Strip the leading slash to get a path relative to the static root.
100
- return normalized.replace(/^\/+/, '')
101
- }
102
-
103
- export interface StaticResolution {
104
- /** Absolute file path to attempt to serve. */
105
- filePath: string
106
- /** When set, the request should 301-redirect to this clean URL. */
107
- redirectTo?: string
108
- }
109
-
110
- /**
111
- * Pure resolution of an incoming request pathname to a candidate file path on
112
- * disk. Does no I/O; the caller checks existence and may fall back (SPA).
113
- *
114
- * Rules:
115
- * - A trailing `/` (or root) resolves to `index.html` in that directory.
116
- * - `cleanUrls` + a `.html` request → 301 to the extensionless URL.
117
- * - Extensionless paths resolve per `pathRewriteStyle`:
118
- * - `directory`: `/about` → `about/index.html`
119
- * - `flat`: `/about` → `about.html`
120
- * - Paths with a real extension (`.css`, `.png`, …) map straight through.
121
- *
122
- * Returns `null` when the path is unsafe (traversal attempt).
123
- */
124
- export function resolveStaticFile(
125
- pathname: string,
126
- route: ResolvedStaticRoute,
127
- ): StaticResolution | null {
128
- const rel = safeRelativePath(pathname)
129
- if (rel === null)
130
- return null
131
-
132
- const ext = path.posix.extname(rel)
133
-
134
- // `cleanUrls`: redirect explicit `.html` requests to the clean URL.
135
- if (route.cleanUrls && ext === '.html') {
136
- const clean = pathname.replace(/\/index\.html$/i, '/').replace(/\.html$/i, '')
137
- return { filePath: path.join(route.dir, rel), redirectTo: clean || '/' }
138
- }
139
-
140
- // Directory or root request → index.html.
141
- if (rel === '' || pathname.endsWith('/'))
142
- return { filePath: path.join(route.dir, rel, 'index.html') }
143
-
144
- // Asset with a concrete extension → serve directly.
145
- if (ext !== '')
146
- return { filePath: path.join(route.dir, rel) }
147
-
148
- // Extensionless route → resolve by SSG style.
149
- if (route.pathRewriteStyle === 'flat')
150
- return { filePath: path.join(route.dir, `${rel}.html`) }
151
- return { filePath: path.join(route.dir, rel, 'index.html') }
152
- }
153
-
154
- /**
155
- * Serve a static file for the matched route. Returns a 301 for clean-URL
156
- * redirects, the file with the right `Content-Type`/`Cache-Control` when it
157
- * exists, the SPA `index.html` fallback when configured, or 404.
158
- */
159
- export async function serveStaticFile(
160
- pathname: string,
161
- route: ResolvedStaticRoute,
162
- ): Promise<Response> {
163
- const resolution = resolveStaticFile(pathname, route)
164
- if (!resolution)
165
- return new Response('Forbidden', { status: 403 })
166
-
167
- if (resolution.redirectTo)
168
- return new Response(null, { status: 301, headers: { Location: resolution.redirectTo } })
169
-
170
- const cacheControl = route.maxAge > 0
171
- ? `public, max-age=${route.maxAge}`
172
- : 'no-cache'
173
-
174
- const file = Bun.file(resolution.filePath)
175
- if (await file.exists()) {
176
- return new Response(file, {
177
- status: 200,
178
- headers: {
179
- 'Content-Type': contentTypeFor(resolution.filePath),
180
- 'Cache-Control': cacheControl,
181
- },
182
- })
183
- }
184
-
185
- // SPA fallback: serve the root index.html so client-side routing works.
186
- if (route.spa) {
187
- const indexPath = path.join(route.dir, 'index.html')
188
- const index = Bun.file(indexPath)
189
- if (await index.exists()) {
190
- return new Response(index, {
191
- status: 200,
192
- headers: {
193
- 'Content-Type': 'text/html; charset=utf-8',
194
- 'Cache-Control': 'no-cache',
195
- },
196
- })
197
- }
198
- }
199
-
200
- return new Response('Not Found', { status: 404 })
201
- }
package/src/types.ts DELETED
@@ -1,267 +0,0 @@
1
- import type { TlsConfig, TlsOption } from '@stacksjs/tlsx'
2
- import type { OriginGuardOptions } from './origin-guard'
3
-
4
- export interface StartOptions {
5
- command: string
6
- cwd?: string
7
- env?: Record<string, string>
8
- }
9
-
10
- export interface PathRewrite {
11
- /** Path prefix to match, e.g. '/api' */
12
- from: string
13
- /** Target backend to route to, e.g. 'localhost:3008' */
14
- to: string
15
- /**
16
- * Strip the matched prefix before forwarding. Default: `false` (preserve path).
17
- *
18
- * Matches the behavior of Vite's `server.proxy`, nginx `proxy_pass http://host:port`
19
- * (no trailing slash), and http-proxy-middleware's default. Most upstreams that own
20
- * a `/api` namespace expect the prefix to remain on the request URL.
21
- *
22
- * Set to `true` only when the upstream registers routes WITHOUT the prefix
23
- * (e.g., upstream serves `/cart/add` and you want `/api/cart/add` to reach it).
24
- */
25
- stripPrefix?: boolean
26
- }
27
-
28
- /**
29
- * How a static-file route maps request paths to files on disk.
30
- *
31
- * - `directory` (default): `/about` → `<root>/about/index.html` (SSG dir style).
32
- * - `flat`: `/about` → `<root>/about.html` (flat-file style).
33
- */
34
- export type PathRewriteStyle = 'directory' | 'flat'
35
-
36
- export interface StaticRouteConfig {
37
- /** Absolute path to the directory served for this route. */
38
- dir: string
39
- /**
40
- * Single-page-app fallback: serve `index.html` for any path that doesn't
41
- * resolve to a real file (so client-side routing works). Default: `false`.
42
- */
43
- spa?: boolean
44
- /**
45
- * Extensionless-URL resolution style for `.html` files. Default: `directory`.
46
- */
47
- pathRewriteStyle?: PathRewriteStyle
48
- /**
49
- * `Cache-Control` max-age (seconds) for served files. Default: `0`.
50
- */
51
- maxAge?: number
52
- }
53
-
54
- export interface BaseProxyConfig {
55
- /**
56
- * Upstream `host:port` to forward to (e.g. `localhost:5173`). Optional when
57
- * `static` is set (the route serves files from disk instead of proxying).
58
- */
59
- from?: string // localhost:5173
60
- to: string // stacks.localhost
61
- /**
62
- * Optional path prefix this route owns under the host `to` (e.g. `'/api'`).
63
- * Lets multiple routes share one host, each serving a different path —
64
- * `/api` proxied to an app, `/docs` from a static dir, `/` from another.
65
- * The longest matching prefix wins; omit (or `'/'`) for the host default.
66
- */
67
- path?: string
68
- start?: StartOptions
69
- pathRewrites?: PathRewrite[]
70
- /**
71
- * Serve a local directory for this route instead of proxying to `from`.
72
- * Provide an absolute directory path (shorthand) or a {@link StaticRouteConfig}.
73
- * When set, `from` is optional; exactly one of `from`/`static` must be present.
74
- */
75
- static?: string | StaticRouteConfig
76
- /**
77
- * Stable id used when registering this proxy with the rpx daemon. Derived
78
- * from `to` if omitted. Must match `/^[a-zA-Z0-9._-]+$/` and be ≤128 chars.
79
- */
80
- id?: string
81
- }
82
-
83
- export type BaseProxyOptions = Partial<BaseProxyConfig>
84
-
85
- export interface CleanupConfig {
86
- domains: string[] // default: [], if only specific domain/s should be cleaned up
87
- hosts: boolean // default: true, if hosts file should be cleaned up
88
- certs: boolean // default: false, if certificates should be cleaned up
89
- verbose: boolean // default: false
90
- vitePluginUsage?: boolean // default: false, if cleanup was initiated by the Vite plugin
91
- }
92
-
93
- export type CleanupOptions = Partial<CleanupConfig>
94
-
95
- /**
96
- * A real PEM cert+key pair on disk for one SNI server name.
97
- */
98
- export interface DomainCert {
99
- /** Absolute path to the PEM certificate (fullchain). */
100
- certPath: string
101
- /** Absolute path to the PEM private key. */
102
- keyPath: string
103
- }
104
-
105
- /**
106
- * Production TLS using real certs (e.g. Let's Encrypt) served per-domain via
107
- * SNI on a single listener. Provide either an explicit `domains` map or a
108
- * `certsDir` convention.
109
- */
110
- export interface ProductionTlsConfig {
111
- /**
112
- * Explicit per-domain cert/key files keyed by SNI server name. Use
113
- * `*.example.com` for a wildcard server name.
114
- */
115
- domains?: Record<string, DomainCert>
116
- /**
117
- * Directory of PEM files following the convention `<domain>.crt` /
118
- * `<domain>.key`. A wildcard pair `_wildcard.<apex>.crt` /
119
- * `_wildcard.<apex>.key` is registered under server name `*.<apex>`.
120
- */
121
- certsDir?: string
122
- }
123
-
124
- /**
125
- * On-demand TLS: issue a real (Let's Encrypt, http-01) certificate for an
126
- * unknown host the first time it's needed, gated by an `ask` callback and/or an
127
- * `allowedSuffixes` allowlist to prevent abuse.
128
- *
129
- * ## Why this is "ask-gated issuance + listener recreate", not at-handshake
130
- *
131
- * Bun (verified on 1.3.14 + 1.4.0) has **no working SNICallback** and
132
- * `server.reload({ tls })` does **not** update certs at runtime. So rpx cannot
133
- * mint a cert during the TLS handshake the way Caddy's on-demand TLS does.
134
- * Instead rpx:
135
- * 1. Sees the first plaintext request for the host on its `:80` listener.
136
- * 2. Asks `ask(host)` / checks `allowedSuffixes`; if approved, drives the
137
- * ACME http-01 flow (serving the challenge from its own `:80`).
138
- * 3. Writes the PEMs into `certsDir` and rebuilds the `:443` listener with the
139
- * augmented SNI cert set (a sub-second `server.stop()` + re-`Bun.serve`).
140
- * The subsequent HTTPS request then finds the freshly-issued cert.
141
- *
142
- * Issuance can also be triggered programmatically via the manager's
143
- * `ensureCert(host)` (e.g. a tunnel server pre-warming a subdomain's cert on
144
- * registration) so the cert exists before the first browser hit.
145
- */
146
- export interface OnDemandTlsConfig {
147
- /** Master switch. On-demand TLS is opt-in; default `false`. */
148
- enabled?: boolean
149
- /**
150
- * Gate issuance for a given hostname. Return `true` to allow rpx to obtain a
151
- * cert, `false` to refuse. Combined with {@link allowedSuffixes} (a host is
152
- * approved if either the suffix allowlist matches OR `ask` returns true). If
153
- * neither is provided, on-demand issuance refuses every host.
154
- */
155
- ask?: (host: string) => boolean | Promise<boolean>
156
- /**
157
- * Allowlist of domain suffixes that may be auto-issued without consulting
158
- * `ask`. A host matches a suffix when it equals it or ends with `.<suffix>`
159
- * (so `example.com` allows `example.com` and `a.example.com`).
160
- */
161
- allowedSuffixes?: string[]
162
- /** Contact email for the ACME account (recommended by Let's Encrypt). */
163
- email?: string
164
- /**
165
- * Use Let's Encrypt **staging** (untrusted but un-rate-limited) instead of
166
- * production. Default `false` (real, trusted, rate-limited certs).
167
- */
168
- staging?: boolean
169
- /**
170
- * Directory where issued PEMs are written (`<host>.crt` / `<host>.key`) and
171
- * from which existing certs are loaded. Should match the SNI `certsDir` so
172
- * issued certs survive restarts. Defaults to the daemon's productionCerts
173
- * `certsDir` when wired through the daemon.
174
- */
175
- certsDir?: string
176
- }
177
-
178
- export interface SharedProxyConfig {
179
- https: boolean | TlsOption
180
- cleanup: boolean | CleanupOptions
181
- vitePluginUsage: boolean
182
- verbose: boolean
183
- _cachedSSLConfig?: SSLConfig | null
184
- start?: StartOptions
185
- cleanUrls: boolean
186
- changeOrigin?: boolean // default: false - changes the origin of the host header to the target URL
187
- regenerateUntrustedCerts?: boolean // If true, will regenerate and re-trust certs that exist but are not trusted by the system.
188
- /**
189
- * Route this proxy through the long-running rpx daemon instead of binding
190
- * its own :443. Lets multiple `rpx start` invocations coexist on shared
191
- * `:443` (Valet-style). Default: `false` for backward compatibility.
192
- */
193
- viaDaemon?: boolean
194
- /**
195
- * Master switch for all `/etc/hosts` reads/writes. Set to `false` on a real
196
- * server with real DNS so rpx never touches `/etc/hosts`. When omitted, the
197
- * legacy behavior applies (driven by `cleanup.hosts`). `cleanup: { hosts:
198
- * false }` also disables hosts management.
199
- */
200
- hostsManagement?: boolean
201
- /**
202
- * Production per-domain SNI certs (Let's Encrypt PEMs already on disk). When
203
- * provided, the listener serves a different real cert per SNI server name
204
- * instead of the dev self-signed shared cert.
205
- */
206
- productionCerts?: ProductionTlsConfig
207
- /**
208
- * On-demand TLS: lazily issue a real cert for an unknown (but approved) host
209
- * the first time it's needed. Opt-in — see {@link OnDemandTlsConfig}.
210
- */
211
- onDemandTls?: OnDemandTlsConfig
212
- /**
213
- * Origin lockdown for "CDN in front of rpx" setups. When set, the shared
214
- * HTTPS handler rejects requests to the listed hosts that lack the CDN's
215
- * shared-secret header — so the publicly-resolvable origin can't be used to
216
- * bypass the CDN. See {@link createOriginGuard}.
217
- */
218
- originGuard?: OriginGuardOptions
219
- }
220
-
221
- export type SharedProxyOptions = Partial<SharedProxyConfig>
222
-
223
- export interface SingleProxyConfig extends BaseProxyConfig, SharedProxyConfig {}
224
-
225
- export interface MultiProxyConfig extends SharedProxyConfig {
226
- proxies: Array<BaseProxyConfig & { cleanUrls: boolean, pathRewrites?: PathRewrite[] }>
227
- }
228
-
229
- export type ProxyConfig = SingleProxyConfig
230
- export type ProxyConfigs = SingleProxyConfig | MultiProxyConfig
231
-
232
- export type BaseProxyOption = Partial<BaseProxyConfig>
233
- export type ProxyOption = Partial<SingleProxyConfig>
234
- export type ProxyOptions = Partial<SingleProxyConfig> | Partial<MultiProxyConfig>
235
-
236
- /**
237
- * Internal shape used by `startProxies` after merging the built-in defaults with
238
- * the caller's single- or multi-proxy options. Every field is optional, and the
239
- * `proxies` array elements tolerate the per-proxy `cleanUrls`/`changeOrigin`
240
- * overrides the runtime reads — so the merged object can be accessed across both
241
- * single and multi shapes without falling back to `any`.
242
- */
243
- export type ResolvedProxyOptions = Partial<SingleProxyConfig> & {
244
- proxies?: Array<BaseProxyConfig & { cleanUrls?: boolean, changeOrigin?: boolean, pathRewrites?: PathRewrite[] }>
245
- }
246
-
247
- export interface SSLConfig {
248
- key: string
249
- cert: string
250
- ca?: string | string[]
251
- }
252
-
253
- export interface ProxySetupOptions extends Omit<ProxyOption, 'from'> {
254
- fromPort: number
255
- sourceUrl: Pick<URL, 'hostname' | 'host'>
256
- ssl: SSLConfig | null
257
- from: string
258
- to: string
259
- portManager?: PortManager
260
- }
261
-
262
- export interface PortManager {
263
- usedPorts: Set<number>
264
- getNextAvailablePort: (startPort: number) => Promise<number>
265
- }
266
-
267
- export type { TlsConfig, TlsOption }