@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 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, HTTPS, and mDNS — written entirely in Rip**
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 certificates, mDNS service discovery, and
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 (green lock, zero setup)
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
- | File | Lines | Role |
26
- |------|-------|------|
27
- | `api.rip` | ~662 | Core framework: routing, validation, `read()`, `session`, `@send`, server |
28
- | `middleware.rip` | ~559 | Built-in middleware: cors, logger, sessions, compression, security, serve |
29
- | `server.rip` | ~1,210 | Process manager: CLI, workers, load balancing, TLS, mDNS |
30
- | `server.html` | ~420 | Built-in dashboard UI |
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
- | Single File | ✅ (~1,200 lines) | ❌ | ❌ |
1157
- | Hot Reload | ✅ (default) | ✅ | ❌ |
1158
- | Directory Watch | ✅ (default) | ✅ | ❌ |
1159
- | Multi-Worker | ✅ | | ✅ |
1160
- | Auto HTTPS | ✅ | ❌ | ❌ |
1161
- | mDNS | ✅ | ❌ | ❌ |
1162
- | Zero Config | ✅ | ❌ | ❌ |
1163
- | Built-in LB | ✅ | ❌ | ✅ |
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
- - [ ] Request ID tracing for debugging
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