otto 1.3.0 → 1.4.0

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: 7974135aa7b516b1d0424fd1fb7f686524f0d091168a19df5527ec1e69d7314f
4
- data.tar.gz: a20a52ce1479f00f880afa3ad43ed43bb85f34e519c9e024cd717a7d873dad5c
3
+ metadata.gz: 746b45b9ceea5aed255a2d39e578f65bbf4438791bf61aad37641cbd4313c365
4
+ data.tar.gz: 8e61944be4c19656684c5630bf74024c3738d4ed6b8891686be4beb735aaaddd
5
5
  SHA512:
6
- metadata.gz: 6a69222b7e5234e487d0098a25bbd36b2e9fc7637f563cf9f6b8562e5b03a314f06b97de755a1ec061310222c8c8ac35f215a8f6ad2b0d3e541ba1eec658f73d
7
- data.tar.gz: 503f510059cc42f553981cd5ec27a19c5655d2047e11263eb81c63d7b2a49a09ebcd7f5df83da6f68e295b9927f8859812a2240ad4b1e05e8365702cfb89f91b
6
+ metadata.gz: 0bd4f87e1d824c2dc6683f6909db32e0e0a460b65a8d1646a35f2ac464a450d8804f72cacad60eae066601867f84b1d97c8bbccb02211ed43e5e408d8b269894
7
+ data.tar.gz: 999b4b96f74cace0b236dea4ef8f16607713a66960be3301b30b3f3e9e9e7f047b6381820cf701d5385afb10641537c2e1726993ff5c84ae75f6325eefe53cc8
@@ -0,0 +1,15 @@
1
+ # To get started with Dependabot version updates, you'll need to specify which
2
+ # package ecosystems to update and where the package manifests are located.
3
+ # Please see the documentation for all configuration options:
4
+ # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5
+
6
+ version: 2
7
+ updates:
8
+ - package-ecosystem: "github-actions"
9
+ directory: "/" # Location of package manifests
10
+ schedule:
11
+ interval: "weekly"
12
+ - package-ecosystem: "bundler"
13
+ directory: "/"
14
+ schedule:
15
+ interval: "weekly"
@@ -0,0 +1,34 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ pull_request:
9
+
10
+ workflow_dispatch:
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ jobs:
16
+ test:
17
+ timeout-minutes: 10
18
+ runs-on: ubuntu-24.04
19
+ name: "RSpec Tests (Ruby ${{ matrix.ruby }})"
20
+ strategy:
21
+ fail-fast: true
22
+ matrix:
23
+ ruby: ["3.2", "3.3", "3.4", "3.5"]
24
+
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ - name: Set up Ruby
28
+ uses: ruby/setup-ruby@v1
29
+ with:
30
+ ruby-version: ${{ matrix.ruby }}
31
+ bundler-cache: true
32
+
33
+ - name: Run RSpec tests
34
+ run: bundle exec rspec
@@ -0,0 +1,107 @@
1
+ ##
2
+ # Pre-Commit Hooks Configuration
3
+ #
4
+ # Fast, lightweight code quality checks that run before each commit
5
+ #
6
+ # Setup:
7
+ # 1. Install pre-commit:
8
+ # $ pip install pre-commit
9
+ #
10
+ # 2. Install git hooks:
11
+ # $ pre-commit install
12
+ #
13
+ # Usage:
14
+ # Hooks run automatically on 'git commit'
15
+ #
16
+ # Manual commands:
17
+ # - Check all files:
18
+ # $ pre-commit run --all-files
19
+ #
20
+ # - Update hooks:
21
+ # $ pre-commit autoupdate
22
+ #
23
+ # - Reinstall after config changes:
24
+ # $ pre-commit install
25
+ #
26
+ # Best Practices:
27
+ # - Reinstall hooks after modifying this config
28
+ # - Commit config changes in isolation
29
+ # - Keep checks fast to maintain workflow
30
+ #
31
+ # Resources:
32
+ # - Docs: https://pre-commit.com
33
+ # - Available hooks: https://pre-commit.com/hooks.html
34
+ #
35
+ # Note: These lightweight checks maintain code quality without
36
+ # slowing down the local development process.
37
+
38
+ # Hook installation configuration
39
+ default_install_hook_types:
40
+ - pre-commit # Primary code quality checks
41
+ - prepare-commit-msg # Commit message preprocessing
42
+ - post-commit # Actions after successful commit
43
+ - post-checkout # Triggered after git checkout
44
+ - post-merge # Triggered after git merge
45
+
46
+ # Default execution stage
47
+ default_stages: [pre-commit]
48
+
49
+ # Avoid multiple sequential commit failures
50
+ fail_fast: false
51
+
52
+ # Ignore generated and dependency directories
53
+ exclude: "^$"
54
+
55
+ repos:
56
+ # Meta hooks: basic checks for pre-commit config itself
57
+ - repo: meta
58
+ hooks:
59
+ - id: check-hooks-apply
60
+ - id: check-useless-excludes
61
+
62
+ # Standard pre-commit hooks: lightweight, universal checks
63
+ - repo: https://github.com/pre-commit/pre-commit-hooks
64
+ rev: v5.0.0
65
+ hooks:
66
+ # Formatting and basic sanitization
67
+ - id: trailing-whitespace # Remove trailing whitespaces
68
+ - id: end-of-file-fixer # Ensure files end with newline
69
+ - id: check-merge-conflict # Detect unresolved merge conflicts
70
+ - id: detect-private-key # Warn about committing private keys
71
+ - id: check-added-large-files # Prevent committing oversized files
72
+ args: ["--maxkb=2500"] # 2.5MB file size threshold
73
+ - id: no-commit-to-branch # Prevent direct commits to critical branches
74
+ args: ["--branch", "main", "--branch", "rel/.*"]
75
+
76
+ # # Ruby code quality and style checks (using local bundler environment)
77
+ # - repo: local
78
+ # hooks:
79
+ # - id: rubocop
80
+ # name: RuboCop
81
+ # description: Ruby static code analyzer and formatter
82
+ # entry: eval "$(rbenv init -)" && bundle exec rubocop
83
+ # language: system
84
+ # files: \.rb$
85
+ # pass_filenames: false
86
+ # - repo: https://github.com/rubocop-hq/rubocop
87
+ # rev: v1.79.1 # Or your desired version
88
+ # hooks:
89
+ # - id: rubocop
90
+ # files: \.rb$
91
+ # pass_filenames: false
92
+ # additional_dependencies:
93
+ # - rubocop-rails # Example of an additional gem
94
+ # - rubocop-performance
95
+ # - rubocop-rspec
96
+
97
+ # Commit message issue tracking integration
98
+ - repo: https://github.com/avilaton/add-msg-issue-prefix-hook
99
+ rev: v0.0.12
100
+ hooks:
101
+ - id: add-msg-issue-prefix
102
+ stages: [prepare-commit-msg]
103
+ description: Automatically prefix commits with issue numbers
104
+ args:
105
+ - "--default="
106
+ - '--pattern=(?:i18n(?=\/)|[a-zA-Z0-9]{0,10}-?[0-9]{1,5})'
107
+ - "--template=[#{}]"
@@ -0,0 +1,88 @@
1
+ ##
2
+ # Pre-Push Hooks Configuration
3
+ #
4
+ # Quality control checks that run before code is pushed to remote repositories
5
+ #
6
+ # Setup:
7
+ # 1. Install the pre-push hook:
8
+ # $ pre-commit install --hook-type pre-push
9
+ #
10
+ # 2. Install required dependencies:
11
+ # - Ruby + Rubocop
12
+ # - Node.js + ESLint
13
+ # - TypeScript compiler
14
+ #
15
+ # Usage:
16
+ # Hooks run automatically on 'git push'
17
+ #
18
+ # Manual execution:
19
+ # - Run all checks:
20
+ # $ pre-commit run --config .pre-push-config.yaml --all-files
21
+ #
22
+ # - Run single check:
23
+ # $ pre-commit run <hook-id> --config .pre-push-config.yaml
24
+ # Example: pre-commit run rubocop --config .pre-push-config.yaml
25
+ #
26
+ # Included Checks:
27
+ # - Full codebase linting (Rubocop, ESLint)
28
+ # - YAML/JSON validation
29
+ # - TypeScript type checking
30
+ # - Code style enforcement
31
+ # - Security vulnerability scanning
32
+ #
33
+ # Related Files:
34
+ # - .pre-commit-config.yaml: Lightweight pre-commit checks
35
+ # - Documentation: https://pre-commit.com
36
+ #
37
+ # Note: These intensive checks run before pushing to catch issues early
38
+ # but allow faster local development with lighter pre-commit hooks.
39
+
40
+ # Allow all failures to happen so they can be corrected in one go
41
+ fail_fast: false
42
+
43
+ # Skip generated/dependency directories
44
+ exclude: "^(vendor|node_modules|dist|build)/"
45
+
46
+ default_install_hook_types:
47
+ - pre-push
48
+ - push
49
+
50
+ default_stages: [push]
51
+
52
+ repos:
53
+ - repo: https://github.com/pre-commit/pre-commit-hooks
54
+ rev: v4.6.0
55
+ hooks:
56
+ - id: check-yaml
57
+ name: Validate YAML files
58
+ args: ["--allow-multiple-documents"]
59
+ files: \.(yaml|yml)$
60
+
61
+ - id: check-toml
62
+ name: Validate TOML files
63
+ files: \.toml$
64
+
65
+ - id: check-json
66
+ name: Validate JSON files
67
+ files: \.json$
68
+
69
+ - id: pretty-format-json
70
+ name: Format JSON files
71
+ args: ["--autofix", "--no-sort-keys"]
72
+ files: \.json$
73
+
74
+ - id: mixed-line-ending
75
+ name: Check line endings
76
+ args: [--fix=lf]
77
+
78
+ - id: check-case-conflict
79
+ name: Check for case conflicts
80
+
81
+ - id: check-executables-have-shebangs
82
+ name: Check executable shebangs
83
+
84
+ - id: check-shebang-scripts-are-executable
85
+ name: Check shebang scripts are executable
86
+
87
+ - id: forbid-submodules
88
+ name: Check for submodules
data/.rubocop.yml CHANGED
@@ -19,7 +19,7 @@ AllCops:
19
19
  DisabledByDefault: false # flip to true for a good autocorrect time
20
20
  UseCache: true
21
21
  MaxFilesInCache: 100
22
- TargetRubyVersion: 3.4
22
+ TargetRubyVersion: 3.2
23
23
  Exclude:
24
24
  - "spec/**/*.rb"
25
25
  - "vendor/**/*"
data/Gemfile CHANGED
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  source 'https://rubygems.org'
4
2
 
5
3
  gemspec
data/Gemfile.lock CHANGED
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- otto (1.3.0)
4
+ otto (1.4.0)
5
+ ostruct
5
6
  rack (~> 3.1, < 4.0)
6
7
  rack-parser (~> 0.7)
7
8
  rexml (>= 3.3.6)
@@ -26,6 +27,7 @@ GEM
26
27
  logger (1.7.0)
27
28
  method_source (1.1.0)
28
29
  minitest (5.25.5)
30
+ ostruct (0.6.3)
29
31
  parallel (1.27.0)
30
32
  parser (3.3.9.0)
31
33
  ast (~> 2.4.1)
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Otto - 1.2 (2025-08-18)
1
+ # Otto - A Ruby Gem
2
2
 
3
3
  **Define your rack-apps in plain-text with built-in security.**
4
4
 
@@ -74,9 +74,65 @@ app = Otto.new("./routes", {
74
74
 
75
75
  Security features include CSRF protection, input validation, security headers, and trusted proxy configuration.
76
76
 
77
+ ## Internationalization Support
78
+
79
+ Otto provides built-in locale detection and management:
80
+
81
+ ```ruby
82
+ # Global configuration (affects all Otto instances)
83
+ Otto.configure do |opts|
84
+ opts.available_locales = { 'en' => 'English', 'es' => 'Spanish', 'fr' => 'French' }
85
+ opts.default_locale = 'en'
86
+ end
87
+
88
+ # Or configure during initialization
89
+ app = Otto.new("./routes", {
90
+ available_locales: { 'en' => 'English', 'es' => 'Spanish', 'fr' => 'French' },
91
+ default_locale: 'en'
92
+ })
93
+
94
+ # Or configure at runtime
95
+ app.configure(
96
+ available_locales: { 'en' => 'English', 'es' => 'Spanish' },
97
+ default_locale: 'en'
98
+ )
99
+
100
+ # Legacy support (still works)
101
+ app = Otto.new("./routes", {
102
+ locale_config: {
103
+ available_locales: { 'en' => 'English', 'es' => 'Spanish', 'fr' => 'French' },
104
+ default_locale: 'en'
105
+ }
106
+ })
107
+ ```
108
+
109
+ In your application, use the locale helper:
110
+
111
+ ```ruby
112
+ class App
113
+ def initialize(req, res)
114
+ @req, @res = req, res
115
+ end
116
+
117
+ def show_product
118
+ # Automatically detects locale from:
119
+ # 1. URL parameter: ?locale=es
120
+ # 2. User preference (if provided)
121
+ # 3. Accept-Language header
122
+ # 4. Default locale
123
+ locale = req.check_locale!
124
+
125
+ # Use locale for localized content
126
+ res.body = localized_content(locale)
127
+ end
128
+ end
129
+ ```
130
+
131
+ The locale helper checks multiple sources in order of precedence and validates against your configured locales.
132
+
77
133
  ## Requirements
78
134
 
79
- - Ruby 3.4+
135
+ - Ruby 3.2+
80
136
  - Rack 3.1+
81
137
 
82
138
  ## Installation
@@ -0,0 +1,244 @@
1
+ require 'otto'
2
+ require 'json'
3
+
4
+ class HelpersDemo
5
+ def initialize(req, res)
6
+ @req, @res = req, res
7
+ end
8
+
9
+ attr_reader :req, :res
10
+
11
+ def index
12
+ res.headers['content-type'] = 'text/html'
13
+ res.body = <<~HTML
14
+ <h1>Otto Request & Response Helpers Demo</h1>
15
+ <p>This demo shows Otto's built-in request and response helpers.</p>
16
+
17
+ <h2>Available Demos:</h2>
18
+ <ul>
19
+ <li><a href="/request-info">Request Information</a> - Shows client IP, user agent, security info</li>
20
+ <li><a href="/locale-demo?locale=es">Locale Detection</a> - Demonstrates locale detection and configuration</li>
21
+ <li><a href="/secure-cookie">Secure Cookies</a> - Sets secure cookies with proper options</li>
22
+ <li><a href="/headers">Response Headers</a> - Shows security headers and custom headers</li>
23
+ <li>
24
+ <form method="POST" action="/csp-demo">
25
+ <button type="submit">CSP Headers Demo</button> - Content Security Policy with nonce
26
+ </form>
27
+ </li>
28
+ </ul>
29
+
30
+ <h2>Try These URLs:</h2>
31
+ <ul>
32
+ <li><a href="/locale-demo?locale=fr">French locale</a></li>
33
+ <li><a href="/locale-demo?locale=invalid">Invalid locale (falls back to default)</a></li>
34
+ </ul>
35
+ HTML
36
+ end
37
+
38
+ def request_info
39
+ # Demonstrate request helpers
40
+ info = {
41
+ 'Client IP' => req.client_ipaddress,
42
+ 'User Agent' => req.user_agent,
43
+ 'HTTP Host' => req.http_host,
44
+ 'Server Name' => req.current_server_name,
45
+ 'Request Path' => req.request_path,
46
+ 'Request URI' => req.request_uri,
47
+ 'Is Local?' => req.local?,
48
+ 'Is Secure?' => req.secure?,
49
+ 'Is AJAX?' => req.ajax?,
50
+ 'Current Absolute URI' => req.current_absolute_uri,
51
+ 'Request Method' => req.request_method
52
+ }
53
+
54
+ # Show collected proxy headers
55
+ proxy_headers = req.collect_proxy_headers(
56
+ header_prefix: 'X_DEMO_',
57
+ additional_keys: ['HTTP_ACCEPT', 'HTTP_ACCEPT_LANGUAGE']
58
+ )
59
+
60
+ # Format request details for logging
61
+ request_details = req.format_request_details(header_prefix: 'X_DEMO_')
62
+
63
+ res.headers['content-type'] = 'text/html'
64
+ res.body = <<~HTML
65
+ <h1>Request Information</h1>
66
+ <p><a href="/">← Back to index</a></p>
67
+
68
+ <h2>Basic Request Info:</h2>
69
+ <table border="1" style="border-collapse: collapse;">
70
+ #{info.map { |k, v| "<tr><td><strong>#{k}</strong></td><td>#{v}</td></tr>" }.join("\n ")}
71
+ </table>
72
+
73
+ <h2>Proxy Headers:</h2>
74
+ <pre>#{proxy_headers}</pre>
75
+
76
+ <h2>Formatted Request Details (for logging):</h2>
77
+ <pre>#{request_details}</pre>
78
+
79
+ <h2>Application Path Helper:</h2>
80
+ <p>App path for ['api', 'v1', 'users']: <code>#{req.app_path('api', 'v1', 'users')}</code></p>
81
+ HTML
82
+ end
83
+
84
+ def locale_demo
85
+ # Demonstrate locale detection with Otto configuration
86
+ current_locale = req.check_locale!(req.params['locale'], {
87
+ preferred_locale: 'es', # Simulate user preference
88
+ locale_env_key: 'demo.locale',
89
+ debug: true
90
+ })
91
+
92
+ # Show what was stored in environment
93
+ stored_locale = req.env['demo.locale']
94
+
95
+ res.headers['content-type'] = 'text/html'
96
+ res.body = <<~HTML
97
+ <h1>Locale Detection Demo</h1>
98
+ <p><a href="/">← Back to index</a></p>
99
+
100
+ <h2>Locale Detection Results:</h2>
101
+ <table border="1" style="border-collapse: collapse;">
102
+ <tr><td><strong>Detected Locale</strong></td><td>#{current_locale}</td></tr>
103
+ <tr><td><strong>Stored in Environment</strong></td><td>#{stored_locale}</td></tr>
104
+ <tr><td><strong>Query Parameter</strong></td><td>#{req.params['locale'] || 'none'}</td></tr>
105
+ <tr><td><strong>Accept-Language Header</strong></td><td>#{req.env['HTTP_ACCEPT_LANGUAGE'] || 'none'}</td></tr>
106
+ </table>
107
+
108
+ <h2>Locale Sources (in precedence order):</h2>
109
+ <ol>
110
+ <li>URL Parameter: <code>?locale=#{req.params['locale'] || 'none'}</code></li>
111
+ <li>User Preference: <code>es</code> (simulated)</li>
112
+ <li>Rack Locale: <code>#{req.env['rack.locale']&.first || 'none'}</code></li>
113
+ <li>Default: <code>en</code></li>
114
+ </ol>
115
+
116
+ <h2>Try Different Locales:</h2>
117
+ <ul>
118
+ <li><a href="/locale-demo?locale=en">English (en)</a></li>
119
+ <li><a href="/locale-demo?locale=es">Spanish (es)</a></li>
120
+ <li><a href="/locale-demo?locale=fr">French (fr)</a></li>
121
+ <li><a href="/locale-demo?locale=invalid">Invalid locale</a></li>
122
+ <li><a href="/locale-demo">No locale parameter</a></li>
123
+ </ul>
124
+ HTML
125
+ end
126
+
127
+ def secure_cookie
128
+ # Demonstrate secure cookie helpers
129
+ res.send_secure_cookie('demo_secure', 'secure_value_123', 3600, {
130
+ path: '/helpers_demo',
131
+ secure: !req.local?, # Only secure in production
132
+ same_site: :strict
133
+ })
134
+
135
+ res.send_session_cookie('demo_session', 'session_value_456', {
136
+ path: '/helpers_demo'
137
+ })
138
+
139
+ res.headers['content-type'] = 'text/html'
140
+ res.body = <<~HTML
141
+ <h1>Secure Cookies Demo</h1>
142
+ <p><a href="/">← Back to index</a></p>
143
+
144
+ <h2>Cookies Set:</h2>
145
+ <ul>
146
+ <li><strong>demo_secure</strong> - Secure cookie with 1 hour TTL</li>
147
+ <li><strong>demo_session</strong> - Session cookie (no expiration)</li>
148
+ </ul>
149
+
150
+ <h2>Cookie Security Features:</h2>
151
+ <ul>
152
+ <li>Secure flag (HTTPS only in production)</li>
153
+ <li>HttpOnly flag (prevents XSS access)</li>
154
+ <li>SameSite=Strict (CSRF protection)</li>
155
+ <li>Proper expiration handling</li>
156
+ </ul>
157
+
158
+ <p>Check your browser's developer tools to see the cookie headers!</p>
159
+ HTML
160
+ end
161
+
162
+ def csp_demo
163
+ # Demonstrate CSP headers with nonce
164
+ nonce = SecureRandom.base64(16)
165
+
166
+ res.send_csp_headers('text/html; charset=utf-8', nonce, {
167
+ development_mode: req.local?,
168
+ debug: true
169
+ })
170
+
171
+ res.body = <<~HTML
172
+ <h1>Content Security Policy Demo</h1>
173
+ <p><a href="/">← Back to index</a></p>
174
+
175
+ <h2>CSP Header Generated</h2>
176
+ <p>This page includes a CSP header with a nonce. Check the response headers!</p>
177
+
178
+ <h2>Nonce Value:</h2>
179
+ <p><code>#{nonce}</code></p>
180
+
181
+ <h2>Inline Script with Nonce:</h2>
182
+ <script nonce="#{nonce}">
183
+ console.log('This script runs because it has the correct nonce!');
184
+ document.addEventListener('DOMContentLoaded', function() {
185
+ document.getElementById('nonce-demo').innerHTML = 'Nonce verification successful!';
186
+ });
187
+ </script>
188
+
189
+ <div id="nonce-demo" style="padding: 10px; background: #d4edda; border: 1px solid #c3e6cb; color: #155724;">
190
+ Loading...
191
+ </div>
192
+
193
+ <p><strong>Note:</strong> Without the nonce, inline scripts would be blocked by CSP.</p>
194
+ HTML
195
+ end
196
+
197
+ def show_headers
198
+ # Demonstrate response headers and security features
199
+ res.set_cookie('demo_header', {
200
+ value: 'header_demo_value',
201
+ max_age: 1800,
202
+ secure: !req.local?,
203
+ httponly: true
204
+ })
205
+
206
+ # Add cache control
207
+ res.no_cache!
208
+
209
+ # Get security headers that would be added
210
+ security_headers = res.cookie_security_headers
211
+
212
+ res.headers['content-type'] = 'text/html'
213
+ res.headers['X-Demo-Header'] = 'Custom header value'
214
+
215
+ res.body = <<~HTML
216
+ <h1>Response Headers Demo</h1>
217
+ <p><a href="/">← Back to index</a></p>
218
+
219
+ <h2>Custom Headers Set:</h2>
220
+ <ul>
221
+ <li><strong>X-Demo-Header:</strong> Custom header value</li>
222
+ <li><strong>Cache-Control:</strong> no-store, no-cache, must-revalidate, max-age=0</li>
223
+ <li><strong>Set-Cookie:</strong> demo_header (with security options)</li>
224
+ </ul>
225
+
226
+ <h2>Security Headers Available:</h2>
227
+ <table border="1" style="border-collapse: collapse;">
228
+ #{security_headers.map { |k, v| "<tr><td><strong>#{k}</strong></td><td>#{v}</td></tr>" }.join("\n ")}
229
+ </table>
230
+
231
+ <p>Use your browser's developer tools to inspect all response headers!</p>
232
+ HTML
233
+ end
234
+
235
+ def not_found
236
+ res.status = 404
237
+ res.headers['content-type'] = 'text/html'
238
+ res.body = <<~HTML
239
+ <h1>404 - Page Not Found</h1>
240
+ <p><a href="/">← Back to index</a></p>
241
+ <p>This is a custom 404 page demonstrating error handling.</p>
242
+ HTML
243
+ end
244
+ end
@@ -0,0 +1,26 @@
1
+ require_relative '../../lib/otto'
2
+ require_relative 'app'
3
+
4
+ # Global configuration for all Otto instances
5
+ Otto.configure do |opts|
6
+ opts.available_locales = {
7
+ 'en' => 'English',
8
+ 'es' => 'Spanish',
9
+ 'fr' => 'French'
10
+ }
11
+ opts.default_locale = 'en'
12
+ end
13
+
14
+ # Configure Otto with security features
15
+ app = Otto.new("./routes", {
16
+ # Security features
17
+ csrf_protection: true,
18
+ request_validation: true,
19
+ trusted_proxies: ['127.0.0.1', '::1']
20
+ })
21
+
22
+ # Enable additional security headers
23
+ app.enable_csp_with_nonce!(debug: true)
24
+ app.enable_frame_protection!('SAMEORIGIN')
25
+
26
+ run app
@@ -0,0 +1,7 @@
1
+ GET / HelpersDemo#index
2
+ GET /request-info HelpersDemo#request_info
3
+ GET /locale-demo HelpersDemo#locale_demo
4
+ GET /secure-cookie HelpersDemo#secure_cookie
5
+ POST /csp-demo HelpersDemo#csp_demo
6
+ GET /headers HelpersDemo#show_headers
7
+ GET /404 HelpersDemo#not_found
@@ -0,0 +1,27 @@
1
+ # lib/otto/helpers/base.rb
2
+
3
+ class Otto
4
+ module BaseHelpers
5
+ # Build application path by joining path segments
6
+ #
7
+ # This method safely joins multiple path segments, handling
8
+ # duplicate slashes and ensuring proper path formatting.
9
+ # Includes the script name (mount point) as the first segment.
10
+ #
11
+ # @param paths [Array<String>] Path segments to join
12
+ # @return [String] Properly formatted path
13
+ #
14
+ # @example
15
+ # app_path('api', 'v1', 'users')
16
+ # # => "/myapp/api/v1/users"
17
+ #
18
+ # @example
19
+ # app_path(['admin', 'settings'])
20
+ # # => "/myapp/admin/settings"
21
+ def app_path(*paths)
22
+ paths = paths.flatten.compact
23
+ paths.unshift(env['SCRIPT_NAME']) if env['SCRIPT_NAME']
24
+ paths.join('/').gsub('//', '/')
25
+ end
26
+ end
27
+ end