@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.
- 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 +95 -36
- 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
package/tests/watchers.rip
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { test, eq } from '
|
|
1
|
+
import { test, eq } from './runner.rip'
|
|
2
2
|
import { shouldTriggerCodeReload } from '../control/watchers.rip'
|
|
3
3
|
|
|
4
4
|
test "shouldTriggerCodeReload matches extension watch specs", ->
|
|
@@ -6,9 +6,9 @@ test "shouldTriggerCodeReload matches extension watch specs", ->
|
|
|
6
6
|
eq shouldTriggerCodeReload('foo.txt', '*.rip'), false
|
|
7
7
|
|
|
8
8
|
test "shouldTriggerCodeReload matches exact filenames", ->
|
|
9
|
-
eq shouldTriggerCodeReload('
|
|
10
|
-
eq shouldTriggerCodeReload('nested/
|
|
11
|
-
eq shouldTriggerCodeReload('other.rip', '
|
|
9
|
+
eq shouldTriggerCodeReload('serve.rip', 'serve.rip'), true
|
|
10
|
+
eq shouldTriggerCodeReload('nested/serve.rip', 'serve.rip'), true
|
|
11
|
+
eq shouldTriggerCodeReload('other.rip', 'serve.rip'), false
|
|
12
12
|
|
|
13
13
|
test "shouldTriggerCodeReload ignores the entry file itself", ->
|
|
14
14
|
eq shouldTriggerCodeReload('index.rip', '*.rip', 'index.rip'), false
|
package/default.rip
DELETED
|
@@ -1,435 +0,0 @@
|
|
|
1
|
-
# ==============================================================================
|
|
2
|
-
# @rip-lang/server — Default Static File Server
|
|
3
|
-
# ==============================================================================
|
|
4
|
-
#
|
|
5
|
-
# Built-in fallback when no index.rip or index.ts exists in the target directory.
|
|
6
|
-
# Activated automatically by `rip server` when no app entry is found.
|
|
7
|
-
#
|
|
8
|
-
# Behavior:
|
|
9
|
-
# - Serves static files with auto-detected MIME types
|
|
10
|
-
# - Directories with index.html serve the index.html
|
|
11
|
-
# - Directories without index.html show a browsable file listing
|
|
12
|
-
# - Hover preview for images and PDFs
|
|
13
|
-
# - Rip browser bundle available at /rip/rip.min.js
|
|
14
|
-
# - Hot-reload via SSE for .rip, .html, and .css changes
|
|
15
|
-
#
|
|
16
|
-
# ==============================================================================
|
|
17
|
-
|
|
18
|
-
import { use, start, notFound } from '@rip-lang/server'
|
|
19
|
-
import { serve } from '@rip-lang/server/middleware'
|
|
20
|
-
import { statSync, readdirSync, readFileSync } from 'node:fs'
|
|
21
|
-
import { join, resolve, basename } from 'node:path'
|
|
22
|
-
|
|
23
|
-
root = resolve(process.env.APP_BASE_DIR or process.cwd())
|
|
24
|
-
rootSlash = root + '/'
|
|
25
|
-
rootName = basename(root) or root
|
|
26
|
-
|
|
27
|
-
use serve dir: root, bundle: [], watch: true
|
|
28
|
-
|
|
29
|
-
# --- Helpers ---
|
|
30
|
-
|
|
31
|
-
esc = (s) ->
|
|
32
|
-
s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
|
33
|
-
|
|
34
|
-
fmtSize = (n) ->
|
|
35
|
-
for u in ['B', 'KB', 'MB', 'GB']
|
|
36
|
-
if n < 1024
|
|
37
|
-
return if u is 'B' then "#{n} B" else "#{n.toFixed(1)} #{u}"
|
|
38
|
-
n /= 1024
|
|
39
|
-
"#{n.toFixed(1)} TB"
|
|
40
|
-
|
|
41
|
-
fmtDate = (ms) ->
|
|
42
|
-
d = new Date(ms)
|
|
43
|
-
mo = d.toLocaleString('en', { month: 'short' })
|
|
44
|
-
day = String(d.getDate()).padStart(2, '0')
|
|
45
|
-
year = d.getFullYear()
|
|
46
|
-
hour = d.getHours()
|
|
47
|
-
min = String(d.getMinutes()).padStart(2, '0')
|
|
48
|
-
ampm = if hour >= 12 then 'PM' else 'AM'
|
|
49
|
-
hour = hour % 12 or 12
|
|
50
|
-
"#{mo} #{day}, #{year} at #{hour}:#{min} #{ampm}"
|
|
51
|
-
|
|
52
|
-
EXT_ICONS =
|
|
53
|
-
html: '🌐', htm: '🌐', css: '🎨', js: '📜', mjs: '📜', ts: '📜'
|
|
54
|
-
json: '📋', rip: '⚡', md: '📝', txt: '📝', pdf: '📕'
|
|
55
|
-
png: '🖼️', jpg: '🖼️', jpeg: '🖼️', gif: '🖼️', svg: '🖼️', webp: '🖼️', ico: '🖼️'
|
|
56
|
-
zip: '📦', gz: '📦', tar: '📦'
|
|
57
|
-
mp3: '🎵', ogg: '🎵', wav: '🎵', mp4: '🎬', webm: '🎬'
|
|
58
|
-
woff: '🔤', woff2: '🔤', ttf: '🔤', otf: '🔤'
|
|
59
|
-
|
|
60
|
-
IMG_EXTS = new Set ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'avif']
|
|
61
|
-
|
|
62
|
-
fileIcon = (name) ->
|
|
63
|
-
EXT_ICONS[name.slice(name.lastIndexOf('.') + 1).toLowerCase()] or '📄'
|
|
64
|
-
|
|
65
|
-
previewAttr = (name, href) ->
|
|
66
|
-
ext = name.slice(name.lastIndexOf('.') + 1).toLowerCase()
|
|
67
|
-
if IMG_EXTS.has(ext) then " data-pv=\"image\" data-src=\"#{esc href}\"" else ''
|
|
68
|
-
|
|
69
|
-
row = (ic, href, label, size = '', date = '', extra = '') ->
|
|
70
|
-
"<tr><td class=\"ic\">#{ic}</td><td><a href=\"#{esc href}\"#{extra}>#{esc label}</a></td><td class=\"sz\">#{size}</td><td class=\"dt\">#{date}</td></tr>"
|
|
71
|
-
|
|
72
|
-
breadcrumbs = (reqPath) ->
|
|
73
|
-
parts = reqPath.split('/').filter((p) -> p)
|
|
74
|
-
crumbs = "<a href=\"/\">#{esc rootName}</a>"
|
|
75
|
-
href = ''
|
|
76
|
-
for part in parts
|
|
77
|
-
href += "/#{part}"
|
|
78
|
-
crumbs += " <span class=\"sep\">/</span> <a href=\"#{href}/\">#{esc part}</a>"
|
|
79
|
-
crumbs
|
|
80
|
-
|
|
81
|
-
# --- Directory Index ---
|
|
82
|
-
|
|
83
|
-
renderIndex = (reqPath, fsPath) ->
|
|
84
|
-
entries = try readdirSync(fsPath, { withFileTypes: true }) catch then []
|
|
85
|
-
dirs = []
|
|
86
|
-
files = []
|
|
87
|
-
for e in entries
|
|
88
|
-
continue if e.name.startsWith('.')
|
|
89
|
-
if e.isDirectory() then dirs.push(e.name) else files.push(e.name)
|
|
90
|
-
dirs.sort()
|
|
91
|
-
files.sort()
|
|
92
|
-
|
|
93
|
-
rows = []
|
|
94
|
-
rows.push row('📁', '../', '..') if reqPath isnt '/'
|
|
95
|
-
for d in dirs
|
|
96
|
-
rows.push row('📁', "#{d}/", d)
|
|
97
|
-
for f in files
|
|
98
|
-
try
|
|
99
|
-
s = statSync(join(fsPath, f))
|
|
100
|
-
rows.push row(fileIcon(f), f, f, fmtSize(s.size), fmtDate(s.mtimeMs), previewAttr(f, f))
|
|
101
|
-
catch
|
|
102
|
-
rows.push row(fileIcon(f), f, f)
|
|
103
|
-
|
|
104
|
-
numDirs = dirs.length
|
|
105
|
-
numFiles = files.length
|
|
106
|
-
summary = []
|
|
107
|
-
summary.push "#{numDirs} folder#{if numDirs isnt 1 then 's' else ''}" if numDirs > 0
|
|
108
|
-
summary.push "#{numFiles} file#{if numFiles isnt 1 then 's' else ''}" if numFiles > 0
|
|
109
|
-
|
|
110
|
-
"""
|
|
111
|
-
<!DOCTYPE html>
|
|
112
|
-
<html lang="en">
|
|
113
|
-
<head>
|
|
114
|
-
<meta charset="UTF-8">
|
|
115
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
116
|
-
<title>Index of #{esc reqPath}</title>
|
|
117
|
-
<style>
|
|
118
|
-
:root {
|
|
119
|
-
--bg: #f3f6f7;
|
|
120
|
-
--card: #fff;
|
|
121
|
-
--border: #e5e7eb;
|
|
122
|
-
--border-row: #f0f0f0;
|
|
123
|
-
--text: #1f2937;
|
|
124
|
-
--muted: #6b7280;
|
|
125
|
-
--link: #2563eb;
|
|
126
|
-
--link-visited: #7c3aed;
|
|
127
|
-
--hover-bg: #f0f7ff;
|
|
128
|
-
--head-bg: #fafbfc;
|
|
129
|
-
--sep: #cbd5e1;
|
|
130
|
-
}
|
|
131
|
-
@media (prefers-color-scheme: dark) {
|
|
132
|
-
:root {
|
|
133
|
-
--bg: #0d1117;
|
|
134
|
-
--card: #161b22;
|
|
135
|
-
--border: #30363d;
|
|
136
|
-
--border-row: #21262d;
|
|
137
|
-
--text: #c9d1d9;
|
|
138
|
-
--muted: #8b949e;
|
|
139
|
-
--link: #58a6ff;
|
|
140
|
-
--link-visited: #bc8cff;
|
|
141
|
-
--hover-bg: #1c2129;
|
|
142
|
-
--head-bg: #1c2129;
|
|
143
|
-
--sep: #484f58;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
147
|
-
body {
|
|
148
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
149
|
-
font-size: 15px;
|
|
150
|
-
line-height: 1.5;
|
|
151
|
-
color: var(--text);
|
|
152
|
-
background: var(--bg);
|
|
153
|
-
-webkit-font-smoothing: antialiased;
|
|
154
|
-
}
|
|
155
|
-
.wrap { max-width: 960px; margin: 2.5rem auto; padding: 0 1rem; }
|
|
156
|
-
.card {
|
|
157
|
-
background: var(--card);
|
|
158
|
-
border: 1px solid var(--border);
|
|
159
|
-
border-radius: 8px;
|
|
160
|
-
overflow: hidden;
|
|
161
|
-
box-shadow: 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04);
|
|
162
|
-
}
|
|
163
|
-
.bar {
|
|
164
|
-
display: flex;
|
|
165
|
-
align-items: center;
|
|
166
|
-
justify-content: space-between;
|
|
167
|
-
padding: .875rem 1.25rem;
|
|
168
|
-
border-bottom: 1px solid var(--border);
|
|
169
|
-
gap: 1rem;
|
|
170
|
-
}
|
|
171
|
-
.crumbs { font-size: .9375rem; font-weight: 500; }
|
|
172
|
-
.crumbs a { color: var(--link); text-decoration: none; }
|
|
173
|
-
.crumbs a:hover { text-decoration: underline; }
|
|
174
|
-
.sep { color: var(--sep); margin: 0 .3rem; font-weight: 400; }
|
|
175
|
-
.meta { font-size: .8125rem; color: var(--muted); white-space: nowrap; }
|
|
176
|
-
table { width: 100%; border-collapse: collapse; }
|
|
177
|
-
th {
|
|
178
|
-
position: sticky; top: 0;
|
|
179
|
-
background: var(--head-bg);
|
|
180
|
-
text-align: left;
|
|
181
|
-
padding: .625rem 1.25rem;
|
|
182
|
-
font-size: .6875rem;
|
|
183
|
-
font-weight: 600;
|
|
184
|
-
color: var(--muted);
|
|
185
|
-
text-transform: uppercase;
|
|
186
|
-
letter-spacing: .05em;
|
|
187
|
-
border-bottom: 1px solid var(--border);
|
|
188
|
-
}
|
|
189
|
-
td {
|
|
190
|
-
padding: .5rem 1.25rem;
|
|
191
|
-
border-bottom: 1px solid var(--border-row);
|
|
192
|
-
font-size: .875rem;
|
|
193
|
-
vertical-align: middle;
|
|
194
|
-
}
|
|
195
|
-
tr:last-child td { border-bottom: none; }
|
|
196
|
-
tbody tr:hover { background: var(--hover-bg); }
|
|
197
|
-
td a { color: var(--link); text-decoration: none; }
|
|
198
|
-
td a:hover { color: var(--link); text-decoration: underline; }
|
|
199
|
-
td a:visited { color: var(--link-visited); }
|
|
200
|
-
.ic { width: 2.5rem; text-align: center; font-size: 1.125rem; }
|
|
201
|
-
.sz { width: 6rem; text-align: right; color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
202
|
-
.dt { color: var(--muted); white-space: nowrap; }
|
|
203
|
-
th.sz { text-align: right; }
|
|
204
|
-
.pop {
|
|
205
|
-
position: fixed; z-index: 9999; pointer-events: none;
|
|
206
|
-
width: 320px; background: var(--card);
|
|
207
|
-
border: 1px solid var(--border); border-radius: 12px;
|
|
208
|
-
box-shadow: 0 10px 30px rgba(0,0,0,.12);
|
|
209
|
-
overflow: hidden; opacity: 0;
|
|
210
|
-
transform: translateY(6px) scale(.985);
|
|
211
|
-
transition: opacity 150ms ease, transform 150ms ease;
|
|
212
|
-
}
|
|
213
|
-
.pop.on { opacity: 1; transform: none; }
|
|
214
|
-
.pop-h {
|
|
215
|
-
padding: 8px 14px; font-size: .6875rem; font-weight: 700;
|
|
216
|
-
letter-spacing: .06em; color: var(--muted); text-transform: uppercase;
|
|
217
|
-
border-bottom: 1px solid var(--border-row); background: var(--head-bg);
|
|
218
|
-
}
|
|
219
|
-
.pop-b { display: flex; align-items: center; justify-content: center; min-height: 100px; }
|
|
220
|
-
.pop-b img { display: block; max-width: 100%; max-height: 240px; }
|
|
221
|
-
@media (max-width: 640px) {
|
|
222
|
-
.wrap { margin: 1rem auto; }
|
|
223
|
-
td, th { padding: .5rem .75rem; }
|
|
224
|
-
.dt, th.dt { display: none; }
|
|
225
|
-
.bar { padding: .75rem 1rem; }
|
|
226
|
-
.pop { display: none; }
|
|
227
|
-
}
|
|
228
|
-
</style>
|
|
229
|
-
</head>
|
|
230
|
-
<body>
|
|
231
|
-
<div class="wrap">
|
|
232
|
-
<div class="card">
|
|
233
|
-
<div class="bar">
|
|
234
|
-
<div class="crumbs">#{breadcrumbs reqPath}</div>
|
|
235
|
-
<div class="meta">#{summary.join(' · ')}</div>
|
|
236
|
-
</div>
|
|
237
|
-
<table>
|
|
238
|
-
<thead><tr><th></th><th>Name</th><th class="sz">Size</th><th class="dt">Modified</th></tr></thead>
|
|
239
|
-
<tbody>
|
|
240
|
-
#{rows.join('\n')}
|
|
241
|
-
</tbody>
|
|
242
|
-
</table>
|
|
243
|
-
</div>
|
|
244
|
-
</div>
|
|
245
|
-
<script>
|
|
246
|
-
(function(){
|
|
247
|
-
var pop,cur,ox=16,oy=16;
|
|
248
|
-
function esc(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')}
|
|
249
|
-
function base(s){return decodeURIComponent(s.split('?')[0].split('/').pop())}
|
|
250
|
-
function mk(){
|
|
251
|
-
if(pop)return pop;
|
|
252
|
-
pop=document.createElement('div');pop.className='pop';
|
|
253
|
-
pop.innerHTML='<div class="pop-h"></div><div class="pop-b"></div>';
|
|
254
|
-
document.body.appendChild(pop);return pop;
|
|
255
|
-
}
|
|
256
|
-
function show(a,e){
|
|
257
|
-
var t=a.dataset.pv,s=a.dataset.src;if(!t||!s)return;
|
|
258
|
-
var el=mk(),h=el.firstChild,b=el.lastChild;
|
|
259
|
-
h.textContent='PREVIEW';b.innerHTML='';
|
|
260
|
-
var img=new Image();img.src=s;img.alt=base(s);b.appendChild(img);
|
|
261
|
-
cur=a;el.classList.add('on');place(e.clientX,e.clientY);
|
|
262
|
-
}
|
|
263
|
-
function hide(){cur=null;if(pop)pop.classList.remove('on')}
|
|
264
|
-
function place(x,y){
|
|
265
|
-
if(!pop)return;var r=pop.getBoundingClientRect(),p=12,l=x+ox,t=y+oy;
|
|
266
|
-
if(l+r.width>innerWidth-p)l=x-r.width-ox;
|
|
267
|
-
if(t+r.height>innerHeight-p)t=innerHeight-r.height-p;
|
|
268
|
-
if(l<p)l=p;if(t<p)t=p;
|
|
269
|
-
pop.style.left=l+'px';pop.style.top=t+'px';
|
|
270
|
-
}
|
|
271
|
-
document.addEventListener('mouseover',function(e){var a=e.target.closest('[data-pv]');if(a)show(a,e)});
|
|
272
|
-
document.addEventListener('mousemove',function(e){if(cur)place(e.clientX,e.clientY)});
|
|
273
|
-
document.addEventListener('mouseout',function(e){var a=e.target.closest('[data-pv]');if(a&&a===cur)hide()});
|
|
274
|
-
document.addEventListener('scroll',hide,{passive:true});
|
|
275
|
-
window.addEventListener('blur',hide);
|
|
276
|
-
})();
|
|
277
|
-
</script>
|
|
278
|
-
</body>
|
|
279
|
-
</html>
|
|
280
|
-
"""
|
|
281
|
-
|
|
282
|
-
renderMarkdown = (reqPath, fsPath) ->
|
|
283
|
-
md = readFileSync(fsPath, 'utf8')
|
|
284
|
-
body = Bun.markdown.html(md)
|
|
285
|
-
name = esc(basename(fsPath))
|
|
286
|
-
"""
|
|
287
|
-
<!DOCTYPE html>
|
|
288
|
-
<html lang="en">
|
|
289
|
-
<head>
|
|
290
|
-
<meta charset="UTF-8">
|
|
291
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
292
|
-
<title>#{name}</title>
|
|
293
|
-
<style>
|
|
294
|
-
:root {
|
|
295
|
-
--bg: #f3f6f7; --card: #fff; --border: #d1d9e0; --border-subtle: #d1d9e0b3;
|
|
296
|
-
--text: #1f2328; --muted: #59636e; --link: #0969da;
|
|
297
|
-
--head-bg: #fafbfc; --sep: #cbd5e1;
|
|
298
|
-
--code-bg: #f6f8fa; --code-inline: #818b981f;
|
|
299
|
-
--table-alt: #f6f8fa; --hr: #d1d9e0;
|
|
300
|
-
--mark-bg: #fff8c5; --mark-text: #1f2328;
|
|
301
|
-
}
|
|
302
|
-
@media (prefers-color-scheme: dark) {
|
|
303
|
-
:root {
|
|
304
|
-
--bg: #0d1117; --card: #161b22; --border: #30363d; --border-subtle: #30363db3;
|
|
305
|
-
--text: #e6edf3; --muted: #8b949e; --link: #58a6ff;
|
|
306
|
-
--head-bg: #1c2129; --sep: #484f58;
|
|
307
|
-
--code-bg: #161b22; --code-inline: #b1bac41f;
|
|
308
|
-
--table-alt: #161b22; --hr: #3d444d;
|
|
309
|
-
--mark-bg: #bb800926; --mark-text: #e6edf3;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
313
|
-
body {
|
|
314
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
|
|
315
|
-
font-size: 16px; line-height: 1.5; color: var(--text); background: var(--bg);
|
|
316
|
-
word-wrap: break-word;
|
|
317
|
-
}
|
|
318
|
-
.wrap { max-width: 820px; margin: 2.5rem auto; padding: 0 1rem; }
|
|
319
|
-
.card {
|
|
320
|
-
background: var(--card); border: 1px solid var(--border); border-radius: 8px;
|
|
321
|
-
overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04);
|
|
322
|
-
}
|
|
323
|
-
.bar {
|
|
324
|
-
display: flex; align-items: center; justify-content: space-between;
|
|
325
|
-
padding: .875rem 1.25rem; border-bottom: 1px solid var(--border);
|
|
326
|
-
}
|
|
327
|
-
.crumbs { font-size: .9375rem; font-weight: 500; }
|
|
328
|
-
.crumbs a { color: var(--link); text-decoration: none; }
|
|
329
|
-
.crumbs a:hover { text-decoration: underline; }
|
|
330
|
-
.sep { color: var(--sep); margin: 0 .3rem; font-weight: 400; }
|
|
331
|
-
article { padding: 2rem 2.5rem; }
|
|
332
|
-
article > *:first-child { margin-top: 0 !important; }
|
|
333
|
-
article > *:last-child { margin-bottom: 0 !important; }
|
|
334
|
-
article h1, article h2, article h3, article h4, article h5, article h6 {
|
|
335
|
-
margin-top: 1.5rem; margin-bottom: 1rem; font-weight: 600; line-height: 1.25;
|
|
336
|
-
}
|
|
337
|
-
article h1 { font-size: 2em; padding-bottom: .3em; border-bottom: 1px solid var(--border-subtle); }
|
|
338
|
-
article h2 { font-size: 1.5em; padding-bottom: .3em; border-bottom: 1px solid var(--border-subtle); }
|
|
339
|
-
article h3 { font-size: 1.25em; }
|
|
340
|
-
article h4 { font-size: 1em; }
|
|
341
|
-
article h5 { font-size: .875em; }
|
|
342
|
-
article h6 { font-size: .85em; color: var(--muted); }
|
|
343
|
-
article p, article blockquote, article ul, article ol, article dl, article table, article pre, article details {
|
|
344
|
-
margin-top: 0; margin-bottom: 1rem;
|
|
345
|
-
}
|
|
346
|
-
article a { color: var(--link); text-decoration: none; }
|
|
347
|
-
article a:hover { text-decoration: underline; }
|
|
348
|
-
article strong { font-weight: 600; }
|
|
349
|
-
article img { max-width: 100%; box-sizing: content-box; border-style: none; }
|
|
350
|
-
article ul, article ol { padding-left: 2em; }
|
|
351
|
-
article ol ol, article ul ol { list-style-type: lower-roman; }
|
|
352
|
-
article ul ul ol, article ul ol ol, article ol ul ol, article ol ol ol { list-style-type: lower-alpha; }
|
|
353
|
-
article ul ul, article ul ol, article ol ol, article ol ul { margin-top: 0; margin-bottom: 0; }
|
|
354
|
-
article li + li { margin-top: .25em; }
|
|
355
|
-
article li > p { margin-top: 1rem; }
|
|
356
|
-
article li > input[type="checkbox"] { margin-right: .5rem; }
|
|
357
|
-
article blockquote { padding: 0 1em; color: var(--muted); border-left: .25em solid var(--border); }
|
|
358
|
-
article blockquote > :first-child { margin-top: 0; }
|
|
359
|
-
article blockquote > :last-child { margin-bottom: 0; }
|
|
360
|
-
article mark { background-color: var(--mark-bg); color: var(--mark-text); }
|
|
361
|
-
article code, article tt {
|
|
362
|
-
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
|
|
363
|
-
font-size: 85%; padding: .2em .4em; margin: 0;
|
|
364
|
-
white-space: break-spaces; background-color: var(--code-inline); border-radius: 6px;
|
|
365
|
-
}
|
|
366
|
-
article pre {
|
|
367
|
-
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
|
|
368
|
-
font-size: 85%; line-height: 1.45; padding: 1rem;
|
|
369
|
-
overflow: auto; background-color: var(--code-bg); border: 1px solid var(--border); border-radius: 6px;
|
|
370
|
-
}
|
|
371
|
-
article pre code, article pre tt {
|
|
372
|
-
display: inline; padding: 0; margin: 0; overflow: visible;
|
|
373
|
-
line-height: inherit; word-wrap: normal; background-color: transparent;
|
|
374
|
-
border: 0; font-size: 100%; white-space: pre;
|
|
375
|
-
}
|
|
376
|
-
article table { display: block; width: max-content; max-width: 100%; overflow: auto; border-spacing: 0; border-collapse: collapse; }
|
|
377
|
-
article table th { font-weight: 600; }
|
|
378
|
-
article table th, article table td { padding: 6px 13px; border: 1px solid var(--border); }
|
|
379
|
-
article table tr { background-color: var(--card); border-top: 1px solid var(--border-subtle); }
|
|
380
|
-
article table tr:nth-child(2n) { background-color: var(--table-alt); }
|
|
381
|
-
article table td > :last-child { margin-bottom: 0; }
|
|
382
|
-
article hr {
|
|
383
|
-
height: .25em; padding: 0; margin: 1.5rem 0;
|
|
384
|
-
background-color: var(--hr); border: 0; overflow: hidden;
|
|
385
|
-
}
|
|
386
|
-
article del { color: var(--muted); }
|
|
387
|
-
article dl dt { padding: 0; margin-top: 1rem; font-size: 1em; font-style: italic; font-weight: 600; }
|
|
388
|
-
article dl dd { padding: 0 1rem; margin-bottom: 1rem; }
|
|
389
|
-
@media (max-width: 640px) { article { padding: 1.25rem 1rem; } .wrap { margin: 1rem auto; } }
|
|
390
|
-
</style>
|
|
391
|
-
</head>
|
|
392
|
-
<body>
|
|
393
|
-
<div class="wrap">
|
|
394
|
-
<div class="card">
|
|
395
|
-
<div class="bar">
|
|
396
|
-
<div class="crumbs">#{breadcrumbs reqPath}</div>
|
|
397
|
-
</div>
|
|
398
|
-
<article>#{body}</article>
|
|
399
|
-
</div>
|
|
400
|
-
</div>
|
|
401
|
-
</body>
|
|
402
|
-
</html>
|
|
403
|
-
"""
|
|
404
|
-
|
|
405
|
-
# --- Request Handler ---
|
|
406
|
-
|
|
407
|
-
notFound ->
|
|
408
|
-
reqPath = try decodeURIComponent(@req.path) catch then @req.path
|
|
409
|
-
path = resolve(root, reqPath.slice(1))
|
|
410
|
-
|
|
411
|
-
unless path is root or path.startsWith(rootSlash)
|
|
412
|
-
return new Response 'Not Found', { status: 404 }
|
|
413
|
-
|
|
414
|
-
try
|
|
415
|
-
stat = statSync(path)
|
|
416
|
-
catch
|
|
417
|
-
return new Response 'Not Found', { status: 404 }
|
|
418
|
-
|
|
419
|
-
if stat.isFile()
|
|
420
|
-
if path.endsWith('.md')
|
|
421
|
-
try
|
|
422
|
-
html = renderMarkdown(reqPath, path)
|
|
423
|
-
return new Response html, { headers: { 'Content-Type': 'text/html; charset=UTF-8' } }
|
|
424
|
-
return @send path
|
|
425
|
-
|
|
426
|
-
if stat.isDirectory()
|
|
427
|
-
return new Response(null, { status: 301, headers: { Location: "#{reqPath}/" } }) unless reqPath.endsWith('/')
|
|
428
|
-
indexPath = join(path, 'index.html')
|
|
429
|
-
if (try statSync(indexPath).isFile())
|
|
430
|
-
return @send indexPath, 'text/html; charset=UTF-8'
|
|
431
|
-
return new Response renderIndex(reqPath, path), { headers: { 'Content-Type': 'text/html; charset=UTF-8' } }
|
|
432
|
-
|
|
433
|
-
new Response 'Not Found', { status: 404 }
|
|
434
|
-
|
|
435
|
-
start()
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
# Edge Config Lifecycle
|
|
2
|
-
|
|
3
|
-
This document describes the implemented v1 lifecycle for `Edgefile.rip` changes.
|
|
4
|
-
|
|
5
|
-
## Goal
|
|
6
|
-
|
|
7
|
-
Config updates must be:
|
|
8
|
-
|
|
9
|
-
- atomic
|
|
10
|
-
- reversible
|
|
11
|
-
- observable
|
|
12
|
-
- safe for in-flight HTTP and websocket proxy traffic
|
|
13
|
-
|
|
14
|
-
## Apply pipeline
|
|
15
|
-
|
|
16
|
-
1. **Parse**
|
|
17
|
-
- Load `Edgefile.rip`.
|
|
18
|
-
- Evaluate it synchronously in config context.
|
|
19
|
-
- Reject on syntax or runtime errors.
|
|
20
|
-
|
|
21
|
-
2. **Validate**
|
|
22
|
-
- Require `version: 1`.
|
|
23
|
-
- Validate only supported top-level keys:
|
|
24
|
-
- `version`
|
|
25
|
-
- `edge`
|
|
26
|
-
- `upstreams`
|
|
27
|
-
- `apps`
|
|
28
|
-
- `routes`
|
|
29
|
-
- `sites`
|
|
30
|
-
- Validate route references, wildcard hosts, websocket route requirements,
|
|
31
|
-
timeout shapes, and verification policy.
|
|
32
|
-
|
|
33
|
-
3. **Normalize**
|
|
34
|
-
- Expand defaults from `CONTRACTS.md`.
|
|
35
|
-
- Normalize upstreams, managed apps, route rules, site groups, timeouts, and
|
|
36
|
-
verification policy.
|
|
37
|
-
- Compile the deterministic route table.
|
|
38
|
-
|
|
39
|
-
4. **Stage**
|
|
40
|
-
- Build a new edge runtime generation:
|
|
41
|
-
- upstream pool
|
|
42
|
-
- route table
|
|
43
|
-
- config metadata
|
|
44
|
-
- verification policy
|
|
45
|
-
- Prepare managed app registry changes for the new config.
|
|
46
|
-
- Do not affect active traffic yet.
|
|
47
|
-
|
|
48
|
-
5. **Activate**
|
|
49
|
-
- Swap the active edge runtime pointer atomically.
|
|
50
|
-
- Begin routing new requests through the new generation immediately.
|
|
51
|
-
- Keep the previous generation as a retired runtime for rollback or drain.
|
|
52
|
-
|
|
53
|
-
6. **Post-activate verify**
|
|
54
|
-
- Run route-aware verification according to `edge.verify`.
|
|
55
|
-
- Check referenced upstreams, healthy target counts, and managed app worker readiness.
|
|
56
|
-
- Emit structured activation events.
|
|
57
|
-
|
|
58
|
-
7. **Rollback (if needed)**
|
|
59
|
-
- Restore the previous app registry snapshot.
|
|
60
|
-
- Restore the previous active edge runtime.
|
|
61
|
-
- Mark the failed generation retired and let it drain if needed.
|
|
62
|
-
- Record structured rollback metadata including reason code and details.
|
|
63
|
-
|
|
64
|
-
## Trigger modes
|
|
65
|
-
|
|
66
|
-
- file watch (`Edgefile.rip`)
|
|
67
|
-
- `SIGHUP`
|
|
68
|
-
- control API: `POST /reload` on the control Unix socket
|
|
69
|
-
|
|
70
|
-
All triggers use the same runtime reload path and the same safeguards.
|
|
71
|
-
|
|
72
|
-
## Determinism rules
|
|
73
|
-
|
|
74
|
-
- No async work while evaluating config.
|
|
75
|
-
- No network I/O while evaluating config.
|
|
76
|
-
- Validation must be repeatable for identical input.
|
|
77
|
-
|
|
78
|
-
## Failure behavior
|
|
79
|
-
|
|
80
|
-
- Parse/validate failure: keep serving with the existing config.
|
|
81
|
-
- Stage failure: keep serving with the existing config.
|
|
82
|
-
- Post-activate verification failure: rollback to the previous runtime automatically.
|
|
83
|
-
|
|
84
|
-
## Draining behavior
|
|
85
|
-
|
|
86
|
-
- Retired runtimes remain alive while:
|
|
87
|
-
- HTTP requests that started on them are still in flight
|
|
88
|
-
- websocket proxy connections that started on them are still open
|
|
89
|
-
- Health checks are stopped only after a retired runtime finishes draining.
|
|
90
|
-
|
|
91
|
-
## Observability
|
|
92
|
-
|
|
93
|
-
Every reload attempt records:
|
|
94
|
-
|
|
95
|
-
- attempt ID
|
|
96
|
-
- source trigger (`startup`, `sighup`, `control_api`)
|
|
97
|
-
- old version
|
|
98
|
-
- new version
|
|
99
|
-
- result (`loaded`, `applied`, `rejected`, `rolled_back`)
|
|
100
|
-
- reason
|
|
101
|
-
- reason code
|
|
102
|
-
- reason details
|
|
103
|
-
- timestamp
|
|
104
|
-
|
|
105
|
-
`/diagnostics` exposes:
|
|
106
|
-
|
|
107
|
-
- active config metadata
|
|
108
|
-
- active runtime inflight and websocket counts
|
|
109
|
-
- retired runtimes still draining
|
|
110
|
-
- last reload attempt
|
|
111
|
-
- bounded reload history
|
package/docs/edge/CONTRACTS.md
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
# Edge Contracts
|
|
2
|
-
|
|
3
|
-
This document describes the implemented core contracts for the unified
|
|
4
|
-
edge/app runtime in `@rip-lang/server`.
|
|
5
|
-
|
|
6
|
-
## Source of truth inputs
|
|
7
|
-
|
|
8
|
-
- Runtime implementation: `packages/server/server.rip`
|
|
9
|
-
- Edge config normalization: `packages/server/edge/config.rip`
|
|
10
|
-
- Edge runtime lifecycle: `packages/server/edge/runtime.rip`
|
|
11
|
-
- Verification policy: `packages/server/edge/verify.rip`
|
|
12
|
-
- TLS findings: `packages/server/spikes/tls/FINDINGS.md`
|
|
13
|
-
|
|
14
|
-
## AppDescriptor
|
|
15
|
-
|
|
16
|
-
```ts
|
|
17
|
-
type AppDescriptor = {
|
|
18
|
-
id: string
|
|
19
|
-
entry: string | null
|
|
20
|
-
appBaseDir: string | null
|
|
21
|
-
hosts: string[]
|
|
22
|
-
workers: number | null
|
|
23
|
-
maxQueue: number
|
|
24
|
-
queueTimeoutMs: number
|
|
25
|
-
readTimeoutMs: number
|
|
26
|
-
env: Record<string, string>
|
|
27
|
-
}
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## WorkerEndpoint
|
|
31
|
-
|
|
32
|
-
```ts
|
|
33
|
-
type WorkerEndpoint = {
|
|
34
|
-
appId: string
|
|
35
|
-
workerId: number
|
|
36
|
-
socketPath: string
|
|
37
|
-
inflight: number
|
|
38
|
-
version: number | null
|
|
39
|
-
startedAt: number
|
|
40
|
-
}
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## RouteRule
|
|
44
|
-
|
|
45
|
-
```ts
|
|
46
|
-
type RouteRule = {
|
|
47
|
-
id: string
|
|
48
|
-
host: string | "*" | "*.example.com"
|
|
49
|
-
path: string
|
|
50
|
-
methods: string[] | "*"
|
|
51
|
-
priority: number
|
|
52
|
-
upstream?: string | null
|
|
53
|
-
app?: string | null
|
|
54
|
-
static?: string | null
|
|
55
|
-
redirect?: { to: string, status: number } | null
|
|
56
|
-
headers?: { set?: Record<string, string>, remove?: string[] } | null
|
|
57
|
-
websocket?: boolean
|
|
58
|
-
timeouts?: Partial<TimeoutPolicy>
|
|
59
|
-
}
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## VerifyPolicy
|
|
63
|
-
|
|
64
|
-
```ts
|
|
65
|
-
type VerifyPolicy = {
|
|
66
|
-
requireHealthyUpstreams: boolean
|
|
67
|
-
requireReadyApps: boolean
|
|
68
|
-
includeUnroutedManagedApps: boolean
|
|
69
|
-
minHealthyTargetsPerUpstream: number
|
|
70
|
-
}
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
Defaults:
|
|
74
|
-
|
|
75
|
-
- `requireHealthyUpstreams: true`
|
|
76
|
-
- `requireReadyApps: true`
|
|
77
|
-
- `includeUnroutedManagedApps: true`
|
|
78
|
-
- `minHealthyTargetsPerUpstream: 1`
|
|
79
|
-
|
|
80
|
-
## EdgeRuntime
|
|
81
|
-
|
|
82
|
-
```ts
|
|
83
|
-
type EdgeRuntime = {
|
|
84
|
-
id: string
|
|
85
|
-
upstreamPool: UpstreamPool
|
|
86
|
-
routeTable: RouteTable
|
|
87
|
-
configInfo: ConfigInfo
|
|
88
|
-
verifyPolicy: VerifyPolicy | null
|
|
89
|
-
inflight: number
|
|
90
|
-
wsConnections: number
|
|
91
|
-
retiredAt: string | null
|
|
92
|
-
}
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
## SchedulerDecision
|
|
96
|
-
|
|
97
|
-
```ts
|
|
98
|
-
type SchedulerDecision = {
|
|
99
|
-
appId: string
|
|
100
|
-
selectedWorker: WorkerEndpoint
|
|
101
|
-
algorithm: "least-inflight"
|
|
102
|
-
fallback: "queue" | "shed-503"
|
|
103
|
-
queuePosition: number | null
|
|
104
|
-
}
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
Defaults:
|
|
108
|
-
|
|
109
|
-
- `algorithm: "least-inflight"`
|
|
110
|
-
- `fallback: "queue"` while queue `< maxQueue`, else `"shed-503"`
|
|
111
|
-
- `queuePosition: null` when immediately assigned
|
|
112
|
-
|
|
113
|
-
## TlsCertRecord
|
|
114
|
-
|
|
115
|
-
```ts
|
|
116
|
-
type TlsCertRecord = {
|
|
117
|
-
id: string
|
|
118
|
-
domains: string[]
|
|
119
|
-
notBefore: number
|
|
120
|
-
notAfter: number
|
|
121
|
-
fingerprint: string
|
|
122
|
-
certPath: string
|
|
123
|
-
keyPath: string
|
|
124
|
-
source: "acme" | "manual" | "shipped"
|
|
125
|
-
lastRenewAttempt: number | null
|
|
126
|
-
lastRenewResult: "success" | "failed" | null
|
|
127
|
-
}
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
## TLS constraints
|
|
131
|
-
|
|
132
|
-
From `packages/server/spikes/tls/FINDINGS.md`:
|
|
133
|
-
|
|
134
|
-
- dynamic SNI selection was not observed
|
|
135
|
-
- in-process cert hot reload was not observed
|
|
136
|
-
- graceful restart cert activation works
|
|
137
|
-
- ACME HTTP-01 is the reliable v1 baseline
|