Skip to content

Commit 54ed3df

Browse files
committed
add design docs
1 parent 2670ca9 commit 54ed3df

File tree

1 file changed

+355
-0
lines changed

1 file changed

+355
-0
lines changed

docs/design.md

Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
# `hooks` — A Pluggable Ruby Webhook Server Framework
2+
3+
## 📜 1. Project Overview
4+
5+
`hooks` is a **pure-Ruby**, **Grape-based**, **Rack-compatible** webhook server gem that:
6+
7+
* Dynamically mounts endpoints from per-team configs under a configurable `root_path`
8+
* Loads **team handlers** and **global plugins** at boot, with priority ordering for hooks
9+
* Validates configs via **Dry::Schema**, failing fast on invalid YAML/JSON/Hash
10+
* Supports **signature validation** (default HMAC) and **custom validator** classes
11+
* Enforces **allowed\_env\_vars** per endpoint—handlers can only read declared ENV keys
12+
* Offers built-in **authentication modules**: IP whitelisting, API key, OAuth flows
13+
* Applies **CORS policy** globally and allows **per-endpoint overrides**
14+
* Enforces **request limits** (body size) and **timeouts**, configurable at runtime
15+
* Emits **metrics events** (`:request_start`, `:request_end`, `:error`) for downstream integration
16+
* Ships with operational endpoints:
17+
18+
* **GET** `<health_path>`: liveness/readiness payload
19+
* **GET** `<metrics_path>`: JSON array of recent events
20+
* **GET** `<version_path>`: current gem version
21+
22+
* Supplies a **scaffold CLI** and **optional test helpers**
23+
* Boots a demo `<root_path>/hello` route when no config is supplied, to verify setup
24+
25+
> **Server Agnostic:** `hooks` exports a Rack-compatible app. Mount under any Rack server (Puma, Unicorn, Thin, etc.).
26+
27+
---
28+
29+
## 🎯 2. Core Goals
30+
31+
1. **Config-Driven Endpoints**
32+
33+
* Single file per endpoint: YAML, JSON, or Ruby Hash
34+
* Merged into `AppConfig` at boot, validated
35+
* Each endpoint `path` is prefixed by global `root_path` (default `/webhooks`)
36+
37+
2. **Plugin Architecture**
38+
39+
* **Team Handlers**: `class MyHandler < Hooks::Handlers::Base`
40+
* **Global Plugins**: `class MyPlugin < Hooks::Plugins::Lifecycle`
41+
* **Signature Validators**: implement `.valid?(payload:, headers:, secret:, config:)`
42+
* **Hook Priority**: specify ordering in global settings
43+
44+
3. **Security & Isolation**
45+
46+
* `allowed_env_vars` restricts ENV access per handler
47+
* **Sandbox** prevents `require`/`load` outside `handler_dir` and `plugin_dir`
48+
* Auth modules guard endpoints before handler invocation
49+
* Default JSON error responses, with detailed hooks
50+
51+
4. **Operational Endpoints**
52+
53+
* **Health**: liveness/readiness, config checksums
54+
* **Metrics**: JSON events log (last N entries)
55+
* **Version**: gem version report
56+
57+
5. **Developer & Operator Experience**
58+
59+
* Single entrypoint: `app = Hooks.build(...)`
60+
* Multiple configuration methods: path(s), ENV, Ruby Hash
61+
* Graceful shutdown on SIGINT/SIGTERM
62+
* Structured JSON logging with `request_id`, `path`, `handler`, timestamp
63+
* Scaffold generators for handlers and plugins
64+
* Optional `hooks-test` gem for RSpec support
65+
66+
---
67+
68+
## ⚙️ 3. Installation & Invocation
69+
70+
### Gemfile
71+
72+
```ruby
73+
gem "hooks"
74+
```
75+
76+
### Programmatic Invocation
77+
78+
```ruby
79+
require "hooks"
80+
81+
# Returns a Rack-compatible app
82+
app = Hooks.build(
83+
config: "/path/to/endpoints/", # Directory or Array/Hash
84+
settings: "/path/to/settings.yaml", # YAML, JSON, or Hash
85+
log: MyCustomLogger.new, # Optional logger (must respond to #info, #error, etc.)
86+
request_limit: 1_048_576, # Default max body size (bytes)
87+
request_timeout: 15, # Default timeout (seconds)
88+
cors: {
89+
allow_origin: "*", # Default CORS (merged with overrides)
90+
allow_methods: ["GET","POST","OPTIONS"],
91+
allow_headers: ["Content-Type","Authorization"]
92+
},
93+
root_path: "/webhooks" # Default mount prefix
94+
)
95+
```
96+
97+
Mount in `config.ru`:
98+
99+
```ruby
100+
run app
101+
```
102+
103+
### ENV-Based Bootstrap
104+
105+
```bash
106+
export HOOKS_CONFIG_DIR=./config/endpoints
107+
export HOOKS_SETTINGS=./config/settings.yaml
108+
export HOOKS_LOGGER_CLASS=MyCustomLogger
109+
export HOOKS_REQUEST_LIMIT=1048576
110+
export HOOKS_REQUEST_TIMEOUT=15
111+
export HOOKS_CORS='{"allow_origin":"*"}'
112+
export HOOKS_ROOT_PATH="/webhooks"
113+
ruby app.rb
114+
```
115+
116+
> **Hello-World Mode**
117+
> If invoked without `config` or `settings`, serves `GET <root_path>/hello`:
118+
>
119+
> ```json
120+
> { "message": "Hooks is working!" }
121+
> ```
122+
123+
---
124+
125+
## 📁 4. Directory Layout
126+
127+
```text
128+
lib/hooks/
129+
├── app/
130+
│ ├── api.rb # Grape::API subclass exporting all endpoints
131+
│ ├── router_builder.rb # Reads AppConfig to define routes
132+
│ └── endpoint_builder.rb # Wraps each route: CORS, auth, signature, hooks, handler
133+
134+
├── core/
135+
│ ├── builder.rb # Hooks.build: config loading, validation, signal handling - builds a rack compatible app
136+
│ ├── config_loader.rb # Loads + merges per-endpoint configs
137+
│ ├── settings_loader.rb # Loads global settings
138+
│ ├── config_validator.rb # Dry::Schema-based validation
139+
│ ├── logger_factory.rb # Structured JSON logger + context enrichment
140+
│ ├── metrics_emitter.rb # Event emitter for request metrics
141+
│ ├── sandbox.rb # Enforce require/load restrictions
142+
│ └── signal_handler.rb # Trap SIGINT/SIGTERM for graceful shutdown
143+
144+
├── handlers/
145+
│ └── base.rb # `Hooks::Handlers::Base` interface: defines #call
146+
147+
├── plugins/
148+
│ ├── lifecycle.rb # `Hooks::Plugins::Lifecycle` hooks (on_request, response, error)
149+
│ └── signature_validator/ # Default & sample validators
150+
│ ├── base.rb # Abstract interface
151+
│ └── hmac_sha256.rb # Default implementation
152+
153+
├── auth/
154+
│ ├── ip_whitelist.rb # Checks `env['REMOTE_ADDR']`
155+
│ ├── api_key.rb # Validates header or param
156+
│ └── oauth.rb # Simple OAuth token validation
157+
158+
├── version.rb # Provides `Hooks::VERSION`
159+
└── hooks.rb # `require 'hooks'` entrypoint defining Hooks module
160+
```
161+
162+
---
163+
164+
## 🛠️ 5. Config Models
165+
166+
### 5.1 Endpoint Config (per-file)
167+
168+
```yaml
169+
# config/endpoints/team1.yaml
170+
path: /team1 # Mounted at <root_path>/team1
171+
handler: Team1Handler # Class in handler_dir
172+
173+
# Signature validation
174+
verify_signature:
175+
type: default # 'default' uses HMACSHA256, or a custom class name
176+
secret_env_key: TEAM1_SECRET
177+
header: X-Hub-Signature
178+
algorithm: sha256
179+
180+
# Authentication (any mix)
181+
auth:
182+
ip_whitelist:
183+
- 192.0.2.0/28
184+
api_keys:
185+
- KEY1
186+
- KEY2
187+
oauth:
188+
client_id_env: TEAM1_OAUTH_ID
189+
190+
allowed_env_vars:
191+
- TEAM1_SECRET
192+
- DATADOG_API_KEY
193+
194+
opts: # Freeform user-defined options
195+
env: staging
196+
teams: ["infra","billing"]
197+
198+
cors: # Overrides global CORS
199+
allow_origin: "https://github.com"
200+
allow_methods: ["POST"]
201+
allow_headers: ["Content-Type","X-Github-Event"]
202+
```
203+
204+
### 5.2 Global Settings Config
205+
206+
```yaml
207+
# config/settings.yaml
208+
plugin_dir: ./plugins # global plugin directory
209+
handler_dir: ./handlers # handler class directory
210+
log_level: info # debug | info | warn | error
211+
request_limit: 1048576 # max request body size (bytes)
212+
request_timeout: 15 # seconds to allow per request
213+
cors: # default CORS policy
214+
allow_origin: "*"
215+
allow_methods: ["GET","POST","OPTIONS"]
216+
allow_headers: ["Content-Type","Authorization"]
217+
root_path: /webhooks # base path for all endpoint routes - can be completely changed in endpoint configs, ex: /foo
218+
health_path: /health # operational health endpoint
219+
metrics_path: /metrics # operational metrics endpoint
220+
version_path: /version # gem version endpoint
221+
environment: production # development | production
222+
```
223+
224+
---
225+
226+
## 🔍 6. Core Components & Flow
227+
228+
1. **Builder (`core/builder.rb`)**
229+
230+
* Load settings (env or file) via `settings_loader`
231+
* Load endpoint configs via `config_loader`
232+
* Validate via `config_validator` (Dry::Schema); halt if invalid at boot
233+
* Initialize structured JSON logger via `logger_factory`
234+
* Emit startup `:request_start` for `/health`, `/metrics`, `/version`
235+
* Trap SIGINT/SIGTERM for graceful shutdown
236+
* Build and return Rack app from `app/api.rb`
237+
238+
2. **API Definition (`app/api.rb`)**
239+
240+
* Uses Grape::API
241+
* Mounts:
242+
243+
* `<root_path>/hello` (demo)
244+
* `<health_path>`, `<metrics_path>`, `<version_path>`
245+
* Each team endpoint under `<root_path>/<path>`
246+
247+
3. **Router & Endpoint Builder**
248+
249+
* For each endpoint config:
250+
251+
* Compose `effective_cors` = deep\_merge(global.cors, endpoint.cors)
252+
* Define Grape route with:
253+
254+
* **Before**: enforce `request_limit`, `request_timeout`, CORS headers
255+
* **Auth**: apply IP whitelist, API key, OAuth
256+
* **Signature**: call custom or default validator
257+
* **Hooks**: run `on_request` plugins in priority order
258+
* **Handler**: invoke `MyHandler.new.call(payload:, headers:, config:)`
259+
* **After**: run `on_response` plugins
260+
* **Rescue**: on exception, emit metrics `:error`, run `on_error`, rethrow or format JSON error
261+
262+
4. **Metrics Emitter**
263+
264+
* Listen to lifecycle events, build in-memory ring buffer of last N events
265+
* `/metrics` returns the JSON array of these events (configurable size)
266+
267+
5. **Graceful Shutdown**
268+
269+
* On SIGINT/SIGTERM: allow in-flight requests to finish, exit
270+
271+
---
272+
273+
## 🔒 7. Security & Isolation
274+
275+
* **Allowed ENV Vars**: endpoints cannot access undisclosed ENV keys
276+
* **Sandbox**: plugin & handler `require` limited to configured dirs
277+
* **Authentication**: built-in modules guard routes
278+
* **Request Validation**: size, timeout, signature, CORS enforced systematically
279+
* **Error Handling**: exceptions bubble to Grape catchall, with JSON schema
280+
281+
---
282+
283+
## 🚨 8. Error Handling & Logging
284+
285+
* **Default JSON Error**:
286+
287+
```json
288+
{ "error": "Error message", "code": 500 }
289+
```
290+
291+
* **Dev Mode**: include full stack trace
292+
* **Prod Mode**: hide backtrace, log internally
293+
* **Structured Logs**: each entry includes:
294+
295+
* `timestamp` (ISO8601)
296+
* `level`, `message`
297+
* `request_id`, `path`, `handler`, `status`, `duration_ms`
298+
* **Lifecycle Hooks**: global plugins get `on_error(exception, env)`
299+
300+
---
301+
302+
## 📈 9. Metrics & Instrumentation
303+
304+
* Hooks for:
305+
306+
* `:request_start` (path, method, request\_id)
307+
* `:request_end` (status, duration)
308+
* `:error` (exception details)
309+
* Users subscribe via global plugins to forward to StatsD, Prometheus, etc.
310+
311+
---
312+
313+
## 🛠️ 11. CLI & Scaffolding
314+
315+
`bin/hooks`:
316+
317+
```bash
318+
# Create a new handler skeleton
319+
hooks scaffold handler my_endpoint
320+
321+
# Create a new global plugin skeleton
322+
hooks scaffold plugin my_plugin
323+
```
324+
325+
Generates:
326+
327+
* `handlers/my_endpoint_handler.rb`
328+
* `config/endpoints/my_endpoint.yaml`
329+
* `plugins/my_plugin.rb`
330+
331+
---
332+
333+
## 🧪 12. Testing Helpers (Optional)
334+
335+
Add to `Gemfile, group :test`:
336+
337+
```ruby
338+
gem "hooks-test"
339+
```
340+
341+
Provides modules and RSpec matchers to:
342+
343+
* Stub ENV safely
344+
* Simulate HTTP requests against Rack app
345+
* Assert metrics and hook invocations
346+
347+
---
348+
349+
## 📦 13. Hello-World Default
350+
351+
If no config provided, `/webhooks/hello` responds:
352+
353+
```json
354+
{ "message": "Hooks is working!" }
355+
```

0 commit comments

Comments
 (0)