@rip-lang/server 1.3.115 → 1.3.117
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +435 -622
- package/api.rip +4 -4
- package/control/cli.rip +221 -1
- package/control/control.rip +9 -0
- package/control/lifecycle.rip +6 -1
- package/control/watchers.rip +10 -0
- package/control/workers.rip +9 -5
- package/default.rip +3 -1
- package/docs/READ_VALIDATORS.md +656 -0
- package/docs/edge/CONFIG_LIFECYCLE.md +70 -32
- package/docs/edge/CONTRACTS.md +60 -69
- package/docs/edge/EDGEFILE_CONTRACT.md +258 -29
- package/docs/edge/M0B_REVIEW_NOTES.md +5 -5
- package/edge/config.rip +584 -52
- package/edge/forwarding.rip +6 -2
- package/edge/metrics.rip +19 -1
- package/edge/registry.rip +29 -3
- package/edge/router.rip +138 -0
- package/edge/runtime.rip +98 -0
- package/edge/static.rip +69 -0
- package/edge/tls.rip +23 -0
- package/edge/upstream.rip +272 -0
- package/edge/verify.rip +73 -0
- package/middleware.rip +3 -3
- package/package.json +2 -2
- package/server.rip +775 -393
- package/tests/control.rip +18 -0
- package/tests/edgefile.rip +165 -0
- package/tests/metrics.rip +16 -0
- package/tests/proxy.rip +22 -1
- package/tests/registry.rip +27 -0
- package/tests/router.rip +101 -0
- package/tests/runtime_entrypoints.rip +16 -0
- package/tests/servers.rip +262 -0
- package/tests/static.rip +64 -0
- package/tests/streams_clienthello.rip +108 -0
- package/tests/streams_index.rip +53 -0
- package/tests/streams_pipe.rip +70 -0
- package/tests/streams_router.rip +39 -0
- package/tests/streams_runtime.rip +38 -0
- package/tests/streams_upstream.rip +34 -0
- package/tests/upstream.rip +191 -0
- package/tests/verify.rip +148 -0
- package/tests/watchers.rip +15 -0
package/README.md
CHANGED
|
@@ -2,42 +2,116 @@
|
|
|
2
2
|
|
|
3
3
|
# Rip Server - @rip-lang/server
|
|
4
4
|
|
|
5
|
-
> **
|
|
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**
|
|
6
6
|
|
|
7
|
-
Rip Server
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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.
|
|
13
|
+
|
|
14
|
+
Written entirely in Rip. Runs on Bun.
|
|
14
15
|
|
|
15
16
|
## Features
|
|
16
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
|
|
17
26
|
- **Multi-worker architecture** — Automatic worker spawning based on CPU cores
|
|
18
27
|
- **Hot module reloading** — Watches `*.rip` files by default, rolling restarts on change
|
|
19
|
-
- **Rolling restarts** — Zero-downtime deployments
|
|
28
|
+
- **Rolling restarts** — Zero-downtime deployments with per-app worker pools
|
|
20
29
|
- **Automatic HTTPS** — Shipped `*.ripdev.io` wildcard cert, or auto-TLS via Let's Encrypt ACME
|
|
21
|
-
- **
|
|
22
|
-
- **Observability** — `/diagnostics` endpoint with request rates, latency percentiles, queue pressure
|
|
23
|
-
- **Appliance-grade reliability** — Graceful shutdown, restart resilience, forced-exit safety net
|
|
30
|
+
- **Observability** — `/diagnostics` with request rates, latency percentiles, queue pressure
|
|
24
31
|
- **mDNS discovery** — `.local` hostname advertisement
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
- **
|
|
28
|
-
- **
|
|
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`
|
|
29
47
|
|
|
30
48
|
| Directory | Role |
|
|
31
49
|
|-----------|------|
|
|
32
50
|
| `api.rip` | Core framework: routing, validation, `read()`, `session`, `@send` |
|
|
33
51
|
| `middleware.rip` | Built-in middleware: cors, logger, sessions, compression, security, serve |
|
|
34
52
|
| `server.rip` | Edge orchestrator: CLI, workers, load balancing, TLS, mDNS |
|
|
35
|
-
| `edge/` | Request path:
|
|
53
|
+
| `edge/` | Request path and edge runtime: config, forwarding, metrics, registry, router, runtime, TLS, upstreams, verification |
|
|
36
54
|
| `control/` | Management: CLI, lifecycle, workers, watchers, mDNS, events |
|
|
55
|
+
| `streams/` | Layer 4 stream routing: ClientHello parsing, SNI routing, stream runtimes, raw TCP upstreams |
|
|
37
56
|
| `acme/` | Auto-TLS: ACME client, crypto, cert store, challenge handler |
|
|
38
57
|
|
|
39
58
|
> **See Also**: For the DuckDB server, see [@rip-lang/db](../db/README.md).
|
|
40
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
|
+
```
|
|
76
|
+
|
|
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
|
+
```
|
|
89
|
+
|
|
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.
|
|
114
|
+
|
|
41
115
|
## Quick Start
|
|
42
116
|
|
|
43
117
|
### Installation
|
|
@@ -73,13 +147,13 @@ Create `index.rip`:
|
|
|
73
147
|
```coffee
|
|
74
148
|
import { get, read, start } from '@rip-lang/server'
|
|
75
149
|
|
|
76
|
-
get '/'
|
|
150
|
+
get '/' ->
|
|
77
151
|
'Hello from Rip Server!'
|
|
78
152
|
|
|
79
|
-
get '/json'
|
|
153
|
+
get '/json' ->
|
|
80
154
|
{ message: 'It works!', timestamp: Date.now() }
|
|
81
155
|
|
|
82
|
-
get '/users/:id'
|
|
156
|
+
get '/users/:id' ->
|
|
83
157
|
id = read 'id', 'id!'
|
|
84
158
|
{ user: { id, name: "User #{id}" } }
|
|
85
159
|
|
|
@@ -108,660 +182,371 @@ curl http://localhost/status
|
|
|
108
182
|
# {"status":"healthy","app":"myapp","workers":5,"ports":{"https":443}}
|
|
109
183
|
```
|
|
110
184
|
|
|
111
|
-
##
|
|
112
|
-
|
|
113
|
-
A validation and parsing powerhouse that eliminates 90% of API boilerplate.
|
|
114
|
-
|
|
115
|
-
### Basic Patterns
|
|
116
|
-
|
|
117
|
-
```coffee
|
|
118
|
-
# Required field (throws if missing)
|
|
119
|
-
email = read 'email', 'email!'
|
|
120
|
-
|
|
121
|
-
# Optional field (returns null if missing)
|
|
122
|
-
phone = read 'phone', 'phone'
|
|
123
|
-
|
|
124
|
-
# With default value
|
|
125
|
-
role = read 'role', ['admin', 'user'], 'user'
|
|
126
|
-
|
|
127
|
-
# Get entire payload
|
|
128
|
-
data = read()
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
### Range Validation
|
|
132
|
-
|
|
133
|
-
The `[min, max]` syntax works for both numbers and string lengths:
|
|
134
|
-
|
|
135
|
-
```coffee
|
|
136
|
-
# Numbers: value range
|
|
137
|
-
age = read 'age', 'int', [18, 120] # Between 18 and 120
|
|
138
|
-
priority = read 'priority', 'int', [1, 10] # 1-10 range
|
|
139
|
-
|
|
140
|
-
# Strings: length range
|
|
141
|
-
username = read 'username', 'string', [3, 20] # 3-20 characters
|
|
142
|
-
bio = read 'bio', 'string', [0, 500] # Up to 500 chars
|
|
143
|
-
|
|
144
|
-
# Named parameters
|
|
145
|
-
views = read 'views', 'int', min: 0 # Non-negative integer
|
|
146
|
-
discount = read 'discount', 'float', max: 100 # Up to 100
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
### Enumeration Validation
|
|
150
|
-
|
|
151
|
-
```coffee
|
|
152
|
-
# Must be one of these values
|
|
153
|
-
role = read 'role', ['admin', 'user', 'guest']
|
|
154
|
-
status = read 'status', ['pending', 'active', 'closed']
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
### Regex Validation
|
|
158
|
-
|
|
159
|
-
```coffee
|
|
160
|
-
# Custom pattern matching
|
|
161
|
-
code = read 'code', /^[A-Z]{3,6}$/
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
## Built-in Validators
|
|
165
|
-
|
|
166
|
-
`@rip-lang/server` includes 37 validators for every common API need:
|
|
167
|
-
|
|
168
|
-
### Numbers & Money
|
|
169
|
-
```coffee
|
|
170
|
-
id = read 'user_id', 'id!' # Positive integer (1+)
|
|
171
|
-
count = read 'count', 'whole' # Non-negative integer (0+)
|
|
172
|
-
price = read 'price', 'float' # Decimal number
|
|
173
|
-
cost = read 'cost', 'money' # Banker's rounding to cents
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
### Text Processing
|
|
177
|
-
```coffee
|
|
178
|
-
title = read 'title', 'string' # Collapses whitespace
|
|
179
|
-
bio = read 'bio', 'text' # Light cleanup
|
|
180
|
-
name = read 'name', 'name' # Trims and normalizes
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
### Contact Information
|
|
184
|
-
```coffee
|
|
185
|
-
email = read 'email', 'email' # Valid email format
|
|
186
|
-
phone = read 'phone', 'phone' # US phone → (555) 123-4567
|
|
187
|
-
address = read 'address', 'address' # Trimmed address
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
### Geographic Data
|
|
191
|
-
```coffee
|
|
192
|
-
state = read 'state', 'state' # Two-letter → uppercase
|
|
193
|
-
zip = read 'zip', 'zip' # 5-digit zip
|
|
194
|
-
zipplus4 = read 'zip', 'zipplus4' # 12345-6789 format
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
### Identity & Security
|
|
198
|
-
```coffee
|
|
199
|
-
ssn = read 'ssn', 'ssn' # SSN → digits only
|
|
200
|
-
sex = read 'gender', 'sex' # m/f/o
|
|
201
|
-
username = read 'username', 'username' # 3-20 chars, lowercase
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
### Web & Technical
|
|
205
|
-
```coffee
|
|
206
|
-
url = read 'website', 'url' # Valid URL
|
|
207
|
-
ip = read 'ip_address', 'ip' # IPv4 address
|
|
208
|
-
mac = read 'mac', 'mac' # MAC address
|
|
209
|
-
color = read 'color', 'color' # Hex color → #abc123
|
|
210
|
-
uuid = read 'user_id', 'uuid' # UUID format
|
|
211
|
-
semver = read 'version', 'semver' # Semantic version
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
### Time & Date
|
|
215
|
-
```coffee
|
|
216
|
-
date = read 'date', 'date' # YYYY-MM-DD
|
|
217
|
-
time = read 'time', 'time' # HH:MM or HH:MM:SS (24-hour)
|
|
218
|
-
time12 = read 'time', 'time12' # 12-hour with am/pm
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
### Boolean & Collections
|
|
222
|
-
```coffee
|
|
223
|
-
active = read 'active', 'truthy' # true/t/1/yes/y/on → true
|
|
224
|
-
inactive = read 'off', 'falsy' # false/f/0/no/n/off → true
|
|
225
|
-
flag = read 'flag', 'bool' # Either → boolean
|
|
226
|
-
tags = read 'tags', 'array' # Must be array
|
|
227
|
-
config = read 'config', 'hash' # Must be object
|
|
228
|
-
settings = read 'data', 'json' # Parse JSON string
|
|
229
|
-
ids = read 'ids', 'ids' # "1,2,3" → [1, 2, 3]
|
|
230
|
-
slug = read 'slug', 'slug' # URL-safe slug
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
### Custom Validators
|
|
234
|
-
|
|
235
|
-
```coffee
|
|
236
|
-
import { registerValidator, read } from '@rip-lang/server'
|
|
237
|
-
|
|
238
|
-
registerValidator 'postalCode', (v) ->
|
|
239
|
-
if v =~ /^[A-Z]\d[A-Z] \d[A-Z]\d$/i
|
|
240
|
-
_[0].toUpperCase()
|
|
241
|
-
else
|
|
242
|
-
null
|
|
243
|
-
|
|
244
|
-
# Now use it
|
|
245
|
-
code = read 'postal', 'postalCode!'
|
|
246
|
-
```
|
|
185
|
+
## Edgefile Quick Start
|
|
247
186
|
|
|
248
|
-
|
|
187
|
+
`Edgefile.rip` is the declarative edge config for the Bun-native edge runtime.
|
|
188
|
+
It gives you:
|
|
249
189
|
|
|
250
|
-
|
|
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`
|
|
251
197
|
|
|
252
|
-
|
|
253
|
-
import { get, post, put, patch, del, all } from '@rip-lang/server'
|
|
254
|
-
|
|
255
|
-
get '/users' -> listUsers!
|
|
256
|
-
post '/users' -> createUser!
|
|
257
|
-
get '/users/:id' -> getUser!
|
|
258
|
-
put '/users/:id' -> updateUser!
|
|
259
|
-
patch '/users/:id' -> patchUser!
|
|
260
|
-
del '/users/:id' -> deleteUser!
|
|
261
|
-
all '/health' -> 'ok' # All methods
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
### Path Parameters
|
|
198
|
+
### Canonical Shape
|
|
265
199
|
|
|
266
200
|
```coffee
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
{
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
name = read 'name'
|
|
281
|
-
{ file: name }
|
|
282
|
-
|
|
283
|
-
# Wildcards
|
|
284
|
-
get '/static/*', (env) ->
|
|
285
|
-
{ path: env.req.path }
|
|
201
|
+
export default
|
|
202
|
+
version: 1
|
|
203
|
+
edge:
|
|
204
|
+
hsts: true
|
|
205
|
+
timeouts:
|
|
206
|
+
connectMs: 2000
|
|
207
|
+
readMs: 30000
|
|
208
|
+
upstreams: {}
|
|
209
|
+
streamUpstreams: {}
|
|
210
|
+
apps: {}
|
|
211
|
+
routes: []
|
|
212
|
+
streams: []
|
|
213
|
+
sites: {}
|
|
286
214
|
```
|
|
287
215
|
|
|
288
|
-
###
|
|
216
|
+
### Edgefile Field Reference
|
|
289
217
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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. |
|
|
296
228
|
|
|
297
|
-
|
|
298
|
-
get '/users' -> listUsersV2!
|
|
299
|
-
```
|
|
229
|
+
### Route Shape
|
|
300
230
|
|
|
301
|
-
|
|
231
|
+
Common route fields:
|
|
302
232
|
|
|
303
|
-
|
|
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 }`
|
|
304
238
|
|
|
305
|
-
|
|
239
|
+
Each route must define exactly one action:
|
|
306
240
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
use logger()
|
|
313
|
-
use logger format: 'tiny' # Minimal output
|
|
314
|
-
use logger format: 'dev' # Colorized (default)
|
|
315
|
-
use logger skip: (c) -> c.req.path is '/health'
|
|
316
|
-
|
|
317
|
-
# CORS
|
|
318
|
-
use cors() # Allow all origins
|
|
319
|
-
use cors origin: 'https://myapp.com' # Specific origin
|
|
320
|
-
use cors origin: ['https://a.com', 'https://b.com']
|
|
321
|
-
use cors credentials: true, maxAge: 86400
|
|
322
|
-
|
|
323
|
-
# Compression (gzip/deflate)
|
|
324
|
-
use compress()
|
|
325
|
-
use compress threshold: 1024 # Min bytes to compress
|
|
326
|
-
|
|
327
|
-
# Security headers
|
|
328
|
-
use secureHeaders()
|
|
329
|
-
use secureHeaders hsts: true, contentSecurityPolicy: "default-src 'self'"
|
|
330
|
-
|
|
331
|
-
# Request limits
|
|
332
|
-
use timeout ms: 30000 # 30 second timeout
|
|
333
|
-
use bodyLimit maxSize: 1024 * 1024 # 1MB max body
|
|
334
|
-
```
|
|
241
|
+
- `upstream: 'name'`
|
|
242
|
+
- `app: 'name'`
|
|
243
|
+
- `static: '/dir'`
|
|
244
|
+
- `redirect: { to: '...', status: 301 }`
|
|
245
|
+
- `headers: { set, remove }`
|
|
335
246
|
|
|
336
|
-
|
|
247
|
+
WebSocket proxy routes use:
|
|
337
248
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
| `logger()` | `format`, `skip`, `stream` |
|
|
341
|
-
| `cors()` | `origin`, `methods`, `headers`, `credentials`, `maxAge`, `exposeHeaders`, `preflight` |
|
|
342
|
-
| `compress()` | `threshold`, `encodings` |
|
|
343
|
-
| `sessions()` | `secret`, `name`, `maxAge`, `secure`, `httpOnly`, `sameSite` |
|
|
344
|
-
| `secureHeaders()` | `hsts`, `hstsMaxAge`, `contentSecurityPolicy`, `frameOptions`, `referrerPolicy` |
|
|
345
|
-
| `timeout()` | `ms`, `message`, `status` |
|
|
346
|
-
| `bodyLimit()` | `maxSize`, `message` |
|
|
249
|
+
- `websocket: true`
|
|
250
|
+
- `upstream: 'name'`
|
|
347
251
|
|
|
348
|
-
###
|
|
252
|
+
### Stream Shape
|
|
349
253
|
|
|
350
|
-
|
|
351
|
-
import { get, use, before, session } from '@rip-lang/server'
|
|
352
|
-
import { sessions } from '@rip-lang/server/middleware'
|
|
254
|
+
Common stream fields:
|
|
353
255
|
|
|
354
|
-
|
|
355
|
-
|
|
256
|
+
- `listen: number` — TCP port to bind
|
|
257
|
+
- `sni: string[]` — exact or wildcard SNI patterns
|
|
258
|
+
- `upstream: 'name'`
|
|
259
|
+
- `timeouts?: { handshakeMs, idleMs, connectMs }`
|
|
356
260
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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.
|
|
360
264
|
|
|
361
|
-
|
|
362
|
-
{ userId: session.userId, views: session.views }
|
|
363
|
-
|
|
364
|
-
get '/login' ->
|
|
365
|
-
session.userId = 123
|
|
366
|
-
{ loggedIn: true }
|
|
367
|
-
|
|
368
|
-
get '/logout' ->
|
|
369
|
-
delete session.userId
|
|
370
|
-
{ loggedOut: true }
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
The `session` import works anywhere via AsyncLocalStorage — no `@` needed, works in helpers and nested callbacks.
|
|
374
|
-
|
|
375
|
-
**Security note:** Without `secret`, sessions use plain base64 (dev only). With `secret`, sessions are HMAC-SHA256 signed (tamper-proof). Always set `secret` in production.
|
|
376
|
-
|
|
377
|
-
### CORS with Preflight
|
|
265
|
+
### Example: Pure Proxy Shape
|
|
378
266
|
|
|
379
267
|
```coffee
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
+
]
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Example: TLS Passthrough For Incus
|
|
388
290
|
|
|
389
291
|
```coffee
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
token = @req.header 'Authorization'
|
|
393
|
-
unless token
|
|
394
|
-
return @json { error: 'Unauthorized' }, 401
|
|
395
|
-
@user = validateToken!(token)
|
|
396
|
-
await next()
|
|
397
|
-
|
|
398
|
-
# Timing middleware
|
|
399
|
-
use (c, next) ->
|
|
400
|
-
start = Date.now()
|
|
401
|
-
await next()
|
|
402
|
-
@header 'X-Response-Time', "#{Date.now() - start}ms"
|
|
403
|
-
```
|
|
292
|
+
export default
|
|
293
|
+
version: 1
|
|
404
294
|
|
|
405
|
-
|
|
295
|
+
edge: {}
|
|
406
296
|
|
|
407
|
-
|
|
297
|
+
streamUpstreams:
|
|
298
|
+
incus:
|
|
299
|
+
targets: ['127.0.0.1:8443']
|
|
408
300
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
# Runs first — modify raw request before body parsing
|
|
413
|
-
raw (req) ->
|
|
414
|
-
if req.headers.get('X-Raw-SQL') is 'true'
|
|
415
|
-
req.headers.set 'content-type', 'text/plain'
|
|
416
|
-
|
|
417
|
-
skipPaths = ['/favicon.ico', '/ping', '/health']
|
|
418
|
-
|
|
419
|
-
# Runs before handler (after body parsing)
|
|
420
|
-
before ->
|
|
421
|
-
@start = Date.now()
|
|
422
|
-
@silent = @req.path in skipPaths
|
|
423
|
-
unless @req.header 'Authorization'
|
|
424
|
-
return @json { error: 'Unauthorized' }, 401
|
|
425
|
-
|
|
426
|
-
# Runs after handler
|
|
427
|
-
after ->
|
|
428
|
-
return if @silent
|
|
429
|
-
console.log "#{@req.method} #{@req.path} - #{Date.now() - @start}ms"
|
|
301
|
+
streams: [
|
|
302
|
+
{ listen: 8443, sni: ['incus.example.com'], upstream: 'incus' }
|
|
303
|
+
]
|
|
430
304
|
```
|
|
431
305
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
**How `@` works:** Handlers are called with `this` bound to the context, so `@foo` is `this.foo`. This gives you Sinatra-like magic access to:
|
|
435
|
-
- `@req` — Request object
|
|
436
|
-
- `@json()`, `@text()`, `@html()`, `@redirect()`, `@send()` — Response helpers
|
|
437
|
-
- `@header()` — Response header modifier
|
|
438
|
-
- `@anything` — Custom per-request state
|
|
439
|
-
|
|
440
|
-
**Imports that work anywhere** (via AsyncLocalStorage or Proxy):
|
|
441
|
-
- `read` — Validated request parameters
|
|
442
|
-
- `session` — Session data (if middleware enabled)
|
|
443
|
-
- `env` — `process.env` shortcut (e.g., `env.DATABASE_URL`)
|
|
444
|
-
|
|
445
|
-
## Context Object
|
|
446
|
-
|
|
447
|
-
Use `@` to access the context directly — no parameter needed:
|
|
306
|
+
### Example: Unified Port For Apps + Incus
|
|
448
307
|
|
|
449
|
-
|
|
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.
|
|
450
311
|
|
|
451
312
|
```coffee
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
@json { data: 'value' }, 201 # With status
|
|
456
|
-
@json { data: 'value' }, 200, { 'X-Custom': 'header' }
|
|
457
|
-
|
|
458
|
-
# Text response
|
|
459
|
-
@text 'Hello'
|
|
460
|
-
@text 'Created', 201
|
|
461
|
-
|
|
462
|
-
# HTML response
|
|
463
|
-
@html '<h1>Hello</h1>'
|
|
464
|
-
|
|
465
|
-
# Redirect
|
|
466
|
-
@redirect '/new-location'
|
|
467
|
-
@redirect '/new-location', 301 # Permanent
|
|
313
|
+
export default
|
|
314
|
+
version: 1
|
|
315
|
+
edge: {}
|
|
468
316
|
|
|
469
|
-
|
|
470
|
-
|
|
317
|
+
streamUpstreams:
|
|
318
|
+
incus:
|
|
319
|
+
targets: ['127.0.0.1:8443']
|
|
471
320
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
321
|
+
streams: [
|
|
322
|
+
{ listen: 443, sni: ['incus.example.com'], upstream: 'incus' }
|
|
323
|
+
]
|
|
475
324
|
```
|
|
476
325
|
|
|
477
|
-
|
|
326
|
+
This means:
|
|
478
327
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
# Headers
|
|
486
|
-
auth = @req.header 'Authorization'
|
|
487
|
-
allHeaders = @req.header()
|
|
488
|
-
|
|
489
|
-
# Body (async)
|
|
490
|
-
json = @req.json!
|
|
491
|
-
text = @req.text!
|
|
492
|
-
form = @req.formData!
|
|
493
|
-
parsed = @req.parseBody!
|
|
494
|
-
|
|
495
|
-
# Raw request
|
|
496
|
-
@req.raw # Native Request object
|
|
497
|
-
@req.method # 'GET', 'POST', etc.
|
|
498
|
-
@req.url # Full URL
|
|
499
|
-
@req.path # Path only
|
|
500
|
-
```
|
|
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
|
|
501
333
|
|
|
502
|
-
###
|
|
334
|
+
### Example: Mixed Apps + Upstreams
|
|
503
335
|
|
|
504
336
|
```coffee
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
+
]
|
|
513
356
|
```
|
|
514
357
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
### `@send(path, type?)`
|
|
358
|
+
### Example: Manual Wildcard TLS
|
|
518
359
|
|
|
519
|
-
|
|
520
|
-
|
|
360
|
+
Use manual cert/key paths for wildcard TLS. ACME HTTP-01 cannot issue
|
|
361
|
+
`*.domain` certificates.
|
|
521
362
|
|
|
522
363
|
```coffee
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
+
]
|
|
531
376
|
```
|
|
532
377
|
|
|
533
|
-
|
|
378
|
+
## Host Blocks
|
|
534
379
|
|
|
535
|
-
|
|
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.
|
|
536
382
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
mimeType 'style.css' # 'text/css; charset=UTF-8'
|
|
541
|
-
mimeType 'app.js' # 'application/javascript'
|
|
542
|
-
mimeType 'photo.png' # 'image/png'
|
|
543
|
-
mimeType 'data.xyz' # 'application/octet-stream'
|
|
544
|
-
```
|
|
383
|
+
`hosts` defines per-domain configuration. `version` and `edge` are optional --
|
|
384
|
+
they default to `1` and `{}` respectively.
|
|
545
385
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
### Custom Error Handler
|
|
386
|
+
### Host Blocks Shape
|
|
549
387
|
|
|
550
388
|
```coffee
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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' }
|
|
415
|
+
]
|
|
416
|
+
```
|
|
417
|
+
|
|
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
|
|
430
|
+
|
|
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 |
|
|
440
|
+
|
|
441
|
+
### Static Routes
|
|
442
|
+
|
|
443
|
+
Static routes serve files from disk with auto-detected MIME types:
|
|
559
444
|
|
|
560
445
|
```coffee
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
notFound (c) ->
|
|
564
|
-
c.json { error: 'Not found', path: c.req.path }, 404
|
|
446
|
+
{ path: '/*', static: '.', spa: true }
|
|
447
|
+
{ path: '/assets/*', static: '/mnt/assets' }
|
|
565
448
|
```
|
|
566
449
|
|
|
567
|
-
|
|
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
|
|
568
456
|
|
|
569
|
-
###
|
|
457
|
+
### Redirect Routes
|
|
570
458
|
|
|
571
459
|
```coffee
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
start port: 3000
|
|
575
|
-
start port: 3000, host: '0.0.0.0'
|
|
460
|
+
{ path: '/old/*', redirect: { to: 'https://new.example.com', status: 301 } }
|
|
576
461
|
```
|
|
577
462
|
|
|
578
|
-
|
|
463
|
+
## Operator Runbook
|
|
579
464
|
|
|
580
|
-
|
|
581
|
-
import { startHandler } from '@rip-lang/server'
|
|
465
|
+
### Start With An Explicit Edgefile
|
|
582
466
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
### App Pattern
|
|
587
|
-
|
|
588
|
-
```coffee
|
|
589
|
-
import { App, get, post } from '@rip-lang/server'
|
|
590
|
-
|
|
591
|
-
export default App ->
|
|
592
|
-
get '/', -> 'Hello'
|
|
593
|
-
post '/echo', -> read()
|
|
467
|
+
```bash
|
|
468
|
+
rip server --edgefile=./Edgefile.rip
|
|
594
469
|
```
|
|
595
470
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
### ctx()
|
|
471
|
+
### Validate Config Without Serving
|
|
599
472
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
import { ctx } from '@rip-lang/server'
|
|
604
|
-
|
|
605
|
-
logRequest = ->
|
|
606
|
-
c = ctx()
|
|
607
|
-
console.log "#{c.req.method} #{c.req.path}" if c
|
|
608
|
-
|
|
609
|
-
get '/demo' ->
|
|
610
|
-
logRequest()
|
|
611
|
-
{ ok: true }
|
|
473
|
+
```bash
|
|
474
|
+
rip server --check-config
|
|
475
|
+
rip server --check-config --edgefile=./Edgefile.rip
|
|
612
476
|
```
|
|
613
477
|
|
|
614
|
-
###
|
|
615
|
-
|
|
616
|
-
Reset all global state (routes, middleware, filters). Useful for testing:
|
|
617
|
-
|
|
618
|
-
```coffee
|
|
619
|
-
import { resetGlobals, get, start } from '@rip-lang/server'
|
|
478
|
+
### Reload Config Safely
|
|
620
479
|
|
|
621
|
-
|
|
622
|
-
resetGlobals()
|
|
480
|
+
Send `SIGHUP` to the long-lived server process:
|
|
623
481
|
|
|
624
|
-
|
|
482
|
+
```bash
|
|
483
|
+
kill -HUP "$(cat /tmp/rip_myapp.pid)"
|
|
625
484
|
```
|
|
626
485
|
|
|
627
|
-
|
|
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`.
|
|
628
489
|
|
|
629
|
-
|
|
490
|
+
You can also trigger reload through the control socket:
|
|
630
491
|
|
|
631
|
-
```
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
isBlank null # true
|
|
635
|
-
isBlank undefined # true
|
|
636
|
-
isBlank '' # true
|
|
637
|
-
isBlank ' ' # true
|
|
638
|
-
isBlank [] # true
|
|
639
|
-
isBlank {} # true
|
|
640
|
-
isBlank false # true
|
|
641
|
-
isBlank 'hello' # false
|
|
642
|
-
isBlank [1, 2] # false
|
|
492
|
+
```bash
|
|
493
|
+
curl --unix-socket /tmp/rip_myapp.ctl.sock -X POST http://localhost/reload
|
|
643
494
|
```
|
|
644
495
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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.
|
|
648
499
|
|
|
649
|
-
|
|
650
|
-
import { toName } from '@rip-lang/server'
|
|
500
|
+
### Inspect Active Config And Diagnostics
|
|
651
501
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
toName "o'brien" # "O'Brien"
|
|
655
|
-
toName 'mcdonald' # 'McDonald'
|
|
656
|
-
toName 'los angeles', 'address' # 'Los Angeles'
|
|
502
|
+
```bash
|
|
503
|
+
curl http://localhost/diagnostics
|
|
657
504
|
```
|
|
658
505
|
|
|
659
|
-
|
|
506
|
+
The `config` block reports:
|
|
660
507
|
|
|
661
|
-
|
|
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
|
|
662
521
|
|
|
663
|
-
|
|
664
|
-
import { toPhone } from '@rip-lang/server'
|
|
522
|
+
The top-level diagnostics payload also includes:
|
|
665
523
|
|
|
666
|
-
|
|
667
|
-
toPhone '555-123-4567' # '(555) 123-4567'
|
|
668
|
-
toPhone '555.123.4567 x99' # '(555) 123-4567, ext. 99'
|
|
669
|
-
toPhone '+1 555 123 4567' # '(555) 123-4567'
|
|
670
|
-
```
|
|
671
|
-
|
|
672
|
-
## Migration from Hono
|
|
524
|
+
- `upstreams`: per-upstream target counts and healthy/unhealthy target totals
|
|
673
525
|
|
|
674
|
-
###
|
|
526
|
+
### Verification Policy
|
|
675
527
|
|
|
676
|
-
|
|
677
|
-
import { Hono } from 'hono'
|
|
528
|
+
Use `edge.verify` to tune post-activate verification:
|
|
678
529
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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
|
|
683
534
|
|
|
684
|
-
|
|
685
|
-
```
|
|
535
|
+
## Validation
|
|
686
536
|
|
|
687
|
-
|
|
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.
|
|
688
540
|
|
|
689
541
|
```coffee
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
{ id }
|
|
695
|
-
|
|
696
|
-
export default startHandler()
|
|
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
|
|
697
546
|
```
|
|
698
547
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
| Hono | @rip-lang/server |
|
|
702
|
-
|------|------------------|
|
|
703
|
-
| `app.get(path, handler)` | `get path, handler` |
|
|
704
|
-
| `app.post(path, handler)` | `post path, handler` |
|
|
705
|
-
| `app.use(middleware)` | `use middleware` |
|
|
706
|
-
| `app.basePath(path)` | `prefix path, -> ...` |
|
|
707
|
-
| `c.json(data)` | `@json(data)` or return `{ data }` |
|
|
708
|
-
| `c.req.param('id')` | `@req.param('id')` or `read 'id'` |
|
|
709
|
-
| `c.req.query('q')` | `@req.query('q')` or `read 'q'` |
|
|
710
|
-
|
|
711
|
-
## Real-World Example
|
|
712
|
-
|
|
713
|
-
```coffee
|
|
714
|
-
import { get, post, put, del, use, read, start, before, after, onError } from '@rip-lang/server'
|
|
715
|
-
import { logger } from '@rip-lang/server/middleware'
|
|
716
|
-
|
|
717
|
-
use logger()
|
|
718
|
-
|
|
719
|
-
before ->
|
|
720
|
-
@start = Date.now()
|
|
721
|
-
|
|
722
|
-
after ->
|
|
723
|
-
console.log "#{@req.method} #{@req.path} - #{Date.now() - @start}ms"
|
|
724
|
-
|
|
725
|
-
onError (err) ->
|
|
726
|
-
@json { error: err.message }, err.status or 500
|
|
727
|
-
|
|
728
|
-
get '/', ->
|
|
729
|
-
{ name: 'My API', version: '1.0' }
|
|
730
|
-
|
|
731
|
-
get '/users', ->
|
|
732
|
-
page = read 'page', 'int', [1, 100]
|
|
733
|
-
limit = read 'limit', 'int', [1, 50]
|
|
734
|
-
users = db.listUsers! page or 1, limit or 10
|
|
735
|
-
{ users, page, limit }
|
|
736
|
-
|
|
737
|
-
get '/users/:id', ->
|
|
738
|
-
id = read 'id', 'id!'
|
|
739
|
-
user = db.getUser!(id)
|
|
740
|
-
unless user
|
|
741
|
-
throw { message: 'User not found', status: 404 }
|
|
742
|
-
{ user }
|
|
743
|
-
|
|
744
|
-
post '/users', ->
|
|
745
|
-
email = read 'email', 'email!'
|
|
746
|
-
name = read 'name', 'string', [1, 100]
|
|
747
|
-
phone = read 'phone', 'phone'
|
|
748
|
-
user = db.createUser! { email, name, phone }
|
|
749
|
-
{ user, created: true }
|
|
750
|
-
|
|
751
|
-
put '/users/:id', ->
|
|
752
|
-
id = read 'id', 'id!'
|
|
753
|
-
email = read 'email', 'email'
|
|
754
|
-
name = read 'name', 'string', [1, 100]
|
|
755
|
-
user = db.updateUser! id, { email, name }
|
|
756
|
-
{ user, updated: true }
|
|
757
|
-
|
|
758
|
-
del '/users/:id', ->
|
|
759
|
-
id = read 'id', 'id!'
|
|
760
|
-
db.deleteUser!(id)
|
|
761
|
-
{ deleted: true }
|
|
762
|
-
|
|
763
|
-
start port: 3000
|
|
764
|
-
```
|
|
548
|
+
See the full [Validation Reference](docs/READ_VALIDATORS.md) for all 37+
|
|
549
|
+
validators, patterns, custom validators, and real-world examples.
|
|
765
550
|
|
|
766
551
|
## App Path & Naming
|
|
767
552
|
|
|
@@ -843,14 +628,18 @@ rip server [flags] [app-path]@<alias1>,<alias2>,...
|
|
|
843
628
|
|------|-------------|---------|
|
|
844
629
|
| `-h`, `--help` | Show help and exit | — |
|
|
845
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 |
|
|
846
633
|
| `--watch=<glob>` | Watch glob pattern | `*.rip` |
|
|
847
634
|
| `--static` | Disable hot reload and file watching | — |
|
|
848
635
|
| `--env=<mode>` | Environment mode (`dev`, `prod`) | `development` |
|
|
849
636
|
| `--debug` | Enable debug logging | Disabled |
|
|
637
|
+
| `--quiet` | Suppress startup URL output | Disabled |
|
|
850
638
|
| `http` | HTTP-only mode (no HTTPS) | HTTPS enabled |
|
|
851
639
|
| `https` | HTTPS mode (explicit) | Auto |
|
|
852
640
|
| `http:<port>` | Set HTTP port | 80, fallback 3000 |
|
|
853
641
|
| `https:<port>` | Set HTTPS port | 443, fallback 3443 |
|
|
642
|
+
| `--socket-prefix=<name>` | Override Unix socket / PID file prefix | `rip_<app-name>` |
|
|
854
643
|
| `w:<n>` | Worker count (`auto`, `half`, `2x`, `3x`, or number) | `half` of cores |
|
|
855
644
|
| `r:<reqs>,<secs>s` | Restart policy: requests, seconds (e.g., `5000,3600s`) | `10000,3600s` |
|
|
856
645
|
| `--cert=<path>` | TLS certificate path | Shipped `*.ripdev.io` cert |
|
|
@@ -902,18 +691,7 @@ rip server --debug
|
|
|
902
691
|
rip server r:5000,3600s
|
|
903
692
|
```
|
|
904
693
|
|
|
905
|
-
##
|
|
906
|
-
|
|
907
|
-
### Edge Planning Docs
|
|
908
|
-
|
|
909
|
-
Current M0 artifacts for the unified edge/app evolution live in:
|
|
910
|
-
|
|
911
|
-
- [Edge contracts](docs/edge/CONTRACTS.md)
|
|
912
|
-
- [Config lifecycle](docs/edge/CONFIG_LIFECYCLE.md)
|
|
913
|
-
- [Scheduler policy](docs/edge/SCHEDULER.md)
|
|
914
|
-
- [Edgefile contract](docs/edge/EDGEFILE_CONTRACT.md)
|
|
915
|
-
- [M0b review notes](docs/edge/M0B_REVIEW_NOTES.md)
|
|
916
|
-
- [TLS spike findings](spikes/tls/FINDINGS.md)
|
|
694
|
+
## Internals
|
|
917
695
|
|
|
918
696
|
### Self-Spawning Design
|
|
919
697
|
|
|
@@ -952,18 +730,34 @@ When `RIP_SETUP_MODE=1` is set, the same file runs the one-time setup phase. Whe
|
|
|
952
730
|
|
|
953
731
|
### Request Flow
|
|
954
732
|
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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:
|
|
747
|
+
|
|
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
|
|
960
753
|
|
|
961
754
|
### Hot Reloading
|
|
962
755
|
|
|
963
756
|
Two layers of hot reload work together by default:
|
|
964
757
|
|
|
965
|
-
- **API changes** — The Manager watches
|
|
758
|
+
- **API changes** — The Manager watches `.rip` files per app directory and triggers rolling restarts for the affected app pool only.
|
|
966
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.
|
|
967
761
|
|
|
968
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.
|
|
969
763
|
|
|
@@ -1129,6 +923,9 @@ Returns:
|
|
|
1129
923
|
"version": { "server": "1.0.0", "rip": "3.13.108" },
|
|
1130
924
|
"uptime": 86400,
|
|
1131
925
|
"apps": [{ "id": "myapp", "workers": 4, "inflight": 2, "queueDepth": 0 }],
|
|
926
|
+
"upstreams": [
|
|
927
|
+
{ "id": "app", "targets": 2, "healthyTargets": 2, "unhealthyTargets": 0 }
|
|
928
|
+
],
|
|
1132
929
|
"metrics": {
|
|
1133
930
|
"requests": 150000,
|
|
1134
931
|
"responses": { "2xx": 148000, "4xx": 1500, "5xx": 500 },
|
|
@@ -1138,9 +935,41 @@ Returns:
|
|
|
1138
935
|
"acme": { "renewals": 1, "failures": 0 },
|
|
1139
936
|
"websocket": { "connections": 45, "messages": 12000, "deliveries": 89000 }
|
|
1140
937
|
},
|
|
1141
|
-
"gauges": {
|
|
938
|
+
"gauges": {
|
|
939
|
+
"workersActive": 4,
|
|
940
|
+
"inflight": 2,
|
|
941
|
+
"queueDepth": 0,
|
|
942
|
+
"upstreamTargetsHealthy": 2,
|
|
943
|
+
"upstreamTargetsUnhealthy": 0
|
|
944
|
+
},
|
|
1142
945
|
"realtime": { "clients": 23, "groups": 8, "deliveries": 89000, "messages": 12000 },
|
|
1143
|
-
"hosts": ["localhost", "myapp.ripdev.io"]
|
|
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
|
+
}
|
|
1144
973
|
}
|
|
1145
974
|
```
|
|
1146
975
|
|
|
@@ -1179,7 +1008,7 @@ Your app must provide a fetch handler. Three patterns are supported:
|
|
|
1179
1008
|
```coffee
|
|
1180
1009
|
import { get, start } from '@rip-lang/server'
|
|
1181
1010
|
|
|
1182
|
-
get '/'
|
|
1011
|
+
get '/' -> 'Hello!'
|
|
1183
1012
|
|
|
1184
1013
|
start()
|
|
1185
1014
|
```
|
|
@@ -1297,7 +1126,7 @@ dir = import.meta.dir
|
|
|
1297
1126
|
|
|
1298
1127
|
use serve dir: dir, title: 'My App', watch: true
|
|
1299
1128
|
|
|
1300
|
-
get '/css/*'
|
|
1129
|
+
get '/css/*' -> @send "#{dir}/css/#{@req.path.slice(5)}"
|
|
1301
1130
|
|
|
1302
1131
|
notFound -> @send "#{dir}/index.html", 'text/html; charset=UTF-8'
|
|
1303
1132
|
|
|
@@ -1322,22 +1151,6 @@ This gives you:
|
|
|
1322
1151
|
|
|
1323
1152
|
See [Hot Reloading](#hot-reloading) for details on how the two layers (API + UI) work together.
|
|
1324
1153
|
|
|
1325
|
-
## Comparison with Other Servers
|
|
1326
|
-
|
|
1327
|
-
| Feature | rip server | PM2 | Nginx | Caddy |
|
|
1328
|
-
|---------|-----------|-----|-------|-------|
|
|
1329
|
-
| Pure Rip | ✅ | ❌ | ❌ | ❌ |
|
|
1330
|
-
| Zero Dependencies | ✅ | ❌ | ❌ | ❌ |
|
|
1331
|
-
| Hot Reload | ✅ (default) | ✅ | ❌ | ❌ |
|
|
1332
|
-
| Multi-Worker | ✅ | ✅ | ✅ | ❌ |
|
|
1333
|
-
| Auto HTTPS (ACME) | ✅ | ❌ | ❌ | ✅ |
|
|
1334
|
-
| Realtime WebSocket | ✅ (Bam-style) | ❌ | ❌ | ❌ |
|
|
1335
|
-
| Runtime Diagnostics | ✅ (latency p50/p95/p99) | ❌ | ❌ | ❌ |
|
|
1336
|
-
| mDNS | ✅ | ❌ | ❌ | ❌ |
|
|
1337
|
-
| Zero Config | ✅ | ❌ | ❌ | ✅ |
|
|
1338
|
-
| Built-in LB | ✅ | ❌ | ✅ | ✅ |
|
|
1339
|
-
| Graceful Shutdown | ✅ | ✅ | ✅ | ✅ |
|
|
1340
|
-
|
|
1341
1154
|
## Multi-App Configuration (`config.rip`)
|
|
1342
1155
|
|
|
1343
1156
|
If a `config.rip` file exists next to your entry file, the server loads it
|
|
@@ -1435,9 +1248,9 @@ Frames flow bidirectionally; close and error propagate between both ends.
|
|
|
1435
1248
|
|
|
1436
1249
|
## Roadmap
|
|
1437
1250
|
|
|
1438
|
-
> *Planned improvements for future releases:*
|
|
1439
|
-
|
|
1440
1251
|
- [ ] Prometheus / OpenTelemetry metrics export
|
|
1252
|
+
- [ ] Inline edge handlers (run small handlers without a worker process)
|
|
1253
|
+
- [ ] HTTP response caching at the edge
|
|
1441
1254
|
|
|
1442
1255
|
## License
|
|
1443
1256
|
|