@rip-lang/server 1.3.126 → 1.4.1

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 (69) hide show
  1. package/{docs/READ_VALIDATORS.md → API.md} +41 -119
  2. package/CONFIG.md +408 -0
  3. package/README.md +246 -1109
  4. package/acme/crypto.rip +0 -2
  5. package/browse.rip +62 -0
  6. package/control/cli.rip +89 -34
  7. package/control/lifecycle.rip +67 -1
  8. package/control/manager.rip +250 -0
  9. package/control/mdns.rip +3 -0
  10. package/middleware.rip +1 -1
  11. package/package.json +14 -11
  12. package/server.rip +189 -673
  13. package/serving/config.rip +766 -0
  14. package/{edge → serving}/forwarding.rip +2 -2
  15. package/serving/logging.rip +101 -0
  16. package/{edge → serving}/metrics.rip +29 -1
  17. package/serving/proxy.rip +99 -0
  18. package/{edge → serving}/queue.rip +1 -1
  19. package/{edge → serving}/ratelimit.rip +1 -1
  20. package/{edge → serving}/realtime.rip +71 -2
  21. package/{edge → serving}/registry.rip +1 -1
  22. package/{edge → serving}/router.rip +3 -3
  23. package/{edge → serving}/runtime.rip +18 -16
  24. package/{edge → serving}/security.rip +1 -1
  25. package/serving/static.rip +393 -0
  26. package/{edge → serving}/tls.rip +3 -7
  27. package/{edge → serving}/upstream.rip +4 -4
  28. package/{edge → serving}/verify.rip +16 -16
  29. package/streams/{tls_clienthello.rip → clienthello.rip} +1 -1
  30. package/streams/config.rip +8 -8
  31. package/streams/index.rip +5 -5
  32. package/streams/router.rip +2 -2
  33. package/tests/acme.rip +1 -1
  34. package/tests/config.rip +215 -0
  35. package/tests/control.rip +1 -1
  36. package/tests/{runtime_entrypoints.rip → entrypoints.rip} +11 -7
  37. package/tests/extracted.rip +118 -0
  38. package/tests/helpers.rip +4 -4
  39. package/tests/metrics.rip +3 -3
  40. package/tests/proxy.rip +9 -8
  41. package/tests/read.rip +1 -1
  42. package/tests/realtime.rip +3 -3
  43. package/tests/registry.rip +4 -4
  44. package/tests/router.rip +27 -27
  45. package/tests/runner.rip +70 -0
  46. package/tests/security.rip +4 -4
  47. package/tests/servers.rip +102 -136
  48. package/tests/static.rip +2 -2
  49. package/tests/streams_clienthello.rip +2 -2
  50. package/tests/streams_index.rip +4 -4
  51. package/tests/streams_pipe.rip +1 -1
  52. package/tests/streams_router.rip +10 -10
  53. package/tests/streams_runtime.rip +4 -4
  54. package/tests/streams_upstream.rip +1 -1
  55. package/tests/upstream.rip +2 -2
  56. package/tests/verify.rip +18 -18
  57. package/tests/watchers.rip +4 -4
  58. package/default.rip +0 -435
  59. package/docs/edge/CONFIG_LIFECYCLE.md +0 -111
  60. package/docs/edge/CONTRACTS.md +0 -137
  61. package/docs/edge/EDGEFILE_CONTRACT.md +0 -282
  62. package/docs/edge/M0B_REVIEW_NOTES.md +0 -102
  63. package/docs/edge/SCHEDULER.md +0 -46
  64. package/docs/logo.png +0 -0
  65. package/docs/logo.svg +0 -13
  66. package/docs/social.png +0 -0
  67. package/edge/config.rip +0 -607
  68. package/edge/static.rip +0 -69
  69. package/tests/edgefile.rip +0 -165
package/README.md CHANGED
@@ -2,147 +2,46 @@
2
2
 
3
3
  # Rip Server - @rip-lang/server
4
4
 
5
- > **One runtime for your app and your edge — web framework, reverse proxy, TLS passthrough, and managed workers in a single zero-dependency Bun-native server**
5
+ > Rip Server serves content: static sites, Rip apps, proxied HTTP services, and TCP/TLS services -- all from one Bun-native runtime.
6
6
 
7
- Rip Server replaces the combination of a web framework (Express, Hono), a
8
- process manager (PM2), and a reverse proxy (Nginx, Caddy, Traefik) with one
9
- unified runtime. Define your API with Sinatra-style routes, serve it with
10
- managed multi-worker processes, proxy external services, and pass through raw
11
- TLS for things like the Incus Web UI all from a single `rip server` command
12
- with zero external dependencies.
7
+ Here, `content` means anything you want to make reachable over the network. That
8
+ may be a static site, a small Rip app, an HTTP service behind a proxy, a raw
9
+ TCP/TLS service, or a containerized tool you want to publish.
10
+
11
+ Rip Server replaces the usual app framework + process manager + reverse proxy
12
+ stack with one runtime. Start with `rip server` for a single app. Add
13
+ `serve.rip` when you want multi-host routing, shared apps, named proxy
14
+ backends, reusable certs, or TCP/TLS passthrough. The job stays the same:
15
+ take something you have and make it reachable safely, cleanly, and coherently.
13
16
 
14
17
  Written entirely in Rip. Runs on Bun.
15
18
 
16
- ## Features
17
-
18
- ### App Framework
19
- - **Sinatra-style routing** — `get`, `post`, `put`, `del` with path parameters and wildcards
20
- - **Request validation** — 40+ built-in validators via [`read()`](docs/READ_VALIDATORS.md) eliminate API boilerplate
21
- - **Middleware** — cors, sessions, compression, security headers, rate limiting, body limits
22
- - **File serving** — Auto-detected MIME types via `@send`, SPA fallback support
23
- - **Realtime WebSocket** — Pub/sub hub where your backend stays HTTP-only
24
-
25
- ### Server Runtime
26
- - **Multi-worker architecture** — Automatic worker spawning based on CPU cores
27
- - **Hot module reloading** — Watches `*.rip` files by default, rolling restarts on change
28
- - **Rolling restarts** — Zero-downtime deployments with per-app worker pools
29
- - **Automatic HTTPS** — Shipped `*.ripdev.io` wildcard cert, or auto-TLS via Let's Encrypt ACME
30
- - **Observability** — `/diagnostics` with request rates, latency percentiles, queue pressure
31
- - **mDNS discovery** — `.local` hostname advertisement
32
-
33
- ### Edge Proxy
34
- - **HTTP/WS reverse proxy** — Route to external upstreams with health checks, circuit breakers, and retry
35
- - **Layer 4 TLS passthrough** — SNI-based raw TCP routing for services that keep their own TLS
36
- - **Unified port multiplexer** — Stream passthrough and normal HTTPS on the same `:443`
37
- - **Atomic config reload** — Staged activation with post-verify and automatic rollback
38
- - **Declarative routing** — Host/path/method matching with wildcard hosts and per-site overrides
39
-
40
- ## What Can You Build?
41
-
42
- - **A single API or web app** — `rip server` with zero config, one command
43
- - **A multi-app platform** — Per-app worker pools, host routing, and rolling restarts via `config.rip`
44
- - **A reverse proxy** — Route traffic to external HTTP/WS upstreams with health checks
45
- - **A TLS passthrough edge** — Expose services like the Incus Web UI without terminating their TLS
46
- - **A full edge runtime** — Replace Nginx, Caddy, or Traefik for single-host deployments via `Edgefile.rip`
47
-
48
- | Directory | Role |
49
- |-----------|------|
50
- | `api.rip` | Core framework: routing, validation, `read()`, `session`, `@send` |
51
- | `middleware.rip` | Built-in middleware: cors, logger, sessions, compression, security, serve |
52
- | `server.rip` | Edge orchestrator: CLI, workers, load balancing, TLS, mDNS |
53
- | `edge/` | Request path and edge runtime: config, forwarding, metrics, registry, router, runtime, TLS, upstreams, verification |
54
- | `control/` | Management: CLI, lifecycle, workers, watchers, mDNS, events |
55
- | `streams/` | Layer 4 stream routing: ClientHello parsing, SNI routing, stream runtimes, raw TCP upstreams |
56
- | `acme/` | Auto-TLS: ACME client, crypto, cert store, challenge handler |
57
-
58
- > **See Also**: For the DuckDB server, see [@rip-lang/db](../db/README.md).
59
-
60
- ## Runtime Tiers
61
-
62
- `rip server` is a graduated runtime. Start simple and add capabilities as you need them:
63
-
64
- 1. **Single-app mode** — one app, zero config, one command
65
- 2. **Managed multi-app mode** — many Rip apps with per-app worker pools via `config.rip`
66
- 3. **Edge mode** — upstream proxying, host/path routing, TLS passthrough, staged reload, verification, and rollback via `Edgefile.rip`
67
-
68
- ```mermaid
69
- flowchart TD
70
- SingleApp["rip server"] --> ManagedApps["config.rip"]
71
- ManagedApps --> EdgeMode["Edgefile.rip"]
72
- EdgeMode --> Upstreams["HTTP and WS upstream routes"]
73
- EdgeMode --> Streams["Layer 4 TLS passthrough"]
74
- EdgeMode --> ManagedRoutes["Managed app routes"]
75
- ```
19
+ ## Serving Modes
76
20
 
77
- ## Architecture
78
-
79
- ```mermaid
80
- flowchart LR
81
- Client[Client] --> Port[":443"]
82
- Port -->|"HTTP/WS"| TLS["TLS termination"]
83
- TLS --> Router["Route matching"]
84
- Router -->|upstream| Proxy["Reverse proxy"]
85
- Router -->|app| Workers["Worker pool"]
86
- Port -->|"TLS passthrough"| SNI["SNI routing"]
87
- SNI --> Upstream["Raw TCP upstream"]
88
- ```
21
+ - Static file and website serving
22
+ - Small app serving with Sinatra-style routing and `read()` validators
23
+ - HTTP / WebSocket reverse proxy
24
+ - Layer 4 TCP / TLS passthrough
89
25
 
90
- When a stream route shares the HTTPS port, Rip uses a multiplexer: matching SNI
91
- traffic passes through at Layer 4, and everything else falls through to the
92
- normal HTTP/WebSocket edge runtime. When no stream routes share the HTTPS port,
93
- there is no multiplexer and no extra hop.
94
-
95
- ## How Does Rip Server Compare?
96
-
97
- | Capability | Rip Server | Caddy | Nginx | Traefik |
98
- |---|---|---|---|---|
99
- | HTTP reverse proxy | Yes | Yes | Yes | Yes |
100
- | WebSocket proxy | Yes | Yes | Yes | Yes |
101
- | Auto-TLS (ACME) | Yes | Yes | Plugin | Yes |
102
- | Layer 4 TLS passthrough | Yes | Yes | Stream module | Yes |
103
- | Managed app workers | Yes | No | No | No |
104
- | Built-in app framework | Yes | No | No | No |
105
- | Hot reload | Yes | Yes | Reload | Yes |
106
- | Atomic rollback | Yes | No | No | No |
107
- | Per-SNI multi-cert TLS | Yes | Yes | Yes | Yes |
108
- | Config validation with hints | Yes | Partial | No | Partial |
109
- | Zero dependencies | Yes | Go binary | C binary | Go binary |
110
-
111
- Use Rip Server when you want one Bun-native runtime for both the app and the
112
- edge. Use Caddy, Nginx, or Traefik when you specifically need mature HTTP
113
- caching, multi-node service discovery, or their broader ecosystem integrations.
26
+ ## Serving Guarantees
27
+
28
+ - Managed worker pools with rolling restarts
29
+ - HTTPS, ACME, certificate reuse, and SNI routing
30
+ - Proxy health checks, retry behavior, and upstream timeouts
31
+ - Shared-port HTTPS multiplexer
32
+ - Atomic config reload with verification and rollback
33
+ - Drain semantics, diagnostics, and control APIs
34
+ - Composable `serve.rip` config with reusable groups and rules
114
35
 
115
36
  ## Quick Start
116
37
 
117
- ### Installation
38
+ ### Install
118
39
 
119
40
  ```bash
120
- # Local (per-project)
121
41
  bun add @rip-lang/server
122
-
123
- # Global
124
- bun add -g rip-lang @rip-lang/server
125
- ```
126
-
127
- ### Running Your App
128
-
129
- ```bash
130
- # From your app directory (uses ./index.rip, watches *.rip)
131
- rip server
132
-
133
- # Name your app (for mDNS: myapp.local)
134
- rip server myapp
135
-
136
- # Explicit entry file
137
- rip server ./app.rip
138
-
139
- # HTTP only mode
140
- rip server http
141
42
  ```
142
43
 
143
- ### Example App
144
-
145
- Create `index.rip`:
44
+ ### Single app
146
45
 
147
46
  ```coffee
148
47
  import { get, read, start } from '@rip-lang/server'
@@ -150,9 +49,6 @@ import { get, read, start } from '@rip-lang/server'
150
49
  get '/' ->
151
50
  'Hello from Rip Server!'
152
51
 
153
- get '/json' ->
154
- { message: 'It works!', timestamp: Date.now() }
155
-
156
52
  get '/users/:id' ->
157
53
  id = read 'id', 'id!'
158
54
  { user: { id, name: "User #{id}" } }
@@ -166,1097 +62,338 @@ Run it:
166
62
  rip server
167
63
  ```
168
64
 
169
- Test it:
65
+ ### Add `serve.rip`
170
66
 
171
- ```bash
172
- curl http://localhost/
173
- # Hello from Rip Server!
67
+ Add `serve.rip` next to your entry file when you want host routing, shared
68
+ apps, proxy backends, reusable TLS, or TCP passthrough.
174
69
 
175
- curl http://localhost/json
176
- # {"message":"It works!","timestamp":1234567890}
70
+ ## Canonical `serve.rip`
177
71
 
178
- curl http://localhost/users/42
179
- # {"user":{"id":42,"name":"User 42"}}
72
+ `serve.rip` is the one config file. There are no alternate config formats.
180
73
 
181
- curl http://localhost/status
182
- # {"status":"healthy","app":"myapp","workers":5,"ports":{"https":443}}
183
- ```
74
+ Canonical top-level keys:
184
75
 
185
- ## Edgefile Quick Start
76
+ - `version`
77
+ - `server` (`edge` is a deprecated alias)
78
+ - `certs`
79
+ - `proxies`
80
+ - `apps`
81
+ - `rules`
82
+ - `groups`
83
+ - `hosts`
84
+ - `streams`
186
85
 
187
- `Edgefile.rip` is the declarative edge config for the Bun-native edge runtime.
188
- It gives you:
86
+ `hosts` is the canonical authoring surface. Reuse happens through:
189
87
 
190
- - **HTTP/WS reverse proxy** route traffic to external upstreams with health checks, retries, and circuit breakers
191
- - **Layer 4 TLS passthrough** route raw TLS connections by SNI to services that keep their own certificates and client-cert auth
192
- - **Shared-port multiplexer** stream passthrough and normal HTTPS on the same `:443`, automatically
193
- - **Managed app routes** host Rip apps with per-app worker pools alongside proxy routes
194
- - **Atomic reload** — staged activation with post-verify and automatic rollback via `SIGHUP` or control API
195
- - **Strict validation** — field-path errors and remediation hints, with `--check-config` for dry-run validation
196
- - **Diagnostics** — config metadata, counts, reload history, and route descriptions in `/diagnostics`
88
+ - `certs` for reusable TLS identities
89
+ - `proxies` for named HTTP or TCP backends
90
+ - `rules` for reusable HTTP route bundles
91
+ - `groups` for reusable hostname lists
197
92
 
198
- ### Canonical Shape
93
+ Everything normalizes into concrete hosts, resolved TLS pairs, concrete route
94
+ lists, and concrete stream routes.
95
+
96
+ ### Minimal shape
199
97
 
200
98
  ```coffee
201
99
  export default
202
100
  version: 1
203
- edge:
204
- hsts: true
205
- timeouts:
206
- connectMs: 2000
207
- readMs: 30000
208
- upstreams: {}
209
- streamUpstreams: {}
101
+ server: {}
102
+ certs: {}
103
+ proxies: {}
210
104
  apps: {}
211
- routes: []
105
+ rules: {}
106
+ groups: {}
107
+ hosts: {}
212
108
  streams: []
213
- sites: {}
214
109
  ```
215
110
 
216
- ### Edgefile Field Reference
111
+ ## Config Reference
217
112
 
218
- | Field | Purpose |
219
- |------|---------|
220
- | `version` | Schema version (optional, defaults to `1`). |
221
- | `edge` | Global edge settings: TLS, trusted proxies, timeouts, verification policy. |
222
- | `upstreams` | Named external HTTP services and their targets. |
223
- | `streamUpstreams` | Named raw TCP upstreams using `host:port` targets and optional `connectTimeoutMs`. |
224
- | `apps` | Managed Rip apps with entry paths, hosts, worker counts, and env. |
225
- | `routes` | Declarative route objects that choose exactly one action. |
226
- | `streams` | Layer 4 stream routes that match by listen port and SNI. |
227
- | `sites` | Per-host route groups and policy overrides. |
113
+ ### `certs`
228
114
 
229
- ### Route Shape
230
-
231
- Common route fields:
232
-
233
- - `host?: string` — exact host, wildcard host like `*.example.com`, or `*`
234
- - `path: string` — must start with `/`
235
- - `methods?: string[] | "*"`
236
- - `priority?: number`
237
- - `timeouts?: { connectMs, readMs }`
238
-
239
- Each route must define exactly one action:
240
-
241
- - `upstream: 'name'`
242
- - `app: 'name'`
243
- - `static: '/dir'`
244
- - `redirect: { to: '...', status: 301 }`
245
- - `headers: { set, remove }`
246
-
247
- WebSocket proxy routes use:
248
-
249
- - `websocket: true`
250
- - `upstream: 'name'`
251
-
252
- ### Stream Shape
253
-
254
- Common stream fields:
255
-
256
- - `listen: number` — TCP port to bind
257
- - `sni: string[]` — exact or wildcard SNI patterns
258
- - `upstream: 'name'`
259
- - `timeouts?: { handshakeMs, idleMs, connectMs }`
260
-
261
- When `listen` matches the active HTTPS port, Rip uses a shared-port
262
- multiplexer. Matching SNI traffic is passed through to the stream upstream, and
263
- everything else falls through to Rip's internal HTTPS server.
264
-
265
- ### Example: Pure Proxy Shape
115
+ Preferred shorthand:
266
116
 
267
117
  ```coffee
268
- export default
269
- version: 1
270
- edge:
271
- hsts: true
272
- trustedProxies: ['10.0.0.0/8']
273
- verify:
274
- requireHealthyUpstreams: true
275
- requireReadyApps: true
276
- includeUnroutedManagedApps: true
277
- minHealthyTargetsPerUpstream: 1
278
- upstreams:
279
- app:
280
- targets: ['http://app.incusbr0:3000']
281
- healthCheck:
282
- path: '/health'
283
- routes: [
284
- { path: '/ws', websocket: true, upstream: 'app' }
285
- { path: '/*', upstream: 'app' }
286
- ]
118
+ certs:
119
+ trusthealth: '/ssl/trusthealth.com'
287
120
  ```
288
121
 
289
- ### Example: TLS Passthrough For Incus
290
-
291
- ```coffee
292
- export default
293
- version: 1
122
+ This expands to:
294
123
 
295
- edge: {}
124
+ - `cert: '/ssl/trusthealth.com.crt'`
125
+ - `key: '/ssl/trusthealth.com.key'`
296
126
 
297
- streamUpstreams:
298
- incus:
299
- targets: ['127.0.0.1:8443']
127
+ Explicit object form is also allowed.
300
128
 
301
- streams: [
302
- { listen: 8443, sni: ['incus.example.com'], upstream: 'incus' }
303
- ]
304
- ```
129
+ ### `proxies`
305
130
 
306
- ### Example: Unified Port For Apps + Incus
131
+ Named backend proxy targets. Transport is inferred from URL scheme:
307
132
 
308
- If a stream route listens on the active HTTPS port, Rip automatically switches
309
- that port into multiplexer mode. Matching SNI traffic is passed through at
310
- Layer 4, and everything else falls through to Rip's internal HTTPS server.
133
+ - `http://...` => HTTP proxy
134
+ - `https://...` => HTTPS proxy
135
+ - `tcp://...` => raw TCP proxy
311
136
 
312
137
  ```coffee
313
- export default
314
- version: 1
315
- edge: {}
316
-
317
- streamUpstreams:
318
- incus:
319
- targets: ['127.0.0.1:8443']
320
-
321
- streams: [
322
- { listen: 443, sni: ['incus.example.com'], upstream: 'incus' }
323
- ]
324
- ```
325
-
326
- This means:
327
-
328
- - `https://incus.example.com` keeps Incus's own TLS and client-certificate flow
329
- - every other HTTPS host on `:443` still terminates TLS in Rip and uses the
330
- normal HTTP/WebSocket edge runtime
331
- - if no stream route shares the HTTPS port, Rip stays on the normal direct
332
- `Bun.serve()` path with no extra hop
333
-
334
- ### Example: Mixed Apps + Upstreams
138
+ proxies:
139
+ api:
140
+ hosts: ['http://127.0.0.1:4000']
141
+ check:
142
+ path: '/health'
143
+ intervalMs: 5000
144
+ timeoutMs: 2000
145
+ retry:
146
+ attempts: 2
147
+ retryOn: [502, 503, 504]
148
+ timeouts:
149
+ connectMs: 2000
150
+ readMs: 30000
335
151
 
336
- ```coffee
337
- export default
338
- version: 1
339
- edge: {}
340
- upstreams:
341
- api:
342
- targets: ['http://api.incusbr0:4000']
343
- apps:
344
- admin:
345
- entry: './admin/index.rip'
346
- hosts: ['admin.example.com']
347
- routes: [
348
- { path: '/api/*', upstream: 'api' }
349
- { path: '/admin/*', app: 'admin' }
350
- ]
351
- sites:
352
- 'admin.example.com':
353
- routes: [
354
- { path: '/*', app: 'admin' }
355
- ]
152
+ incus:
153
+ hosts: ['tcp://127.0.0.1:8443']
154
+ connectTimeoutMs: 5000
356
155
  ```
357
156
 
358
- ### Example: Manual Wildcard TLS
157
+ Mixed scheme families in one proxy are invalid.
359
158
 
360
- Use manual cert/key paths for wildcard TLS. ACME HTTP-01 cannot issue
361
- `*.domain` certificates.
159
+ ### `apps`
362
160
 
363
- ```coffee
364
- export default
365
- version: 1
366
- edge:
367
- cert: './certs/wildcard.example.com.crt'
368
- key: './certs/wildcard.example.com.key'
369
- hsts: true
370
- upstreams:
371
- web:
372
- targets: ['http://web.incusbr0:3000']
373
- routes: [
374
- { path: '/*', upstream: 'web', host: '*.example.com' }
375
- ]
376
- ```
377
-
378
- ## Host Blocks
161
+ Named managed Rip apps with worker and queue settings.
379
162
 
380
- Use `hosts` to group TLS certificates, routes, and static file serving
381
- under each hostname. This is the cleanest way to configure multi-domain hosting.
163
+ ### `rules`
382
164
 
383
- `hosts` defines per-domain configuration. `version` and `edge` are optional --
384
- they default to `1` and `{}` respectively.
385
-
386
- ### Host Blocks Shape
165
+ Reusable HTTP rule bundles:
387
166
 
388
167
  ```coffee
389
- export default
390
- hosts:
391
- '*.trusthealth.com':
392
- cert: '/ssl/trusthealth.com.crt'
393
- key: '/ssl/trusthealth.com.key'
394
- root: '/mnt/trusthealth/website'
395
- routes: [
396
- { path: '/*', static: '.', spa: true }
397
- ]
398
-
399
- '*.zionlabshare.com':
400
- cert: '/ssl/zionlabshare.com.crt'
401
- key: '/ssl/zionlabshare.com.key'
402
- routes: [
403
- { path: '/api/*', upstream: 'api' }
404
- { path: '/*', static: '/mnt/zion/dist', spa: true }
405
- ]
406
-
407
- upstreams:
408
- api: { targets: ['http://127.0.0.1:3807'] }
409
-
410
- streamUpstreams:
411
- incus: { targets: ['127.0.0.1:8443'] }
412
-
413
- streams: [
414
- { listen: 443, sni: ['incus.trusthealth.com'], upstream: 'incus' }
168
+ rules:
169
+ web: [
170
+ { path: '/api/*', proxy: 'api' }
171
+ { path: '/*', app: 'web' }
415
172
  ]
416
173
  ```
417
174
 
418
- ### How Host Blocks Work
419
-
420
- - Each server block is keyed by hostname (exact or wildcard)
421
- - Per-server `cert`/`key` enable SNI-based certificate selection (multiple domains on one port)
422
- - `edge.cert` and `edge.key` are the optional fallback TLS identity
423
- - A server with just `root` and no `routes` serves static files automatically
424
- - A server with `passthrough` routes raw TLS to another address (no termination)
425
- - `streams`, `streamUpstreams`, `upstreams`, and `apps` stay top-level and are shared
426
- - Routes inside a server block inherit the server hostname automatically
427
- - Hosts not matching any server block or stream route fall through to the default app
428
-
429
- ### Host Block Fields
175
+ You do not have to use `rules`. Hosts may also define inline rules.
430
176
 
431
- | Field | Purpose |
432
- |-------|---------|
433
- | `passthrough` | Raw TLS passthrough to `host:port` (no cert, no routes needed) |
434
- | `cert` | TLS certificate path for this hostname (must pair with `key`) |
435
- | `key` | TLS private key path for this hostname (must pair with `cert`) |
436
- | `root` | Default filesystem base; if present with no `routes`, serves static files |
437
- | `spa` | Server-level SPA fallback (boolean, used with `root`-only blocks) |
438
- | `routes` | Array of route objects (optional if `root` or `passthrough` is set) |
439
- | `timeouts` | Per-server timeout defaults |
177
+ ### `groups`
440
178
 
441
- ### Static Routes
179
+ Reusable hostname lists.
442
180
 
443
- Static routes serve files from disk with auto-detected MIME types:
444
-
445
- ```coffee
446
- { path: '/*', static: '.', spa: true }
447
- { path: '/assets/*', static: '/mnt/assets' }
448
- ```
181
+ ### `hosts`
449
182
 
450
- - `static` is the directory to serve from (absolute path or relative to `root`)
451
- - `root` on a route overrides the server-level `root`
452
- - `spa: true` enables SPA fallback: when a file is not found and the request
453
- accepts `text/html`, serves `index.html` instead
454
- - Directory requests serve `index.html` if present
455
- - Path traversal is rejected
183
+ The canonical config surface. Each binding resolves to one or more concrete
184
+ hosts and owns the HTTP or passthrough behavior for those hosts.
456
185
 
457
- ### Redirect Routes
186
+ Examples:
458
187
 
459
188
  ```coffee
460
- { path: '/old/*', redirect: { to: 'https://new.example.com', status: 301 } }
461
- ```
462
-
463
- ## Operator Runbook
464
-
465
- ### Start With An Explicit Edgefile
466
-
467
- ```bash
468
- rip server --edgefile=./Edgefile.rip
469
- ```
470
-
471
- ### Validate Config Without Serving
472
-
473
- ```bash
474
- rip server --check-config
475
- rip server --check-config --edgefile=./Edgefile.rip
476
- ```
477
-
478
- ### Reload Config Safely
479
-
480
- Send `SIGHUP` to the long-lived server process:
481
-
482
- ```bash
483
- kill -HUP "$(cat /tmp/rip_myapp.pid)"
484
- ```
485
-
486
- The default PID file is `/tmp/rip_<app-name>.pid`. If you use a custom socket
487
- prefix, the PID file follows `/tmp/<socket-prefix>.pid`.
488
- Reload success or rejection is printed to stderr and reflected in `/diagnostics`.
489
-
490
- You can also trigger reload through the control socket:
491
-
492
- ```bash
493
- curl --unix-socket /tmp/rip_myapp.ctl.sock -X POST http://localhost/reload
494
- ```
495
-
496
- When file watching is enabled, application code changes use the same staged
497
- reload path automatically. You can always reload Edgefile changes explicitly via
498
- `SIGHUP` or the control socket API.
499
-
500
- ### Inspect Active Config And Diagnostics
501
-
502
- ```bash
503
- curl http://localhost/diagnostics
189
+ hosts:
190
+ 'example.com':
191
+ cert: 'main'
192
+ rules: [
193
+ { path: '/api/*', proxy: 'api' }
194
+ { path: '/*', app: 'web' }
195
+ ]
196
+
197
+ publicWeb:
198
+ hosts: 'publicWeb'
199
+ cert: 'main'
200
+ rules: 'web'
201
+
202
+ hosts: *{
203
+ ['example.com', 'foo.bar.com']:
204
+ cert: 'main'
205
+ rules: 'web'
206
+ }
504
207
  ```
505
208
 
506
- The `config` block reports:
507
-
508
- - config kind (`edge`, `legacy`, or `none`)
509
- - active path
510
- - schema version
511
- - counts for apps, upstreams, routes, and sites
512
- - active compiled route descriptions
513
- - last result (`loaded`, `rejected`, etc.)
514
- - reload timestamp
515
- - last error, if any
516
- - active edge runtime inflight/WS counts
517
- - retired edge runtimes still draining after a config swap
518
- - last reload attempt record
519
- - bounded reload history with source, versions, result, and reason
520
- - structured rollback/rejection code and details for failed reloads
521
-
522
- The top-level diagnostics payload also includes:
523
-
524
- - `upstreams`: per-upstream target counts and healthy/unhealthy target totals
209
+ Host block fields:
525
210
 
526
- ### Verification Policy
211
+ - `hosts`
212
+ - `cert`
213
+ - `key`
214
+ - `rules`
215
+ - `proxy`
216
+ - `app`
217
+ - `root`
218
+ - `spa`
219
+ - `browse`
220
+ - `timeouts`
527
221
 
528
- Use `edge.verify` to tune post-activate verification:
222
+ Rules:
529
223
 
530
- - `requireHealthyUpstreams`: require referenced upstreams to prove healthy targets
531
- - `requireReadyApps`: require referenced managed apps to have ready workers
532
- - `includeUnroutedManagedApps`: also verify managed apps not directly referenced by a route
533
- - `minHealthyTargetsPerUpstream`: minimum healthy targets required per referenced upstream
224
+ - host rules use `proxy`, not `upstream`
225
+ - rule arrays can be inline, reusable, or mixed
226
+ - rules inside a host block must not specify `host`
227
+ - `proxy` shorthand can target HTTP or TCP proxies
228
+ - if host-level `proxy` points to a TCP proxy, Rip creates the default `:443` SNI stream route for that host
534
229
 
535
- ## Validation
230
+ ### `streams`
536
231
 
537
- The `read()` function is a validation and parsing powerhouse that eliminates
538
- 90% of API boilerplate. It supports 40+ built-in validators including `email`,
539
- `phone`, `money`, `uuid`, `json`, `regex`, and composable object schemas.
232
+ Explicit Layer 4 routes:
540
233
 
541
234
  ```coffee
542
- email = read 'email', 'email!' # required, validated
543
- phone = read 'phone', 'phone' # optional, formatted
544
- role = read 'role', ['admin', 'user'] # enum
545
- age = read 'age', 'int', [18, 120] # range-checked
546
- ```
547
-
548
- See the full [Validation Reference](docs/READ_VALIDATORS.md) for all 37+
549
- validators, patterns, custom validators, and real-world examples.
550
-
551
- ## App Path & Naming
552
-
553
- ### Entry File Resolution
554
-
555
- When you run `rip server`, it looks for your app's entry file:
556
-
557
- ```bash
558
- # No arguments: looks for index.rip (or index.ts) in current directory
559
- rip server
560
-
561
- # Directory path: looks for index.rip (or index.ts) in that directory
562
- rip server ./myapp/
563
-
564
- # Explicit file: uses that file directly
565
- rip server ./app.rip
566
- rip server ./src/server.ts
235
+ streams: [
236
+ { listen: 443, sni: ['db.example.com'], proxy: 'db' }
237
+ ]
567
238
  ```
568
239
 
569
- ### App Naming
570
-
571
- The **app name** is used for mDNS discovery (e.g., `myapp.local`) and logging. It's determined by:
572
-
573
- ```bash
574
- # Default: current directory name becomes app name
575
- ~/projects/api$ rip server # app name = "api"
576
-
577
- # Explicit name: pass a name that's not a file path
578
- rip server myapp # app name = "myapp"
579
-
580
- # With aliases: name@alias1,alias2
581
- rip server myapp@api,backend # accessible at myapp.local, api.local, backend.local
582
-
583
- # Path with alias
584
- rip server ./app.rip@myapp # explicit file + custom app name
585
- ```
586
-
587
- **Examples:**
588
-
589
- ```bash
590
- # In ~/projects/api/ with index.rip
591
- rip server # app = "api", entry = ./index.rip
592
- rip server myapp # app = "myapp", entry = ./index.rip
593
- rip server ./server.rip # app = "api", entry = ./server.rip
594
- rip server ./server.rip@myapp # app = "myapp", entry = ./server.rip
595
- ```
596
-
597
- ## File Watching
598
-
599
- Directory watching is **on by default** — any `.rip` file change in your app directory triggers an automatic rolling restart. Use `--watch=<glob>` to customize the pattern, or `--static` to disable watching entirely.
600
-
601
- ```bash
602
- rip server # Watches *.rip (default)
603
- rip server --watch=*.ts # Watch TypeScript files instead
604
- rip server --static # No watching, no hot reload (production)
605
- ```
606
-
607
- **How it works:**
608
-
609
- 1. Uses OS-native file watching (FSEvents on macOS, inotify on Linux)
610
- 2. Watches the entire app directory recursively
611
- 3. When a matching file changes, touches the entry file
612
- 4. The hot-reload mechanism detects the mtime change and does a rolling restart
613
-
614
- This is a single kernel-level file descriptor in the main process — no polling, zero overhead when files aren't changing.
615
-
616
- ## CLI Reference
617
-
618
- ### Basic Syntax
619
-
620
- ```bash
621
- rip server [flags] [app-path] [app-name]
622
- rip server [flags] [app-path]@<alias1>,<alias2>,...
623
- ```
624
-
625
- ### Flags
626
-
627
- | Flag | Description | Default |
628
- |------|-------------|---------|
629
- | `-h`, `--help` | Show help and exit | — |
630
- | `-v`, `--version` | Show version and exit | — |
631
- | `--edgefile=<path>` | Load `Edgefile.rip` from an explicit path | Auto-discover or none |
632
- | `--check-config` | Validate `Edgefile.rip` or `config.rip` and exit | Disabled |
633
- | `--watch=<glob>` | Watch glob pattern | `*.rip` |
634
- | `--static` | Disable hot reload and file watching | — |
635
- | `--env=<mode>` | Environment mode (`dev`, `prod`) | `development` |
636
- | `--debug` | Enable debug logging | Disabled |
637
- | `--quiet` | Suppress startup URL output | Disabled |
638
- | `http` | HTTP-only mode (no HTTPS) | HTTPS enabled |
639
- | `https` | HTTPS mode (explicit) | Auto |
640
- | `http:<port>` | Set HTTP port | 80, fallback 3000 |
641
- | `https:<port>` | Set HTTPS port | 443, fallback 3443 |
642
- | `--socket-prefix=<name>` | Override Unix socket / PID file prefix | `rip_<app-name>` |
643
- | `w:<n>` | Worker count (`auto`, `half`, `2x`, `3x`, or number) | `half` of cores |
644
- | `r:<reqs>,<secs>s` | Restart policy: requests, seconds (e.g., `5000,3600s`) | `10000,3600s` |
645
- | `--cert=<path>` | TLS certificate path | Shipped `*.ripdev.io` cert |
646
- | `--key=<path>` | TLS private key path | Shipped `*.ripdev.io` key |
647
- | `--hsts` | Enable HSTS headers | Disabled |
648
- | `--no-redirect-http` | Don't redirect HTTP to HTTPS | Redirects enabled |
649
- | `--json-logging` | Output JSON access logs | Human-readable |
650
- | `--no-access-log` | Disable access logging | Enabled |
651
- | `--acme` | Enable auto-TLS via Let's Encrypt | Disabled |
652
- | `--acme-staging` | Use Let's Encrypt staging CA | Disabled |
653
- | `--acme-domain=<d>` | Domain for ACME certificate | — |
654
- | `--realtime-path=<p>` | WebSocket endpoint path | `/realtime` |
655
- | `--rate-limit=<n>` | Max requests per IP per window | Disabled (0) |
656
- | `--rate-limit-window=<ms>` | Rate limit window in ms | `60000` (1 min) |
657
- | `--publish-secret=<s>` | Bearer token for `/publish` endpoint | None (open) |
658
-
659
- ### Subcommands
660
-
661
- ```bash
662
- rip server stop # Stop running server
663
- rip server list # List registered hosts
664
- ```
665
-
666
- ### Examples
667
-
668
- ```bash
669
- # Development (default: watches *.rip, HTTPS, hot reload)
670
- rip server
671
-
672
- # HTTP only
673
- rip server http
674
-
675
- # Production: 8 workers, no hot reload
676
- rip server --static w:8
677
-
678
- # Custom port
679
- rip server http:3000
680
-
681
- # With mDNS aliases (accessible as myapp.local and api.local)
682
- rip server myapp@api
683
-
684
- # Watch TypeScript files instead of Rip
685
- rip server --watch=*.ts
686
-
687
- # Debug mode
688
- rip server --debug
689
-
690
- # Restart workers after 5000 requests or 1 hour
691
- rip server r:5000,3600s
692
- ```
693
-
694
- ## Internals
695
-
696
- ### Self-Spawning Design
697
-
698
- The server uses a single-file, self-spawning architecture:
699
-
700
- ```
701
- ┌─────────────────────────────────────────────────────────┐
702
- │ Main Process │
703
- │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
704
- │ │ Server │ │ Manager │ │ Control Socket │ │
705
- │ │ (HTTP/HTTPS)│ │ (Workers) │ │ (Commands) │ │
706
- │ └──────┬──────┘ └──────┬──────┘ └────────┬────────┘ │
707
- └─────────┼────────────────┼──────────────────┼───────────┘
708
- │ │ │
709
- ▼ ▼ │
710
- ┌──────────┐ ┌──────────────┐ │
711
- │ Requests │ │ Spawn/Monitor│ │
712
- └────┬─────┘ └──────┬───────┘ │
713
- │ │ │
714
- ▼ ▼ │
715
- ┌─────────────────────────────────────────────│───┐
716
- │ Worker Processes │ │
717
- │ ┌────────┐ ┌────────┐ ┌────────┐ │ │
718
- │ │Worker 0│ │Worker 1│ │Worker N│ ◄────────┘ │
719
- │ │(Unix) │ │(Unix) │ │(Unix) │ │
720
- │ └────────┘ └────────┘ └────────┘ │
721
- └─────────────────────────────────────────────────┘
722
- ```
723
-
724
- When `RIP_SETUP_MODE=1` is set, the same file runs the one-time setup phase. When `RIP_WORKER_MODE=1` is set, it runs as a worker.
725
-
726
- ### Startup Lifecycle
727
-
728
- 1. **Setup** — If `setup.rip` exists next to the entry file, it runs once in a temporary process before any workers spawn. Use this for database migrations, table creation, and seeding.
729
- 2. **Workers** — N worker processes are spawned, each loading the entry file and serving requests.
240
+ If a stream route shares the HTTPS port, Rip switches that port into shared
241
+ multiplexer mode: matching SNI is passed through at Layer 4, and everything
242
+ else falls through to Rip's internal HTTPS server.
730
243
 
731
- ### Request Flow
244
+ ## Clean Example
732
245
 
733
- ```mermaid
734
- flowchart TD
735
- request[Incoming Request] --> validate[ValidateRequest]
736
- validate --> rateLimit[RateLimit]
737
- rateLimit --> edgeRoute{Edge Route Match}
738
- edgeRoute -->|upstream| upstream[Proxy To Upstream]
739
- edgeRoute -->|app| worker[Forward To Managed Worker]
740
- edgeRoute -->|no edge match| registry[Host Registry]
741
- registry --> worker
742
- worker --> response[Response]
743
- upstream --> response
744
- ```
745
-
746
- At runtime:
246
+ ```coffee
247
+ export default
248
+ version: 1
747
249
 
748
- 1. validate request and rate-limit early
749
- 2. check the active edge route table
750
- 3. proxy to an upstream if an `upstream` route matches
751
- 4. otherwise resolve a managed app and forward to its worker pool
752
- 5. keep retired edge runtimes alive while in-flight HTTP and websocket traffic drains
250
+ server:
251
+ hsts: true
252
+ trustedProxies: ['10.0.0.0/8', '127.0.0.1']
253
+ timeouts:
254
+ connectMs: 2000
255
+ readMs: 30000
256
+ verify:
257
+ requireHealthyProxies: true
258
+ requireReadyApps: true
259
+ includeUnroutedManagedApps: false
260
+ minHealthyTargetsPerProxy: 1
753
261
 
754
- ### Hot Reloading
262
+ certs:
263
+ trusthealth: '/ssl/trusthealth.com'
264
+ zion: '/ssl/zionlabshare.com'
755
265
 
756
- Two layers of hot reload work together by default:
266
+ proxies:
267
+ api:
268
+ hosts: ['http://127.0.0.1:7201']
269
+ check:
270
+ path: '/health'
271
+ intervalMs: 5000
272
+ timeoutMs: 2000
757
273
 
758
- - **API changes** — The Manager watches `.rip` files per app directory and triggers rolling restarts for the affected app pool only.
759
- - **UI changes** (`watch: true` in `serve` middleware) — Workers register their component directories with the Manager via the control socket. The Manager watches those directories and broadcasts SSE reload events to connected browsers (client-side).
760
- - **Edge config changes** — `Edgefile.rip` changes use the staged reload path with verification, rollback, and bounded reload history.
274
+ redmine:
275
+ hosts: ['http://127.0.0.1:7101']
276
+ check:
277
+ path: '/'
278
+ intervalMs: 5000
279
+ timeoutMs: 2000
761
280
 
762
- SSE connections are held by the long-lived Server process, not by recyclable workers, ensuring stable hot-reload connections. Each app prefix gets its own SSE pool for multi-app isolation.
281
+ incus:
282
+ hosts: ['tcp://127.0.0.1:8443']
763
283
 
764
- Use `--static` in production to disable hot reload entirely.
284
+ apps:
285
+ web:
286
+ entry: './apps/web/index.rip'
287
+ workers: 4
765
288
 
766
- ### Worker Lifecycle
289
+ admin:
290
+ entry: './apps/admin/index.rip'
291
+ workers: 2
767
292
 
768
- Workers are automatically recycled to prevent memory leaks and ensure reliability:
293
+ rules:
294
+ web: [
295
+ { path: '/api/*', proxy: 'api' }
296
+ { path: '/*', app: 'web' }
297
+ ]
769
298
 
770
- - **maxRequests**: Restart worker after N requests (default: 10,000)
771
- - **maxSeconds**: Restart worker after N seconds (default: 3,600)
299
+ admin: [
300
+ { path: '/api/*', proxy: 'api' }
301
+ { path: '/*', app: 'admin' }
302
+ ]
772
303
 
773
- ## Built-in Endpoints
304
+ redmine: [
305
+ { path: '/*', proxy: 'redmine' }
306
+ ]
774
307
 
775
- The server provides these endpoints automatically:
308
+ groups:
309
+ trustSites: ['trusthealth.com', 'www.trusthealth.com']
310
+ zionSites: ['zionlabshare.com', 'www.zionlabshare.com']
776
311
 
777
- | Endpoint | Description |
778
- |----------|-------------|
779
- | `/status` | Health check with worker count and uptime |
780
- | `/diagnostics` | Full runtime telemetry (metrics, latency, queue pressure) |
781
- | `/server` | Simple "ok" response for load balancer probes |
782
- | `/publish` | External WebSocket broadcast (when `--realtime` enabled) |
783
- | `/realtime` | WebSocket upgrade endpoint (when `--realtime` enabled) |
312
+ hosts:
313
+ trustSites:
314
+ hosts: 'trustSites'
315
+ cert: 'trusthealth'
316
+ rules: 'web'
784
317
 
785
- ## TLS Certificates
318
+ zionSites:
319
+ hosts: 'zionSites'
320
+ cert: 'zion'
321
+ rules: 'web'
786
322
 
787
- ### Shipped Wildcard Cert (`*.ripdev.io`)
323
+ 'admin.trusthealth.com':
324
+ cert: 'trusthealth'
325
+ rules: 'admin'
788
326
 
789
- The server ships with a GlobalSign wildcard certificate for `*.ripdev.io`. Combined with DNS (`*.ripdev.io → 127.0.0.1`), every app gets trusted HTTPS automatically:
327
+ 'projects.trusthealth.com':
328
+ cert: 'trusthealth'
329
+ rules: 'redmine'
790
330
 
791
- ```bash
792
- rip server streamline # → https://streamline.ripdev.io (green lock)
793
- rip server analytics # → https://analytics.ripdev.io (green lock)
794
- rip server myapp # → https://myapp.ripdev.io (green lock)
331
+ 'incus.trusthealth.com':
332
+ proxy: 'incus'
795
333
  ```
796
334
 
797
- No setup, no flags, no certificate generation. The app name becomes the subdomain.
798
-
799
- ### Custom Certificates
335
+ ## Operator Runbook
800
336
 
801
- For production domains or custom setups, provide your own cert/key:
337
+ ### Start with an explicit config file
802
338
 
803
339
  ```bash
804
- rip server --cert=/path/to/cert.pem --key=/path/to/key.pem
340
+ rip server --file=./serve.rip
805
341
  ```
806
342
 
807
- ### ACME Auto-TLS (Let's Encrypt)
808
-
809
- Automatic TLS certificate management via Let's Encrypt HTTP-01 challenges.
810
- Zero dependencies — uses `node:crypto` for all cryptographic operations.
343
+ ### Validate config without serving
811
344
 
812
345
  ```bash
813
- # Production Let's Encrypt
814
- rip server --acme --acme-domain=example.com
815
-
816
- # Staging CA (for testing, no rate limits)
817
- rip server --acme-staging --acme-domain=test.example.com
818
- ```
819
-
820
- How it works:
821
-
822
- 1. Edge server generates an EC P-256 account key and registers with Let's Encrypt
823
- 2. Creates a certificate order for your domain
824
- 3. Serves HTTP-01 challenge tokens on port 80 at `/.well-known/acme-challenge/`
825
- 4. Finalizes the order with a CSR and downloads the certificate chain
826
- 5. Stores cert and key at `~/.rip/certs/{domain}/`
827
- 6. Starts a renewal loop (checks every 12 hours, renews 30 days before expiry)
828
-
829
- The ACME crypto stack has been validated against the real Let's Encrypt staging
830
- server — account creation, JWS signing, and nonce management all confirmed working.
831
-
832
- ## Realtime WebSocket (Bam-style)
833
-
834
- Built-in WebSocket pub/sub where **your backend stays HTTP-only**. The edge
835
- server manages all WebSocket connections, group membership, and message routing.
836
- Your app just responds to HTTP POSTs with JSON instructions.
837
-
838
- Realtime is always on — no flags needed. WebSocket connections are accepted
839
- at `/realtime` by default. Customize the path with `--realtime-path=/ws`.
840
-
841
- ### How it works
842
-
843
- 1. Client connects via WebSocket to `/realtime` (configurable)
844
- 2. Edge forwards the event to a worker as a POST to `/v1/realtime` with `Sec-WebSocket-Frame: open` — using the same worker pool and scheduler as regular HTTP requests
845
- 3. Your handler responds with JSON: `{ "+": ["room1"], "@": ["user1"], "welcome": "hello" }`
846
- 4. Edge updates group membership and delivers messages to targets
847
-
848
- ### Protocol
849
-
850
- Backend JSON response keys:
851
-
852
- | Key | Meaning |
853
- |-----|---------|
854
- | `@` | Target groups — who receives the message |
855
- | `+` | Subscribe — add client to these groups |
856
- | `-` | Unsubscribe — remove client from these groups |
857
- | `>` | Senders — exclude these clients from delivery |
858
- | Any other | Event payload — delivered to all targets |
859
-
860
- Groups starting with `/` are channel groups (members receive messages).
861
- Other groups are direct client targets.
862
-
863
- ### Example backend handler
864
-
865
- ```coffee
866
- post '/v1/realtime' ->
867
- frame = @req.header 'Sec-WebSocket-Frame'
868
-
869
- if frame is 'open'
870
- user = authenticate!(@req)
871
- return { '+': ["/lobby", "/user-#{user.id}"], 'connected': { userId: user.id } }
872
-
873
- if frame is 'text'
874
- data = @req.json!
875
- return { '@': ["/lobby"], 'chat': { from: data.from, text: data.text } }
876
-
877
- { ok: true }
878
- ```
879
-
880
- ### External publish
881
-
882
- Server-side code can broadcast messages without a WebSocket connection:
883
-
884
- ```bash
885
- curl -X POST http://localhost/publish \
886
- -d '{"@": ["/lobby"], "announcement": "Server restarting in 5 minutes"}'
346
+ rip server --check-config
347
+ rip server --check-config --file=./serve.rip
887
348
  ```
888
349
 
889
- **Production security:** Set a publish secret to require authentication:
350
+ ### Reload config safely
890
351
 
891
352
  ```bash
892
- # Via flag
893
- rip server --publish-secret=my-secret-token
894
-
895
- # Via environment variable
896
- RIP_PUBLISH_SECRET=my-secret-token rip server
353
+ kill -HUP "$(cat /tmp/rip_myapp.pid)"
897
354
  ```
898
355
 
899
- When set, `/publish` requires `Authorization: Bearer my-secret-token`. Without a
900
- secret, `/publish` is open to any client that can reach a valid host — fine for
901
- development, but always set a secret in production.
902
-
903
- ## Diagnostics & Observability
904
-
905
- ### `/status` — Health check
356
+ or:
906
357
 
907
358
  ```bash
908
- curl http://localhost/status
909
- # {"status":"healthy","app":"myapp","workers":4,"uptime":86400,"hosts":["localhost"]}
359
+ curl --unix-socket /tmp/rip_myapp.ctl.sock -X POST http://localhost/reload
910
360
  ```
911
361
 
912
- ### `/diagnostics` — Full runtime telemetry
362
+ ### Diagnostics
913
363
 
914
364
  ```bash
915
365
  curl http://localhost/diagnostics
916
366
  ```
917
367
 
918
- Returns:
919
-
920
- ```json
921
- {
922
- "status": "healthy",
923
- "version": { "server": "1.0.0", "rip": "3.13.108" },
924
- "uptime": 86400,
925
- "apps": [{ "id": "myapp", "workers": 4, "inflight": 2, "queueDepth": 0 }],
926
- "upstreams": [
927
- { "id": "app", "targets": 2, "healthyTargets": 2, "unhealthyTargets": 0 }
928
- ],
929
- "metrics": {
930
- "requests": 150000,
931
- "responses": { "2xx": 148000, "4xx": 1500, "5xx": 500 },
932
- "latency": { "p50": 0.012, "p95": 0.085, "p99": 0.210 },
933
- "queue": { "queued": 3200, "timeouts": 5, "shed": 12 },
934
- "workers": { "restarts": 2 },
935
- "acme": { "renewals": 1, "failures": 0 },
936
- "websocket": { "connections": 45, "messages": 12000, "deliveries": 89000 }
937
- },
938
- "gauges": {
939
- "workersActive": 4,
940
- "inflight": 2,
941
- "queueDepth": 0,
942
- "upstreamTargetsHealthy": 2,
943
- "upstreamTargetsUnhealthy": 0
944
- },
945
- "realtime": { "clients": 23, "groups": 8, "deliveries": 89000, "messages": 12000 },
946
- "hosts": ["localhost", "myapp.ripdev.io"],
947
- "config": {
948
- "kind": "edge",
949
- "path": "/srv/Edgefile.rip",
950
- "version": 1,
951
- "counts": { "apps": 2, "upstreams": 1, "routes": 4, "sites": 1 },
952
- "lastResult": "applied",
953
- "lastError": null,
954
- "lastErrorCode": null,
955
- "lastErrorDetails": null,
956
- "activeRouteDescriptions": [
957
- "* /api/* proxy:api",
958
- "admin.example.com /* app:admin"
959
- ],
960
- "activeRuntime": { "id": "edge-123", "inflight": 1, "wsConnections": 2 },
961
- "retiredRuntimes": [],
962
- "lastReload": {
963
- "id": "reload-4",
964
- "source": "control_api",
965
- "oldVersion": 1,
966
- "newVersion": 1,
967
- "result": "applied",
968
- "reason": null,
969
- "code": null
970
- },
971
- "reloadHistory": []
972
- }
973
- }
974
- ```
975
-
976
- ### Structured event logging
977
-
978
- With `--json-logging`, lifecycle events are emitted as structured JSON:
979
-
980
- ```json
981
- {"t":"2026-03-14T12:00:00.000Z","event":"server_start","app":"myapp","workers":4}
982
- {"t":"2026-03-14T12:05:00.000Z","event":"worker_restart","workerId":3,"attempt":1}
983
- {"t":"2026-03-14T12:10:00.000Z","event":"ws_open","clientId":"a1b2c3d4"}
984
- ```
985
-
986
- Events: `server_start`, `server_stop`, `worker_restart`, `worker_abandon`, `ws_open`, `ws_close`
987
-
988
- ## mDNS Service Discovery
989
-
990
- The server automatically advertises itself via mDNS (Bonjour/Zeroconf):
991
-
992
- ```bash
993
- # App accessible at myapp.local
994
- rip server myapp
995
-
996
- # Multiple aliases
997
- rip server myapp@api,backend
998
- ```
999
-
1000
- Requires `dns-sd` (available on macOS by default).
1001
-
1002
- ## App Requirements
1003
-
1004
- Your app must provide a fetch handler. Three patterns are supported:
1005
-
1006
- ### Pattern 1: Use `@rip-lang/server` with `start()` (Recommended)
1007
-
1008
- ```coffee
1009
- import { get, start } from '@rip-lang/server'
1010
-
1011
- get '/' -> 'Hello!'
1012
-
1013
- start()
1014
- ```
1015
-
1016
- The `start()` function automatically detects when running under `rip server` and registers the handler.
1017
-
1018
- ### Pattern 2: Export fetch function directly
1019
-
1020
- ```coffee
1021
- export default (req) ->
1022
- new Response('Hello!')
1023
- ```
1024
-
1025
- ### Pattern 3: Export object with fetch method
1026
-
1027
- ```coffee
1028
- export default
1029
- fetch: (req) -> new Response('Hello!')
1030
- ```
1031
-
1032
- ## One-Time Setup
1033
-
1034
- If a `setup.rip` file exists next to your entry file, `rip server` runs it
1035
- automatically **once** before spawning any workers. This is ideal for database
1036
- migrations, table creation, and seeding.
1037
-
1038
- ```coffee
1039
- # setup.rip — runs once before workers start
1040
- export setup = ->
1041
- await createTables()
1042
- await seedData()
1043
- p 'Database ready'
1044
- ```
1045
-
1046
- The setup function can export as `setup` or `default`. If the file doesn't
1047
- exist, the setup phase is skipped entirely (no overhead). If setup fails,
1048
- the server exits immediately.
1049
-
1050
- When `setup.rip` is present, the `rip-server: https://...` URL lines are
1051
- **suppressed at the top** of the output — they will appear later via `[setup]`
1052
- instead, after migrations complete. The actual server URLs are passed to the
1053
- setup process as `process.env.RIP_URLS` (comma-separated), so you can display
1054
- them exactly as rip-server computed them:
1055
-
1056
- ```coffee
1057
- # setup.rip — print URLs at the bottom after setup completes
1058
- export setup = ->
1059
- await runMigrations()
1060
- urls = process.env.RIP_URLS?.split(',') or []
1061
- p "[setup] #{urls.join(' | ')}"
1062
- ```
1063
-
1064
- ## Environment Variables
1065
-
1066
- Most settings are configured via CLI flags, but environment variables provide an alternative for containers, CI/CD, or system-wide defaults.
1067
-
1068
- **Essential:**
1069
-
1070
- | Variable | CLI Equivalent | Default | Description |
1071
- |----------|----------------|---------|-------------|
1072
- | `NODE_ENV` | `--env=` | `development` | Environment mode (`development` or `production`) |
1073
- | `RIP_DEBUG` | `--debug` | — | Enable debug logging |
1074
- | `RIP_STATIC` | `--static` | `0` | Set to `1` to disable hot reload |
1075
-
1076
- **Advanced (rarely needed):**
1077
-
1078
- | Variable | CLI Equivalent | Default | Description |
1079
- |----------|----------------|---------|-------------|
1080
- | `RIP_MAX_REQUESTS` | `r:N,...` | `10000` | Max requests before worker recycle |
1081
- | `RIP_MAX_SECONDS` | `r:...,Ns` | `3600` | Max seconds before worker recycle |
1082
- | `RIP_MAX_QUEUE` | `--max-queue=` | `512` | Request queue limit |
1083
- | `RIP_QUEUE_TIMEOUT_MS` | `--queue-timeout-ms=` | `30000` | Queue wait timeout (ms) |
1084
- | `RIP_CONNECT_TIMEOUT_MS` | `--connect-timeout-ms=` | `2000` | Reserved for future use |
1085
- | `RIP_READ_TIMEOUT_MS` | `--read-timeout-ms=` | `30000` | Worker read timeout (ms) |
1086
-
1087
- ## Dashboard
1088
-
1089
- The server includes a built-in dashboard accessible at `http://rip.local/` (when mDNS is active). This is a **meta-UI for the server itself**, not your application.
1090
-
1091
- **Dashboard Features:**
1092
-
1093
- - **Server Status** — Health status and uptime
1094
- - **Worker Overview** — Active worker count
1095
- - **Registered Hosts** — All mDNS aliases being advertised
1096
- - **Server Ports** — HTTP/HTTPS port configuration
1097
-
1098
- The dashboard uses the same mDNS infrastructure as your app, so it's always available at `rip.local` when any `rip server` instance is running.
368
+ The `config` section reports:
1099
369
 
1100
- ## Troubleshooting
370
+ - active `serve.rip` path
371
+ - version
372
+ - counts for apps, proxies, hosts, routes, and streams
373
+ - active route descriptions
374
+ - reload history and rollback details
1101
375
 
1102
- **Port 80/443 requires sudo**: Use `http:3000` or another high port, or run with sudo.
1103
-
1104
- **mDNS not working**: Ensure `dns-sd` is available (built into macOS). On Linux, install Avahi.
1105
-
1106
- **Workers keep restarting**: Use `--debug` (or `RIP_DEBUG=1`) to see import errors in your app.
1107
-
1108
- **Changes not triggering reload**: Ensure you're not using `--static`. Check that the file matches the watch pattern (default: `*.rip`).
1109
-
1110
- ## Serving Rip UI Apps
1111
-
1112
- Rip Server works seamlessly with the `serve` middleware for serving
1113
- reactive web applications with hot reload. The `serve` middleware handles
1114
- framework files, page manifests, and SSE hot-reload — `rip server` adds HTTPS,
1115
- mDNS, multi-worker load balancing, and rolling restarts on top.
1116
-
1117
- ### Example: Rip UI App
1118
-
1119
- Create `index.rip`:
1120
-
1121
- ```coffee
1122
- import { get, use, start, notFound } from '@rip-lang/server'
1123
- import { serve } from '@rip-lang/server/middleware'
1124
-
1125
- dir = import.meta.dir
1126
-
1127
- use serve dir: dir, title: 'My App', watch: true
1128
-
1129
- get '/css/*' -> @send "#{dir}/css/#{@req.path.slice(5)}"
1130
-
1131
- notFound -> @send "#{dir}/index.html", 'text/html; charset=UTF-8'
1132
-
1133
- start()
1134
- ```
1135
-
1136
- Run it:
1137
-
1138
- ```bash
1139
- rip server
1140
- ```
376
+ ## Realtime
1141
377
 
1142
- This gives you:
1143
-
1144
- - **Framework bundle** served at `/rip/rip.min.js`
1145
- - **App bundle** auto-generated at `/{app}/bundle`
1146
- - **Hot reload** via SSE at `/{app}/watch` — save a `.rip` file and the browser
1147
- updates instantly
1148
- - **HTTPS + mDNS** — access at `https://myapp.local`
1149
- - **Multi-worker** — load balanced across CPU cores
1150
- - **Rolling restarts** — zero-downtime file-watch reloading
1151
-
1152
- See [Hot Reloading](#hot-reloading) for details on how the two layers (API + UI) work together.
1153
-
1154
- ## Multi-App Configuration (`config.rip`)
1155
-
1156
- If a `config.rip` file exists next to your entry file, the server loads it
1157
- and registers additional apps with their own hosts and worker pools.
1158
-
1159
- ```coffee
1160
- # config.rip
1161
- export default
1162
- apps:
1163
- main:
1164
- entry: './index.rip'
1165
- hosts: ['example.com', 'www.example.com']
1166
- workers: 4
1167
- api:
1168
- entry: './api/index.rip'
1169
- hosts: ['api.example.com']
1170
- workers: 2
1171
- maxQueue: 1024
1172
- admin:
1173
- entry: './admin/index.rip'
1174
- hosts: ['admin.example.com']
1175
- workers: 1
1176
- ```
1177
-
1178
- Each app gets its own worker pool, queue, and host routing. The edge server
1179
- routes requests to the correct app based on the `Host` header.
1180
-
1181
- If no `config.rip` exists, the server runs in single-app mode as usual.
1182
-
1183
- ## Reverse Proxy
1184
-
1185
- Forward requests to external HTTP upstreams with proper header handling:
1186
-
1187
- ```coffee
1188
- import { get } from '@rip-lang/server'
1189
- import { proxyToUpstream } from '@rip-lang/server/edge/forwarding.rip'
1190
-
1191
- get '/api/*' -> proxyToUpstream!(@req.raw, 'http://backend:8080', { clientIp: @req.header('x-real-ip') or '127.0.0.1' })
1192
- ```
1193
-
1194
- Features:
1195
- - Strips hop-by-hop headers (Connection, Transfer-Encoding, etc.)
1196
- - Sets `X-Forwarded-For` to actual client IP (overwrites, never appends — prevents spoofing)
1197
- - Adds `X-Forwarded-Proto` and `X-Forwarded-Host`
1198
- - Streaming response passthrough
1199
- - Timeout with 504, connect failure with 502
1200
- - Manual redirect handling (no auto-follow)
1201
-
1202
- Pass `clientIp` in options so upstream services see the real client address.
1203
- Pass `timeoutMs` to override the default 30-second upstream timeout.
1204
-
1205
- ## Request ID Tracing
1206
-
1207
- Every request gets a unique `X-Request-Id` header for end-to-end correlation.
1208
- If the client sends an `X-Request-Id`, it's preserved; otherwise one is generated.
1209
-
1210
- ```bash
1211
- curl -v http://localhost/users/42
1212
- # < X-Request-Id: req-a8f3b2c1d4e5
1213
- ```
1214
-
1215
- ## Rate Limiting
1216
-
1217
- Per-IP sliding window rate limiting, disabled by default:
1218
-
1219
- ```bash
1220
- rip server --rate-limit=100 # 100 requests per minute per IP
1221
- rip server --rate-limit=1000 --rate-limit-window=3600000 # 1000 per hour
1222
- ```
1223
-
1224
- Returns `429 Too Many Requests` with `Retry-After` header when exceeded.
378
+ Rip Server includes built-in WebSocket pub/sub while your backend stays
379
+ HTTP-oriented.
1225
380
 
1226
381
  ## Security
1227
382
 
1228
- Built-in request smuggling defenses (always on):
1229
-
1230
- - Rejects conflicting `Content-Length` + `Transfer-Encoding` headers
1231
- - Rejects multiple `Host` headers
1232
- - Rejects null bytes in URLs
1233
- - Rejects oversized URLs (>8KB)
1234
- - Path traversal normalized by URL parser
1235
-
1236
- ## WebSocket Passthrough
1237
-
1238
- For reverse proxying WebSocket connections to external upstreams:
1239
-
1240
- ```coffee
1241
- import { createWsPassthrough } from '@rip-lang/server/edge/forwarding.rip'
1242
-
1243
- # In a WebSocket handler, tunnel to an external upstream
1244
- upstream = createWsPassthrough(clientWs, 'ws://backend:8080/ws')
1245
- ```
383
+ Built-in protections include:
1246
384
 
1247
- Frames flow bidirectionally; close and error propagate between both ends.
385
+ - conflicting `Content-Length` + `Transfer-Encoding` rejection
386
+ - multiple `Host` header rejection
387
+ - oversized URL rejection
388
+ - null-byte URL rejection
389
+ - path traversal protection for static serving
1248
390
 
1249
391
  ## Roadmap
1250
392
 
1251
- - [ ] Prometheus / OpenTelemetry metrics export
1252
- - [ ] Inline edge handlers (run small handlers without a worker process)
1253
- - [ ] HTTP response caching at the edge
393
+ - Prometheus / OpenTelemetry metrics export
394
+ - Inline edge handlers
395
+ - HTTP response caching at the edge
1254
396
 
1255
397
  ## License
1256
398
 
1257
399
  MIT
1258
-
1259
- ## Links
1260
-
1261
- - [Rip Language](https://github.com/shreeve/rip-lang) — Compiler + reactive UI framework
1262
- - [Report Issues](https://github.com/shreeve/rip-lang/issues)