@rip-lang/server 1.3.97 → 1.3.99
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 +292 -25
- package/docs/edge/CONFIG_LIFECYCLE.md +73 -0
- package/docs/edge/CONTRACTS.md +146 -0
- package/docs/edge/EDGEFILE_CONTRACT.md +53 -0
- package/docs/edge/M0B_REVIEW_NOTES.md +102 -0
- package/docs/edge/SCHEDULER.md +46 -0
- package/middleware.rip +6 -4
- package/package.json +2 -2
- package/server.rip +469 -636
- package/tests/acme.rip +124 -0
- package/tests/helpers.rip +90 -0
- package/tests/metrics.rip +73 -0
- package/tests/proxy.rip +99 -0
- package/tests/{read.test.rip → read.rip} +10 -11
- package/tests/realtime.rip +147 -0
- package/tests/registry.rip +125 -0
- package/tests/security.rip +95 -0
package/README.md
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
# Rip Server - @rip-lang/server
|
|
4
4
|
|
|
5
|
-
> **A full-stack web framework and production server — routing, middleware, multi-worker processes, hot reload,
|
|
5
|
+
> **A full-stack web framework and production edge server — routing, middleware, multi-worker processes, hot reload, ACME auto-TLS, realtime WebSocket, observability, and mDNS — written entirely in Rip with zero dependencies**
|
|
6
6
|
|
|
7
7
|
Rip Server is a unified web framework and application server. It provides
|
|
8
8
|
Sinatra-style routing, built-in validators, file serving, and middleware
|
|
9
9
|
composition for defining your API, plus multi-worker process management,
|
|
10
|
-
rolling restarts, automatic TLS
|
|
10
|
+
rolling restarts, automatic TLS via Let's Encrypt, Bam-style realtime
|
|
11
|
+
WebSocket support, runtime diagnostics, mDNS service discovery, and
|
|
11
12
|
request load balancing for running it in production — all with zero external
|
|
12
13
|
dependencies.
|
|
13
14
|
|
|
@@ -16,18 +17,24 @@ dependencies.
|
|
|
16
17
|
- **Multi-worker architecture** — Automatic worker spawning based on CPU cores
|
|
17
18
|
- **Hot module reloading** — Watches `*.rip` files by default, rolling restarts on change
|
|
18
19
|
- **Rolling restarts** — Zero-downtime deployments
|
|
19
|
-
- **Automatic HTTPS** — Shipped `*.ripdev.io` wildcard cert
|
|
20
|
+
- **Automatic HTTPS** — Shipped `*.ripdev.io` wildcard cert, or auto-TLS via Let's Encrypt ACME
|
|
21
|
+
- **Realtime WebSocket** — Bam-style pub/sub hub where your backend stays HTTP-only
|
|
22
|
+
- **Observability** — `/diagnostics` endpoint with request rates, latency percentiles, queue pressure
|
|
23
|
+
- **Appliance-grade reliability** — Graceful shutdown, restart resilience, forced-exit safety net
|
|
20
24
|
- **mDNS discovery** — `.local` hostname advertisement
|
|
25
|
+
- **Per-app worker pools** — Multi-app registry with host-to-app routing
|
|
21
26
|
- **Request queue** — Built-in request buffering and load balancing
|
|
22
27
|
- **Built-in dashboard** — Server status UI at `rip.local`
|
|
23
28
|
- **Unified package** — Web framework + production server in one
|
|
24
29
|
|
|
25
|
-
|
|
|
26
|
-
|
|
27
|
-
| `api.rip` |
|
|
28
|
-
| `middleware.rip` |
|
|
29
|
-
| `server.rip` |
|
|
30
|
-
| `
|
|
30
|
+
| Directory | Role |
|
|
31
|
+
|-----------|------|
|
|
32
|
+
| `api.rip` | Core framework: routing, validation, `read()`, `session`, `@send` |
|
|
33
|
+
| `middleware.rip` | Built-in middleware: cors, logger, sessions, compression, security, serve |
|
|
34
|
+
| `server.rip` | Edge orchestrator: CLI, workers, load balancing, TLS, mDNS |
|
|
35
|
+
| `edge/` | Request path: forwarding, scheduling, metrics, registry, helpers, TLS, realtime |
|
|
36
|
+
| `control/` | Management: CLI, lifecycle, workers, watchers, mDNS, events |
|
|
37
|
+
| `acme/` | Auto-TLS: ACME client, crypto, cert store, challenge handler |
|
|
31
38
|
|
|
32
39
|
> **See Also**: For the DuckDB server, see [@rip-lang/db](../db/README.md).
|
|
33
40
|
|
|
@@ -852,6 +859,13 @@ rip server [flags] [app-path]@<alias1>,<alias2>,...
|
|
|
852
859
|
| `--no-redirect-http` | Don't redirect HTTP to HTTPS | Redirects enabled |
|
|
853
860
|
| `--json-logging` | Output JSON access logs | Human-readable |
|
|
854
861
|
| `--no-access-log` | Disable access logging | Enabled |
|
|
862
|
+
| `--acme` | Enable auto-TLS via Let's Encrypt | Disabled |
|
|
863
|
+
| `--acme-staging` | Use Let's Encrypt staging CA | Disabled |
|
|
864
|
+
| `--acme-domain=<d>` | Domain for ACME certificate | — |
|
|
865
|
+
| `--realtime-path=<p>` | WebSocket endpoint path | `/realtime` |
|
|
866
|
+
| `--rate-limit=<n>` | Max requests per IP per window | Disabled (0) |
|
|
867
|
+
| `--rate-limit-window=<ms>` | Rate limit window in ms | `60000` (1 min) |
|
|
868
|
+
| `--publish-secret=<s>` | Bearer token for `/publish` endpoint | None (open) |
|
|
855
869
|
|
|
856
870
|
### Subcommands
|
|
857
871
|
|
|
@@ -890,6 +904,17 @@ rip server r:5000,3600s
|
|
|
890
904
|
|
|
891
905
|
## Architecture
|
|
892
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)
|
|
917
|
+
|
|
893
918
|
### Self-Spawning Design
|
|
894
919
|
|
|
895
920
|
The server uses a single-file, self-spawning architecture:
|
|
@@ -958,7 +983,10 @@ The server provides these endpoints automatically:
|
|
|
958
983
|
| Endpoint | Description |
|
|
959
984
|
|----------|-------------|
|
|
960
985
|
| `/status` | Health check with worker count and uptime |
|
|
986
|
+
| `/diagnostics` | Full runtime telemetry (metrics, latency, queue pressure) |
|
|
961
987
|
| `/server` | Simple "ok" response for load balancer probes |
|
|
988
|
+
| `/publish` | External WebSocket broadcast (when `--realtime` enabled) |
|
|
989
|
+
| `/realtime` | WebSocket upgrade endpoint (when `--realtime` enabled) |
|
|
962
990
|
|
|
963
991
|
## TLS Certificates
|
|
964
992
|
|
|
@@ -982,6 +1010,152 @@ For production domains or custom setups, provide your own cert/key:
|
|
|
982
1010
|
rip server --cert=/path/to/cert.pem --key=/path/to/key.pem
|
|
983
1011
|
```
|
|
984
1012
|
|
|
1013
|
+
### ACME Auto-TLS (Let's Encrypt)
|
|
1014
|
+
|
|
1015
|
+
Automatic TLS certificate management via Let's Encrypt HTTP-01 challenges.
|
|
1016
|
+
Zero dependencies — uses `node:crypto` for all cryptographic operations.
|
|
1017
|
+
|
|
1018
|
+
```bash
|
|
1019
|
+
# Production Let's Encrypt
|
|
1020
|
+
rip server --acme --acme-domain=example.com
|
|
1021
|
+
|
|
1022
|
+
# Staging CA (for testing, no rate limits)
|
|
1023
|
+
rip server --acme-staging --acme-domain=test.example.com
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
How it works:
|
|
1027
|
+
|
|
1028
|
+
1. Edge server generates an EC P-256 account key and registers with Let's Encrypt
|
|
1029
|
+
2. Creates a certificate order for your domain
|
|
1030
|
+
3. Serves HTTP-01 challenge tokens on port 80 at `/.well-known/acme-challenge/`
|
|
1031
|
+
4. Finalizes the order with a CSR and downloads the certificate chain
|
|
1032
|
+
5. Stores cert and key at `~/.rip/certs/{domain}/`
|
|
1033
|
+
6. Starts a renewal loop (checks every 12 hours, renews 30 days before expiry)
|
|
1034
|
+
|
|
1035
|
+
The ACME crypto stack has been validated against the real Let's Encrypt staging
|
|
1036
|
+
server — account creation, JWS signing, and nonce management all confirmed working.
|
|
1037
|
+
|
|
1038
|
+
## Realtime WebSocket (Bam-style)
|
|
1039
|
+
|
|
1040
|
+
Built-in WebSocket pub/sub where **your backend stays HTTP-only**. The edge
|
|
1041
|
+
server manages all WebSocket connections, group membership, and message routing.
|
|
1042
|
+
Your app just responds to HTTP POSTs with JSON instructions.
|
|
1043
|
+
|
|
1044
|
+
Realtime is always on — no flags needed. WebSocket connections are accepted
|
|
1045
|
+
at `/realtime` by default. Customize the path with `--realtime-path=/ws`.
|
|
1046
|
+
|
|
1047
|
+
### How it works
|
|
1048
|
+
|
|
1049
|
+
1. Client connects via WebSocket to `/realtime` (configurable)
|
|
1050
|
+
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
|
|
1051
|
+
3. Your handler responds with JSON: `{ "+": ["room1"], "@": ["user1"], "welcome": "hello" }`
|
|
1052
|
+
4. Edge updates group membership and delivers messages to targets
|
|
1053
|
+
|
|
1054
|
+
### Protocol
|
|
1055
|
+
|
|
1056
|
+
Backend JSON response keys:
|
|
1057
|
+
|
|
1058
|
+
| Key | Meaning |
|
|
1059
|
+
|-----|---------|
|
|
1060
|
+
| `@` | Target groups — who receives the message |
|
|
1061
|
+
| `+` | Subscribe — add client to these groups |
|
|
1062
|
+
| `-` | Unsubscribe — remove client from these groups |
|
|
1063
|
+
| `>` | Senders — exclude these clients from delivery |
|
|
1064
|
+
| Any other | Event payload — delivered to all targets |
|
|
1065
|
+
|
|
1066
|
+
Groups starting with `/` are channel groups (members receive messages).
|
|
1067
|
+
Other groups are direct client targets.
|
|
1068
|
+
|
|
1069
|
+
### Example backend handler
|
|
1070
|
+
|
|
1071
|
+
```coffee
|
|
1072
|
+
post '/v1/realtime' ->
|
|
1073
|
+
frame = @req.header 'Sec-WebSocket-Frame'
|
|
1074
|
+
|
|
1075
|
+
if frame is 'open'
|
|
1076
|
+
user = authenticate!(@req)
|
|
1077
|
+
return { '+': ["/lobby", "/user-#{user.id}"], 'connected': { userId: user.id } }
|
|
1078
|
+
|
|
1079
|
+
if frame is 'text'
|
|
1080
|
+
data = @req.json!
|
|
1081
|
+
return { '@': ["/lobby"], 'chat': { from: data.from, text: data.text } }
|
|
1082
|
+
|
|
1083
|
+
{ ok: true }
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
### External publish
|
|
1087
|
+
|
|
1088
|
+
Server-side code can broadcast messages without a WebSocket connection:
|
|
1089
|
+
|
|
1090
|
+
```bash
|
|
1091
|
+
curl -X POST http://localhost/publish \
|
|
1092
|
+
-d '{"@": ["/lobby"], "announcement": "Server restarting in 5 minutes"}'
|
|
1093
|
+
```
|
|
1094
|
+
|
|
1095
|
+
**Production security:** Set a publish secret to require authentication:
|
|
1096
|
+
|
|
1097
|
+
```bash
|
|
1098
|
+
# Via flag
|
|
1099
|
+
rip server --publish-secret=my-secret-token
|
|
1100
|
+
|
|
1101
|
+
# Via environment variable
|
|
1102
|
+
RIP_PUBLISH_SECRET=my-secret-token rip server
|
|
1103
|
+
```
|
|
1104
|
+
|
|
1105
|
+
When set, `/publish` requires `Authorization: Bearer my-secret-token`. Without a
|
|
1106
|
+
secret, `/publish` is open to any client that can reach a valid host — fine for
|
|
1107
|
+
development, but always set a secret in production.
|
|
1108
|
+
|
|
1109
|
+
## Diagnostics & Observability
|
|
1110
|
+
|
|
1111
|
+
### `/status` — Health check
|
|
1112
|
+
|
|
1113
|
+
```bash
|
|
1114
|
+
curl http://localhost/status
|
|
1115
|
+
# {"status":"healthy","app":"myapp","workers":4,"uptime":86400,"hosts":["localhost"]}
|
|
1116
|
+
```
|
|
1117
|
+
|
|
1118
|
+
### `/diagnostics` — Full runtime telemetry
|
|
1119
|
+
|
|
1120
|
+
```bash
|
|
1121
|
+
curl http://localhost/diagnostics
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
Returns:
|
|
1125
|
+
|
|
1126
|
+
```json
|
|
1127
|
+
{
|
|
1128
|
+
"status": "healthy",
|
|
1129
|
+
"version": { "server": "1.0.0", "rip": "3.13.108" },
|
|
1130
|
+
"uptime": 86400,
|
|
1131
|
+
"apps": [{ "id": "myapp", "workers": 4, "inflight": 2, "queueDepth": 0 }],
|
|
1132
|
+
"metrics": {
|
|
1133
|
+
"requests": 150000,
|
|
1134
|
+
"responses": { "2xx": 148000, "4xx": 1500, "5xx": 500 },
|
|
1135
|
+
"latency": { "p50": 0.012, "p95": 0.085, "p99": 0.210 },
|
|
1136
|
+
"queue": { "queued": 3200, "timeouts": 5, "shed": 12 },
|
|
1137
|
+
"workers": { "restarts": 2 },
|
|
1138
|
+
"acme": { "renewals": 1, "failures": 0 },
|
|
1139
|
+
"websocket": { "connections": 45, "messages": 12000, "deliveries": 89000 }
|
|
1140
|
+
},
|
|
1141
|
+
"gauges": { "workersActive": 4, "inflight": 2, "queueDepth": 0 },
|
|
1142
|
+
"realtime": { "clients": 23, "groups": 8, "deliveries": 89000, "messages": 12000 },
|
|
1143
|
+
"hosts": ["localhost", "myapp.ripdev.io"]
|
|
1144
|
+
}
|
|
1145
|
+
```
|
|
1146
|
+
|
|
1147
|
+
### Structured event logging
|
|
1148
|
+
|
|
1149
|
+
With `--json-logging`, lifecycle events are emitted as structured JSON:
|
|
1150
|
+
|
|
1151
|
+
```json
|
|
1152
|
+
{"t":"2026-03-14T12:00:00.000Z","event":"server_start","app":"myapp","workers":4}
|
|
1153
|
+
{"t":"2026-03-14T12:05:00.000Z","event":"worker_restart","workerId":3,"attempt":1}
|
|
1154
|
+
{"t":"2026-03-14T12:10:00.000Z","event":"ws_open","clientId":"a1b2c3d4"}
|
|
1155
|
+
```
|
|
1156
|
+
|
|
1157
|
+
Events: `server_start`, `server_stop`, `worker_restart`, `worker_abandon`, `ws_open`, `ws_close`
|
|
1158
|
+
|
|
985
1159
|
## mDNS Service Discovery
|
|
986
1160
|
|
|
987
1161
|
The server automatically advertises itself via mDNS (Bonjour/Zeroconf):
|
|
@@ -1150,27 +1324,120 @@ See [Hot Reloading](#hot-reloading) for details on how the two layers (API + UI)
|
|
|
1150
1324
|
|
|
1151
1325
|
## Comparison with Other Servers
|
|
1152
1326
|
|
|
1153
|
-
| Feature | rip server | PM2 | Nginx |
|
|
1154
|
-
|
|
1155
|
-
| Pure Rip | ✅ | ❌ | ❌ |
|
|
1156
|
-
|
|
|
1157
|
-
| Hot Reload | ✅ (default) | ✅ | ❌ |
|
|
1158
|
-
|
|
|
1159
|
-
|
|
|
1160
|
-
|
|
|
1161
|
-
|
|
|
1162
|
-
|
|
|
1163
|
-
|
|
|
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
|
+
## Multi-App Configuration (`config.rip`)
|
|
1342
|
+
|
|
1343
|
+
If a `config.rip` file exists next to your entry file, the server loads it
|
|
1344
|
+
and registers additional apps with their own hosts and worker pools.
|
|
1345
|
+
|
|
1346
|
+
```coffee
|
|
1347
|
+
# config.rip
|
|
1348
|
+
export default
|
|
1349
|
+
apps:
|
|
1350
|
+
main:
|
|
1351
|
+
entry: './index.rip'
|
|
1352
|
+
hosts: ['example.com', 'www.example.com']
|
|
1353
|
+
workers: 4
|
|
1354
|
+
api:
|
|
1355
|
+
entry: './api/index.rip'
|
|
1356
|
+
hosts: ['api.example.com']
|
|
1357
|
+
workers: 2
|
|
1358
|
+
maxQueue: 1024
|
|
1359
|
+
admin:
|
|
1360
|
+
entry: './admin/index.rip'
|
|
1361
|
+
hosts: ['admin.example.com']
|
|
1362
|
+
workers: 1
|
|
1363
|
+
```
|
|
1364
|
+
|
|
1365
|
+
Each app gets its own worker pool, queue, and host routing. The edge server
|
|
1366
|
+
routes requests to the correct app based on the `Host` header.
|
|
1367
|
+
|
|
1368
|
+
If no `config.rip` exists, the server runs in single-app mode as usual.
|
|
1369
|
+
|
|
1370
|
+
## Reverse Proxy
|
|
1371
|
+
|
|
1372
|
+
Forward requests to external HTTP upstreams with proper header handling:
|
|
1373
|
+
|
|
1374
|
+
```coffee
|
|
1375
|
+
import { get } from '@rip-lang/server'
|
|
1376
|
+
import { proxyToUpstream } from '@rip-lang/server/edge/forwarding.rip'
|
|
1377
|
+
|
|
1378
|
+
get '/api/*' -> proxyToUpstream!(@req.raw, 'http://backend:8080', { clientIp: @req.header('x-real-ip') or '127.0.0.1' })
|
|
1379
|
+
```
|
|
1380
|
+
|
|
1381
|
+
Features:
|
|
1382
|
+
- Strips hop-by-hop headers (Connection, Transfer-Encoding, etc.)
|
|
1383
|
+
- Sets `X-Forwarded-For` to actual client IP (overwrites, never appends — prevents spoofing)
|
|
1384
|
+
- Adds `X-Forwarded-Proto` and `X-Forwarded-Host`
|
|
1385
|
+
- Streaming response passthrough
|
|
1386
|
+
- Timeout with 504, connect failure with 502
|
|
1387
|
+
- Manual redirect handling (no auto-follow)
|
|
1388
|
+
|
|
1389
|
+
Pass `clientIp` in options so upstream services see the real client address.
|
|
1390
|
+
Pass `timeoutMs` to override the default 30-second upstream timeout.
|
|
1391
|
+
|
|
1392
|
+
## Request ID Tracing
|
|
1393
|
+
|
|
1394
|
+
Every request gets a unique `X-Request-Id` header for end-to-end correlation.
|
|
1395
|
+
If the client sends an `X-Request-Id`, it's preserved; otherwise one is generated.
|
|
1396
|
+
|
|
1397
|
+
```bash
|
|
1398
|
+
curl -v http://localhost/users/42
|
|
1399
|
+
# < X-Request-Id: req-a8f3b2c1d4e5
|
|
1400
|
+
```
|
|
1401
|
+
|
|
1402
|
+
## Rate Limiting
|
|
1403
|
+
|
|
1404
|
+
Per-IP sliding window rate limiting, disabled by default:
|
|
1405
|
+
|
|
1406
|
+
```bash
|
|
1407
|
+
rip server --rate-limit=100 # 100 requests per minute per IP
|
|
1408
|
+
rip server --rate-limit=1000 --rate-limit-window=3600000 # 1000 per hour
|
|
1409
|
+
```
|
|
1410
|
+
|
|
1411
|
+
Returns `429 Too Many Requests` with `Retry-After` header when exceeded.
|
|
1412
|
+
|
|
1413
|
+
## Security
|
|
1414
|
+
|
|
1415
|
+
Built-in request smuggling defenses (always on):
|
|
1416
|
+
|
|
1417
|
+
- Rejects conflicting `Content-Length` + `Transfer-Encoding` headers
|
|
1418
|
+
- Rejects multiple `Host` headers
|
|
1419
|
+
- Rejects null bytes in URLs
|
|
1420
|
+
- Rejects oversized URLs (>8KB)
|
|
1421
|
+
- Path traversal normalized by URL parser
|
|
1422
|
+
|
|
1423
|
+
## WebSocket Passthrough
|
|
1424
|
+
|
|
1425
|
+
For reverse proxying WebSocket connections to external upstreams:
|
|
1426
|
+
|
|
1427
|
+
```coffee
|
|
1428
|
+
import { createWsPassthrough } from '@rip-lang/server/edge/forwarding.rip'
|
|
1429
|
+
|
|
1430
|
+
# In a WebSocket handler, tunnel to an external upstream
|
|
1431
|
+
upstream = createWsPassthrough(clientWs, 'ws://backend:8080/ws')
|
|
1432
|
+
```
|
|
1433
|
+
|
|
1434
|
+
Frames flow bidirectionally; close and error propagate between both ends.
|
|
1164
1435
|
|
|
1165
1436
|
## Roadmap
|
|
1166
1437
|
|
|
1167
1438
|
> *Planned improvements for future releases:*
|
|
1168
1439
|
|
|
1169
|
-
- [ ]
|
|
1170
|
-
- [ ] Metrics endpoint (Prometheus format)
|
|
1171
|
-
- [ ] Static file serving
|
|
1172
|
-
- [ ] Rate limiting
|
|
1173
|
-
- [ ] Performance benchmarks
|
|
1440
|
+
- [ ] Prometheus / OpenTelemetry metrics export
|
|
1174
1441
|
|
|
1175
1442
|
## License
|
|
1176
1443
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Edge Config Lifecycle (M0b)
|
|
2
|
+
|
|
3
|
+
This document freezes config apply behavior for v1.
|
|
4
|
+
|
|
5
|
+
## Goal
|
|
6
|
+
|
|
7
|
+
Config updates must be atomic, reversible, and observable.
|
|
8
|
+
|
|
9
|
+
## Apply pipeline
|
|
10
|
+
|
|
11
|
+
1. **Parse**
|
|
12
|
+
- Load `Edgefile.rip`.
|
|
13
|
+
- Compile/evaluate in restricted config context.
|
|
14
|
+
- Reject if syntax/runtime errors occur.
|
|
15
|
+
|
|
16
|
+
2. **Validate**
|
|
17
|
+
- Validate schema version (`version: 1` required).
|
|
18
|
+
- Validate required keys (`edge`, `app`, `site` domains).
|
|
19
|
+
- Validate host/path conflicts and ambiguous route precedence.
|
|
20
|
+
- Validate policy constraints (timeouts, queue caps, worker counts).
|
|
21
|
+
|
|
22
|
+
3. **Normalize**
|
|
23
|
+
- Expand defaults from `CONTRACTS.md`.
|
|
24
|
+
- Compile route rules into deterministic route table.
|
|
25
|
+
- Resolve app entry paths and socket namespaces.
|
|
26
|
+
|
|
27
|
+
4. **Stage**
|
|
28
|
+
- Build a staged config object with new version ID.
|
|
29
|
+
- Dry-run app pool preparation (spawn readiness checks if needed).
|
|
30
|
+
- Do not affect active traffic yet.
|
|
31
|
+
|
|
32
|
+
5. **Activate**
|
|
33
|
+
- Swap active route/config pointer atomically.
|
|
34
|
+
- Begin routing new traffic using new route table.
|
|
35
|
+
- Keep previous config pointer for rollback.
|
|
36
|
+
|
|
37
|
+
6. **Post-activate verify**
|
|
38
|
+
- Ensure app pools healthy under new config.
|
|
39
|
+
- Emit structured "config-activated" event.
|
|
40
|
+
|
|
41
|
+
7. **Rollback (if needed)**
|
|
42
|
+
- Revert active pointer to previous known-good config.
|
|
43
|
+
- Emit structured "config-rollback" event with reason.
|
|
44
|
+
|
|
45
|
+
## Trigger modes
|
|
46
|
+
|
|
47
|
+
- file watch (`Edgefile.rip`)
|
|
48
|
+
- `SIGHUP`
|
|
49
|
+
- control API (`POST /control/reload`)
|
|
50
|
+
|
|
51
|
+
All triggers use the same pipeline and safeguards.
|
|
52
|
+
|
|
53
|
+
## Determinism rules
|
|
54
|
+
|
|
55
|
+
- No async and no network I/O while evaluating config.
|
|
56
|
+
- Any disallowed operation fails validation.
|
|
57
|
+
- Config evaluation must be repeatable with same input.
|
|
58
|
+
|
|
59
|
+
## Failure behavior
|
|
60
|
+
|
|
61
|
+
- Parse/validate failure: keep serving with existing config.
|
|
62
|
+
- Stage failure: keep serving with existing config.
|
|
63
|
+
- Post-activate health failure: rollback to previous config.
|
|
64
|
+
|
|
65
|
+
## Observability
|
|
66
|
+
|
|
67
|
+
Every config attempt emits:
|
|
68
|
+
- attempt ID
|
|
69
|
+
- source trigger (watch/signal/api)
|
|
70
|
+
- old version
|
|
71
|
+
- new version (if any)
|
|
72
|
+
- result: applied / rejected / rolled_back
|
|
73
|
+
- reason code
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Edge Contracts (M0b)
|
|
2
|
+
|
|
3
|
+
This document freezes the core data contracts for the unified edge/app runtime in `@rip-lang/server`.
|
|
4
|
+
|
|
5
|
+
Status: Draft frozen for M0b review.
|
|
6
|
+
|
|
7
|
+
## Source of truth inputs
|
|
8
|
+
|
|
9
|
+
- Plan: `.cursor/plans/rip_unified_master_plan_v2_2892e891.plan.md`
|
|
10
|
+
- TLS findings: `packages/server/spikes/tls/FINDINGS.md`
|
|
11
|
+
|
|
12
|
+
## AppDescriptor
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
type AppDescriptor = {
|
|
16
|
+
id: string
|
|
17
|
+
entry: string
|
|
18
|
+
hosts: string[]
|
|
19
|
+
prefixes: string[]
|
|
20
|
+
workers: number
|
|
21
|
+
maxQueue: number
|
|
22
|
+
maxInflight: number
|
|
23
|
+
env: Record<string, string>
|
|
24
|
+
restartPolicy: {
|
|
25
|
+
maxRestarts: number
|
|
26
|
+
backoffMs: number
|
|
27
|
+
windowMs: number
|
|
28
|
+
}
|
|
29
|
+
healthCheck: {
|
|
30
|
+
path: string
|
|
31
|
+
intervalMs: number
|
|
32
|
+
timeoutMs: number
|
|
33
|
+
unhealthyThreshold: number
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Defaults:
|
|
39
|
+
- `workers`: `cpus().length`, min `2`
|
|
40
|
+
- `maxQueue`: `1000`
|
|
41
|
+
- `maxInflight`: `workers * 32`
|
|
42
|
+
- `env`: `{}`
|
|
43
|
+
- `restartPolicy`: `{ maxRestarts: 10, backoffMs: 1000, windowMs: 60000 }`
|
|
44
|
+
- `healthCheck`: `{ path: "/ready", intervalMs: 5000, timeoutMs: 2000, unhealthyThreshold: 3 }`
|
|
45
|
+
|
|
46
|
+
## WorkerEndpoint
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
type WorkerEndpoint = {
|
|
50
|
+
appId: string
|
|
51
|
+
workerId: number
|
|
52
|
+
socketPath: string
|
|
53
|
+
inflight: number
|
|
54
|
+
version: number
|
|
55
|
+
state: "starting" | "ready" | "draining" | "stopped"
|
|
56
|
+
startedAt: number
|
|
57
|
+
requestCount: number
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Defaults:
|
|
62
|
+
- `inflight`: `0`
|
|
63
|
+
- `state`: `"starting"`
|
|
64
|
+
- `requestCount`: `0`
|
|
65
|
+
|
|
66
|
+
## RouteAction
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
type RouteAction =
|
|
70
|
+
| { kind: "toApp"; appId: string }
|
|
71
|
+
| { kind: "proxy"; upstream: string; host?: string }
|
|
72
|
+
| { kind: "static"; dir: string; spaFallback?: string }
|
|
73
|
+
| { kind: "redirect"; to: string; status: number }
|
|
74
|
+
| { kind: "headers"; set?: Record<string, string>; remove?: string[] }
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## RouteRule
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
type RouteRule = {
|
|
81
|
+
id: string
|
|
82
|
+
host: string | "*"
|
|
83
|
+
pathPattern: string
|
|
84
|
+
methods: string[] | "*"
|
|
85
|
+
action: RouteAction
|
|
86
|
+
priority: number
|
|
87
|
+
timeouts?: Partial<TimeoutPolicy>
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Defaults:
|
|
92
|
+
- `host`: `"*"`
|
|
93
|
+
- `methods`: `"*"`
|
|
94
|
+
- `priority`: declaration order
|
|
95
|
+
- `timeouts`: inherit site/app/global defaults
|
|
96
|
+
|
|
97
|
+
## SchedulerDecision
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
type SchedulerDecision = {
|
|
101
|
+
appId: string
|
|
102
|
+
selectedWorker: WorkerEndpoint
|
|
103
|
+
algorithm: "least-inflight"
|
|
104
|
+
fallback: "queue" | "shed-503"
|
|
105
|
+
queuePosition: number | null
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Defaults:
|
|
110
|
+
- `algorithm`: `"least-inflight"`
|
|
111
|
+
- `fallback`: `"queue"` while queue `< maxQueue`, else `"shed-503"`
|
|
112
|
+
- `queuePosition`: `null` when immediately assigned
|
|
113
|
+
|
|
114
|
+
## TlsCertRecord
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
type TlsCertRecord = {
|
|
118
|
+
id: string
|
|
119
|
+
domains: string[]
|
|
120
|
+
notBefore: number
|
|
121
|
+
notAfter: number
|
|
122
|
+
fingerprint: string
|
|
123
|
+
certPath: string
|
|
124
|
+
keyPath: string
|
|
125
|
+
source: "acme" | "manual" | "shipped"
|
|
126
|
+
lastRenewAttempt: number | null
|
|
127
|
+
lastRenewResult: "success" | "failed" | null
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Defaults:
|
|
132
|
+
- `source`: `"acme"` for auto-managed certificates
|
|
133
|
+
- `lastRenewAttempt`: `null`
|
|
134
|
+
- `lastRenewResult`: `null`
|
|
135
|
+
|
|
136
|
+
## Constraint from M0a
|
|
137
|
+
|
|
138
|
+
From `packages/server/spikes/tls/FINDINGS.md`:
|
|
139
|
+
- dynamic SNI selection and ALPN-driven cert selection were not observed
|
|
140
|
+
- in-process cert hot reload was not observed
|
|
141
|
+
- graceful restart reload works
|
|
142
|
+
|
|
143
|
+
Therefore v1 contract assumptions:
|
|
144
|
+
- TLS config is loaded at process start
|
|
145
|
+
- cert activation uses graceful restart path in v1
|
|
146
|
+
- ACME HTTP-01 is the reliable v1 baseline
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Edgefile Contract (M0b)
|
|
2
|
+
|
|
3
|
+
This document freezes v1 config expectations for `Edgefile.rip`.
|
|
4
|
+
|
|
5
|
+
## Required top-level shape
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
version: 1
|
|
9
|
+
edge: ...
|
|
10
|
+
app "...": ...
|
|
11
|
+
site "...": ...
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Determinism policy
|
|
15
|
+
|
|
16
|
+
- Config evaluation is synchronous.
|
|
17
|
+
- Async operations are disallowed.
|
|
18
|
+
- Network I/O is disallowed.
|
|
19
|
+
- Validation errors must include line, field path, and remediation hint.
|
|
20
|
+
|
|
21
|
+
## Route actions (v1)
|
|
22
|
+
|
|
23
|
+
- `toApp(appId)`
|
|
24
|
+
- `proxy(upstream)`
|
|
25
|
+
- `static(dir)` (optional; app-side static remains valid)
|
|
26
|
+
- `redirect(to, status)`
|
|
27
|
+
- `headers(set/remove)`
|
|
28
|
+
|
|
29
|
+
## Host/path precedence
|
|
30
|
+
|
|
31
|
+
1. exact host > wildcard host
|
|
32
|
+
2. more specific path > less specific path
|
|
33
|
+
3. lower explicit priority number first
|
|
34
|
+
4. declaration order tie-break
|
|
35
|
+
|
|
36
|
+
## Timeout inheritance
|
|
37
|
+
|
|
38
|
+
- Global (`edge`) defaults
|
|
39
|
+
- overridden by app/site policy
|
|
40
|
+
- overridden by route-level explicit timeout values
|
|
41
|
+
|
|
42
|
+
## Reload semantics
|
|
43
|
+
|
|
44
|
+
- Any trigger runs the atomic pipeline from `CONFIG_LIFECYCLE.md`.
|
|
45
|
+
- Invalid config never replaces active config.
|
|
46
|
+
|
|
47
|
+
## v1 TLS implications from M0a
|
|
48
|
+
|
|
49
|
+
Based on `packages/server/spikes/tls/FINDINGS.md`:
|
|
50
|
+
- v1 should not assume dynamic per-SNI cert switching
|
|
51
|
+
- v1 should not assume in-process cert hot reload
|
|
52
|
+
- v1 should use graceful restart for cert activation
|
|
53
|
+
- v1 ACME baseline should prioritize HTTP-01
|