@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
@@ -0,0 +1,393 @@
1
+ # ==============================================================================
2
+ # serving/static.rip — static file serving and redirect route handlers
3
+ # ==============================================================================
4
+
5
+ import { resolve, join, basename, dirname } from 'node:path'
6
+ import { statSync, readdirSync, readFileSync } from 'node:fs'
7
+ import { mimeType } from '../api.rip'
8
+
9
+ findHljsRip = ->
10
+ bases = [
11
+ join(import.meta.dir, '..', '..', 'print', 'hljs-rip.js')
12
+ join(import.meta.dir, '..', '..', '..', 'print', 'hljs-rip.js')
13
+ ]
14
+ try bases.push(import.meta.resolve('@rip-lang/print/hljs-rip.js').replace(/^file:\/\//, ''))
15
+ for p in bases
16
+ try
17
+ statSync(p)
18
+ return p
19
+ null
20
+
21
+ HLJS_RIP_PATH = findHljsRip()
22
+
23
+ stripRoutePrefix = (pathname, routePath) ->
24
+ return pathname if routePath is '/' or routePath is '/*'
25
+ prefix = if routePath.endsWith('/*') then routePath.slice(0, -2) else routePath
26
+ if pathname.startsWith(prefix)
27
+ rest = pathname.slice(prefix.length)
28
+ return if rest is '' then '/' else rest
29
+ pathname
30
+
31
+ isSafeWithinRoot = (root, resolved) ->
32
+ rootSlash = if root.endsWith('/') then root else root + '/'
33
+ resolved is root or resolved.startsWith(rootSlash)
34
+
35
+ acceptsHtml = (req) ->
36
+ accept = req.headers?.get?('accept') or ''
37
+ accept.includes('text/html')
38
+
39
+ # --- Directory listing ---
40
+
41
+ esc = (s) -> s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
42
+
43
+ fmtSize = (n) ->
44
+ for u in ['B', 'KB', 'MB', 'GB']
45
+ if n < 1024
46
+ return if u is 'B' then "#{n} B" else "#{n.toFixed(1)} #{u}"
47
+ n /= 1024
48
+ "#{n.toFixed(1)} TB"
49
+
50
+ fmtDate = (ms) ->
51
+ d = new Date(ms)
52
+ mo = d.toLocaleString('en', { month: 'short' })
53
+ day = String(d.getDate()).padStart(2, '0')
54
+ "#{mo} #{day}, #{d.getFullYear()}"
55
+
56
+ IMG_EXTS = new Set ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'avif']
57
+ CODE_EXTS = new Set ['js', 'mjs', 'ts', 'rip', 'html', 'htm', 'css', 'json', 'xml', 'yaml', 'yml', 'sh']
58
+ DOC_EXTS = new Set ['pdf', 'md', 'txt', 'doc', 'docx', 'rtf']
59
+ ARCHIVE_EXTS = new Set ['zip', 'gz', 'tar', 'bz2', 'xz', '7z', 'rar']
60
+
61
+ SVG_FOLDER = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" fill="currentColor" fill-opacity=".08"/></svg>'
62
+ SVG_FILE = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>'
63
+ SVG_IMAGE = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>'
64
+ SVG_CODE = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>'
65
+ SVG_DOC = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>'
66
+ SVG_ARCHIVE = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>'
67
+ SVG_UP = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l-6-6 6-6"/><path d="M3 12h18"/></svg>'
68
+
69
+ fileIcon = (name) ->
70
+ ext = name.slice(name.lastIndexOf('.') + 1).toLowerCase()
71
+ if IMG_EXTS.has(ext) then SVG_IMAGE
72
+ else if CODE_EXTS.has(ext) then SVG_CODE
73
+ else if DOC_EXTS.has(ext) then SVG_DOC
74
+ else if ARCHIVE_EXTS.has(ext) then SVG_ARCHIVE
75
+ else SVG_FILE
76
+
77
+ previewAttr = (name, href) ->
78
+ ext = name.slice(name.lastIndexOf('.') + 1).toLowerCase()
79
+ if IMG_EXTS.has(ext) then " data-pv=\"image\" data-src=\"#{esc href}\"" else ''
80
+
81
+ dirRow = (ic, href, label, size = '', date = '', extra = '') ->
82
+ "<tr><td class=\"ic\">#{ic}</td><td class=\"nm\"><a href=\"#{esc href}\"#{extra}>#{esc label}</a></td><td class=\"sz\">#{size}</td><td class=\"dt\">#{date}</td></tr>"
83
+
84
+ export renderDirectoryListing = (reqPath, fsPath, rootName) ->
85
+ entries = try readdirSync(fsPath, { withFileTypes: true }) catch then []
86
+ dirs = []
87
+ files = []
88
+ for e in entries
89
+ continue if e.name.startsWith('.')
90
+ if e.isDirectory() then dirs.push(e.name) else files.push(e.name)
91
+ dirs.sort()
92
+ files.sort()
93
+
94
+ rows = []
95
+ rows.push dirRow(SVG_UP, '../', '..') if reqPath isnt '/'
96
+ for d in dirs
97
+ rows.push dirRow(SVG_FOLDER, "#{d}/", d)
98
+ for f in files
99
+ try
100
+ s = statSync(join(fsPath, f))
101
+ rows.push dirRow(fileIcon(f), f, f, fmtSize(s.size), fmtDate(s.mtimeMs), previewAttr(f, f))
102
+ catch
103
+ rows.push dirRow(fileIcon(f), f, f)
104
+
105
+ numDirs = dirs.length
106
+ numFiles = files.length
107
+ counts = []
108
+ counts.push "#{numDirs} folder#{if numDirs isnt 1 then 's' else ''}" if numDirs > 0
109
+ counts.push "#{numFiles} file#{if numFiles isnt 1 then 's' else ''}" if numFiles > 0
110
+
111
+ crumbs = "<a href=\"/\">#{esc rootName}</a>"
112
+ href = ''
113
+ for part in reqPath.split('/').filter((p) -> p)
114
+ href += "/#{part}"
115
+ crumbs += "<span class=\"sep\">/</span><a href=\"#{href}/\">#{esc part}</a>"
116
+
117
+ """
118
+ <!DOCTYPE html>
119
+ <html lang="en">
120
+ <head>
121
+ <meta charset="UTF-8">
122
+ <meta name="viewport" content="width=device-width, initial-scale=1">
123
+ <title>#{esc(if reqPath is '/' then rootName else reqPath.split('/').filter((p) -> p).slice(-1)[0] or rootName)}</title>
124
+ <style>
125
+ :root {
126
+ --bg: #f8f9fa; --card: #fff; --border: #e2e5e9; --row-border: #f0f1f3;
127
+ --text: #1a1d21; --secondary: #6b7280; --icon: #9ca3af;
128
+ --link: #2563eb; --link-visited: #7c3aed; --link-hover: #1d4ed8;
129
+ --hover: #f0f5ff; --sep: #cbd5e1; --bar-bg: #f3f4f6; --head-bg: #f9fafb;
130
+ --radius: 10px; --shadow: 0 1px 3px rgba(0,0,0,.05), 0 0 0 1px rgba(0,0,0,.03);
131
+ }
132
+ @media (prefers-color-scheme: dark) { :root {
133
+ --bg: #0a0a0c; --card: #161618; --border: #2a2a2e; --row-border: #1e1e22;
134
+ --text: #e4e4e7; --secondary: #71717a; --icon: #52525b;
135
+ --link: #60a5fa; --link-visited: #a78bfa; --link-hover: #93c5fd;
136
+ --hover: #1a1d24; --sep: #3f3f46; --bar-bg: #1c1c1f; --head-bg: #19191c;
137
+ --shadow: 0 1px 3px rgba(0,0,0,.3), 0 0 0 1px rgba(255,255,255,.04);
138
+ }}
139
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
140
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui, sans-serif;
141
+ font-size: 14px; line-height: 1.5; color: var(--text); background: var(--bg);
142
+ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
143
+ .wrap { max-width: 880px; margin: 3rem auto; padding: 0 1.25rem; }
144
+ .card { background: var(--card); border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden; }
145
+ .bar { display: flex; align-items: center; justify-content: space-between;
146
+ padding: .75rem 1rem; gap: .75rem; border-bottom: 1px solid var(--border); background: var(--bar-bg); }
147
+ .crumbs { font-size: 13px; font-weight: 500; display: flex; align-items: center; gap: 0; flex-wrap: wrap; }
148
+ .crumbs a { color: var(--link); text-decoration: none; padding: 2px 6px; border-radius: 4px; transition: all 120ms; }
149
+ .crumbs a:last-child { color: var(--text); font-weight: 600; }
150
+ .crumbs a:hover { color: var(--link-hover); background: var(--hover); }
151
+ .sep { color: var(--sep); font-size: 11px; margin: 0 1px; user-select: none; }
152
+ .meta { font-size: 12px; color: var(--secondary); white-space: nowrap; font-weight: 400; }
153
+ table { width: 100%; border-collapse: collapse; }
154
+ th { background: var(--head-bg); text-align: left; padding: 6px 1rem; font-size: 11px;
155
+ font-weight: 500; color: var(--secondary); letter-spacing: .03em; border-bottom: 1px solid var(--border); }
156
+ th.sz { text-align: right; }
157
+ td { padding: 7px 1rem; vertical-align: middle; font-size: 13px; border-bottom: 1px solid var(--row-border); }
158
+ tr:last-child td { border-bottom: none; }
159
+ tbody tr { position: relative; transition: background 80ms; cursor: pointer; }
160
+ tbody tr:hover { background: var(--hover); }
161
+ .ic { width: 2.25rem; text-align: center; color: var(--icon); line-height: 0; }
162
+ .ic svg { vertical-align: middle; }
163
+ .nm a { color: var(--link); text-decoration: none; font-weight: 450; letter-spacing: -.01em; }
164
+ .nm a::after { content: ''; position: absolute; inset: 0; }
165
+ .nm a:visited { color: var(--link-visited); }
166
+ .nm a:hover { color: var(--link-hover); }
167
+ .sz { width: 5.5rem; text-align: right; color: var(--secondary); white-space: nowrap;
168
+ font-variant-numeric: tabular-nums; font-size: 12px; letter-spacing: .01em; }
169
+ .dt { color: var(--secondary); white-space: nowrap; font-size: 12px; }
170
+ .pop { position: fixed; z-index: 9999; pointer-events: none; width: 300px;
171
+ background: var(--card); border-radius: 10px; box-shadow: 0 20px 40px rgba(0,0,0,.15), 0 0 0 1px rgba(0,0,0,.05);
172
+ overflow: hidden; opacity: 0; transform: translateY(4px) scale(.98);
173
+ transition: opacity 120ms ease, transform 120ms ease; }
174
+ .pop.on { opacity: 1; transform: none; }
175
+ .pop-b { display: flex; align-items: center; justify-content: center; min-height: 80px; padding: 8px; }
176
+ .pop-b img { display: block; max-width: 100%; max-height: 220px; border-radius: 4px; }
177
+ @media (max-width: 640px) {
178
+ .wrap { margin: 1rem auto; } td { padding: 8px .75rem; }
179
+ .dt { display: none; } .bar { padding: .625rem .75rem; } .pop { display: none; }
180
+ }
181
+ </style>
182
+ </head>
183
+ <body>
184
+ <div class="wrap"><div class="card">
185
+ <div class="bar"><div class="crumbs">#{crumbs}</div><div class="meta">#{counts.join(' · ')}</div></div>
186
+ <table>
187
+ <thead><tr><th></th><th>Name</th><th class="sz">Size</th><th class="dt">Modified</th></tr></thead>
188
+ <tbody>#{rows.join('\n')}</tbody>
189
+ </table>
190
+ </div></div>
191
+ <script>
192
+ !function(){var p,c,ox=14,oy=14;function mk(){if(p)return p;p=document.createElement('div');p.className='pop';p.innerHTML='<div class="pop-b"></div>';document.body.appendChild(p);return p}function show(a,e){var s=a.dataset.src;if(!s)return;var el=mk(),b=el.firstChild;b.innerHTML='';var img=new Image();img.src=s;b.appendChild(img);c=a;el.classList.add('on');place(e.clientX,e.clientY)}function hide(){c=null;if(p)p.classList.remove('on')}function place(x,y){if(!p)return;var r=p.getBoundingClientRect(),m=12,l=x+ox,t=y+oy;if(l+r.width>innerWidth-m)l=x-r.width-ox;if(t+r.height>innerHeight-m)t=innerHeight-r.height-m;if(l<m)l=m;if(t<m)t=m;p.style.left=l+'px';p.style.top=t+'px'}document.addEventListener('mouseover',function(e){var a=e.target.closest('[data-pv]');if(a)show(a,e)});document.addEventListener('mousemove',function(e){if(c)place(e.clientX,e.clientY)});document.addEventListener('mouseout',function(e){var a=e.target.closest('[data-pv]');if(a&&a===c)hide()});document.addEventListener('scroll',hide,{passive:true});document.addEventListener('keydown',function(e){if(e.key==='Escape')hide()});window.addEventListener('blur',hide)}();
193
+ </script>
194
+ </body>
195
+ </html>
196
+ """
197
+
198
+ # --- Markdown rendering ---
199
+
200
+ export renderMarkdown = (filePath) ->
201
+ md = readFileSync(filePath, 'utf8')
202
+ body = Bun.markdown.html(md)
203
+ name = esc(basename(filePath))
204
+ """
205
+ <!DOCTYPE html>
206
+ <html lang="en">
207
+ <head>
208
+ <meta charset="UTF-8">
209
+ <meta name="viewport" content="width=device-width, initial-scale=1">
210
+ <title>#{name}</title>
211
+ <style>
212
+ :root { --bg: #fff; --text: #1a1d21; --muted: #6b7280; --link: #2563eb; --code-bg: #f4f4f5; --border: #e2e5e9; --pre-bg: #fafafa; }
213
+ @media (prefers-color-scheme: dark) { :root { --bg: #0a0a0c; --text: #e4e4e7; --muted: #71717a; --link: #60a5fa; --code-bg: #1e1e22; --border: #2a2a2e; --pre-bg: #161618; } }
214
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
215
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui, sans-serif;
216
+ font-size: 16px; line-height: 1.7; color: var(--text); background: var(--bg);
217
+ max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem;
218
+ -webkit-font-smoothing: antialiased; }
219
+ h1, h2, h3, h4 { margin: 1.75em 0 .5em; line-height: 1.3; }
220
+ h1 { font-size: 1.75rem; } h2 { font-size: 1.375rem; } h3 { font-size: 1.125rem; }
221
+ p, ul, ol, blockquote, pre, table { margin: 0 0 1em; }
222
+ a { color: var(--link); text-decoration: none; } a:hover { text-decoration: underline; }
223
+ code { font-family: 'SF Mono', 'Fira Code', monospace; font-size: .875em; background: var(--code-bg); padding: 2px 5px; border-radius: 4px; }
224
+ pre { background: var(--pre-bg); border: 1px solid var(--border); border-radius: 8px; padding: 1rem; overflow-x: auto; }
225
+ pre code { background: none; padding: 0; font-size: .8125rem; }
226
+ blockquote { border-left: 3px solid var(--border); padding-left: 1rem; color: var(--muted); }
227
+ img { max-width: 100%; border-radius: 6px; }
228
+ table { border-collapse: collapse; width: 100%; }
229
+ th, td { padding: .5rem .75rem; border: 1px solid var(--border); text-align: left; font-size: .875rem; }
230
+ th { font-weight: 600; background: var(--pre-bg); }
231
+ hr { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }
232
+ @page { margin: 0.5in; }
233
+ @media print {
234
+ body { background: #fff; color: #1a1a1a; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
235
+ pre { background: #f4f4f5; box-shadow: inset 0 0 0 1000px #f4f4f5; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
236
+ code { background: #f0f0f2; box-shadow: inset 0 0 0 1000px #f0f0f2; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
237
+ pre code { background: none; box-shadow: none; }
238
+ th { background: #f6f8fa; box-shadow: inset 0 0 0 1000px #f6f8fa; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
239
+ a { color: #2563eb; }
240
+ }
241
+ </style>
242
+ </head>
243
+ <body>#{body}</body>
244
+ </html>
245
+ """
246
+
247
+ # --- Text file rendering with syntax highlighting ---
248
+
249
+ TEXT_EXTS = new Set [
250
+ 'js', 'mjs', 'cjs', 'ts', 'tsx', 'jsx', 'json', 'jsonc'
251
+ 'css', 'scss', 'less', 'html', 'htm', 'xml', 'svg'
252
+ 'rip', 'coffee', 'rb', 'py', 'go', 'rs', 'c', 'cpp', 'h', 'hpp', 'java', 'kt', 'swift', 'cs'
253
+ 'sh', 'bash', 'zsh', 'fish', 'ps1', 'bat', 'cmd'
254
+ 'yaml', 'yml', 'toml', 'ini', 'conf', 'cfg', 'env'
255
+ 'md', 'txt', 'log', 'csv', 'tsv', 'diff', 'patch'
256
+ 'sql', 'graphql', 'gql', 'prisma'
257
+ 'dockerfile', 'makefile', 'gemfile', 'rakefile'
258
+ 'gitignore', 'gitattributes', 'editorconfig', 'eslintrc', 'prettierrc'
259
+ 'lock', 'license', 'licence', 'readme', 'changelog', 'contributing', 'authors'
260
+ ]
261
+
262
+ HLJS_LANG_MAP =
263
+ js: 'javascript', mjs: 'javascript', cjs: 'javascript', jsx: 'javascript'
264
+ ts: 'typescript', tsx: 'typescript'
265
+ rb: 'ruby', py: 'python', rs: 'rust', go: 'go', sh: 'bash', zsh: 'bash'
266
+ yml: 'yaml', toml: 'ini', conf: 'ini', cfg: 'ini'
267
+ rip: 'rip', coffee: 'coffeescript'
268
+ md: 'markdown', htm: 'xml', svg: 'xml'
269
+ dockerfile: 'dockerfile', makefile: 'makefile'
270
+ jsonc: 'json', gql: 'graphql'
271
+
272
+ export isTextFile = (filePath) ->
273
+ name = basename(filePath).toLowerCase()
274
+ ext = name.slice(name.lastIndexOf('.') + 1)
275
+ TEXT_EXTS.has(ext) or TEXT_EXTS.has(name)
276
+
277
+ hljsLang = (filePath) ->
278
+ name = basename(filePath).toLowerCase()
279
+ ext = name.slice(name.lastIndexOf('.') + 1)
280
+ HLJS_LANG_MAP[ext] or HLJS_LANG_MAP[name] or ext
281
+
282
+ export renderTextFile = (filePath) ->
283
+ content = readFileSync(filePath, 'utf8')
284
+ name = basename(filePath)
285
+ lang = hljsLang(filePath)
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>#{esc name}</title>
293
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github.min.css" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)">
294
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css" media="(prefers-color-scheme: dark)">
295
+ <style>
296
+ :root { --bg: #fff; --text: #1a1d21; --border: #e2e5e9; --bar-bg: #f3f4f6; --secondary: #6b7280; }
297
+ @media (prefers-color-scheme: dark) { :root { --bg: #0d1117; --text: #e4e4e7; --border: #2a2a2e; --bar-bg: #161618; --secondary: #71717a; } }
298
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
299
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif; background: var(--bg); color: var(--text); }
300
+ .wrap { max-width: 960px; margin: 3rem auto; padding: 0 1.25rem; }
301
+ .card { border: 1px solid var(--border); border-radius: 10px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,.05); }
302
+ .bar { display: flex; align-items: center; justify-content: space-between; padding: .625rem 1.25rem; background: var(--bar-bg); border-bottom: 1px solid var(--border); }
303
+ .name { font-size: 13px; font-weight: 600; }
304
+ .meta { font-size: 12px; color: var(--secondary); }
305
+ pre { margin: 0; border: none; border-radius: 0; }
306
+ pre code, pre code.hljs { display: block; padding: 1rem 1.25rem 1.5rem; font-size: 13px; line-height: 1.6; font-family: ui-monospace, 'SF Mono', 'Fira Code', monospace; overflow-x: auto; }
307
+ @page { margin: 0.5in; }
308
+ @media print { .bar { background: #f3f4f6; box-shadow: inset 0 0 0 1000px #f3f4f6; -webkit-print-color-adjust: exact; print-color-adjust: exact; } }
309
+ </style>
310
+ </head>
311
+ <body>
312
+ <div class="wrap"><div class="card">
313
+ <div class="bar"><span class="name">#{esc name}</span><span class="meta">#{n = content.split('\\n').length}#{n} line#{if n isnt 1 then 's' else ''}</span></div>
314
+ <pre><code class="language-#{lang}">#{esc content}</code></pre>
315
+ </div></div>
316
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
317
+ #{if lang is 'rip' and HLJS_RIP_PATH then '<script src="/_rip/hljs-rip.js"></script><script>hljs.registerLanguage("rip",hljsRip);hljs.highlightAll()</script>' else '<script>hljs.highlightAll()</script>'}
318
+ </body>
319
+ </html>
320
+ """
321
+
322
+ # --- Static file serving ---
323
+
324
+ export serveRipHighlightGrammar = ->
325
+ return null unless HLJS_RIP_PATH
326
+ content = readFileSync(HLJS_RIP_PATH, 'utf8')
327
+ wrapped = content.replace('export default function', 'var hljsRip = function')
328
+ file = Bun.file(HLJS_RIP_PATH)
329
+ etag = "W/\"#{file.lastModified}-#{file.size}\""
330
+ new Response(wrapped, { headers: { 'content-type': 'application/javascript; charset=UTF-8', 'etag': etag, 'cache-control': 'public, max-age=86400' } })
331
+
332
+ export serveStaticRoute = (req, url, route) ->
333
+ method = req.method or 'GET'
334
+ return new Response(null, { status: 405 }) unless method is 'GET' or method is 'HEAD'
335
+
336
+ if url.pathname is '/_rip/hljs-rip.js'
337
+ res = serveRipHighlightGrammar()
338
+ return res if res
339
+
340
+ base = route.root or route.static
341
+ return new Response('Not Found', { status: 404 }) unless base
342
+
343
+ staticDir = if route.static and route.static isnt '.'
344
+ if route.static.startsWith('/') then route.static else resolve(base, route.static)
345
+ else
346
+ base
347
+
348
+ pathname = try decodeURIComponent(url.pathname) catch then url.pathname
349
+ relative = stripRoutePrefix(pathname, route.path or '/*')
350
+ filePath = resolve(staticDir, '.' + relative)
351
+
352
+ return new Response('Forbidden', { status: 403 }) unless isSafeWithinRoot(staticDir, filePath)
353
+
354
+ try
355
+ stat = statSync(filePath)
356
+ if stat.isDirectory()
357
+ unless pathname.endsWith('/')
358
+ return Response.redirect("#{pathname}/", 301)
359
+ indexPath = join(filePath, 'index.html')
360
+ try
361
+ indexStat = statSync(indexPath)
362
+ if indexStat.isFile()
363
+ file = Bun.file(indexPath)
364
+ return new Response(file, { headers: { 'content-type': 'text/html; charset=UTF-8' } })
365
+ if route.browse and acceptsHtml(req)
366
+ rootName = basename(staticDir)
367
+ html = renderDirectoryListing(pathname, filePath, rootName)
368
+ return new Response(html, { headers: { 'content-type': 'text/html; charset=UTF-8' } })
369
+ if stat.isFile()
370
+ if route.browse and acceptsHtml(req)
371
+ if filePath.endsWith('.md')
372
+ html = renderMarkdown(filePath)
373
+ return new Response(html, { headers: { 'content-type': 'text/html; charset=UTF-8' } })
374
+ if isTextFile(filePath)
375
+ html = renderTextFile(filePath)
376
+ return new Response(html, { headers: { 'content-type': 'text/html; charset=UTF-8' } })
377
+ file = Bun.file(filePath)
378
+ return new Response(file, { headers: { 'content-type': mimeType(filePath) } })
379
+
380
+ if route.spa and acceptsHtml(req)
381
+ spaPath = join(staticDir, 'index.html')
382
+ try
383
+ spaStat = statSync(spaPath)
384
+ if spaStat.isFile()
385
+ file = Bun.file(spaPath)
386
+ return new Response(file, { headers: { 'content-type': 'text/html; charset=UTF-8' } })
387
+
388
+ new Response('Not Found', { status: 404 })
389
+
390
+ export buildRedirectResponse = (req, url, route) ->
391
+ target = route.redirect?.to or '/'
392
+ status = route.redirect?.status or 302
393
+ Response.redirect(target, status)
@@ -1,5 +1,5 @@
1
1
  # ==============================================================================
2
- # edge/tls.rip — TLS material loading helpers
2
+ # serving/tls.rip — TLS material loading helpers
3
3
  # ==============================================================================
4
4
 
5
5
  import { readFileSync } from 'node:fs'
@@ -29,13 +29,13 @@ export buildTlsArray = (defaultMaterial, certMap) ->
29
29
  result.push({ serverName: fallbackName, cert: defaultMaterial.cert, key: defaultMaterial.key })
30
30
  result
31
31
 
32
- export printCertSummary = (certPem) ->
32
+ export printCertSummary = (certPem, quiet = false) ->
33
33
  try
34
34
  x = new X509Certificate(certPem)
35
35
  subject = x.subject.split(/,/)[0]?.trim() or x.subject
36
36
  issuer = x.issuer.split(/,/)[0]?.trim() or x.issuer
37
37
  exp = new Date(x.validTo)
38
- p "rip-server: tls cert #{subject} issued by #{issuer} expires #{exp.toISOString()}"
38
+ p "rip-server: tls cert #{subject} issued by #{issuer} expires #{exp.toISOString()}" unless quiet
39
39
 
40
40
  export loadTlsMaterial = (flags, serverDir, acmeLoadCertFn) ->
41
41
  # Explicit cert/key paths
@@ -43,7 +43,6 @@ export loadTlsMaterial = (flags, serverDir, acmeLoadCertFn) ->
43
43
  try
44
44
  cert = readFileSync(flags.certPath, 'utf8')
45
45
  key = readFileSync(flags.keyPath, 'utf8')
46
- printCertSummary(cert)
47
46
  return { cert, key }
48
47
  catch
49
48
  console.error 'Failed to read TLS cert/key from provided paths. Use http or fix paths.'
@@ -53,8 +52,6 @@ export loadTlsMaterial = (flags, serverDir, acmeLoadCertFn) ->
53
52
  if acmeLoadCertFn and (flags.acme or flags.acmeStaging) and flags.acmeDomain
54
53
  acmeCert = acmeLoadCertFn(flags.acmeDomain)
55
54
  if acmeCert
56
- p "rip-server: using ACME cert for #{flags.acmeDomain}"
57
- printCertSummary(acmeCert.cert)
58
55
  return { cert: acmeCert.cert, key: acmeCert.key }
59
56
 
60
57
  # Shipped wildcard cert for *.ripdev.io (GlobalSign, valid for all subdomains)
@@ -64,7 +61,6 @@ export loadTlsMaterial = (flags, serverDir, acmeLoadCertFn) ->
64
61
  try
65
62
  cert = readFileSync(certPath, 'utf8')
66
63
  key = readFileSync(keyPath, 'utf8')
67
- printCertSummary(cert)
68
64
  return { cert, key }
69
65
  catch e
70
66
  console.error "rip-server: failed to load TLS certs from #{certsDir}: #{e.message}"
@@ -1,5 +1,5 @@
1
1
  # ==============================================================================
2
- # edge/upstream.rip — upstream pools, health checks, and retry helpers
2
+ # serving/upstream.rip — upstream pools, health checks, and retry helpers
3
3
  # ==============================================================================
4
4
 
5
5
  DEFAULT_HEALTH =
@@ -228,12 +228,12 @@ export checkTargetHealth = (pool, target, fetchFn = null) ->
228
228
  runFetch = fetchFn or pool.fetchFn or fetch
229
229
  url = target.url + target.health.path
230
230
  try
231
- res = runFetch(url)
232
- res = res! if res?.then
231
+ res = await runFetch(url)
233
232
  target.lastStatus = res.status
234
233
  updateTargetHealth(target, res.ok, pool)
235
234
  res.ok
236
- catch
235
+ catch e
236
+ console.error "[health] #{url} failed: #{e?.message or e}" if process.env.RIP_DEBUG?
237
237
  target.lastStatus = null
238
238
  updateTargetHealth(target, false, pool)
239
239
  false
@@ -1,29 +1,29 @@
1
1
  # ==============================================================================
2
- # edge/verify.rip — edge runtime verification helpers
2
+ # serving/verify.rip — post-activate runtime verification
3
3
  # ==============================================================================
4
4
 
5
5
  export buildVerificationResult = (ok, code = null, message = null, details = null) ->
6
6
  { ok, code, message, details }
7
7
 
8
8
  defaultPolicy =
9
- requireHealthyUpstreams: true
9
+ requireHealthyProxies: true
10
10
  requireReadyApps: true
11
11
  includeUnroutedManagedApps: true
12
- minHealthyTargetsPerUpstream: 1
12
+ minHealthyTargetsPerProxy: 1
13
13
 
14
14
  mergePolicy = (policy = {}) ->
15
- requireHealthyUpstreams: if policy.requireHealthyUpstreams? then policy.requireHealthyUpstreams is true else defaultPolicy.requireHealthyUpstreams
15
+ requireHealthyProxies: if policy.requireHealthyProxies? then policy.requireHealthyProxies is true else defaultPolicy.requireHealthyProxies
16
16
  requireReadyApps: if policy.requireReadyApps? then policy.requireReadyApps is true else defaultPolicy.requireReadyApps
17
17
  includeUnroutedManagedApps: if policy.includeUnroutedManagedApps? then policy.includeUnroutedManagedApps is true else defaultPolicy.includeUnroutedManagedApps
18
- minHealthyTargetsPerUpstream: Math.max(1, parseInt(String(policy.minHealthyTargetsPerUpstream or defaultPolicy.minHealthyTargetsPerUpstream)) or 1)
18
+ minHealthyTargetsPerProxy: Math.max(1, parseInt(String(policy.minHealthyTargetsPerProxy or defaultPolicy.minHealthyTargetsPerProxy)) or 1)
19
19
 
20
20
  export collectRouteRequirements = (routeTable, appRegistry = null, defaultAppId = null, policy = {}) ->
21
21
  policy = mergePolicy(policy)
22
- upstreamIds = new Set()
22
+ proxyIds = new Set()
23
23
  appIds = new Set()
24
24
 
25
25
  for route in (routeTable?.routes or [])
26
- upstreamIds.add(route.upstream) if route.upstream
26
+ proxyIds.add(route.proxy) if route.proxy
27
27
  appIds.add(route.app) if route.app
28
28
 
29
29
  if appRegistry and policy.includeUnroutedManagedApps
@@ -33,7 +33,7 @@ export collectRouteRequirements = (routeTable, appRegistry = null, defaultAppId
33
33
  appIds.add(appId)
34
34
 
35
35
  {
36
- upstreamIds: Array.from(upstreamIds)
36
+ proxyIds: Array.from(proxyIds)
37
37
  appIds: Array.from(appIds)
38
38
  }
39
39
 
@@ -41,21 +41,21 @@ export verifyRouteRuntime = (runtime, appRegistry, defaultAppId, getUpstreamFn,
41
41
  policy = mergePolicy(policy)
42
42
  requirements = collectRouteRequirements(runtime?.routeTable, appRegistry, defaultAppId, policy)
43
43
 
44
- if policy.requireHealthyUpstreams
45
- for upstreamId in requirements.upstreamIds
46
- upstream = getUpstreamFn(runtime.upstreamPool, upstreamId)
47
- return buildVerificationResult(false, 'upstream_missing', "Referenced upstream #{upstreamId} is missing", { upstreamId }) unless upstream
44
+ if policy.requireHealthyProxies
45
+ for proxyId in requirements.proxyIds
46
+ upstream = getUpstreamFn(runtime.upstreamPool, proxyId)
47
+ return buildVerificationResult(false, 'proxy_missing', "Referenced proxy #{proxyId} is missing", { proxyId }) unless upstream
48
48
 
49
49
  healthyTargets = []
50
50
  for target in upstream.targets
51
51
  healthyTargets.push(target.targetId) if checkTargetHealthFn(runtime.upstreamPool, target)
52
52
 
53
- unless healthyTargets.length >= policy.minHealthyTargetsPerUpstream
53
+ unless healthyTargets.length >= policy.minHealthyTargetsPerProxy
54
54
  return buildVerificationResult(
55
55
  false
56
- 'upstream_no_healthy_targets'
57
- "Upstream #{upstreamId} has too few healthy targets after activation"
58
- { upstreamId, targetCount: upstream.targets.length, healthyTargets: healthyTargets.length, requiredHealthyTargets: policy.minHealthyTargetsPerUpstream }
56
+ 'proxy_no_healthy_targets'
57
+ "Proxy #{proxyId} has too few healthy targets after activation"
58
+ { proxyId, targetCount: upstream.targets.length, healthyTargets: healthyTargets.length, requiredHealthyTargets: policy.minHealthyTargetsPerProxy }
59
59
  )
60
60
 
61
61
  if policy.requireReadyApps
@@ -1,5 +1,5 @@
1
1
  # ==============================================================================
2
- # streams/tls_clienthello.rip — minimal ClientHello SNI extractor
2
+ # streams/clienthello.rip — minimal ClientHello SNI extractor
3
3
  # ==============================================================================
4
4
 
5
5
  MAX_CLIENT_HELLO_BYTES = 16384
@@ -93,28 +93,28 @@ normalizeStreamTimeouts = (value, path, errors) ->
93
93
  connectMs: normalizePositiveInt(value.connectMs, 5000, "#{path}.connectMs", errors, 'connectMs')
94
94
  }
95
95
 
96
- export normalizeStreams = (rawStreams, knownUpstreams, errors, path = 'streams') ->
96
+ export normalizeStreams = (rawStreams, knownProxies, errors, path = 'streams') ->
97
97
  streams = []
98
98
  return streams unless rawStreams?
99
99
  unless Array.isArray(rawStreams)
100
- pushError(errors, 'E_STREAMS_TYPE', path, 'streams must be an array', "Use `streams: [{ listen: 8443, sni: ['incus.example.com'], upstream: 'incus' }]`.")
100
+ pushError(errors, 'E_STREAMS_TYPE', path, 'streams must be an array', "Use `streams: [{ listen: 8443, sni: ['incus.example.com'], proxy: 'incus' }]`.")
101
101
  return streams
102
102
  for route, idx in rawStreams
103
103
  itemPath = "#{path}[#{idx}]"
104
104
  unless isPlainObject(route)
105
- pushError(errors, 'E_STREAM_ROUTE_TYPE', itemPath, 'stream route must be an object', "Use `{ listen: 8443, sni: ['incus.example.com'], upstream: 'incus' }`.")
105
+ pushError(errors, 'E_STREAM_ROUTE_TYPE', itemPath, 'stream route must be an object', "Use `{ listen: 8443, sni: ['incus.example.com'], proxy: 'incus' }`.")
106
106
  continue
107
107
  listen = normalizePositiveInt(route.listen, 0, "#{itemPath}.listen", errors, 'listen')
108
108
  pushError(errors, 'E_STREAM_LISTEN_RANGE', "#{itemPath}.listen", 'listen port must be between 1 and 65535', 'Choose a valid TCP port.') unless listen > 0 and listen <= 65535
109
- unless typeof route.upstream is 'string' and route.upstream
110
- pushError(errors, 'E_STREAM_UPSTREAM_REF', "#{itemPath}.upstream", 'stream route must reference an upstream', "Use `upstream: 'incus'`.")
111
- else unless knownUpstreams.has(route.upstream)
112
- pushError(errors, 'E_STREAM_UPSTREAM_REF', "#{itemPath}.upstream", "unknown stream upstream #{route.upstream}", 'Define the upstream under `streamUpstreams` first.')
109
+ unless typeof route.proxy is 'string' and route.proxy
110
+ pushError(errors, 'E_STREAM_PROXY_REF', "#{itemPath}.proxy", 'stream route must reference a TCP proxy', "Use `proxy: 'incus'`.")
111
+ else unless knownProxies.has(route.proxy)
112
+ pushError(errors, 'E_STREAM_PROXY_REF', "#{itemPath}.proxy", "unknown TCP proxy #{route.proxy}", 'Define the proxy under `proxies` with host:port entries first.')
113
113
  streams.push
114
114
  id: route.id or "stream-#{idx + 1}"
115
115
  order: idx
116
116
  listen: listen
117
117
  sni: normalizeSniPatterns(route.sni, "#{itemPath}.sni", errors)
118
- upstream: route.upstream or null
118
+ proxy: route.proxy or null
119
119
  timeouts: normalizeStreamTimeouts(route.timeouts, "#{itemPath}.timeouts", errors)
120
120
  streams
package/streams/index.rip CHANGED
@@ -2,8 +2,8 @@
2
2
  # streams/index.rip — stream runtime facade and listener orchestration
3
3
  # ==============================================================================
4
4
 
5
- import { parseSNI } from './tls_clienthello.rip'
6
- import { compileStreamTable, matchStreamRoute, describeStreamRoute } from './router.rip'
5
+ import { parseSNI } from './clienthello.rip'
6
+ import { compileStreamTable, matchStreamRoute } from './router.rip'
7
7
  import { createStreamRuntime, buildStreamConfigInfo, describeStreamRuntime } from './runtime.rip'
8
8
  import { createStreamUpstreamPool, addStreamUpstream, getStreamUpstream, selectStreamTarget, openStreamConnection, releaseStreamConnection } from './upstream.rip'
9
9
  import { createPipeState, writeChunk, flushPending, markPaused, markResumed } from './pipe.rip'
@@ -20,7 +20,7 @@ concatBytes = (a, b) ->
20
20
  export resolveHandshakeTarget = (runtime, listenPort, sni, httpFallback = {}) ->
21
21
  route = matchStreamRoute(runtime.routeTable, listenPort, sni)
22
22
  if route
23
- upstream = getStreamUpstream(runtime.upstreamPool, route.upstream)
23
+ upstream = getStreamUpstream(runtime.upstreamPool, route.proxy)
24
24
  unless upstream
25
25
  return {
26
26
  kind: 'reject'
@@ -52,8 +52,8 @@ export resolveHandshakeTarget = (runtime, listenPort, sni, httpFallback = {}) ->
52
52
  export buildStreamRuntime = (normalized) ->
53
53
  routeTable = compileStreamTable(normalized.streams or [])
54
54
  upstreamPool = createStreamUpstreamPool()
55
- for upstreamId, upstreamConfig of (normalized.streamUpstreams or {})
56
- addStreamUpstream(upstreamPool, upstreamId, upstreamConfig)
55
+ for proxyId, upstreamConfig of (normalized.streamUpstreams or {})
56
+ addStreamUpstream(upstreamPool, proxyId, upstreamConfig)
57
57
  configInfo = buildStreamConfigInfo(normalized)
58
58
  createStreamRuntime(configInfo, upstreamPool, routeTable)
59
59
 
@@ -30,7 +30,7 @@ export compileStreamTable = (routes = []) ->
30
30
  id: route.id
31
31
  order: route.order or 0
32
32
  listen: route.listen
33
- upstream: route.upstream
33
+ proxy: route.proxy
34
34
  sni: pattern
35
35
  sniKind: sniKind(pattern)
36
36
  timeouts: route.timeouts or {}
@@ -54,4 +54,4 @@ export matchStreamRoute = (table, listenPort, sni) ->
54
54
 
55
55
  export describeStreamRoute = (route) ->
56
56
  return null unless route
57
- "#{route.listen} #{route.sni} -> #{route.upstream}"
57
+ "#{route.listen} #{route.sni} -> #{route.proxy}"
package/tests/acme.rip CHANGED
@@ -1,6 +1,6 @@
1
1
  # test-acme.rip — ACME challenges, store, and crypto tests
2
2
 
3
- import { test, eq, ok } from '../test.rip'
3
+ import { test, eq, ok } from './runner.rip'
4
4
  import { createChallengeStore, handleChallengeRequest } from '../acme/store.rip'
5
5
  import { defaultCertDir, ensureDir, saveCert, loadCert, needsRenewal, listCerts } from '../acme/store.rip'
6
6
  import { b64url, generateAccountKeyPair, exportPublicJwk, thumbprint, signJws } from '../acme/crypto.rip'