stipa 0.1.4 → 0.1.6

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9782fb4be8afbf16b547e95cbbe0db7933a75753757946bd47dd7242a1a2864d
4
- data.tar.gz: e27667633ab5c3a8cdbf1703415e28802ef0402a58fa6a481b70346df5597d82
3
+ metadata.gz: 4e71cdd27e54722ed384abca34ca3478d671839ccf026abc8f42378791b1b20a
4
+ data.tar.gz: 1ba6eb20c650ab6a2f092334c6831a6d4ea4bcbf0c92305b85ebebcd483add53
5
5
  SHA512:
6
- metadata.gz: d3dec633968314bec376aa4ecf37a8a81bdb64fd4d199201c9fe60cd7eaaf001b195f51eed11e46854e250830730a032aad6c3a5f662ade1ddfdaff62f87527d
7
- data.tar.gz: 4b472b41d500d494a3ed0fe2f5b2b6b76bdf755709bf506857a0ef45b923cc7765942a7ea29256e749d780d8be2d9d1b60499a378334520c841fc32f9204ec52
6
+ metadata.gz: c8452941f67b0cfdda3345059dc0da6d21d5ef50275028d633da2bcdd0c80cf37ff159a0516b11e2737470f1758e37ba7b83be9ff78707340e2bedff2b3e2494
7
+ data.tar.gz: 1f16b15656290daa904eefaf5953353d5874de701b529831b40d00111c49d2cf88f294b8e1be061806657c502030e333e056283f477118d772b9d444c6f40ca9
data/CHANGELOG.md CHANGED
@@ -5,6 +5,48 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.6] - 2026-03-20
9
+
10
+ ### Added
11
+
12
+ - **Colon-segment route patterns** — string routes now accept `:param` placeholders
13
+ (e.g. `'/users/:id'`, `'/admin/users/:id'`). Segments are converted to anchored
14
+ named-capture Regexps at match time; captured values are available via `req.params`.
15
+ - **Namespaced controller resolution** — `to:` values such as `'admin/users#update'`
16
+ are now resolved to `Admin::UsersController#update`. Slash separators become `::` and
17
+ each segment is camel-cased independently (snake_case preserved).
18
+
19
+ ## [0.1.5] - 2026-03-20
20
+
21
+ ### Added
22
+
23
+ - **Hot reload** (`STIPA_RELOAD=1`) — background thread watches project `.rb` files and
24
+ restarts the process via `exec` when a change is detected
25
+ - **Syntax guard** — if a changed file has a syntax error, the reloader logs the
26
+ problem and keeps watching; it will not restart until the error is fixed, preventing
27
+ the process from dying on a bad save
28
+ - **`.gitignore` generation** — `stipa new` now writes a `.gitignore` covering Ruby,
29
+ Node/npm, OS artefacts, and Vue build output
30
+ - **Automatic git init** — `stipa new` runs `git init && git add . && git commit` after
31
+ scaffolding so the project starts with a clean history
32
+ - **`interface Window { _t0?: number }`** added to the generated `shims-vue.d.ts`
33
+
34
+ ### Fixed
35
+
36
+ - **CORS header injection** — `Middleware::Cors` no longer reflects an arbitrary
37
+ `Origin` request header. Wildcard config sends the literal `*`; an explicit allowlist
38
+ only echoes origins that are in the list
39
+ - **`instance_variable_set` removed** — the generated `MethodOverride` middleware now
40
+ uses the public `req.method =` setter instead of reaching into private state
41
+ - **Shell-form `system()` calls** — all internal `git` invocations in the generator now
42
+ use array form (`system('git', 'init', '-q')`) so no shell is spawned and there is no
43
+ injection surface
44
+ - **Default bind address changed from `0.0.0.0` to `127.0.0.1`** — the server and
45
+ generated templates now bind localhost-only by default; pass `host: '0.0.0.0'` to
46
+ expose on all interfaces
47
+ - **`BadRequest` details no longer sent to the client** — the error message is logged
48
+ server-side; the response body is now the generic string `'Bad Request'`
49
+
8
50
  ## [0.1.0] - 2024-03-18
9
51
 
10
52
  ### Added
data/README.md CHANGED
@@ -52,7 +52,7 @@ app.start(port: 3710)
52
52
 
53
53
  ```bash
54
54
  ruby server.rb
55
- # => Stīpa listening on 0.0.0.0:3710
55
+ # => Stīpa listening on 127.0.0.1:3710
56
56
  ```
57
57
 
58
58
  ---
@@ -96,6 +96,16 @@ npm run build # compile Vue components
96
96
  bundle exec ruby server.rb
97
97
  ```
98
98
 
99
+ Hot reload during development:
100
+
101
+ ```bash
102
+ STIPA_RELOAD=1 bundle exec ruby server.rb
103
+ ```
104
+
105
+ The reloader watches all `.rb` files under the project root. When a change is saved it
106
+ restarts the process automatically. If the changed file has a syntax error the reloader
107
+ logs the problem and keeps watching — the process will not die on a bad save.
108
+
99
109
  ---
100
110
 
101
111
  ## Routing
@@ -283,16 +293,17 @@ Build: `npm run build` → outputs `public/components/Counter.js`.
283
293
 
284
294
  ```ruby
285
295
  app.start(
286
- host: '0.0.0.0',
296
+ host: '127.0.0.1', # default; use '0.0.0.0' to bind all interfaces
287
297
  port: 3710,
288
- pool_size: 32, # worker threads
289
- queue_depth: 64, # max queued jobs before backpressure
290
- drain_timeout: 30, # graceful shutdown wait (seconds)
298
+ pool_size: 32, # worker threads
299
+ queue_depth: 64, # max queued jobs before backpressure
300
+ drain_timeout: 30, # graceful shutdown wait (seconds)
291
301
  keepalive_timeout: 5,
292
- max_requests: 100, # per connection
302
+ max_requests: 100, # per connection
293
303
  max_body_size: 1_048_576,
294
- backpressure: :drop, # :drop (503) or :block
304
+ backpressure: :drop, # :drop (503) or :block
295
305
  log_level: :info,
306
+ reload: false, # or set STIPA_RELOAD=1 in the environment
296
307
  )
297
308
  ```
298
309
 
@@ -300,6 +311,19 @@ Handles `SIGTERM` / `SIGINT` with graceful drain.
300
311
 
301
312
  ---
302
313
 
314
+ ## Security notes
315
+
316
+ - **Bind address** — the default `host: '127.0.0.1'` exposes the server only on
317
+ localhost. Set `host: '0.0.0.0'` (or the specific interface IP) when running behind a
318
+ reverse proxy or in a container.
319
+ - **CORS** — `Middleware::Cors` never reflects an arbitrary `Origin` header back to the
320
+ client. Wildcard config (`origins: ['*']`) sends the literal `*`; an explicit list
321
+ only allows origins that are in the list.
322
+ - **Hot reload** — `STIPA_RELOAD=1` is intended for development only. Do not enable it
323
+ in production.
324
+
325
+ ---
326
+
303
327
  ## License
304
328
 
305
329
  MIT
data/lib/stipa/app.rb CHANGED
@@ -95,8 +95,14 @@ module Stipa
95
95
 
96
96
  match = case pattern
97
97
  when String
98
- # Exact string match
99
- req.path == pattern ? true : nil
98
+ if pattern.include?(':')
99
+ # Colon-segment pattern: /users/:id named-capture Regexp
100
+ re = pattern.gsub(%r{:([a-zA-Z_][a-zA-Z0-9_]*)}) { "(?<#{Regexp.last_match(1)}>[^/]+)" }
101
+ Regexp.new("\\A#{re}\\z").match(req.path)
102
+ else
103
+ # Exact string match
104
+ req.path == pattern ? true : nil
105
+ end
100
106
  when Regexp
101
107
  # Full Regexp match — named captures become req.params
102
108
  pattern.match(req.path)
@@ -146,8 +146,9 @@ module Stipa
146
146
  rescue BadRequest => e
147
147
  # Protocol violation — send 400 and close the connection.
148
148
  # Closing prevents further requests on a potentially corrupt stream.
149
+ @logger.warn("bad request peer=#{@peer}: #{e.message}")
149
150
  res.status = 400
150
- res.body = "Bad Request: #{e.message}"
151
+ res.body = 'Bad Request'
151
152
  false
152
153
  rescue => e
153
154
  req_id = req.id || '-'
@@ -52,7 +52,7 @@ module Stipa
52
52
 
53
53
  Routes.draw(app)
54
54
 
55
- app.start(host: '0.0.0.0', port: 3710)
55
+ app.start(host: '127.0.0.1', port: 3710)
56
56
  RUBY
57
57
  end
58
58
 
@@ -46,9 +46,9 @@ module Stipa
46
46
  return unless git_available?
47
47
 
48
48
  Dir.chdir(target) do
49
- system('git init -q')
50
- system('git add .')
51
- system("git commit -q -m 'Initial commit'")
49
+ system('git', 'init', '-q')
50
+ system('git', 'add', '.')
51
+ system('git', 'commit', '-q', '-m', 'Initial commit')
52
52
  end
53
53
  say ' git initialized repository with initial commit'
54
54
  rescue => e
@@ -62,7 +62,7 @@ module Stipa
62
62
  end
63
63
 
64
64
  def git_available?
65
- system('git --version > /dev/null 2>&1')
65
+ system('git', '--version', out: File::NULL, err: File::NULL)
66
66
  end
67
67
 
68
68
  # ── Shared templates ───────────────────────────────────────────────────────
@@ -103,7 +103,7 @@ module Stipa
103
103
  h[k] = URI.decode_www_form_component(v.to_s) if k
104
104
  end
105
105
  override = form['_method']&.upcase
106
- req.instance_variable_set(:@method, override) if %w[PUT PATCH DELETE].include?(override)
106
+ req.method = override if %w[PUT PATCH DELETE].include?(override)
107
107
  end
108
108
  next_app.call(req, res)
109
109
  end
@@ -132,7 +132,10 @@ module Stipa
132
132
 
133
133
  def resolve(to)
134
134
  ctrl, action = to.split('#', 2)
135
- klass = Object.const_get(ctrl.split('_').map(&:capitalize).join + 'Controller')
135
+ # 'admin/users' 'Admin::UsersController'
136
+ # 'users' → 'UsersController'
137
+ class_name = ctrl.split('/').map { |seg| seg.split('_').map(&:capitalize).join }.join('::') + 'Controller'
138
+ klass = Object.const_get(class_name)
136
139
  [klass, action.to_sym]
137
140
  end
138
141
 
@@ -169,7 +169,7 @@ module Stipa
169
169
  res.json({ status: 'ok', framework: 'Stipa', version: Stipa::VERSION, ts: Time.now.utc.iso8601 })
170
170
  end
171
171
 
172
- app.start(host: '0.0.0.0', port: 3710)
172
+ app.start(host: '127.0.0.1', port: 3710)
173
173
  RUBY
174
174
  end
175
175
 
@@ -102,16 +102,22 @@ module Stipa
102
102
  end
103
103
 
104
104
  def call(req, res)
105
- origin = req['origin'] || '*'
106
- allowed = @origins.include?('*') || @origins.include?(origin)
105
+ origin = req['origin']
106
+ wildcard = @origins.include?('*')
107
+ allowed = wildcard || (origin && @origins.include?(origin))
107
108
 
108
109
  if allowed
109
- res.set_header('Access-Control-Allow-Origin', origin)
110
+ # Never reflect an arbitrary Origin back. When the allowlist is '*',
111
+ # set the header to the literal '*'. When using an explicit list, only
112
+ # echo origins that are actually in the list (already guaranteed by
113
+ # the `allowed` check above).
114
+ res.set_header('Access-Control-Allow-Origin',
115
+ wildcard ? '*' : origin)
110
116
  res.set_header('Access-Control-Allow-Methods', @methods)
111
117
  res.set_header('Access-Control-Allow-Headers',
112
118
  'Content-Type, Authorization, X-Request-Id')
113
119
  # Vary tells caches that the response differs by Origin
114
- res.set_header('Vary', 'Origin')
120
+ res.set_header('Vary', 'Origin') unless wildcard
115
121
  end
116
122
 
117
123
  # OPTIONS preflight: respond immediately without hitting the router
@@ -44,7 +44,16 @@ module Stipa
44
44
  def watch_loop
45
45
  loop do
46
46
  sleep @interval
47
- if changed?
47
+ dirty = dirty_files
48
+ next if dirty.empty?
49
+
50
+ bad = dirty.select { |f| f.end_with?('.rb') && !syntax_ok?(f) }
51
+ if bad.any?
52
+ bad.each { |f| @logger.warn("syntax error in #{f} — fix and save to restart") }
53
+ # Advance mtime only for clean files so bad ones stay dirty until fixed
54
+ (dirty - bad).each { |f| @mtimes[f] = mtime(f) }
55
+ else
56
+ dirty.each { |f| @mtimes[f] = mtime(f) }
48
57
  @logger.warn('file change detected — restarting')
49
58
  $stdout.flush
50
59
  $stderr.flush
@@ -68,13 +77,21 @@ module Stipa
68
77
  end
69
78
  end
70
79
 
80
+ # Returns files whose mtime has changed since the last snapshot, without
81
+ # updating @mtimes (callers decide when to advance the snapshot).
82
+ def dirty_files
83
+ watched_files.select { |path| mtime(path) != @mtimes[path] }
84
+ end
85
+
86
+ # Kept for backwards compatibility with tests / external callers.
71
87
  def changed?
72
- watched_files.any? do |path|
73
- current = mtime(path)
74
- previous = @mtimes[path]
75
- @mtimes[path] = current
76
- current != previous
77
- end
88
+ dirty = dirty_files
89
+ dirty.each { |f| @mtimes[f] = mtime(f) }
90
+ dirty.any?
91
+ end
92
+
93
+ def syntax_ok?(path)
94
+ system(RbConfig.ruby, '-c', path, out: File::NULL, err: File::NULL)
78
95
  end
79
96
 
80
97
  def perform_restart
data/lib/stipa/request.rb CHANGED
@@ -23,8 +23,8 @@ module Stipa
23
23
  MAX_BODY_SIZE = 1 * 1024 * 1024 # 1 MB default; configurable per-server
24
24
  VALID_METHODS = %w[GET POST PUT PATCH DELETE HEAD OPTIONS TRACE CONNECT].freeze
25
25
 
26
- attr_accessor :id, :params
27
- attr_reader :method, :path, :query_string, :http_version,
26
+ attr_accessor :id, :params, :method
27
+ attr_reader :path, :query_string, :http_version,
28
28
  :headers, :body, :bytes_in
29
29
 
30
30
  # Factory — called by Connection after reading the header block.
data/lib/stipa/server.rb CHANGED
@@ -39,7 +39,7 @@ module Stipa
39
39
  # 3. server socket is closed in the ensure block of start
40
40
  class Server
41
41
  DEFAULT_CONFIG = {
42
- host: '0.0.0.0',
42
+ host: '127.0.0.1',
43
43
  port: 3710,
44
44
  pool_size: 32,
45
45
  queue_depth: 64,
data/lib/stipa/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Stipa
2
- VERSION = '0.1.4'
2
+ VERSION = '0.1.6'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stipa
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pedro Harbs