@rip-lang/server 1.4.2 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -359,6 +359,18 @@ or:
359
359
  curl --unix-socket /tmp/rip_myapp.ctl.sock -X POST http://localhost/reload
360
360
  ```
361
361
 
362
+ ### Binding to ports 80 and 443
363
+
364
+ Ports below 1024 require elevated privileges. If you see a permission
365
+ error on startup, grant Bun the capability once:
366
+
367
+ ```bash
368
+ sudo setcap cap_net_bind_service=+ep $(which bun)
369
+ ```
370
+
371
+ This survives reboots but **not Bun upgrades** — re-run it after
372
+ `bun upgrade`.
373
+
362
374
  ### Diagnostics
363
375
 
364
376
  ```bash
package/acme/manager.rip CHANGED
@@ -95,7 +95,7 @@ export createAcmeManager = (options = {}) ->
95
95
  for domain in domains
96
96
  try manager.renewIfNeeded!(domain)
97
97
  catch e
98
- console.error "rip-acme: renewal failed for #{domain}:", e.message
98
+ warn "rip-acme: renewal failed for #{domain}:", e.message
99
99
  renewalTimer = setInterval(check, intervalMs)
100
100
  # Run first check after a short delay
101
101
  setTimeout(check, 5000)
package/api.rip CHANGED
@@ -272,7 +272,7 @@ smart = (fn) ->
272
272
  result
273
273
  catch err
274
274
  status = err?.status or 500
275
- console.error 'Handler error:', err if status >= 500
275
+ warn 'Handler error:', err if status >= 500
276
276
  if err?.notice
277
277
  body = JSON.stringify { error: { notice: err.notice } }
278
278
  new Response body, { status, headers: { 'Content-Type': 'application/json' } }
@@ -389,7 +389,7 @@ runHandler = (c, handler) ->
389
389
  compose!(_middlewares, _beforeFilters, _afterFilters, handler)(c)
390
390
  c._response or new Response('', { status: 204 })
391
391
  catch err
392
- console.error 'Request error:', err if not err?.status or err.status >= 500
392
+ warn 'Request error:', err if not err?.status or err.status >= 500
393
393
  if _errorHandler?
394
394
  _errorHandler.call!(c, err, c)
395
395
  else
@@ -413,7 +413,7 @@ export start = (opts = {}) ->
413
413
  host = opts.host or 'localhost'
414
414
  port = opts.port or 3000
415
415
  server = Bun.serve { hostname: host, port: port, fetch: handler }
416
- console.log "rip-api listening on http://#{host}:#{port}" unless opts.silent
416
+ p "rip-api listening on http://#{host}:#{port}" unless opts.silent
417
417
  server
418
418
 
419
419
  export App = (fn) ->
package/browse.rip CHANGED
@@ -12,7 +12,7 @@
12
12
  import { use, start, notFound } from '@rip-lang/server'
13
13
  import { statSync } from 'node:fs'
14
14
  import { join, resolve, basename } from 'node:path'
15
- import { renderDirectoryListing, renderMarkdown, renderTextFile, isTextFile, serveRipHighlightGrammar, findIndexFile } from './serving/static.rip'
15
+ import { renderDirectoryListing, renderMarkdown, renderTextFile, isTextFile, serveRipHighlightGrammar, findIndexFile, INDEX_FILES } from './serving/static.rip'
16
16
 
17
17
  root = resolve(process.env.APP_BASE_DIR or process.cwd())
18
18
  rootSlash = root + '/'
@@ -41,8 +41,7 @@ notFound ->
41
41
  try
42
42
  html = renderMarkdown(path)
43
43
  return new Response html, { headers: { 'Content-Type': 'text/html; charset=UTF-8' } }
44
- # Text files that can't "run" get syntax highlighted
45
- if isTextFile(path) and not path.endsWith('.html') and not path.endsWith('.htm') and not path.endsWith('.rip')
44
+ if isTextFile(path) and not INDEX_FILES.includes(basename(path))
46
45
  try
47
46
  html = renderTextFile(path)
48
47
  return new Response html, { headers: { 'Content-Type': 'text/html; charset=UTF-8' } }
package/control/cli.rip CHANGED
@@ -51,7 +51,7 @@ export resolveAppEntry = (appPathInput, defaultEntryPath = null) ->
51
51
  entryPath = defaultEntryPath or abs
52
52
  else
53
53
  unless existsSync(abs)
54
- console.error "App path not found: #{abs}"
54
+ warn "App path not found: #{abs}"
55
55
  exit 2
56
56
  baseDir = dirname(abs)
57
57
  entryPath = abs
@@ -383,7 +383,7 @@ export stopServer = (prefix, getPidFilePathFn, existsSyncFn, readFileSyncFn, kil
383
383
  p "rip-server: no PID file found at #{pidFile}, trying pkill..."
384
384
  spawnSyncFn(['pkill', '-f', importMetaPath])
385
385
  catch e
386
- console.error "rip-server: stop failed: #{e.message}"
386
+ warn "rip-server: stop failed: #{e.message}"
387
387
 
388
388
  export reloadConfig = (prefix, getControlSocketPathFn, fetchFn, exitFn) ->
389
389
  controlUnix = getControlSocketPathFn(prefix)
@@ -394,10 +394,10 @@ export reloadConfig = (prefix, getControlSocketPathFn, fetchFn, exitFn) ->
394
394
  p "rip-server: config reload applied"
395
395
  else
396
396
  reason = j?.reason or 'unknown'
397
- console.error "rip-server: config reload rejected: #{reason}"
397
+ warn "rip-server: config reload rejected: #{reason}"
398
398
  exitFn(1)
399
399
  catch e
400
- console.error "rip-server: reload failed: #{e?.message or e}"
400
+ warn "rip-server: reload failed: #{e?.message or e}"
401
401
  exitFn(1)
402
402
 
403
403
  export listApps = (prefix, getControlSocketPathFn, fetchFn, exitFn) ->
@@ -409,7 +409,7 @@ export listApps = (prefix, getControlSocketPathFn, fetchFn, exitFn) ->
409
409
  hosts = if Array.isArray(j?.hosts) then j.hosts else []
410
410
  p if hosts.length then hosts.join('\n') else '(no hosts)'
411
411
  catch e
412
- console.error "rip-server: list failed: #{e?.message or e}"
412
+ warn "rip-server: list failed: #{e?.message or e}"
413
413
  exitFn(1)
414
414
 
415
415
  formatUptime = (seconds) ->
@@ -437,5 +437,5 @@ export showInfo = (fetchFn, exitFn) ->
437
437
  p "rip server v#{version} | #{status} | #{workers} workers | #{hosts} hosts#{upstreamPart} | uptime #{uptime}"
438
438
  exitFn(if status is 'healthy' then 0 else 1)
439
439
  catch e
440
- console.error "rip-server: info failed: #{e?.message or e}"
440
+ warn "rip-server: info failed: #{e?.message or e}"
441
441
  exitFn(1)
@@ -133,10 +133,10 @@ export installShutdownHandlers = (cleanup, processObj, onReload = null) ->
133
133
  processObj.on 'SIGHUP', ->
134
134
  try onReload!()
135
135
  catch err
136
- console.error 'rip-server: config reload failed:', err?.message or err
136
+ warn 'rip-server: config reload failed:', err?.message or err
137
137
  processObj.on 'uncaughtException', (err) ->
138
- console.error 'rip-server: uncaught exception:', err
138
+ warn 'rip-server: uncaught exception:', err
139
139
  cleanup()
140
140
  processObj.on 'unhandledRejection', (err) ->
141
- console.error 'rip-server: unhandled rejection:', err
141
+ warn 'rip-server: unhandled rejection:', err
142
142
  cleanup()
@@ -73,7 +73,7 @@ export class Manager
73
73
 
74
74
  code = await proc.exited
75
75
  if code isnt 0
76
- console.error "rip-server: setup exited with code #{code} for app #{app.appId}"
76
+ warn "rip-server: setup exited with code #{code} for app #{app.appId}"
77
77
  exit 1
78
78
 
79
79
  start: ->
@@ -208,7 +208,7 @@ export class Manager
208
208
  readyResults = Promise.all!(pairs.map((p) => @waitWorkerReady(p.replacement.socketPath, 3000)))
209
209
  allReady = readyResults.every((ready) -> ready)
210
210
  unless allReady
211
- console.error "[manager] Rolling restart aborted: not all new workers ready for app #{app.appId}"
211
+ warn "[manager] Rolling restart aborted: not all new workers ready for app #{app.appId}"
212
212
  for pair, i in pairs
213
213
  unless readyResults[i]
214
214
  try pair.replacement.process.kill()
package/control/mdns.rip CHANGED
@@ -53,4 +53,4 @@ export startMdnsAdvertisement = (host, mdnsProcesses, getLanIP, flags, formatPor
53
53
  url = "#{protocol}://#{host}#{formatPort(protocol, port)}"
54
54
  publishUrl(url)
55
55
  catch e
56
- console.error "rip-server: failed to advertise #{host} via mDNS:", e.message
56
+ warn "rip-server: failed to advertise #{host} via mDNS:", e.message
@@ -15,7 +15,7 @@ export runSetupMode = ->
15
15
  if typeof fn is 'function'
16
16
  await fn()
17
17
  catch e
18
- console.error "rip-server: setup failed:", e
18
+ warn "rip-server: setup failed:", e
19
19
  exit 1
20
20
 
21
21
  export runWorkerMode = ->
@@ -62,7 +62,7 @@ export runWorkerMode = ->
62
62
  workerState.handler = h if h
63
63
  workerState.handler or (-> new Response('not ready', { status: 503 }))
64
64
  catch e
65
- console.error "[worker #{workerId}] import failed:", e
65
+ warn "[worker #{workerId}] import failed:", e
66
66
  workerState.handler or (-> new Response('not ready', { status: 503 }))
67
67
 
68
68
  selfJoin = ->
@@ -106,7 +106,7 @@ export runWorkerMode = ->
106
106
  res = res!(req) if typeof res is 'function'
107
107
  if res instanceof Response then res else new Response(String(res))
108
108
  catch err
109
- console.error "[worker #{workerId}] ERROR:", err
109
+ warn "[worker #{workerId}] ERROR:", err
110
110
  new Response('error', { status: 500 })
111
111
  finally
112
112
  workerState.inflight = false
@@ -129,8 +129,8 @@ export runWorkerMode = ->
129
129
  process.on 'SIGTERM', shutdown
130
130
  process.on 'SIGINT', shutdown
131
131
  process.on 'uncaughtException', (err) ->
132
- console.error "[worker #{workerId}] uncaught exception:", err
132
+ warn "[worker #{workerId}] uncaught exception:", err
133
133
  shutdown()
134
134
  process.on 'unhandledRejection', (err) ->
135
- console.error "[worker #{workerId}] unhandled rejection:", err
135
+ warn "[worker #{workerId}] unhandled rejection:", err
136
136
  shutdown()
package/middleware.rip CHANGED
@@ -100,7 +100,7 @@ export cors = (opts = {}) ->
100
100
  export logger = (opts = {}) ->
101
101
  format = opts.format or 'dev'
102
102
  skip = opts.skip or null
103
- stream = opts.stream or { write: (msg) -> console.log msg.trim() }
103
+ stream = opts.stream or { write: (msg) -> p msg.trim() }
104
104
 
105
105
  formatters =
106
106
  tiny: (info) -> "#{info.method} #{info.path} #{info.status} - #{info.ms}ms"
@@ -274,7 +274,7 @@ export sessions = (opts = {}) ->
274
274
 
275
275
  # Warn if no secret in production
276
276
  if not secret and process.env.NODE_ENV is 'production'
277
- console.warn 'WARNING: sessions() without secret is insecure. Set secret option for production.'
277
+ warn 'WARNING: sessions() without secret is insecure. Set secret option for production.'
278
278
 
279
279
  (c, next) ->
280
280
  # Parse cookie header
@@ -587,6 +587,6 @@ export serve = (opts = {}) ->
587
587
  body = JSON.stringify({ op: 'watch', prefix, dirs: watchDirs })
588
588
 
589
589
  fetch('http://localhost/watch', { method: 'POST', body, headers: { 'content-type': 'application/json' }, unix: ctl }).catch (e) ->
590
- console.warn "[Rip] Watch registration failed: #{e.message}"
590
+ warn "[Rip] Watch registration failed: #{e.message}"
591
591
 
592
592
  (c, next) -> next!()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rip-lang/server",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
4
4
  "description": "Bun-native content server: static sites, apps, HTTP proxy, and TCP/TLS passthrough",
5
5
  "type": "module",
6
6
  "main": "api.rip",
@@ -47,7 +47,7 @@
47
47
  "author": "Steve Shreeve <steve.shreeve@gmail.com>",
48
48
  "license": "MIT",
49
49
  "dependencies": {
50
- "rip-lang": ">=3.13.133"
50
+ "rip-lang": ">=3.13.134"
51
51
  },
52
52
  "files": [
53
53
  "api.rip",
package/server.rip CHANGED
@@ -223,7 +223,9 @@ class Server
223
223
  return Bun.serve(Object.assign({ port, idleTimeout: 0, fetch: fetchFn }, wsOpts, opts))
224
224
  catch e
225
225
  if e?.code is 'EACCES' and port < 1024
226
- port = if opts.tls then 3443 else 3000
226
+ fallback = if opts.tls then 3443 else 3000
227
+ warn "rip-server: port #{port} requires elevated privileges, falling back to #{fallback}"
228
+ port = fallback
227
229
  p = port
228
230
  continue
229
231
  throw e unless e?.code is 'EADDRINUSE'
@@ -422,9 +424,9 @@ class Server
422
424
  @appendReloadHistory(entry)
423
425
  if e.validationErrors
424
426
  label = 'serve.rip'
425
- console.error formatConfigErrors(label, e.validationErrors)
427
+ warn formatConfigErrors(label, e.validationErrors)
426
428
  else
427
- console.error "rip-server: failed to load active config: #{e.message or e}"
429
+ warn "rip-server: failed to load active config: #{e.message or e}"
428
430
  logEvent('config_loaded',
429
431
  id: attemptId
430
432
  source: source
@@ -819,7 +821,7 @@ class Server
819
821
  @releaseWorker(retry, app)
820
822
  return @buildResponse(res, req, start, workerSeconds, requestId)
821
823
  catch err
822
- console.error "[server] forwardToWorker error:", err.message or err if isDebug()
824
+ warn "[server] forwardToWorker error:", err.message or err if isDebug()
823
825
  app.sockets = app.sockets.filter((x) -> x.socket isnt socket.socket)
824
826
  app.availableWorkers = app.availableWorkers.filter((x) -> x.socket isnt socket.socket)
825
827
  released = true
@@ -1030,13 +1032,13 @@ main = ->
1030
1032
  svr.internalHttpsServer?.reload({ tls: svr.tlsMaterial }) if svr.internalHttpsServer
1031
1033
  p "rip-acme: TLS reloaded with renewed cert for #{domain}"
1032
1034
  catch e
1033
- console.error "rip-acme: TLS reload failed after renewal: #{e.message}"
1035
+ warn "rip-acme: TLS reload failed after renewal: #{e.message}"
1034
1036
  svr.acmeManager = acmeMgr
1035
1037
  for domain in acmeDomains
1036
1038
  try acmeMgr.obtainCert!(domain)
1037
1039
  catch e
1038
- console.error "rip-acme: cert obtain failed for #{domain}: #{e.message}"
1039
- console.error "rip-acme: will retry during renewal loop"
1040
+ warn "rip-acme: cert obtain failed for #{domain}: #{e.message}"
1041
+ warn "rip-acme: will retry during renewal loop"
1040
1042
  acmeMgr.startRenewalLoop(acmeDomains)
1041
1043
 
1042
1044
  process.env.RIP_URLS = svr.urls.join(',') # exact URLs the Server just built
@@ -616,7 +616,7 @@ normalizeServeConfig = (config, baseDir) ->
616
616
  pushError(errors, 'E_UNKNOWN_KEY', key, "unknown top-level key #{key}", 'Allowed keys: version, server, proxies, apps, certs, rules, groups, hosts, streams.') unless TOP_LEVEL_KEYS.has(key)
617
617
 
618
618
  if config.edge? and not config.server?
619
- console.warn "rip-server: 'edge' is deprecated in serve.rip; use 'server' instead"
619
+ warn "rip-server: 'edge' is deprecated in serve.rip; use 'server' instead"
620
620
  serverSettings = normalizeServerSettings(config.server or config.edge or {}, errors)
621
621
 
622
622
  proxies = {}
@@ -123,7 +123,7 @@ export createWsPassthrough = (clientWs, upstreamUrl, protocols = []) ->
123
123
  try clientWs.close()
124
124
 
125
125
  upstream.addEventListener 'error', (err) ->
126
- console.error "ws-passthrough: upstream error:", err.message or err
126
+ warn "ws-passthrough: upstream error:", err.message or err
127
127
  try clientWs.close()
128
128
 
129
129
  # Return object for client-side event wiring
package/serving/proxy.rip CHANGED
@@ -10,7 +10,7 @@ import { toUpstreamWsUrl } from './runtime.rip'
10
10
  export proxyRouteToUpstream = (req, route, requestId, clientIp, runtime, metrics, flags, addSecurityHeaders, logAccess) ->
11
11
  upstream = getUpstream(runtime.upstreamPool, route.proxy)
12
12
  unless upstream
13
- console.error "[proxy] no proxy '#{route.proxy}'" if isDebug()
13
+ warn "[proxy] no proxy '#{route.proxy}'" if isDebug()
14
14
  return serviceUnavailableResponse()
15
15
 
16
16
  attempt = 1
@@ -19,7 +19,7 @@ export proxyRouteToUpstream = (req, route, requestId, clientIp, runtime, metrics
19
19
  while attempt <= upstream.retry.attempts
20
20
  target = selectTarget(upstream, runtime.upstreamPool.nowFn)
21
21
  unless target
22
- console.error "[proxy] no healthy target for '#{route.proxy}'" if isDebug()
22
+ warn "[proxy] no healthy target for '#{route.proxy}'" if isDebug()
23
23
  return serviceUnavailableResponse()
24
24
 
25
25
  markTargetBusy(target)
@@ -36,9 +36,9 @@ export proxyRouteToUpstream = (req, route, requestId, clientIp, runtime, metrics
36
36
  )
37
37
  workerSeconds = (performance.now() - t0) / 1000
38
38
  success = res.status < 500
39
- console.error "[proxy] #{route.proxy} -> #{target.url} status=#{res.status} attempt=#{attempt}" if isDebug()
39
+ warn "[proxy] #{route.proxy} -> #{target.url} status=#{res.status} attempt=#{attempt}" if isDebug()
40
40
  catch err
41
- console.error "[proxy] #{route.proxy} error: #{err.message or err}" if isDebug()
41
+ warn "[proxy] #{route.proxy} error: #{err.message or err}" if isDebug()
42
42
  res = serviceUnavailableResponse()
43
43
  finally
44
44
  releaseTarget(target, workerSeconds * 1000, success, runtime.upstreamPool)
@@ -139,7 +139,7 @@ export processResponse = (hub, body, clientId) ->
139
139
  clients = resolveTargets(hub, groups)
140
140
  deliverToClients(hub, clients, senders, JSON.stringify(bundle))
141
141
  catch e
142
- console.error "realtime: invalid response:", e.message
142
+ warn "realtime: invalid response:", e.message
143
143
 
144
144
  # --- Proxy to backend ---
145
145
 
@@ -160,7 +160,7 @@ export proxyToBackend = (hub, headers, frameType, body) ->
160
160
  body: body or ''
161
161
  res.text!
162
162
  catch e
163
- console.error "realtime: proxy failed:", e.message
163
+ warn "realtime: proxy failed:", e.message
164
164
  null
165
165
 
166
166
  # --- Binary delivery (for CRDT and raw data) ---
@@ -219,7 +219,7 @@ export proxyRealtimeToWorker = (headers, frameType, body, getSocket, releaseSock
219
219
  decompress: false
220
220
  res.text!
221
221
  catch e
222
- console.error "realtime: worker proxy failed:", e.message
222
+ warn "realtime: worker proxy failed:", e.message
223
223
  null
224
224
  finally
225
225
  releaseSocket(sock)
@@ -86,7 +86,7 @@ export printCheckConfigResult = (loaded) ->
86
86
  export runCheckConfig = (flags) ->
87
87
  source = resolveConfigSource(flags.appEntry, flags.configPath)
88
88
  unless source?.path
89
- console.error 'rip-server: no serve.rip found to validate'
89
+ warn 'rip-server: no serve.rip found to validate'
90
90
  exit 1
91
91
  try
92
92
  checked = checkConfigFile!(source.path)
@@ -94,7 +94,7 @@ export runCheckConfig = (flags) ->
94
94
  catch e
95
95
  if e.validationErrors
96
96
  label = 'serve.rip'
97
- console.error formatConfigErrors(label, e.validationErrors)
97
+ warn formatConfigErrors(label, e.validationErrors)
98
98
  else
99
- console.error "rip-server: config check failed: #{e.message or e}"
99
+ warn "rip-server: config check failed: #{e.message or e}"
100
100
  exit 1
@@ -321,7 +321,7 @@ export renderTextFile = (filePath) ->
321
321
 
322
322
  # --- Static file serving ---
323
323
 
324
- INDEX_FILES = ['index.html', 'index.rip', 'index.ts', 'index.tsx', 'index.jsx', 'index.js']
324
+ export INDEX_FILES = ['index.html', 'index.rip', 'index.ts', 'index.tsx', 'index.jsx', 'index.js']
325
325
 
326
326
  export findIndexFile = (dir) ->
327
327
  for name in INDEX_FILES
package/serving/tls.rip CHANGED
@@ -21,7 +21,7 @@ export buildTlsArray = (defaultMaterial, certMap) ->
21
21
  key = readFileSync(certInfo.keyPath, 'utf8')
22
22
  entries.push({ serverName, cert, key, specificity: hostSpecificity(serverName) })
23
23
  catch e
24
- console.error "rip-server: failed to load cert for #{serverName}: #{e.message}"
24
+ warn "rip-server: failed to load cert for #{serverName}: #{e.message}"
25
25
  entries.sort((a, b) -> b.specificity - a.specificity)
26
26
  result = entries.map((e) -> { serverName: e.serverName, cert: e.cert, key: e.key })
27
27
  if defaultMaterial?.cert and defaultMaterial?.key
@@ -45,7 +45,7 @@ export loadTlsMaterial = (flags, serverDir, acmeLoadCertFn) ->
45
45
  key = readFileSync(flags.keyPath, 'utf8')
46
46
  return { cert, key }
47
47
  catch
48
- console.error 'Failed to read TLS cert/key from provided paths. Use http or fix paths.'
48
+ warn 'Failed to read TLS cert/key from provided paths. Use http or fix paths.'
49
49
  exit 2
50
50
 
51
51
  # ACME-managed cert (if domain is configured)
@@ -63,6 +63,6 @@ export loadTlsMaterial = (flags, serverDir, acmeLoadCertFn) ->
63
63
  key = readFileSync(keyPath, 'utf8')
64
64
  return { cert, key }
65
65
  catch e
66
- console.error "rip-server: failed to load TLS certs from #{certsDir}: #{e.message}"
67
- console.error 'Use --cert/--key to provide your own, or use http to disable TLS.'
66
+ warn "rip-server: failed to load TLS certs from #{certsDir}: #{e.message}"
67
+ warn 'Use --cert/--key to provide your own, or use http to disable TLS.'
68
68
  exit 2
@@ -233,7 +233,7 @@ export checkTargetHealth = (pool, target, fetchFn = null) ->
233
233
  updateTargetHealth(target, res.ok, pool)
234
234
  res.ok
235
235
  catch e
236
- console.error "[health] #{url} failed: #{e?.message or e}" if process.env.RIP_DEBUG?
236
+ warn "[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
package/streams/index.rip CHANGED
@@ -273,12 +273,32 @@ export startStreamListeners = (runtime, options = {}) ->
273
273
  handshakeMs: Math.max(...portRoutes.map((route) -> route.timeouts?.handshakeMs or 5000), 5000)
274
274
  idleMs: Math.max(...portRoutes.map((route) -> route.timeouts?.idleMs or 300000), 300000)
275
275
  connectMs: Math.max(...portRoutes.map((route) -> route.timeouts?.connectMs or 5000), 5000)
276
- listener = Bun.listen
277
- hostname: options.hostname or '0.0.0.0'
278
- port: port
279
- allowHalfOpen: true
280
- socket: createClientHandlers(runtime, port, listenerTimeouts, options).socket
281
- runtime.listeners.set(port, listener)
276
+ try
277
+ listener = Bun.listen
278
+ hostname: options.hostname or '0.0.0.0'
279
+ port: port
280
+ allowHalfOpen: true
281
+ socket: createClientHandlers(runtime, port, listenerTimeouts, options).socket
282
+ runtime.listeners.set(port, listener)
283
+ catch e
284
+ if e?.code is 'EACCES' and port < 1024
285
+ warn ""
286
+ warn "rip-server: cannot bind to port #{port} — permission denied"
287
+ warn ""
288
+ warn " Ports below 1024 require elevated privileges."
289
+ warn " This commonly happens after upgrading Bun, which resets capabilities."
290
+ warn ""
291
+ warn " Options:"
292
+ warn ""
293
+ warn " 1. Grant Bun the capability (recommended, one-time):"
294
+ warn " sudo setcap cap_net_bind_service=+ep $(which bun)"
295
+ warn ""
296
+ warn " 2. Run with sudo:"
297
+ warn " sudo rip server --file serve.rip"
298
+ warn ""
299
+ warn " Note: upgrading Bun resets the capability — re-run setcap after upgrades."
300
+ warn ""
301
+ throw e
282
302
  runtime
283
303
 
284
304
  export stopStreamListeners = (runtime, closeActiveConnections = false) ->