belt 0.2.9 → 0.2.11

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +119 -0
  3. data/README.md +217 -8
  4. data/lib/belt/action_router.rb +33 -5
  5. data/lib/belt/assets/welcome.css +119 -47
  6. data/lib/belt/cli/bucket_security.rb +10 -4
  7. data/lib/belt/cli/deploy_command.rb +48 -0
  8. data/lib/belt/cli/destroy_command.rb +119 -10
  9. data/lib/belt/cli/doctor_command.rb +208 -0
  10. data/lib/belt/cli/environment_command.rb +38 -6
  11. data/lib/belt/cli/frontend_command.rb +25 -12
  12. data/lib/belt/cli/frontend_setup_command.rb +41 -0
  13. data/lib/belt/cli/new_command.rb +106 -38
  14. data/lib/belt/cli/path_gem_materializer.rb +141 -0
  15. data/lib/belt/cli/plugin_command.rb +221 -0
  16. data/lib/belt/cli/setup_command.rb +35 -10
  17. data/lib/belt/cli.rb +8 -0
  18. data/lib/belt/configuration.rb +30 -0
  19. data/lib/belt/controllers/welcome_controller.rb +69 -2
  20. data/lib/belt/helpers/cors_origin.rb +19 -2
  21. data/lib/belt/helpers/response.rb +48 -6
  22. data/lib/belt/http_status.rb +89 -0
  23. data/lib/belt/lambda_handler.rb +14 -5
  24. data/lib/belt/rendering.rb +1 -0
  25. data/lib/belt/version.rb +1 -1
  26. data/lib/belt/views/welcome/show.html.erb +31 -31
  27. data/lib/belt.rb +16 -0
  28. data/lib/belt_controller/base.rb +24 -1
  29. data/lib/belt_controller/implicit_response.rb +104 -0
  30. data/lib/templates/environment/backend.tf.erb +1 -1
  31. data/lib/templates/frontend/react/src/index.css +4 -4
  32. data/lib/templates/frontend/react/src/lib/apiClient.js.erb +20 -3
  33. data/lib/templates/frontend/react/src/pages/Home.jsx.erb +283 -4
  34. data/lib/templates/module/main.tf.erb +13 -3
  35. data/lib/templates/new_app/config/routes.tf.rb.erb +2 -1
  36. data/lib/templates/plugin/AGENTS.md.erb +135 -0
  37. data/lib/templates/plugin/CHANGELOG.md.erb +5 -0
  38. data/lib/templates/plugin/Gemfile.erb +11 -0
  39. data/lib/templates/plugin/LICENSE.erb +21 -0
  40. data/lib/templates/plugin/README.md.erb +63 -0
  41. data/lib/templates/plugin/Rakefile.erb +7 -0
  42. data/lib/templates/plugin/gemspec.erb +26 -0
  43. data/lib/templates/plugin/gitignore.erb +11 -0
  44. data/lib/templates/plugin/lib/belt/generators/generator.rb.erb +122 -0
  45. data/lib/templates/plugin/lib/belt/module/configuration.rb.erb +15 -0
  46. data/lib/templates/plugin/lib/belt/module/version.rb.erb +7 -0
  47. data/lib/templates/plugin/lib/belt/module.rb.erb +27 -0
  48. data/lib/templates/plugin/lib/entry.rb.erb +3 -0
  49. data/lib/templates/plugin/rspec.erb +3 -0
  50. data/lib/templates/plugin/spec/configuration_spec.rb.erb +17 -0
  51. data/lib/templates/plugin/spec/spec_helper.rb.erb +19 -0
  52. metadata +37 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 37cf283dc30b43f875f8600eb4b00e46b5782f374d3be306d0c5adaec7e9024a
4
- data.tar.gz: 1153f552cef87426f91afcf28647065017b77a09bdd9347241366ed9cc455b76
3
+ metadata.gz: 43ae84461595be08ca6caada3afd148ccfa0db94bfc2e2bd864a9b466d41f087
4
+ data.tar.gz: 1f33826b8c7a7b068458db8ca38aa9910079e16f04c3c5c727550f084ed85075
5
5
  SHA512:
6
- metadata.gz: 4cafc0f29e0148e74950e9fe0898e74632f2563980f60ffece7bfbb58ec7187ec30f928a0958dff9d9832fe79b21e8a87e512f26c4cddfa58c1f243776cea5eb
7
- data.tar.gz: 7850fc8e4c5921646c25f77f67fdfcae663e148ec597b514b095b637733b0e55f34182e8c64ea52f2294ed687b3a24f7b6a56462b30df07811a01ffae23da952
6
+ metadata.gz: 81532edca53a084bdd6aabf5e7027fd1653ec0fc4c592b0bdb60374283c7c59512a2217405d3a19f32a975bcbcbbebdf9f9a97d952a25b1567e05af8e2cb879d
7
+ data.tar.gz: d5ff609d6526f8cc59964df6da091a43150eca9d1c6f625dee27b070db0c56bd90ef58ef11c42f57e27259f169c477a4c1c548f9ca0f4c4f916c8bea540763bb
data/CHANGELOG.md CHANGED
@@ -1,5 +1,124 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.2.11
6
+
7
+ ### Security hardening
8
+
9
+ - **CORS origin validation**: format validation (scheme required, no paths/queries/fragments), length limit (253 chars) to prevent ReDoS, restrict wildcard matching to valid subdomain chars `[a-z0-9-]`.
10
+ - **Security headers** on all responses: `X-Content-Type-Options: nosniff`, `Vary: Origin`; HTML responses also get `X-Frame-Options: DENY` and `Referrer-Policy: strict-origin-when-cross-origin`.
11
+ - **Path traversal protection** in `ActionRouter`: strict regex for controller names, reject `..` sequences / absolute paths / backslashes, `realpath` validation in `resolve_from_paths`.
12
+ - **Request body size limit** (10 MB): returns 413 for oversized bodies; proper 400 for invalid JSON.
13
+
14
+ ### Safe `belt destroy environment`
15
+
16
+ Previously `belt destroy environment <name>` immediately deleted the infrastructure directory with no checks — leaving orphaned AWS resources.
17
+
18
+ Now the command:
19
+ 1. Checks if Terraform state exists (local `.terraform` dir or remote state)
20
+ 2. If state found, prompts user to run `terraform destroy` first
21
+ 3. Requires explicit confirmation before deleting files
22
+ 4. Supports `--force` (skip prompts) and `--skip-terraform` flags
23
+
24
+ ### Environment generator state bucket fix
25
+
26
+ The environment generator was writing `belt-terraform-state` as the backend bucket name, missing the account-ID suffix. Now resolves via sibling `backend.tf`, STS `get-caller-identity`, or placeholder (for later `belt setup state` patch).
27
+
28
+ ### Contributing & plugins
29
+
30
+ - README: **Plugins** and **Contributing** sections — how to contribute to belt, how plugins register via `GeneratorRegistry`, layout used by `belt-messaging` / `belt-pay`, generator checklist for humans and agents.
31
+ - **`belt plugin new <name>`** — scaffold a new plugin gem (gemspec, `Belt::<Name>` module, generator stub, RSpec, README, AGENTS.md), similar to `rails plugin new`.
32
+ - **`AGENTS.md`** at the belt gem root — agent-oriented map of core layout, CLI, and the plugin/GeneratorRegistry contract (complements README).
33
+ - Plugin scaffold includes **`AGENTS.md`** so new plugins match the agent guidance pattern already used by `belt new` apps.
34
+
35
+ ## 0.2.10
36
+
37
+ ### Quiet `belt new` (with optional verbose)
38
+
39
+ `belt new` prints a short phase summary by default (skeleton, environments, frontend,
40
+ bundle, state bucket) instead of every path plus AWS/npm noise.
41
+
42
+ Use `-v` / `--verbose` for Rails-style per-file `create` lines when you want the
43
+ inventory. Nested generators skip their own next-steps banners; the final success
44
+ block still owns next steps. State-bucket setup stays non-interactive either way.
45
+
46
+ ### State bucket naming (global uniqueness)
47
+
48
+ S3 bucket names are a **global** namespace across all AWS accounts. The previous default
49
+ `belt-terraform-state` only works for the first account that creates it — everyone else
50
+ hits "owned by a different AWS account".
51
+
52
+ **Fix:** shared bucket is still one-per-account (all apps share it), but the name is now:
53
+
54
+ ```
55
+ belt-terraform-state-<account_id>
56
+ ```
57
+
58
+ - Still one bucket for all belt apps in an account (state key: `<app>/<env>/terraform.tfstate`)
59
+ - `--bucket` still overrides when you want a custom name
60
+ - `belt setup state` rewrites `backend.tf` with the resolved name
61
+ - Existing installs that already own `belt-terraform-state` can keep using `--bucket belt-terraform-state`
62
+
63
+ ### Implicit responses (Rails-style assigns)
64
+
65
+ Controllers can set instance variables and skip the explicit `success_response` call:
66
+
67
+ ```ruby
68
+ def index
69
+ @posts = Post.all
70
+ end
71
+
72
+ def show
73
+ @post = Post.find(params[:id])
74
+ end
75
+ ```
76
+
77
+ Belt auto-builds `success_response({ posts: [...] })` / `{ post: {...} }` from assigns set
78
+ during the action. Serialization:
79
+
80
+ - Models → `to_h` (ActiveItem already defines this)
81
+ - `ActiveItem::Relation` / Enumerable → map each record via `to_h`
82
+ - Nested hashes/arrays recurse
83
+
84
+ Explicit `success_response` / `error_response` / `html_response` / `render` / `head` still win
85
+ when returned. Rescue handlers still use `error_response`.
86
+
87
+ ### Default format (`:json` / `:html`)
88
+
89
+ ```ruby
90
+ # App-wide (lambda/config/environment.rb)
91
+ Belt.configure do |c|
92
+ c.default_format = :json # default — API first
93
+ end
94
+
95
+ # Per-controller override (inherits down the chain)
96
+ class PagesController < ApplicationController
97
+ self.default_format = :html
98
+ end
99
+ ```
100
+
101
+ | `default_format` | Implicit response (no explicit helper) |
102
+ |---|---|
103
+ | `:json` (default) | `success_response` from assigns |
104
+ | `:html` | `render` template `views/<controller>/<action>.html.erb` (missing → `TemplateNotFound`) |
105
+
106
+ ### Symbol status codes, `head`, `response_status`
107
+
108
+ ```ruby
109
+ success_response({ post: post.to_h }, :created) # 201
110
+ error_response("Nope", :unprocessable_entity) # 422
111
+ head :no_content # 204 empty body
112
+ head :created # 201 empty body
113
+
114
+ def create
115
+ @post = Post.create!(...)
116
+ response_status :created # 201 + implicit assigns body
117
+ end
118
+ ```
119
+
120
+ Bare integers still work. Symbols match the Rack/Rails names (`:created`, `:not_found`, …).
121
+
3
122
  ## 0.3.0
4
123
 
5
124
  ### Pre-Deploy Backups
data/README.md CHANGED
@@ -71,24 +71,31 @@ require "belt"
71
71
  class PostsController < BeltController::Base
72
72
  before_action :authenticate!
73
73
 
74
+ # Implicit response: assigns become the JSON body
75
+ # → { "posts": [ { "id": "...", "title": "..." }, ... ] }
74
76
  def index
75
- posts = Post.where(user_id: current_user_id, index: "UserIndex")
76
- success_response(posts.map(&:attributes))
77
+ @posts = Post.where(user_id: current_user_id, index: "UserIndex")
77
78
  end
78
79
 
79
80
  def show
80
- post = Post.find(params["id"])
81
- success_response(post.attributes)
81
+ @post = Post.find(params["id"])
82
82
  end
83
83
 
84
+ # Implicit body + non-200 status
84
85
  def create
85
86
  attrs = params.require(:post).permit(:title, :body).to_h
86
- post = Post.create!(attrs.merge(user_id: current_user_id))
87
- success_response(post.attributes, 201)
87
+ @post = Post.create!(attrs.merge(user_id: current_user_id))
88
+ response_status :created
89
+ end
90
+
91
+ def destroy
92
+ Post.find(params["id"]).destroy
93
+ head :no_content
88
94
  end
89
95
  end
90
96
  ```
91
97
 
98
+
92
99
  ### 4. Lambda entry point
93
100
 
94
101
  Use `Belt::LambdaHandler` to get automatic observability, CORS preflight handling, and error wrapping:
@@ -199,9 +206,42 @@ end
199
206
 
200
207
  ```ruby
201
208
  success_response({ id: "123", name: "Example" }) # 200 JSON with CORS
202
- success_response({ id: "123" }, 201) # 201 Created
203
- error_response("Not found", 404) # 404 JSON error
209
+ success_response({ id: "123" }, :created) # 201 Created (symbol or int)
210
+ error_response("Not found", :not_found) # 404 JSON error
211
+ error_response("Nope", :unprocessable_entity) # 422
204
212
  html_response("<h1>Hello</h1>") # 200 HTML with CORS
213
+ head :no_content # 204 empty body
214
+ head :created # 201 empty body
215
+ ```
216
+
217
+ ### Default format (API vs HTML)
218
+
219
+ ```ruby
220
+ # App-wide — typically in lambda/config/environment.rb
221
+ Belt.configure do |config|
222
+ config.default_format = :json # default
223
+ end
224
+
225
+ # Per-controller override
226
+ class PagesController < ApplicationController
227
+ self.default_format = :html
228
+ end
229
+ ```
230
+
231
+ - **`:json`** (default): action assigns → `success_response({ posts: [...] })`
232
+ - **`:html`**: action assigns stay on the controller; Belt implicitly `render`s
233
+ `views/<controller>/<action>.html.erb`. Missing template raises `Belt::TemplateNotFound`
234
+ (no silent JSON fallback).
235
+ - Explicit helpers always win: `success_response`, `error_response`, `html_response`,
236
+ `render`, `head`.
237
+
238
+ ### Non-200 with implicit assigns
239
+
240
+ ```ruby
241
+ def create
242
+ @post = Post.create!(...)
243
+ response_status :created # → 201 + { post: {...} }
244
+ end
205
245
  ```
206
246
 
207
247
  ## Controller Discovery
@@ -535,6 +575,175 @@ These are set via Terraform variables in each environment's `terraform.tfvars`.
535
575
 
536
576
  The backup phase reads table names from Terraform outputs. On a brand-new environment that has never been deployed, there are no outputs yet — Belt will warn and skip the backup phase gracefully. After the first successful deploy, backups run normally on subsequent deploys.
537
577
 
578
+ ## Plugins
579
+
580
+ Belt is designed to stay lean. Optional capabilities ship as **separate gems** that plug into the CLI and runtime the same way Rails engines and generators do.
581
+
582
+ ### Official / example plugins
583
+
584
+ | Gem | Purpose | Status |
585
+ |-----|---------|--------|
586
+ | [`belt-messaging`](https://github.com/stowzilla/belt-messaging) | Two-way SMS via AWS End User Messaging (Pinpoint) | Early (not production-hardened) |
587
+ | [`belt-pay`](https://github.com/stowzilla/belt-pay) | Stripe payments & subscriptions | Early (not production-hardened) |
588
+
589
+ Install a plugin in a Belt app:
590
+
591
+ ```ruby
592
+ # Gemfile
593
+ gem "belt-messaging"
594
+ # gem "belt-pay"
595
+ ```
596
+
597
+ ```bash
598
+ bundle install
599
+ belt generate messaging # or: belt g pay
600
+ belt generate --help # lists built-ins + gem generators
601
+ ```
602
+
603
+ ### How plugins register
604
+
605
+ No central registry file, no initializer hook. Belt discovers generators by scanning loaded gems:
606
+
607
+ 1. Gem is in the app `Gemfile` and bundled
608
+ 2. Gem ships a file at `lib/belt/generators/<name>_generator.rb`
609
+ 3. Class lives in `Belt::Generators`, named `<Name>Generator`
610
+ 4. Implements `.run(args)` (required), `.destroy(args)` and `.description` (optional)
611
+
612
+ That is the whole contract. After `bundle install`, `belt generate <name>` and `belt destroy <name>` just work.
613
+
614
+ See `Belt::CLI::GeneratorRegistry` and the CHANGELOG entry for **0.1.13 Generator Extension API**.
615
+
616
+ ### Plugin layout (canonical)
617
+
618
+ ```
619
+ belt-messaging/
620
+ ├── belt-messaging.gemspec
621
+ ├── lib/
622
+ │ ├── belt-messaging.rb # require entrypoint
623
+ │ └── belt/
624
+ │ ├── messaging.rb # Belt::Messaging API
625
+ │ ├── messaging/
626
+ │ │ ├── configuration.rb
627
+ │ │ ├── version.rb
628
+ │ │ ├── controllers/ # default controllers (optional)
629
+ │ │ └── templates/ # ERB templates for the generator
630
+ │ │ ├── terraform/
631
+ │ │ ├── lambda/
632
+ │ │ ├── config/
633
+ │ │ └── controllers/
634
+ │ └── generators/
635
+ │ └── messaging_generator.rb # ← auto-discovered
636
+ └── spec/
637
+ ```
638
+
639
+ **Runtime code stays in the gem.** Generators copy only what the host app must own (Terraform modules, Lambda entrypoints, optional controller overrides). Prefer gem defaults + `belt g <plugin> --controllers` over dumping everything into the app (same idea as `rails g devise:views`).
640
+
641
+ ### Creating a new plugin
642
+
643
+ Scaffold a ready-to-fill gem (Rails-style `plugin new`):
644
+
645
+ ```bash
646
+ belt plugin new notifications
647
+ # → ./belt-notifications/
648
+
649
+ belt plugin new pay --path ~/Code --summary "Stripe payments for Belt"
650
+ # → ~/Code/belt-pay/
651
+ ```
652
+
653
+ Then:
654
+
655
+ ```bash
656
+ cd belt-notifications
657
+ bundle install
658
+ # implement lib/belt/notifications/* and the generator
659
+ ```
660
+
661
+ Point a Belt app at it while developing:
662
+
663
+ ```ruby
664
+ # In the app Gemfile
665
+ gem "belt-notifications", path: "../belt-notifications"
666
+ ```
667
+
668
+ ```bash
669
+ bundle install
670
+ belt generate notifications
671
+ ```
672
+
673
+ When packaging Lambdas, `belt deploy` vendors path gems into `vendor/cache` so conveyor-belt can package them.
674
+
675
+ ### Generator checklist (for humans and agents)
676
+
677
+ A solid plugin generator typically:
678
+
679
+ 1. **Terraform module** → `infrastructure/modules/<name>/` (`main.tf`, `variables.tf`, `outputs.tf`)
680
+ 2. **Lambda config** → `config/lambda/<name>.yml` (timeout, memory, env, triggers)
681
+ 3. **Lambda entrypoint** → `lambda/<name>.rb` using `Belt::LambdaHandler`
682
+ 4. **Routes / schema** → inject into `config/routes.tf.rb` or `infrastructure/schema.tf.rb` when needed
683
+ 5. **Optional overrides** → `--controllers` flag for app-local subclasses
684
+ 6. **Destroy path** → `belt destroy <name>` removes what generate created
685
+ 7. **Help text** → `.description` + `--help` explaining what was installed and next steps
686
+
687
+ Copy from `belt-messaging` or `belt-pay` rather than inventing a new structure.
688
+
689
+ ## Contributing
690
+
691
+ Bug reports and pull requests are welcome on [GitHub](https://github.com/stowzilla/belt).
692
+
693
+ ### Development setup
694
+
695
+ ```bash
696
+ git clone https://github.com/stowzilla/belt.git
697
+ cd belt
698
+ bundle install
699
+ bundle exec rspec
700
+ bundle exec rubocop
701
+ ```
702
+
703
+ ### Guidelines
704
+
705
+ 1. **Branch from `master`** — open a PR against `master`
706
+ 2. **Keep changes focused** — one concern per PR when practical
707
+ 3. **Follow existing patterns** — look at neighboring files and `lib/belt/cli/` before inventing new ones
708
+ 4. **Tests + lint** — run `bundle exec rspec` and `bundle exec rubocop` before opening (or updating) a PR. CI requires both (plus `bundler-audit`); failing checks block merge on `master`
709
+ 5. **Changelog** — note user-facing changes in `CHANGELOG.md` under the next version / Unreleased
710
+ 6. **No secrets** — never commit AWS keys, tokens, or real account IDs
711
+
712
+ ### Project layout (for contributors)
713
+
714
+ | Path | What lives there |
715
+ |------|------------------|
716
+ | `lib/belt.rb` | Public require entry |
717
+ | `lib/belt/` | Framework core (controller, router, handler, observability) |
718
+ | `lib/belt/cli/` | CLI commands (`new`, `generate`, `deploy`, `plugin`, …) |
719
+ | `lib/belt_controller/` | `BeltController::Base` |
720
+ | `lib/templates/` | ERB templates for `belt new`, generators, plugin scaffold |
721
+ | `exe/belt` | CLI executable |
722
+ | `spec/` | RSpec suite |
723
+ | `AGENTS.md` | Agent-oriented map of this gem (layout, CLI, plugin contract) |
724
+
725
+ AI / coding agents: start with **[AGENTS.md](AGENTS.md)**. Belt apps and plugins scaffolded by the CLI get their own `AGENTS.md` as well (`belt new`, `belt plugin new`).
726
+
727
+ ### Local gem development against an app
728
+
729
+ ```ruby
730
+ # In a Belt app's Gemfile
731
+ gem "belt", path: "../belt"
732
+ ```
733
+
734
+ `belt deploy` detects `path:` gems and materializes them into `vendor/cache` for Lambda packaging so you can iterate without publishing a gem for every try.
735
+
736
+ ### Contributing a plugin
737
+
738
+ Plugins are separate repositories (not vendored into this repo). To add a new first-class capability:
739
+
740
+ 1. `belt plugin new <name>` (or copy `belt-messaging` / `belt-pay`)
741
+ 2. Implement the runtime API + generator contract above
742
+ 3. Document install steps in the plugin README (`gem …` → `belt generate …`)
743
+ 4. Open a PR on the plugin repo; optionally link it from this README's plugin table
744
+
745
+ Questions about plugin design or core changes: open a GitHub issue or discuss in the project Discord.
746
+
538
747
  ## License
539
748
 
540
749
  MIT
@@ -102,15 +102,35 @@ module Belt
102
102
  end
103
103
 
104
104
  def dispatch_to_controller(route_info, event, body)
105
- controller_class = resolve_controller(route_info[:controller])
105
+ controller_name = route_info[:controller]
106
+
107
+ # Prevent path traversal — controller names must be simple identifiers or single-level nested
108
+ unless valid_controller_name?(controller_name)
109
+ Belt::Observability::Logger.instance&.warn('Invalid controller name', controller: controller_name)
110
+ return error_response('Not found', 404, event)
111
+ end
112
+
113
+ controller_class = resolve_controller(controller_name)
106
114
  controller = controller_class.new(event: event, body: body)
107
115
 
108
- controller_name = controller_class.name.split('::').last.gsub('Controller', '')
109
- Belt::Observability::Logger.instance&.info("Processing by #{controller_name}##{route_info[:action]}")
116
+ class_label = controller_class.name.split('::').last.gsub('Controller', '')
117
+ Belt::Observability::Logger.instance&.info("Processing by #{class_label}##{route_info[:action]}")
110
118
 
111
119
  controller.dispatch(route_info[:action].to_sym)
112
120
  end
113
121
 
122
+ # Validate controller names to prevent path traversal.
123
+ # Allows: "posts", "admin/posts" (single nesting level, no dots or special chars)
124
+ def valid_controller_name?(name)
125
+ return false if name.nil? || name.empty?
126
+ return false if name.include?('..')
127
+ return false if name.start_with?('/') || name.end_with?('/')
128
+ return false if name.include?('\\')
129
+
130
+ # Allow only lowercase letters, digits, underscores, and a single forward slash for nesting
131
+ name.match?(%r{\A[a-z][a-z0-9_]*(/[a-z][a-z0-9_]*)?\z})
132
+ end
133
+
114
134
  def resolve_controller(controller_name)
115
135
  # Try namespace module first (app's own controllers)
116
136
  begin
@@ -137,10 +157,18 @@ module Belt
137
157
  def resolve_from_paths(controller_name)
138
158
  if controller_name.include?('/')
139
159
  end
140
- file_name = "#{controller_name}_controller.rb"
160
+ file_name = "#{controller_name.gsub('/', '_')}_controller.rb"
141
161
 
142
162
  Belt.all_controller_paths.each do |path|
143
163
  full_path = File.join(path, file_name)
164
+
165
+ # Ensure resolved path stays within the allowed controller directory
166
+ begin
167
+ resolved = File.realpath(full_path)
168
+ rescue Errno::ENOENT, Errno::EACCES
169
+ next
170
+ end
171
+ next unless resolved.start_with?(File.realpath(path))
144
172
  next unless File.exist?(full_path)
145
173
 
146
174
  require full_path
@@ -165,7 +193,7 @@ module Belt
165
193
  def error_response(message, status_code, event = nil)
166
194
  origin = Belt::Helpers::CorsOrigin.resolve_origin(Belt::Helpers::CorsOrigin.origin_from_event(event))
167
195
  headers = {
168
- 'Access-Control-Allow-Headers' => 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token',
196
+ 'Access-Control-Allow-Headers' => 'Content-Type,Accept,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token',
169
197
  'Access-Control-Allow-Methods' => 'GET,POST,PUT,DELETE,PATCH,OPTIONS',
170
198
  'Content-Type' => 'application/json'
171
199
  }
@@ -1,82 +1,136 @@
1
1
  * { margin: 0; padding: 0; box-sizing: border-box; }
2
+ html, body {
3
+ height: 100%;
4
+ }
2
5
  body {
3
6
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
4
- background: #f8f9fa;
7
+ background: #0b1220;
5
8
  color: #2d3748;
6
9
  min-height: 100vh;
10
+ overflow: hidden;
7
11
  }
8
12
 
9
- /* Hero sectionfull width background image */
13
+ /* Hero fills the viewport all content lives in the overlay (no scroll) */
10
14
  .hero {
11
15
  position: relative;
12
16
  width: 100%;
17
+ height: 100vh;
13
18
  overflow: hidden;
14
19
  }
15
20
  .hero-bg {
16
21
  width: 100%;
17
- height: auto;
22
+ height: 100%;
23
+ object-fit: cover;
24
+ /* Pin the logo / sky — crop hills at the bottom if the viewport is short */
25
+ object-position: top center;
18
26
  display: block;
19
27
  }
20
28
  .overlay {
21
29
  position: absolute;
22
- top: 35%;
30
+ top: 50%;
23
31
  left: 50%;
24
32
  transform: translate(-50%, -50%);
25
- padding: 2rem 3rem;
26
- background: rgba(255, 255, 255, 0.50);
27
- backdrop-filter: blur(6px);
28
- -webkit-backdrop-filter: blur(6px);
29
- border-radius: 12px;
33
+ padding: 1.35rem 1.6rem 1.25rem;
34
+ background: rgba(255, 255, 255, 0.55);
35
+ backdrop-filter: blur(8px);
36
+ -webkit-backdrop-filter: blur(8px);
37
+ border-radius: 14px;
30
38
  text-align: center;
31
- max-width: 600px;
32
- width: 90%;
33
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
39
+ max-width: 560px;
40
+ width: min(92vw, 560px);
41
+ max-height: min(92vh, 620px);
42
+ overflow: auto;
43
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
34
44
  }
35
45
  .overlay h1 {
36
- font-size: 2.2rem;
46
+ font-size: 1.85rem;
37
47
  font-weight: 700;
38
48
  color: #2d3748;
39
- margin-bottom: 0.4rem;
49
+ margin-bottom: 0.3rem;
50
+ line-height: 1.2;
40
51
  }
41
52
  .overlay .subtitle {
42
- font-size: 1.1rem;
53
+ font-size: 0.95rem;
43
54
  color: #4a5568;
55
+ line-height: 1.4;
44
56
  }
45
57
 
46
- /* Content below the hero */
47
- .container {
48
- max-width: 700px;
49
- margin: 0 auto;
50
- padding: 2rem;
51
- }
58
+ /* Stack + next steps sit inside the white card */
52
59
  .stack-check {
53
60
  display: flex;
54
61
  align-items: center;
55
62
  justify-content: center;
56
- gap: 0.75rem;
57
- margin-bottom: 2rem;
63
+ gap: 0.4rem;
64
+ margin: 1rem 0 0.85rem;
58
65
  flex-wrap: wrap;
59
66
  }
60
67
  .check-item {
61
68
  display: flex;
62
69
  align-items: center;
63
- gap: 0.4rem;
64
- padding: 0.5rem 1rem;
70
+ gap: 0.35rem;
71
+ padding: 0.35rem 0.7rem;
65
72
  border-radius: 8px;
66
73
  font-weight: 500;
74
+ font-size: 0.88rem;
67
75
  }
68
76
  .check-pass {
69
- background: rgba(39, 174, 96, 0.1);
70
- border: 1px solid rgba(39, 174, 96, 0.3);
77
+ background: rgba(39, 174, 96, 0.12);
78
+ border: 1px solid rgba(39, 174, 96, 0.35);
71
79
  color: #1e8449;
72
80
  }
81
+ /* Optional / empty DynamoDB — not an error, just not in use yet */
82
+ .check-neutral {
83
+ background: rgba(113, 128, 150, 0.1);
84
+ border: 1px solid rgba(113, 128, 150, 0.28);
85
+ color: #4a5568;
86
+ }
73
87
  .check-warn {
74
88
  background: rgba(243, 156, 18, 0.1);
75
89
  border: 1px solid rgba(243, 156, 18, 0.3);
76
90
  color: #d68910;
77
91
  }
78
- .icon { font-size: 1.1rem; }
79
- .arrow { color: #718096; font-size: 1.2rem; }
92
+ .icon { font-size: 1rem; }
93
+ .arrow { color: #718096; font-size: 1.05rem; }
94
+
95
+ .next-steps {
96
+ text-align: left;
97
+ border-top: 1px solid rgba(45, 55, 72, 0.12);
98
+ padding-top: 0.75rem;
99
+ margin-top: 0.15rem;
100
+ }
101
+ .next-steps h2 {
102
+ font-size: 0.92rem;
103
+ margin-bottom: 0.45rem;
104
+ color: #2d3748;
105
+ text-align: left;
106
+ }
107
+ .next-steps ol {
108
+ padding-left: 1.2rem;
109
+ margin: 0;
110
+ }
111
+ .next-steps li {
112
+ margin-bottom: 0.4rem;
113
+ color: #4a5568;
114
+ line-height: 1.4;
115
+ font-size: 0.88rem;
116
+ }
117
+ .next-steps li:last-child { margin-bottom: 0; }
118
+ .next-steps code,
119
+ .info code {
120
+ background: rgba(99, 45, 145, 0.1);
121
+ padding: 0.12rem 0.35rem;
122
+ border-radius: 4px;
123
+ color: #632d91;
124
+ font-size: 0.78rem;
125
+ word-break: break-word;
126
+ }
127
+
128
+ /* Legacy blocks (local server welcome, older layouts) */
129
+ .container {
130
+ max-width: 700px;
131
+ margin: 0 auto;
132
+ padding: 2rem;
133
+ }
80
134
  .info {
81
135
  background: #ffffff;
82
136
  border: 1px solid #e2e8f0;
@@ -87,26 +141,44 @@ body {
87
141
  .info p { margin-bottom: 0.5rem; color: #4a5568; }
88
142
  .info p:last-child { margin-bottom: 0; }
89
143
  .info strong { color: #2d3748; }
90
- .info code {
91
- background: rgba(99, 45, 145, 0.08);
92
- padding: 0.15rem 0.4rem;
144
+ .dynamodb-hint {
145
+ text-align: center;
146
+ margin: 0.5rem 0;
147
+ color: #4a5568;
148
+ font-size: 0.9rem;
149
+ line-height: 1.45;
150
+ }
151
+ .dynamodb-hint code {
152
+ background: rgba(99, 45, 145, 0.1);
153
+ padding: 0.12rem 0.35rem;
93
154
  border-radius: 4px;
94
155
  color: #632d91;
95
- font-size: 0.85rem;
156
+ font-size: 0.8rem;
157
+ word-break: break-word;
96
158
  }
97
- .next-steps {
98
- background: #ffffff;
99
- border: 1px solid #e2e8f0;
100
- border-radius: 12px;
101
- padding: 1.5rem;
159
+ .footer-note {
160
+ margin-top: 0.75rem;
161
+ padding-top: 0.65rem;
162
+ border-top: 1px solid rgba(45, 55, 72, 0.1);
163
+ font-size: 0.78rem;
164
+ color: #718096;
165
+ text-align: center;
102
166
  }
103
- .next-steps h2 { font-size: 1.1rem; margin-bottom: 1rem; color: #2d3748; }
104
- .next-steps ol { padding-left: 1.5rem; }
105
- .next-steps li { margin-bottom: 0.6rem; color: #4a5568; line-height: 1.5; }
106
- .next-steps code {
107
- background: rgba(99, 45, 145, 0.08);
108
- padding: 0.15rem 0.4rem;
109
- border-radius: 4px;
110
- color: #632d91;
111
- font-size: 0.85rem;
167
+ .footer-note code {
168
+ background: rgba(45, 55, 72, 0.06);
169
+ padding: 0.08rem 0.3rem;
170
+ border-radius: 3px;
171
+ color: #4a5568;
172
+ }
173
+
174
+ /* Tiny viewports: shrink type a bit so everything still fits */
175
+ @media (max-height: 640px) {
176
+ .overlay {
177
+ padding: 1rem 1.15rem;
178
+ }
179
+ .overlay h1 { font-size: 1.5rem; }
180
+ .overlay .subtitle { font-size: 0.88rem; }
181
+ .stack-check { margin: 0.7rem 0 0.55rem; }
182
+ .check-item { padding: 0.28rem 0.55rem; font-size: 0.8rem; }
183
+ .next-steps li { font-size: 0.82rem; }
112
184
  }