@rip-lang/server 1.3.125 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/{docs/READ_VALIDATORS.md → API.md} +41 -119
  2. package/CONFIG.md +408 -0
  3. package/README.md +246 -1109
  4. package/acme/crypto.rip +0 -2
  5. package/browse.rip +62 -0
  6. package/control/cli.rip +95 -36
  7. package/control/lifecycle.rip +67 -1
  8. package/control/manager.rip +250 -0
  9. package/control/mdns.rip +3 -0
  10. package/middleware.rip +1 -1
  11. package/package.json +14 -11
  12. package/server.rip +189 -673
  13. package/serving/config.rip +766 -0
  14. package/{edge → serving}/forwarding.rip +2 -2
  15. package/serving/logging.rip +101 -0
  16. package/{edge → serving}/metrics.rip +29 -1
  17. package/serving/proxy.rip +99 -0
  18. package/{edge → serving}/queue.rip +1 -1
  19. package/{edge → serving}/ratelimit.rip +1 -1
  20. package/{edge → serving}/realtime.rip +71 -2
  21. package/{edge → serving}/registry.rip +1 -1
  22. package/{edge → serving}/router.rip +3 -3
  23. package/{edge → serving}/runtime.rip +18 -16
  24. package/{edge → serving}/security.rip +1 -1
  25. package/serving/static.rip +393 -0
  26. package/{edge → serving}/tls.rip +3 -7
  27. package/{edge → serving}/upstream.rip +4 -4
  28. package/{edge → serving}/verify.rip +16 -16
  29. package/streams/{tls_clienthello.rip → clienthello.rip} +1 -1
  30. package/streams/config.rip +8 -8
  31. package/streams/index.rip +5 -5
  32. package/streams/router.rip +2 -2
  33. package/tests/acme.rip +1 -1
  34. package/tests/config.rip +215 -0
  35. package/tests/control.rip +1 -1
  36. package/tests/{runtime_entrypoints.rip → entrypoints.rip} +11 -7
  37. package/tests/extracted.rip +118 -0
  38. package/tests/helpers.rip +4 -4
  39. package/tests/metrics.rip +3 -3
  40. package/tests/proxy.rip +9 -8
  41. package/tests/read.rip +1 -1
  42. package/tests/realtime.rip +3 -3
  43. package/tests/registry.rip +4 -4
  44. package/tests/router.rip +27 -27
  45. package/tests/runner.rip +70 -0
  46. package/tests/security.rip +4 -4
  47. package/tests/servers.rip +102 -136
  48. package/tests/static.rip +2 -2
  49. package/tests/streams_clienthello.rip +2 -2
  50. package/tests/streams_index.rip +4 -4
  51. package/tests/streams_pipe.rip +1 -1
  52. package/tests/streams_router.rip +10 -10
  53. package/tests/streams_runtime.rip +4 -4
  54. package/tests/streams_upstream.rip +1 -1
  55. package/tests/upstream.rip +2 -2
  56. package/tests/verify.rip +18 -18
  57. package/tests/watchers.rip +4 -4
  58. package/default.rip +0 -435
  59. package/docs/edge/CONFIG_LIFECYCLE.md +0 -111
  60. package/docs/edge/CONTRACTS.md +0 -137
  61. package/docs/edge/EDGEFILE_CONTRACT.md +0 -282
  62. package/docs/edge/M0B_REVIEW_NOTES.md +0 -102
  63. package/docs/edge/SCHEDULER.md +0 -46
  64. package/docs/logo.png +0 -0
  65. package/docs/logo.svg +0 -13
  66. package/docs/social.png +0 -0
  67. package/edge/config.rip +0 -607
  68. package/edge/static.rip +0 -69
  69. package/tests/edgefile.rip +0 -165
@@ -1,6 +1,16 @@
1
- # Validation Reference — `read()`
1
+ # Server API Reference
2
2
 
3
- > Extracted from the main [@rip-lang/server README](../README.md).
3
+ > Extracted from the main [@rip-lang/server README](./README.md).
4
+
5
+ This document covers the framework-facing API for `@rip-lang/server`, including
6
+ validation, routing, middleware, request/response helpers, and app entrypoints.
7
+
8
+ The framework API is one way to create served content inside Rip Server. It is
9
+ not the whole product identity: the same runtime also serves static content,
10
+ proxied HTTP services, and TCP/TLS services. This reference covers the app and
11
+ API surface within that broader serving model.
12
+
13
+ ## Validation with `read()`
4
14
 
5
15
  The `read()` function is a validation and parsing powerhouse that eliminates
6
16
  90% of API boilerplate.
@@ -265,8 +275,6 @@ get '/logout' ->
265
275
 
266
276
  The `session` import works anywhere via AsyncLocalStorage — no `@` needed, works in helpers and nested callbacks.
267
277
 
268
- **Security note:** Without `secret`, sessions use plain base64 (dev only). With `secret`, sessions are HMAC-SHA256 signed (tamper-proof). Always set `secret` in production.
269
-
270
278
  ### CORS with Preflight
271
279
 
272
280
  ```coffee
@@ -297,105 +305,61 @@ use (c, next) ->
297
305
 
298
306
  ### Request Lifecycle Filters
299
307
 
300
- Three filters run at different stages: `raw` `before` handler `after`
308
+ Three filters run at different stages: `raw` -> `before` -> handler -> `after`
301
309
 
302
310
  ```coffee
303
311
  import { raw, before, after, get } from '@rip-lang/server'
304
312
 
305
- # Runs first — modify raw request before body parsing
306
313
  raw (req) ->
307
314
  if req.headers.get('X-Raw-SQL') is 'true'
308
315
  req.headers.set 'content-type', 'text/plain'
309
316
 
310
317
  skipPaths = ['/favicon.ico', '/ping', '/health']
311
318
 
312
- # Runs before handler (after body parsing)
313
319
  before ->
314
320
  @start = Date.now()
315
321
  @silent = @req.path in skipPaths
316
322
  unless @req.header 'Authorization'
317
323
  return @json { error: 'Unauthorized' }, 401
318
324
 
319
- # Runs after handler
320
325
  after ->
321
326
  return if @silent
322
327
  console.log "#{@req.method} #{@req.path} - #{Date.now() - @start}ms"
323
328
  ```
324
329
 
325
- **Note:** `raw` receives the native `Request` object (before parsing). `before` and `after` use `@` to access the context.
326
-
327
- **How `@` works:** Handlers are called with `this` bound to the context, so `@foo` is `this.foo`. This gives you Sinatra-like magic access to:
328
- - `@req` — Request object
329
- - `@json()`, `@text()`, `@html()`, `@redirect()`, `@send()` — Response helpers
330
- - `@header()` — Response header modifier
331
- - `@anything` — Custom per-request state
332
-
333
- **Imports that work anywhere** (via AsyncLocalStorage or Proxy):
334
- - `read` — Validated request parameters
335
- - `session` — Session data (if middleware enabled)
336
- - `env` — `process.env` shortcut (e.g., `env.DATABASE_URL`)
337
-
338
330
  ## Context Object
339
331
 
340
- Use `@` to access the context directly — no parameter needed:
332
+ Use `@` to access the context directly — no parameter needed.
341
333
 
342
334
  ### Response Helpers
343
335
 
344
336
  ```coffee
345
337
  get '/demo' ->
346
- # JSON response
347
338
  @json { data: 'value' }
348
- @json { data: 'value' }, 201 # With status
349
- @json { data: 'value' }, 200, { 'X-Custom': 'header' }
350
-
351
- # Text response
352
339
  @text 'Hello'
353
- @text 'Created', 201
354
-
355
- # HTML response
356
340
  @html '<h1>Hello</h1>'
357
-
358
- # Redirect
359
341
  @redirect '/new-location'
360
- @redirect '/new-location', 301 # Permanent
361
-
362
- # Raw body
363
342
  @body data, 200, { 'Content-Type': 'application/octet-stream' }
364
-
365
- # File serving (auto-detected MIME type via Bun.file)
366
- @send 'public/style.css' # text/css
367
- @send 'data/export.json', 'application/json' # explicit type
343
+ @send 'public/style.css'
368
344
  ```
369
345
 
370
346
  ### Request Helpers
371
347
 
372
348
  ```coffee
373
349
  get '/info' ->
374
- # Path and query parameters — use read() for validation!
375
350
  id = read 'id', 'id!'
376
351
  q = read 'q'
377
-
378
- # Headers
379
352
  auth = @req.header 'Authorization'
380
- allHeaders = @req.header()
381
-
382
- # Body (async)
383
353
  json = @req.json!
384
- text = @req.text!
385
- form = @req.formData!
386
- parsed = @req.parseBody!
387
-
388
- # Raw request
389
- @req.raw # Native Request object
390
- @req.method # 'GET', 'POST', etc.
391
- @req.url # Full URL
392
- @req.path # Path only
354
+ @req.raw
355
+ @req.method
356
+ @req.url
357
+ @req.path
393
358
  ```
394
359
 
395
360
  ### Request-Scoped State
396
361
 
397
362
  ```coffee
398
- # Store data for later middleware/handlers
399
363
  use (c, next) ->
400
364
  @user = { id: 1, name: 'Alice' }
401
365
  @startTime = Date.now()
@@ -410,30 +374,22 @@ get '/profile' ->
410
374
  ### `@send(path, type?)`
411
375
 
412
376
  Serve a file with auto-detected MIME type. Uses `Bun.file()` internally for
413
- efficient streaming — the file is never buffered in memory.
377
+ efficient streaming.
414
378
 
415
379
  ```coffee
416
- # Auto-detected content type (30+ extensions supported)
417
380
  get '/css/*' -> @send "css/#{@req.path.slice(5)}"
418
-
419
- # Explicit content type
420
381
  get '/files/*' -> @send "uploads/#{@req.path.slice(7)}", 'application/octet-stream'
421
-
422
- # SPA fallback — serve index.html for all unmatched routes
423
382
  notFound -> @send 'index.html', 'text/html; charset=UTF-8'
424
383
  ```
425
384
 
426
385
  ### `mimeType(path)`
427
386
 
428
- Exported utility that returns the MIME type for a file path:
429
-
430
387
  ```coffee
431
388
  import { mimeType } from '@rip-lang/server'
432
389
 
433
- mimeType 'style.css' # 'text/css; charset=UTF-8'
434
- mimeType 'app.js' # 'application/javascript'
435
- mimeType 'photo.png' # 'image/png'
436
- mimeType 'data.xyz' # 'application/octet-stream'
390
+ mimeType 'style.css'
391
+ mimeType 'app.js'
392
+ mimeType 'photo.png'
437
393
  ```
438
394
 
439
395
  ## Error Handling
@@ -468,7 +424,7 @@ start port: 3000
468
424
  start port: 3000, host: '0.0.0.0'
469
425
  ```
470
426
 
471
- ### Handler Only (for custom servers)
427
+ ### Handler Only
472
428
 
473
429
  ```coffee
474
430
  import { startHandler } from '@rip-lang/server'
@@ -488,9 +444,9 @@ export default App ->
488
444
 
489
445
  ## Context Utilities
490
446
 
491
- ### ctx()
447
+ ### `ctx()`
492
448
 
493
- Get the current request context from anywhere (via AsyncLocalStorage):
449
+ Get the current request context from anywhere:
494
450
 
495
451
  ```coffee
496
452
  import { ctx } from '@rip-lang/server'
@@ -498,74 +454,52 @@ import { ctx } from '@rip-lang/server'
498
454
  logRequest = ->
499
455
  c = ctx()
500
456
  console.log "#{c.req.method} #{c.req.path}" if c
501
-
502
- get '/demo' ->
503
- logRequest()
504
- { ok: true }
505
457
  ```
506
458
 
507
- ### resetGlobals()
459
+ ### `resetGlobals()`
508
460
 
509
- Reset all global state (routes, middleware, filters). Useful for testing:
461
+ Reset all global state (routes, middleware, filters). Useful for testing.
510
462
 
511
463
  ```coffee
512
- import { resetGlobals, get, start } from '@rip-lang/server'
464
+ import { resetGlobals, get } from '@rip-lang/server'
513
465
 
514
466
  beforeEach ->
515
467
  resetGlobals()
516
-
517
- get '/test' -> { test: true }
518
468
  ```
519
469
 
520
470
  ## Utility Functions
521
471
 
522
- ### isBlank
472
+ ### `isBlank`
523
473
 
524
474
  ```coffee
525
475
  import { isBlank } from '@rip-lang/server'
526
476
 
527
- isBlank null # true
528
- isBlank undefined # true
529
- isBlank '' # true
530
- isBlank ' ' # true
531
- isBlank [] # true
532
- isBlank {} # true
533
- isBlank false # true
534
- isBlank 'hello' # false
535
- isBlank [1, 2] # false
477
+ isBlank null
478
+ isBlank ''
479
+ isBlank {}
536
480
  ```
537
481
 
538
- ### toName
539
-
540
- Advanced name formatting with intelligent capitalization:
482
+ ### `toName`
541
483
 
542
484
  ```coffee
543
485
  import { toName } from '@rip-lang/server'
544
486
 
545
- toName 'john doe' # 'John Doe'
546
- toName 'JANE SMITH' # 'Jane Smith'
547
- toName "o'brien" # "O'Brien"
548
- toName 'mcdonald' # 'McDonald'
549
- toName 'P. o. bOX #44', 'address' # 'PO Box #44'
550
- toName '123 main st ne', 'address' # '123 Main St NE'
487
+ toName 'john doe'
488
+ toName "o'brien"
551
489
  ```
552
490
 
553
- ### toPhone
554
-
555
- US phone number formatting:
491
+ ### `toPhone`
556
492
 
557
493
  ```coffee
558
494
  import { toPhone } from '@rip-lang/server'
559
495
 
560
- toPhone '5551234567' # '(555) 123-4567'
561
- toPhone '555-123-4567' # '(555) 123-4567'
562
- toPhone '555.123.4567 x99' # '(555) 123-4567, ext. 99'
563
- toPhone '+1 555 123 4567' # '(555) 123-4567'
496
+ toPhone '5551234567'
497
+ toPhone '555.123.4567 x99'
564
498
  ```
565
499
 
566
500
  ## Migration from Hono
567
501
 
568
- ### Before (Hono)
502
+ ### Before
569
503
 
570
504
  ```coffee
571
505
  import { Hono } from 'hono'
@@ -578,7 +512,7 @@ app.get '/users/:id', (c) ->
578
512
  export default app
579
513
  ```
580
514
 
581
- ### After (@rip-lang/server)
515
+ ### After
582
516
 
583
517
  ```coffee
584
518
  import { get, read, startHandler } from '@rip-lang/server'
@@ -590,18 +524,6 @@ get '/users/:id' ->
590
524
  export default startHandler()
591
525
  ```
592
526
 
593
- ### API Compatibility
594
-
595
- | Hono | @rip-lang/server |
596
- |------|------------------|
597
- | `app.get(path, handler)` | `get path, handler` |
598
- | `app.post(path, handler)` | `post path, handler` |
599
- | `app.use(middleware)` | `use middleware` |
600
- | `app.basePath(path)` | `prefix path, -> ...` |
601
- | `c.json(data)` | `@json(data)` or return `{ data }` |
602
- | `c.req.param('id')` | `@req.param('id')` or `read 'id'` |
603
- | `c.req.query('q')` | `@req.query('q')` or `read 'q'` |
604
-
605
527
  ## Real-World Example
606
528
 
607
529
  ```coffee
package/CONFIG.md ADDED
@@ -0,0 +1,408 @@
1
+ # Serve Reference
2
+
3
+ This document is the reference for `serve.rip` and the runtime behavior behind
4
+ it.
5
+
6
+ Rip Server serves content. Here, `content` means anything you want to make
7
+ reachable over the network: a static site, a Rip app, a proxied HTTP service,
8
+ or a TCP/TLS service.
9
+
10
+ `serve.rip` is the operator model for publishing and routing that content. It
11
+ combines:
12
+
13
+ - config shape
14
+ - config lifecycle
15
+ - runtime contracts
16
+ - scheduler policy
17
+ - implementation constraints that matter to operators
18
+
19
+ Those lifecycle and runtime constraints are part of serving, not side systems.
20
+ TLS, proxy health, verification, rollback, drain, reload, and diagnostics are
21
+ the guarantees that make serving safe and coherent.
22
+
23
+ ## Canonical shape
24
+
25
+ `serve.rip` is the only config file and `hosts` is the canonical authoring
26
+ surface.
27
+
28
+ ```coffee
29
+ export default
30
+ version: 1
31
+ server: {}
32
+ certs: {}
33
+ proxies: {}
34
+ apps: {}
35
+ rules: {}
36
+ groups: {}
37
+ hosts: {}
38
+ streams: []
39
+ ```
40
+
41
+ There are no alternate top-level route/site models.
42
+
43
+ ## Top-level sections
44
+
45
+ ### `server`
46
+
47
+ `edge` is accepted as an alias for backward compatibility.
48
+
49
+ Global settings:
50
+
51
+ - `acme` (boolean or array of domains)
52
+ - `cert`
53
+ - `key`
54
+ - `hsts`
55
+ - `trustedProxies`
56
+ - `timeouts`
57
+ - `verify`
58
+
59
+ Verification settings:
60
+
61
+ - `requireHealthyProxies`
62
+ - `requireReadyApps`
63
+ - `includeUnroutedManagedApps`
64
+ - `minHealthyTargetsPerProxy`
65
+
66
+ ### `certs`
67
+
68
+ Reusable TLS identities.
69
+
70
+ Preferred shorthand:
71
+
72
+ ```coffee
73
+ certs:
74
+ trusthealth: '/ssl/trusthealth.com'
75
+ ```
76
+
77
+ This expands to:
78
+
79
+ - `cert: '/ssl/trusthealth.com.crt'`
80
+ - `key: '/ssl/trusthealth.com.key'`
81
+
82
+ Explicit object form is also valid:
83
+
84
+ ```coffee
85
+ certs:
86
+ trusthealth:
87
+ cert: '/ssl/trusthealth.com.crt'
88
+ key: '/ssl/trusthealth.com.key'
89
+ ```
90
+
91
+ ### `proxies`
92
+
93
+ Named backend proxy targets.
94
+
95
+ Transport kind is inferred from URL scheme:
96
+
97
+ - `http://...` => HTTP proxy
98
+ - `https://...` => HTTPS proxy
99
+ - `tcp://...` => raw TCP proxy
100
+
101
+ ```coffee
102
+ proxies:
103
+ api:
104
+ hosts: ['http://127.0.0.1:4000']
105
+ check:
106
+ path: '/health'
107
+ intervalMs: 5000
108
+ timeoutMs: 2000
109
+ retry:
110
+ attempts: 2
111
+ retryOn: [502, 503, 504]
112
+ timeouts:
113
+ connectMs: 2000
114
+ readMs: 30000
115
+
116
+ incus:
117
+ hosts: ['tcp://127.0.0.1:8443']
118
+ connectTimeoutMs: 5000
119
+ ```
120
+
121
+ Rules:
122
+
123
+ - HTTP proxies may use `check`, `retry`, and `timeouts`
124
+ - TCP proxies may use `connectTimeoutMs`
125
+ - mixed scheme families in one proxy are invalid
126
+
127
+ ### `apps`
128
+
129
+ Named managed Rip applications with worker and queue settings.
130
+
131
+ ```coffee
132
+ apps:
133
+ web:
134
+ entry: './apps/web/index.rip'
135
+ workers: 4
136
+ maxQueue: 512
137
+ queueTimeoutMs: 30000
138
+ readTimeoutMs: 30000
139
+ ```
140
+
141
+ ### `rules`
142
+
143
+ Named reusable HTTP rule bundles.
144
+
145
+ ```coffee
146
+ rules:
147
+ web: [
148
+ { path: '/api/*', proxy: 'api' }
149
+ { path: '/*', app: 'web' }
150
+ ]
151
+ ```
152
+
153
+ You do not have to use `rules`. Hosts may also define inline rules or mixed
154
+ arrays of named and inline rules.
155
+
156
+ ### `groups`
157
+
158
+ Named reusable host lists.
159
+
160
+ ```coffee
161
+ groups:
162
+ publicWeb: ['example.com', 'www.example.com']
163
+ ```
164
+
165
+ ### `hosts`
166
+
167
+ The canonical config surface. Each binding resolves to one or more concrete
168
+ hosts and owns the HTTP or passthrough behavior for those hosts.
169
+
170
+ Examples:
171
+
172
+ ```coffee
173
+ hosts:
174
+ 'example.com':
175
+ cert: 'main'
176
+ rules: [
177
+ { path: '/api/*', proxy: 'api' }
178
+ { path: '/*', app: 'web' }
179
+ ]
180
+
181
+ publicWeb:
182
+ hosts: 'publicWeb'
183
+ cert: 'main'
184
+ rules: 'web'
185
+
186
+ hosts: *{
187
+ ['example.com', 'foo.bar.com']:
188
+ cert: 'main'
189
+ rules: 'web'
190
+ }
191
+ ```
192
+
193
+ Host block fields:
194
+
195
+ - `hosts`
196
+ - `cert`
197
+ - `key`
198
+ - `rules`
199
+ - `proxy`
200
+ - `app`
201
+ - `root`
202
+ - `spa`
203
+ - `browse`
204
+ - `timeouts`
205
+
206
+ Rules:
207
+
208
+ - `cert` references a named entry in `certs`, unless paired with inline `key`
209
+ - `rules` can reference named rules, inline rules, or both
210
+ - rules inside a host block must not specify `host`
211
+ - `proxy` is a shorthand catch-all proxy binding
212
+ - `app` is a shorthand catch-all app binding
213
+ - `root` is a shorthand catch-all static binding
214
+ - if `proxy` targets a TCP proxy, Rip generates the default `:443` SNI stream route automatically
215
+
216
+ ### `streams`
217
+
218
+ Explicit Layer 4 stream routes matched by `listen` port and SNI.
219
+
220
+ ```coffee
221
+ streams: [
222
+ { listen: 443, sni: ['db.example.com'], proxy: 'db' }
223
+ ]
224
+ ```
225
+
226
+ If a stream route shares the HTTPS port, Rip switches that port into shared
227
+ multiplexer mode: matching SNI is passed through at Layer 4, and everything
228
+ else falls through to Rip's internal HTTPS server.
229
+
230
+ ## Config lifecycle
231
+
232
+ ### Goal
233
+
234
+ Config updates must be:
235
+
236
+ - atomic
237
+ - reversible
238
+ - observable
239
+ - safe for in-flight HTTP and websocket proxy traffic
240
+
241
+ ### Apply pipeline
242
+
243
+ 1. Parse
244
+ - Load `serve.rip`
245
+ - Evaluate synchronously in config context
246
+ - Reject on syntax or runtime errors
247
+ 2. Validate
248
+ - Require `version: 1`
249
+ - Validate supported top-level keys
250
+ - Validate reusable config references, wildcard hosts, websocket route requirements, timeout shapes, and verification policy
251
+ 3. Normalize
252
+ - Expand defaults
253
+ - Normalize certs, proxies, apps, rules, groups, concrete host bindings, timeouts, and verification policy
254
+ - Compile the deterministic route table
255
+ 4. Stage
256
+ - Build a new edge runtime generation
257
+ - Prepare managed app registry changes
258
+ - Do not affect active traffic yet
259
+ 5. Activate
260
+ - Swap the active runtime atomically
261
+ - Route new traffic through the new generation
262
+ - Keep the old generation retired for rollback or drain
263
+ 6. Post-activate verify
264
+ - Check referenced HTTP proxies, healthy target counts, and managed app worker readiness
265
+ 7. Rollback
266
+ - Restore the previous registry snapshot and active runtime if verification fails
267
+
268
+ ### Trigger modes
269
+
270
+ - file watch on `serve.rip`
271
+ - `SIGHUP`
272
+ - control API: `POST /reload`
273
+
274
+ All triggers use the same reload path and safeguards.
275
+
276
+ ### Failure behavior
277
+
278
+ - Parse or validate failure: keep serving the existing config
279
+ - Stage failure: keep serving the existing config
280
+ - Post-activate verification failure: rollback automatically
281
+
282
+ ### Draining behavior
283
+
284
+ - Retired runtimes remain alive while:
285
+ - HTTP requests that started on them are still in flight
286
+ - websocket proxy connections that started on them are still open
287
+ - Health checks stop only after a retired runtime finishes draining
288
+
289
+ ## Contracts
290
+
291
+ ### `AppDescriptor`
292
+
293
+ ```ts
294
+ type AppDescriptor = {
295
+ id: string
296
+ entry: string | null
297
+ appBaseDir: string | null
298
+ hosts: string[]
299
+ workers: number | null
300
+ maxQueue: number
301
+ queueTimeoutMs: number
302
+ readTimeoutMs: number
303
+ env: Record<string, string>
304
+ }
305
+ ```
306
+
307
+ ### `RouteRule`
308
+
309
+ ```ts
310
+ type RouteRule = {
311
+ id: string
312
+ host: string | "*" | "*.example.com"
313
+ path: string
314
+ methods: string[] | "*"
315
+ priority: number
316
+ proxy?: string | null
317
+ app?: string | null
318
+ static?: string | null
319
+ redirect?: { to: string, status: number } | null
320
+ headers?: { set?: Record<string, string>, remove?: string[] } | null
321
+ websocket?: boolean
322
+ timeouts?: Partial<TimeoutPolicy>
323
+ }
324
+ ```
325
+
326
+ Route rules are normalized from `serve.rip` host bindings. Authoring
327
+ composition through `rules`, `groups`, `certs`, and `proxies` must be fully
328
+ resolved before route compilation.
329
+
330
+ ### `VerifyPolicy`
331
+
332
+ ```ts
333
+ type VerifyPolicy = {
334
+ requireHealthyProxies: boolean
335
+ requireReadyApps: boolean
336
+ includeUnroutedManagedApps: boolean
337
+ minHealthyTargetsPerProxy: number
338
+ }
339
+ ```
340
+
341
+ Defaults:
342
+
343
+ - `requireHealthyProxies: true`
344
+ - `requireReadyApps: true`
345
+ - `includeUnroutedManagedApps: true`
346
+ - `minHealthyTargetsPerProxy: 1`
347
+
348
+ ### `EdgeRuntime`
349
+
350
+ ```ts
351
+ type EdgeRuntime = {
352
+ id: string
353
+ upstreamPool: UpstreamPool
354
+ routeTable: RouteTable
355
+ configInfo: ConfigInfo
356
+ verifyPolicy: VerifyPolicy | null
357
+ inflight: number
358
+ wsConnections: number
359
+ retiredAt: string | null
360
+ }
361
+ ```
362
+
363
+ ## Scheduler policy
364
+
365
+ v1 uses **least-inflight** per app pool.
366
+
367
+ Selection order:
368
+
369
+ 1. filter to workers in `ready` state
370
+ 2. choose the worker with lowest `inflight`
371
+ 3. tie-break by lowest `workerId`
372
+
373
+ Fallback behavior:
374
+
375
+ - if all workers are saturated and queue depth `< maxQueue`: enqueue
376
+ - if queue depth `>= maxQueue`: return `503` with `Retry-After`
377
+
378
+ Retry interaction:
379
+
380
+ - retry only idempotent requests by default
381
+ - never retry after partial upstream response
382
+ - retry decisions occur after scheduler selection and only on retry-eligible failures
383
+
384
+ ## Operational constraints
385
+
386
+ - Config evaluation must be deterministic
387
+ - No async work while evaluating config
388
+ - No network I/O while evaluating config
389
+ - Validation errors must include field path, message, and remediation hint
390
+
391
+ TLS posture in v1:
392
+
393
+ - dynamic SNI selection was not observed
394
+ - in-process cert hot reload was not observed
395
+ - graceful restart cert activation works
396
+ - ACME HTTP-01 is the reliable baseline
397
+
398
+ ## Historical notes worth keeping
399
+
400
+ Accepted design choices that still matter:
401
+
402
+ - single project architecture with two runtime roles:
403
+ - EdgeControlPlane
404
+ - AppDataPlane
405
+ - direct request path remains `client -> ingress -> worker`
406
+ - config apply lifecycle is atomic
407
+ - `server.rip` should stay orchestration/wiring, not business logic
408
+ - prefer themed files over generic utility dumping grounds