@rip-lang/server 1.3.126 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{docs/READ_VALIDATORS.md → API.md} +41 -119
- package/CONFIG.md +408 -0
- package/README.md +246 -1109
- package/acme/crypto.rip +0 -2
- package/browse.rip +62 -0
- package/control/cli.rip +89 -34
- package/control/lifecycle.rip +67 -1
- package/control/manager.rip +250 -0
- package/control/mdns.rip +3 -0
- package/middleware.rip +1 -1
- package/package.json +14 -11
- package/server.rip +189 -673
- package/serving/config.rip +766 -0
- package/{edge → serving}/forwarding.rip +2 -2
- package/serving/logging.rip +101 -0
- package/{edge → serving}/metrics.rip +29 -1
- package/serving/proxy.rip +99 -0
- package/{edge → serving}/queue.rip +1 -1
- package/{edge → serving}/ratelimit.rip +1 -1
- package/{edge → serving}/realtime.rip +71 -2
- package/{edge → serving}/registry.rip +1 -1
- package/{edge → serving}/router.rip +3 -3
- package/{edge → serving}/runtime.rip +18 -16
- package/{edge → serving}/security.rip +1 -1
- package/serving/static.rip +393 -0
- package/{edge → serving}/tls.rip +3 -7
- package/{edge → serving}/upstream.rip +4 -4
- package/{edge → serving}/verify.rip +16 -16
- package/streams/{tls_clienthello.rip → clienthello.rip} +1 -1
- package/streams/config.rip +8 -8
- package/streams/index.rip +5 -5
- package/streams/router.rip +2 -2
- package/tests/acme.rip +1 -1
- package/tests/config.rip +215 -0
- package/tests/control.rip +1 -1
- package/tests/{runtime_entrypoints.rip → entrypoints.rip} +11 -7
- package/tests/extracted.rip +118 -0
- package/tests/helpers.rip +4 -4
- package/tests/metrics.rip +3 -3
- package/tests/proxy.rip +9 -8
- package/tests/read.rip +1 -1
- package/tests/realtime.rip +3 -3
- package/tests/registry.rip +4 -4
- package/tests/router.rip +27 -27
- package/tests/runner.rip +70 -0
- package/tests/security.rip +4 -4
- package/tests/servers.rip +102 -136
- package/tests/static.rip +2 -2
- package/tests/streams_clienthello.rip +2 -2
- package/tests/streams_index.rip +4 -4
- package/tests/streams_pipe.rip +1 -1
- package/tests/streams_router.rip +10 -10
- package/tests/streams_runtime.rip +4 -4
- package/tests/streams_upstream.rip +1 -1
- package/tests/upstream.rip +2 -2
- package/tests/verify.rip +18 -18
- package/tests/watchers.rip +4 -4
- package/default.rip +0 -435
- package/docs/edge/CONFIG_LIFECYCLE.md +0 -111
- package/docs/edge/CONTRACTS.md +0 -137
- package/docs/edge/EDGEFILE_CONTRACT.md +0 -282
- package/docs/edge/M0B_REVIEW_NOTES.md +0 -102
- package/docs/edge/SCHEDULER.md +0 -46
- package/docs/logo.png +0 -0
- package/docs/logo.svg +0 -13
- package/docs/social.png +0 -0
- package/edge/config.rip +0 -607
- package/edge/static.rip +0 -69
- package/tests/edgefile.rip +0 -165
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Server API Reference
|
|
2
2
|
|
|
3
|
-
> Extracted from the main [@rip-lang/server README](
|
|
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`
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
|
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'
|
|
434
|
-
mimeType 'app.js'
|
|
435
|
-
mimeType 'photo.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
|
|
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
|
|
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
|
|
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
|
|
528
|
-
isBlank
|
|
529
|
-
isBlank
|
|
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'
|
|
546
|
-
toName '
|
|
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'
|
|
561
|
-
toPhone '555
|
|
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
|
|
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
|
|
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
|