@rip-lang/server 1.2.11 → 1.3.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/middleware.rip ADDED
@@ -0,0 +1,558 @@
1
+ # ==============================================================================
2
+ # @rip-lang/server/middleware — Built-in Middleware
3
+ # ==============================================================================
4
+ #
5
+ # Usage:
6
+ # import { cors, logger, compress, sessions, htmlJson, serve } from '@rip-lang/server/middleware'
7
+ # import { session } from '@rip-lang/server'
8
+ #
9
+ # use logger()
10
+ # use cors origin: 'https://myapp.com'
11
+ # use compress()
12
+ # use sessions()
13
+ # use htmlJson # Pretty JSON for iOS mobile browsers
14
+ #
15
+ # before ->
16
+ # session.userId = 123 # Works anywhere via AsyncLocalStorage
17
+ #
18
+ # Note: read() and session work automatically — no setup required!
19
+ #
20
+ # ==============================================================================
21
+
22
+ # ==============================================================================
23
+ # cors — Cross-Origin Resource Sharing
24
+ # ==============================================================================
25
+ #
26
+ # Options:
27
+ # origin: '*' | string | string[] | (origin) -> boolean
28
+ # methods: 'GET,POST,...' | string[]
29
+ # headers: 'Content-Type,...' | string[]
30
+ # credentials: boolean
31
+ # maxAge: number (seconds)
32
+ # exposeHeaders: string | string[]
33
+ # preflight: boolean — handle OPTIONS before route matching (default: false)
34
+ #
35
+
36
+ import { get, enableCorsPreflight } from '@rip-lang/server'
37
+ import { fileURLToPath } from 'node:url'
38
+ import { dirname } from 'node:path'
39
+ import { existsSync } from 'node:fs'
40
+
41
+ export cors = (opts = {}) ->
42
+ origin = opts.origin or '*'
43
+ methods = opts.methods or 'GET,POST,PUT,PATCH,DELETE,OPTIONS'
44
+ headers = opts.headers or 'Content-Type,Authorization,X-Requested-With'
45
+ credentials = opts.credentials or false
46
+ maxAge = opts.maxAge or 86400
47
+ exposeHeaders = opts.exposeHeaders or ''
48
+
49
+ # Normalize arrays to comma-separated strings
50
+ methods = methods.join(',') if Array.isArray(methods)
51
+ headers = headers.join(',') if Array.isArray(headers)
52
+ exposeHeaders = exposeHeaders.join(',') if Array.isArray(exposeHeaders)
53
+
54
+ # Enable early OPTIONS handling if preflight option is set
55
+ enableCorsPreflight() if opts.preflight
56
+
57
+ (c, next) ->
58
+ requestOrigin = c.req.header('Origin')
59
+
60
+ # Determine allowed origin
61
+ allowedOrigin = if typeof origin is 'function'
62
+ if origin(requestOrigin) then requestOrigin else null
63
+ else if Array.isArray(origin)
64
+ if origin.includes(requestOrigin) then requestOrigin else null
65
+ else if origin is '*'
66
+ '*'
67
+ else
68
+ origin
69
+
70
+ return next!() unless allowedOrigin
71
+
72
+ # Set CORS headers
73
+ c.header 'Access-Control-Allow-Origin', allowedOrigin
74
+ c.header 'Access-Control-Allow-Methods', methods
75
+ c.header 'Access-Control-Allow-Headers', headers
76
+
77
+ if credentials
78
+ c.header 'Access-Control-Allow-Credentials', 'true'
79
+
80
+ if exposeHeaders
81
+ c.header 'Access-Control-Expose-Headers', exposeHeaders
82
+
83
+ # Handle preflight OPTIONS request
84
+ if c.req.method is 'OPTIONS'
85
+ c.header 'Access-Control-Max-Age', String(maxAge)
86
+ return c.body null, 204
87
+
88
+ await next()
89
+
90
+ # ==============================================================================
91
+ # logger — Request Logging
92
+ # ==============================================================================
93
+ #
94
+ # Options:
95
+ # format: 'tiny' | 'short' | 'dev' | 'full' | (info) -> string
96
+ # skip: (c) -> boolean
97
+ # stream: { write: (msg) -> } (default: console)
98
+ #
99
+
100
+ export logger = (opts = {}) ->
101
+ format = opts.format or 'dev'
102
+ skip = opts.skip or null
103
+ stream = opts.stream or { write: (msg) -> console.log msg.trim() }
104
+
105
+ formatters =
106
+ tiny: (info) -> "#{info.method} #{info.path} #{info.status} - #{info.ms}ms"
107
+ short: (info) -> "#{info.method} #{info.path} #{info.status} #{info.size} - #{info.ms}ms"
108
+ dev: (info) ->
109
+ color = if info.status >= 500 then '\x1b[31m' # red
110
+ else if info.status >= 400 then '\x1b[33m' # yellow
111
+ else if info.status >= 300 then '\x1b[36m' # cyan
112
+ else '\x1b[32m' # green
113
+ reset = '\x1b[0m'
114
+ "#{info.method} #{info.path} #{color}#{info.status}#{reset} - #{info.ms}ms"
115
+ full: (info) -> "[#{info.time}] #{info.method} #{info.path} #{info.status} #{info.size} - #{info.ms}ms"
116
+
117
+ (c, next) ->
118
+ return next!() if skip?(c)
119
+
120
+ start = Date.now()
121
+ await next()
122
+ ms = Date.now() - start
123
+
124
+ info =
125
+ method: c.req.method
126
+ path: c.req.path
127
+ status: c._response?.status or 200
128
+ ms: ms
129
+ size: c._response?.headers?.get('Content-Length') or '-'
130
+ time: new Date().toISOString()
131
+
132
+ msg = if typeof format is 'function' then format(info) else formatters[format]?(info) or formatters.dev(info)
133
+ stream.write "#{msg}\n"
134
+
135
+ # ==============================================================================
136
+ # compress — Response Compression (gzip, deflate)
137
+ # ==============================================================================
138
+ #
139
+ # Options:
140
+ # threshold: number (min bytes to compress, default 1024)
141
+ # encodings: string[] (default ['gzip', 'deflate'])
142
+ #
143
+ # Note: Requires Bun's built-in compression support
144
+ #
145
+
146
+ export compress = (opts = {}) ->
147
+ threshold = opts.threshold or 1024
148
+ encodings = opts.encodings or ['gzip', 'deflate']
149
+
150
+ (c, next) ->
151
+ await next()
152
+
153
+ # Skip if no response body
154
+ response = c._response
155
+ return unless response?
156
+
157
+ # Check Accept-Encoding header
158
+ acceptEncoding = c.req.header('Accept-Encoding') or ''
159
+
160
+ # Find supported encoding
161
+ encoding = null
162
+ for enc in encodings
163
+ if acceptEncoding.includes(enc)
164
+ encoding = enc
165
+ break
166
+
167
+ return unless encoding
168
+
169
+ # Get body and check threshold
170
+ try
171
+ body = response.body
172
+ return unless body
173
+
174
+ # Clone to read the body
175
+ clone = response.clone()
176
+ text = clone.text!
177
+ return if text.length < threshold
178
+
179
+ # Compress using Bun's built-in CompressionStream
180
+ compressed = if encoding is 'gzip'
181
+ new Response(text).body.pipeThrough(new CompressionStream('gzip'))
182
+ else if encoding is 'deflate'
183
+ new Response(text).body.pipeThrough(new CompressionStream('deflate'))
184
+ else
185
+ return
186
+
187
+ # Create new response with compressed body
188
+ headers = new Headers(response.headers)
189
+ headers.set 'Content-Encoding', encoding
190
+ headers.delete 'Content-Length' # Length changed
191
+
192
+ c._response = new Response compressed,
193
+ status: response.status
194
+ headers: headers
195
+ catch
196
+ # If compression fails, keep original response
197
+ return
198
+
199
+ # ==============================================================================
200
+ # sessions — Session Management
201
+ # ==============================================================================
202
+ #
203
+ # Options:
204
+ # secret: string (REQUIRED for production — signs cookie to prevent tampering)
205
+ # name: string (cookie name, default 'session')
206
+ # maxAge: number (seconds, default 86400 = 24 hours)
207
+ # secure: boolean (HTTPS only)
208
+ # httpOnly: boolean (default true)
209
+ # sameSite: 'Strict' | 'Lax' | 'None' (default 'Lax')
210
+ #
211
+ # Usage:
212
+ # import { session } from '@rip-lang/server'
213
+ # import { sessions } from '@rip-lang/server/middleware'
214
+ #
215
+ # use sessions secret: process.env.SESSION_SECRET
216
+ #
217
+ # before ->
218
+ # session.userId = 123
219
+ #
220
+ # get '/profile', ->
221
+ # { userId: session.userId }
222
+ #
223
+ # Security:
224
+ # Without `secret`, sessions use plain base64 (INSECURE — dev only).
225
+ # With `secret`, sessions are HMAC-SHA256 signed (tamper-proof).
226
+ #
227
+ # Note: Self-contained — parses cookies directly from request headers.
228
+ #
229
+
230
+ # HMAC-SHA256 signing helpers
231
+ hmacSign = (data, secret) ->
232
+ encoder = new TextEncoder()
233
+ key = crypto.subtle.importKey! 'raw', encoder.encode(secret),
234
+ { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']
235
+ signature = crypto.subtle.sign! 'HMAC', key, encoder.encode(data)
236
+ Array.from(new Uint8Array(signature)).map((b) -> b.toString(16).padStart(2, '0')).join('')
237
+
238
+ hmacVerify = (data, signature, secret) ->
239
+ expected = hmacSign! data, secret
240
+ # Constant-time comparison to prevent timing attacks
241
+ return false if expected.length isnt signature.length
242
+ result = 0
243
+ for i in [0...expected.length]
244
+ result |= expected.charCodeAt(i) ^ signature.charCodeAt(i)
245
+ result is 0
246
+
247
+ encodeSession = (data, secret) ->
248
+ json = JSON.stringify(data)
249
+ payload = btoa(unescape(encodeURIComponent(json)))
250
+ return payload unless secret
251
+ signature = hmacSign! payload, secret
252
+ "#{payload}--#{signature}"
253
+
254
+ decodeSession = (cookie, secret) ->
255
+ return {} unless cookie
256
+ try
257
+ if secret
258
+ [payload, signature] = cookie.split('--')
259
+ return {} unless payload and signature
260
+ return {} unless hmacVerify! payload, signature, secret
261
+ JSON.parse(decodeURIComponent(escape(atob(payload))))
262
+ else
263
+ JSON.parse(decodeURIComponent(escape(atob(cookie))))
264
+ catch
265
+ {}
266
+
267
+ export sessions = (opts = {}) ->
268
+ secret = opts.secret
269
+ cookieName = opts.name or 'session'
270
+ maxAge = opts.maxAge or 86400
271
+ secure = opts.secure or false
272
+ httpOnly = opts.httpOnly ?? true
273
+ sameSite = opts.sameSite or 'Lax'
274
+
275
+ # Warn if no secret in production
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.'
278
+
279
+ (c, next) ->
280
+ # Parse cookie header
281
+ cookieHeader = c.req.header('Cookie') or ''
282
+ cookies = {}
283
+ for pair in cookieHeader.split(';')
284
+ [key, val] = pair.trim().split('=')
285
+ cookies[key] = decodeURIComponent(val) if key and val
286
+
287
+ # Decode existing session
288
+ raw = cookies[cookieName]
289
+ c.session = decodeSession! raw, secret
290
+
291
+ # Track original for change detection
292
+ original = JSON.stringify(c.session)
293
+
294
+ await next()
295
+
296
+ # Save session if changed
297
+ current = JSON.stringify(c.session)
298
+ if current isnt original and c._response?
299
+ encoded = encodeSession! c.session, secret
300
+ parts = ["#{cookieName}=#{encodeURIComponent(encoded)}"]
301
+ parts.push "Path=/"
302
+ parts.push "Max-Age=#{maxAge}"
303
+ parts.push "HttpOnly" if httpOnly
304
+ parts.push "Secure" if secure
305
+ parts.push "SameSite=#{sameSite}"
306
+ cookie = parts.join('; ')
307
+
308
+ # Clone response with new header
309
+ headers = new Headers(c._response.headers)
310
+ headers.append 'Set-Cookie', cookie
311
+ c._response = new Response c._response.body,
312
+ status: c._response.status
313
+ headers: headers
314
+
315
+ # ==============================================================================
316
+ # secureHeaders — Security Headers
317
+ # ==============================================================================
318
+ #
319
+ # Sets common security headers:
320
+ # X-Content-Type-Options: nosniff
321
+ # X-Frame-Options: DENY
322
+ # X-XSS-Protection: 1; mode=block
323
+ # Referrer-Policy: strict-origin-when-cross-origin
324
+ # Content-Security-Policy (optional)
325
+ #
326
+
327
+ export secureHeaders = (opts = {}) ->
328
+ (c, next) ->
329
+ c.header 'X-Content-Type-Options', 'nosniff'
330
+ c.header 'X-Frame-Options', opts.frameOptions or 'DENY'
331
+ c.header 'X-XSS-Protection', '1; mode=block'
332
+ c.header 'Referrer-Policy', opts.referrerPolicy or 'strict-origin-when-cross-origin'
333
+
334
+ if opts.contentSecurityPolicy
335
+ c.header 'Content-Security-Policy', opts.contentSecurityPolicy
336
+
337
+ if opts.hsts
338
+ maxAge = opts.hstsMaxAge or 31536000
339
+ c.header 'Strict-Transport-Security', "max-age=#{maxAge}; includeSubDomains"
340
+
341
+ await next()
342
+
343
+ # ==============================================================================
344
+ # timeout — Request Timeout
345
+ # ==============================================================================
346
+ #
347
+ # Options:
348
+ # ms: number (timeout in milliseconds, default 30000)
349
+ # message: string (error message)
350
+ # status: number (status code, default 408)
351
+ #
352
+
353
+ export timeout = (opts = {}) ->
354
+ ms = opts.ms or 30000
355
+ message = opts.message or 'Request Timeout'
356
+ status = opts.status or 408
357
+
358
+ (c, next) ->
359
+ timer = null
360
+ timedOut = false
361
+
362
+ timeoutPromise = new Promise (_, reject) ->
363
+ timer = setTimeout ->
364
+ timedOut = true
365
+ reject new Error message
366
+ , ms
367
+
368
+ try
369
+ await Promise.race [next(), timeoutPromise]
370
+ catch err
371
+ if timedOut
372
+ return c.json { error: message }, status
373
+ throw err
374
+ finally
375
+ clearTimeout timer if timer
376
+
377
+ # ==============================================================================
378
+ # bodyLimit — Request Body Size Limit
379
+ # ==============================================================================
380
+ #
381
+ # Options:
382
+ # maxSize: number (bytes, default 1MB)
383
+ # message: string (error message)
384
+ #
385
+
386
+ export bodyLimit = (opts = {}) ->
387
+ maxSize = opts.maxSize or 1024 * 1024 # 1MB default
388
+ message = opts.message or 'Request body too large'
389
+
390
+ (c, next) ->
391
+ contentLength = parseInt(c.req.header('Content-Length') or '0')
392
+
393
+ if contentLength > maxSize
394
+ return c.json { error: message }, 413
395
+
396
+ await next()
397
+
398
+ # ==============================================================================
399
+ # htmlJson — Pretty JSON for iOS Mobile Browsers
400
+ # ==============================================================================
401
+ #
402
+ # Renders JSON as syntax-highlighted HTML for iOS mobile browsers.
403
+ # Solves the iOS Safari/Chrome "download" footer issue when viewing JSON.
404
+ #
405
+ # Usage:
406
+ # use htmlJson
407
+ #
408
+ # Only activates when:
409
+ # 1. Request is from iOS (iPhone/iPad)
410
+ # 2. Browser is directly viewing (Accept: text/html, not API call)
411
+ #
412
+
413
+ export htmlJson = (c, next) ->
414
+ originalJson = c.json.bind(c)
415
+
416
+ c.json = (data, status = 200, headers = {}) ->
417
+ if _shouldRenderHtmlJson(c.req.raw)
418
+ return _renderHtmlJson(data, status)
419
+ originalJson(data, status, headers)
420
+
421
+ next!
422
+
423
+ _shouldRenderHtmlJson = (req) ->
424
+ ua = req.headers.get('user-agent') or ''
425
+ accept = req.headers.get('accept') or ''
426
+ dest = req.headers.get('sec-fetch-dest') or ''
427
+
428
+ isIOS = /iPhone|iPad|iPod/i.test(ua)
429
+ acceptsHtml = accept.includes('text/html')
430
+ isNavigation = dest is 'document' or (acceptsHtml and not accept.startsWith('application/json'))
431
+
432
+ isIOS and isNavigation
433
+
434
+ _renderHtmlJson = (data, status) ->
435
+ json = JSON.stringify(data, null, 2)
436
+
437
+ # HTML escape then syntax highlight
438
+ escaped = json
439
+ .replace(/&/g, '&amp;')
440
+ .replace(/</g, '&lt;')
441
+ .replace(/>/g, '&gt;')
442
+
443
+ highlighted = escaped
444
+ .replace(/"([^"]+)":/g, '<span class="k">"$1"</span>:')
445
+ .replace(/: "([^"]*)"/g, ': <span class="s">"$1"</span>')
446
+ .replace(/: (-?\d+\.?\d*)/g, ': <span class="n">$1</span>')
447
+ .replace(/: (true|false)/g, ': <span class="b">$1</span>')
448
+ .replace(/: (null)/g, ': <span class="b">$1</span>')
449
+
450
+ html = """<!DOCTYPE html>
451
+ <html><head>
452
+ <meta charset="utf-8">
453
+ <meta name="viewport" content="width=device-width,initial-scale=1">
454
+ <title>JSON</title>
455
+ <style>
456
+ *{box-sizing:border-box}
457
+ body{font:14px/1.5 ui-monospace,monospace;background:#1a1a2e;color:#e0e0e0;margin:0;padding:16px}
458
+ pre{margin:0;white-space:pre-wrap;word-break:break-word}
459
+ .k{color:#82aaff}.s{color:#c3e88d}.n{color:#f78c6c}.b{color:#89ddff}
460
+ </style>
461
+ </head><body><pre>#{highlighted}</pre></body></html>"""
462
+
463
+ new Response html, {
464
+ status: status
465
+ headers: new Headers({ 'content-type': 'text/html; charset=utf-8' })
466
+ }
467
+
468
+ # ==============================================================================
469
+ # serve — Serve Rip Applications
470
+ # ==============================================================================
471
+ #
472
+ # Serves the Rip browser bundle, app component files, and the component bundle.
473
+ # Hot-reload SSE is handled by rip-server; this middleware registers watch dirs.
474
+ #
475
+ # Options:
476
+ # dir: string — app directory on disk (default: '.')
477
+ # routes: string — page components directory, relative to dir (default: 'routes')
478
+ # components: string[] — shared component directories, relative to dir (default: ['components'])
479
+ # app: string — URL mount point (default: '')
480
+ # title: string — document title
481
+ # state: object — initial app state passed via bundle
482
+ # watch: boolean — enable hot-reload (registers watch dirs with rip-server)
483
+ #
484
+
485
+ export serve = (opts = {}) ->
486
+ prefix = opts.app or ''
487
+ appDir = opts.dir or '.'
488
+ routesDir = "#{appDir}/#{opts.routes or 'routes'}"
489
+ componentDirs = (opts.components or ['components']).map (d) -> "#{appDir}/#{d}"
490
+ enableWatch = opts.watch ? opts.watch : process.env.SOCKET_PREFIX?
491
+ appState = opts.state or null
492
+ appTitle = opts.title or null
493
+
494
+ # Resolve rip.min.js — walk up from cwd to find it (dev), then module resolution (prod)
495
+ bundlePath = do ->
496
+ dir = process.cwd()
497
+ while dir isnt dirname(dir)
498
+ candidate = "#{dir}/docs/dist/rip.min.js"
499
+ return candidate if existsSync(candidate)
500
+ dir = dirname(dir)
501
+ ripDir = dirname(dirname(fileURLToPath(import.meta.resolve('rip-lang'))))
502
+ "#{ripDir}/docs/dist/rip.min.js"
503
+ bundlePathBr = "#{bundlePath}.br"
504
+ hasBrotli = existsSync(bundlePathBr)
505
+
506
+ # Route: /rip/rip.min.js — serve the bundle, prefer pre-compressed Brotli
507
+ unless serve._registered
508
+ baseFile = Bun.file(bundlePath)
509
+ baseEtag = "W/\"#{baseFile.lastModified}-#{baseFile.size}\""
510
+ get "/rip/rip.min.js", (c) ->
511
+ if c.req.header('if-none-match') is baseEtag
512
+ return new Response(null, { status: 304, headers: { 'ETag': baseEtag, 'Cache-Control': 'no-cache' } })
513
+ useBr = hasBrotli and (c.req.header('Accept-Encoding') or '').includes('br')
514
+ headers = { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-cache', 'ETag': baseEtag }
515
+ headers['Content-Encoding'] = 'br' if useBr
516
+ new Response (if useBr then Bun.file(bundlePathBr) else Bun.file(bundlePath)), { headers }
517
+ serve._registered = true
518
+
519
+ # Route: {prefix}/components/* — individual .rip component files
520
+ get "#{prefix}/components/*", (c) ->
521
+ name = c.req.path.slice("#{prefix}/components/".length)
522
+ c.send "#{routesDir}/#{name}", 'text/plain; charset=UTF-8'
523
+
524
+ # Route: {prefix}/bundle — all components + app data as JSON
525
+ get "#{prefix}/bundle", (c) ->
526
+ glob = new Bun.Glob("**/*.rip")
527
+ components = {}
528
+ paths = Array.from(glob.scanSync(routesDir)).sort()
529
+ for path in paths
530
+ components["components/#{path}"] = Bun.file("#{routesDir}/#{path}").text!
531
+
532
+ for dir in componentDirs
533
+ incPaths = Array.from(glob.scanSync(dir)).sort()
534
+ for path in incPaths
535
+ key = "components/_lib/#{path}"
536
+ components[key] = Bun.file("#{dir}/#{path}").text! unless components[key]
537
+
538
+ data = {}
539
+ data.title = appTitle if appTitle
540
+ data.watch = enableWatch
541
+ if appState
542
+ data[k] = v for k, v of appState
543
+
544
+ json = JSON.stringify({ components, data })
545
+ etag = "W/\"#{Bun.hash(json).toString(36)}\""
546
+ if c.req.header('if-none-match') is etag
547
+ return new Response(null, { status: 304, headers: { 'ETag': etag } })
548
+ new Response json, headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache', 'ETag': etag }
549
+
550
+ # Register watch directories with rip-server via control socket
551
+ if enableWatch and process.env.SOCKET_PREFIX
552
+ ctl = "/tmp/#{process.env.SOCKET_PREFIX}.ctl.sock"
553
+ dirs = [routesDir, ...componentDirs, "#{appDir}/css"].filter existsSync
554
+ body = JSON.stringify({ op: 'watch', prefix, dirs })
555
+ fetch('http://localhost/watch', { method: 'POST', body, headers: { 'content-type': 'application/json' }, unix: ctl }).catch (e) ->
556
+ console.warn "[Rip] Watch registration failed: #{e.message}"
557
+
558
+ (c, next) -> next!()
package/package.json CHANGED
@@ -1,21 +1,28 @@
1
1
  {
2
2
  "name": "@rip-lang/server",
3
- "version": "1.2.11",
4
- "description": "Pure Rip application server multi-worker, hot reload, HTTPS, mDNS",
3
+ "version": "1.3.1",
4
+ "description": "Pure Rip web framework and application server",
5
5
  "type": "module",
6
- "main": "server.rip",
6
+ "main": "api.rip",
7
7
  "exports": {
8
- ".": "./server.rip"
8
+ ".": "./api.rip",
9
+ "./middleware": "./middleware.rip",
10
+ "./server": "./server.rip"
9
11
  },
10
12
  "bin": {
11
13
  "rip-server": "./bin/rip-server"
12
14
  },
13
15
  "scripts": {
14
16
  "build": "rip -c server.rip",
15
- "test": "echo \"Tests coming soon\" && exit 0"
17
+ "test": "rip tests/read.test.rip"
16
18
  },
17
19
  "keywords": [
18
20
  "server",
21
+ "api",
22
+ "web-framework",
23
+ "routing",
24
+ "middleware",
25
+ "validation",
19
26
  "app-server",
20
27
  "hot-reload",
21
28
  "multi-worker",
@@ -38,13 +45,15 @@
38
45
  "author": "Steve Shreeve <steve.shreeve@gmail.com>",
39
46
  "license": "MIT",
40
47
  "dependencies": {
41
- "rip-lang": ">=3.13.10",
42
- "@rip-lang/api": "^1.2.7"
48
+ "rip-lang": ">=3.13.14"
43
49
  },
44
50
  "files": [
45
- "bin/",
51
+ "api.rip",
52
+ "middleware.rip",
46
53
  "server.rip",
47
54
  "server.html",
55
+ "bin/",
56
+ "tests/",
48
57
  "docs/",
49
58
  "README.md"
50
59
  ]