rerout-rails 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0c0b86702f8da895a164e119949243826b64a33677c516f410a5284c7d91333c
4
+ data.tar.gz: 0557d87f5d559532ab23b11c96634e02a7a0887b3aec4a5ab60abc5b61745dcd
5
+ SHA512:
6
+ metadata.gz: 3deaaaf8a1ca54421f48c6fec592bbfa633411871ecc96f744fcb50ceb7fb278086b211aa03e4d417869bc2091ece0b18ad34ac07db3b31236a213b9c983b937
7
+ data.tar.gz: 6a9c8ffda128b81176bfdbbf72f9e71078abeb9d6aa0ed2db9f28b87c1c3e2cee5e875b1dbf486bf68ba05234ca1d8455357ef6a20d18d546965298d15bca345
data/CHANGELOG.md ADDED
@@ -0,0 +1,31 @@
1
+ # Changelog
2
+
3
+ All notable changes to the `rerout-rails` gem are documented in this file. The
4
+ format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.1.0] - 2026-05-20
8
+
9
+ ### Added
10
+
11
+ - Initial public release.
12
+ - `Rerout::Rails.configure` — initializer-driven configuration with
13
+ environment-variable fallbacks for `api_key`, `webhook_secret`, and
14
+ `base_url`.
15
+ - `Rerout::Rails.client` — a process-wide, lazily-built, cached
16
+ `Rerout::Client`.
17
+ - `Rerout::Rails::WebhookController` — verifies the `X-Rerout-Signature`
18
+ header and responds `200` / `401` / `400`. CSRF protection is skipped for
19
+ server-to-server deliveries.
20
+ - `Rerout::Rails::Events` — dispatches verified deliveries through
21
+ `ActiveSupport::Notifications`: a `rerout.webhook` catch-all plus per-event
22
+ topics (`rerout.link.created`, `rerout.link.updated`, `rerout.link.deleted`,
23
+ `rerout.link.clicked`, `rerout.qr.scanned`).
24
+ - `Rerout::Rails::Railtie` — hooks the integration into the Rails boot
25
+ process.
26
+ - `rails generate rerout:install` — drops `config/initializers/rerout.rb` and
27
+ mounts the webhook route.
28
+ - `Rerout::Rails::ConfigurationError` raised on missing `api_key` or
29
+ `webhook_secret`.
30
+
31
+ [0.1.0]: https://github.com/ModestNerds-Co/rerout-sdks/releases/tag/ruby-rails-v0.1.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Codecraft Solutions
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,166 @@
1
+ # rerout-rails
2
+
3
+ Official Rails integration for the [Rerout](https://rerout.co) API.
4
+
5
+ Wraps the base [`rerout`](https://rubygems.org/gems/rerout) gem with
6
+ Rails-native ergonomics: a cached, initializer-driven API client, an install
7
+ generator, and a webhook controller that verifies signatures and dispatches
8
+ each delivery through `ActiveSupport::Notifications`.
9
+
10
+ ## Install
11
+
12
+ Add to your `Gemfile`:
13
+
14
+ ```ruby
15
+ gem 'rerout-rails'
16
+ ```
17
+
18
+ Then run:
19
+
20
+ ```bash
21
+ bundle install
22
+ bin/rails generate rerout:install
23
+ ```
24
+
25
+ Requires Ruby 3.0+ and Rails 7.0+. The `rerout` gem is pulled in
26
+ automatically.
27
+
28
+ The generator drops `config/initializers/rerout.rb` and mounts the webhook
29
+ route in `config/routes.rb`.
30
+
31
+ ## Configuration
32
+
33
+ `rails generate rerout:install` writes `config/initializers/rerout.rb`:
34
+
35
+ ```ruby
36
+ Rerout::Rails.configure do |config|
37
+ config.api_key = ENV.fetch('REROUT_API_KEY', nil)
38
+ config.webhook_secret = ENV.fetch('REROUT_WEBHOOK_SECRET', nil)
39
+
40
+ # config.base_url = 'https://api.rerout.co'
41
+ # config.timeout = 30
42
+ # config.signature_tolerance_seconds = 300
43
+ end
44
+ ```
45
+
46
+ `api_key`, `webhook_secret`, and `base_url` fall back to the `REROUT_API_KEY`,
47
+ `REROUT_WEBHOOK_SECRET`, and `REROUT_BASE_URL` environment variables when not
48
+ set explicitly. Keep secrets out of source control — prefer Rails encrypted
49
+ credentials or environment variables.
50
+
51
+ ## API client
52
+
53
+ `Rerout::Rails.client` is a process-wide, lazily-built `Rerout::Client`. It is
54
+ created once from your configuration and reused across requests — the
55
+ underlying Faraday connection is thread-safe, so sharing one instance is the
56
+ recommended pattern.
57
+
58
+ ```ruby
59
+ class LinksController < ApplicationController
60
+ def create
61
+ link = Rerout::Rails.client.links.create(
62
+ Rerout::CreateLinkInput.new(target_url: params[:target_url])
63
+ )
64
+ render json: { short_url: link.short_url }
65
+ end
66
+ end
67
+ ```
68
+
69
+ `Rerout::Rails.client` raises `Rerout::Rails::ConfigurationError` if `api_key`
70
+ is unset. See the [`rerout` gem README](https://rubygems.org/gems/rerout) for
71
+ the full client surface — `links`, `project`, and `qr` namespaces.
72
+
73
+ ## Webhooks
74
+
75
+ The install generator mounts the receiver:
76
+
77
+ ```ruby
78
+ # config/routes.rb
79
+ post '/rerout/webhooks', to: 'rerout/rails/webhook#receive', as: :rerout_webhook
80
+ ```
81
+
82
+ Point your Rerout dashboard webhook endpoint at `POST /rerout/webhooks`. The
83
+ controller:
84
+
85
+ - verifies the `X-Rerout-Signature` header against `webhook_secret` with a
86
+ constant-time HMAC check (CSRF protection is skipped — webhooks are
87
+ server-to-server),
88
+ - responds `200` when the signature is valid and the body is a JSON object,
89
+ - responds `401` when the signature is missing, malformed, stale, or wrong,
90
+ - responds `400` when the body is not a JSON object.
91
+
92
+ To wire it up manually instead of using the generator, add the route above to
93
+ `config/routes.rb` yourself.
94
+
95
+ ## Reacting to events
96
+
97
+ Every verified delivery is instrumented through
98
+ `ActiveSupport::Notifications`. Subscribe anywhere — an initializer, an
99
+ `ApplicationJob`, a service object:
100
+
101
+ ```ruby
102
+ # Fires for every verified webhook, regardless of event type.
103
+ ActiveSupport::Notifications.subscribe('rerout.webhook') do |event|
104
+ Rails.logger.info("Rerout webhook: #{event.payload[:event]}")
105
+ end
106
+
107
+ # Fires only for link.clicked events.
108
+ ActiveSupport::Notifications.subscribe('rerout.link.clicked') do |event|
109
+ code = event.payload[:body]['code']
110
+ ClickCounter.increment(code)
111
+ end
112
+ ```
113
+
114
+ The instrumentation payload carries:
115
+
116
+ - `:event` — the event-type string (e.g. `"link.clicked"`), or `""`.
117
+ - `:body` — the full parsed JSON body as a Hash.
118
+ - `:request` — the `ActionDispatch::Request` that delivered the webhook.
119
+
120
+ Topics emitted: `rerout.webhook` (catch-all) plus `rerout.link.created`,
121
+ `rerout.link.updated`, `rerout.link.deleted`, `rerout.link.clicked`, and
122
+ `rerout.qr.scanned` for recognised events.
123
+
124
+ ## Webhook signature verification
125
+
126
+ The controller verifies signatures for you. To verify a signature elsewhere —
127
+ in a background job, a Rack endpoint, a custom controller — use the base SDK
128
+ helper directly:
129
+
130
+ ```ruby
131
+ ok = Rerout::Webhooks.verify_signature(
132
+ raw_body: request.raw_post,
133
+ signature_header: request.headers['X-Rerout-Signature'],
134
+ secret: Rerout::Rails.config.webhook_secret!
135
+ )
136
+ ```
137
+
138
+ The five-minute timestamp tolerance is configurable via
139
+ `config.signature_tolerance_seconds` (`0` disables the staleness check).
140
+
141
+ ## Error handling
142
+
143
+ API calls through `Rerout::Rails.client` raise `Rerout::Error`, with a stable
144
+ `code`, an HTTP `status`, and `rate_limited?` / `server_error?` flags:
145
+
146
+ ```ruby
147
+ begin
148
+ Rerout::Rails.client.links.get(params[:code])
149
+ rescue Rerout::Error => e
150
+ return head(:not_found) if e.code == 'not_found'
151
+ raise
152
+ end
153
+ ```
154
+
155
+ Misconfiguration (missing `api_key` or `webhook_secret`) raises
156
+ `Rerout::Rails::ConfigurationError`.
157
+
158
+ ## License
159
+
160
+ MIT — see [LICENSE](LICENSE), a copy of the workspace
161
+ [LICENSE](https://github.com/ModestNerds-Co/rerout-sdks/blob/main/LICENSE).
162
+
163
+ ## Links
164
+
165
+ - API docs: <https://rerout.co/docs>
166
+ - Source: <https://github.com/ModestNerds-Co/rerout-sdks>
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/base'
5
+
6
+ module Rerout
7
+ module Generators
8
+ # `rails generate rerout:install`
9
+ #
10
+ # Drops a `config/initializers/rerout.rb` initializer and appends the
11
+ # webhook route to `config/routes.rb` so a fresh app is wired up in one
12
+ # command.
13
+ class InstallGenerator < ::Rails::Generators::Base
14
+ source_root File.expand_path('templates', __dir__)
15
+
16
+ desc 'Creates a Rerout initializer and mounts the webhook route.'
17
+
18
+ # Copy the commented initializer into the host application.
19
+ #
20
+ # @return [void]
21
+ def create_initializer
22
+ template 'rerout.rb', 'config/initializers/rerout.rb'
23
+ end
24
+
25
+ # Append the webhook receiver route to `config/routes.rb`.
26
+ #
27
+ # @return [void]
28
+ def add_webhook_route
29
+ route_line = "post '/rerout/webhooks', " \
30
+ "to: 'rerout/rails/webhook#receive', as: :rerout_webhook"
31
+ route(route_line)
32
+ end
33
+
34
+ # Print next steps.
35
+ #
36
+ # @return [void]
37
+ def print_post_install
38
+ readme 'POST_INSTALL' if behavior == :invoke
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,24 @@
1
+
2
+ Rerout is installed.
3
+
4
+ 1. Set your credentials (environment variables or Rails credentials):
5
+
6
+ REROUT_API_KEY=rrk_...
7
+ REROUT_WEBHOOK_SECRET=whsec_...
8
+
9
+ 2. Review config/initializers/rerout.rb.
10
+
11
+ 3. The webhook receiver is mounted at:
12
+
13
+ POST /rerout/webhooks (route name: rerout_webhook)
14
+
15
+ Point your Rerout dashboard endpoint at that URL.
16
+
17
+ 4. Subscribe to delivery events anywhere in your app:
18
+
19
+ ActiveSupport::Notifications.subscribe('rerout.webhook') do |event|
20
+ Rails.logger.info("Rerout webhook: #{event.payload[:event]}")
21
+ end
22
+
23
+ Use the API client with Rerout::Rails.client. Docs: https://rerout.co/docs
24
+
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Rerout configuration.
4
+ #
5
+ # Keep real credentials out of source control — prefer Rails encrypted
6
+ # credentials (`Rails.application.credentials`) or environment variables.
7
+ #
8
+ # Docs: https://rerout.co/docs
9
+ Rerout::Rails.configure do |config|
10
+ # Project API key (`rrk_…`). Required to use `Rerout::Rails.client`.
11
+ config.api_key = ENV.fetch('REROUT_API_KEY', nil)
12
+
13
+ # Endpoint signing secret (`whsec_…`). Required for webhook verification.
14
+ config.webhook_secret = ENV.fetch('REROUT_WEBHOOK_SECRET', nil)
15
+
16
+ # Override the API base URL (staging / self-hosted). Optional.
17
+ # config.base_url = 'https://api.rerout.co'
18
+
19
+ # Per-request timeout in seconds. Optional, defaults to 30.
20
+ # config.timeout = 30
21
+
22
+ # Webhook signature timestamp tolerance in seconds. `0` disables the
23
+ # staleness check. Optional, defaults to 300.
24
+ # config.signature_tolerance_seconds = 300
25
+ end
26
+
27
+ # React to verified webhook deliveries via ActiveSupport::Notifications.
28
+ # `rerout.webhook` fires for every delivery; per-event topics
29
+ # (`rerout.link.clicked`, `rerout.qr.scanned`, …) fire for known events.
30
+ #
31
+ # ActiveSupport::Notifications.subscribe('rerout.link.clicked') do |event|
32
+ # code = event.payload[:body]['code']
33
+ # Rails.logger.info("Rerout: link #{code} clicked")
34
+ # end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rerout'
4
+
5
+ module Rerout
6
+ module Rails
7
+ # Holds the `rerout-rails` configuration and lazily builds a process-wide,
8
+ # cached {Rerout::Client}.
9
+ #
10
+ # Configure it from an initializer (the `rerout:install` generator drops
11
+ # one for you):
12
+ #
13
+ # Rerout::Rails.configure do |config|
14
+ # config.api_key = ENV.fetch('REROUT_API_KEY')
15
+ # config.webhook_secret = ENV.fetch('REROUT_WEBHOOK_SECRET')
16
+ # # config.base_url = 'https://api.rerout.co'
17
+ # # config.timeout = 30
18
+ # # config.signature_tolerance_seconds = 300
19
+ # end
20
+ #
21
+ # The client is built once on first access and reused — {Rerout::Client}
22
+ # wraps a thread-safe Faraday connection, so sharing one instance is the
23
+ # recommended pattern.
24
+ class Configuration
25
+ # @return [String, nil] project API key (`rrk_…`). Required before
26
+ # {#client} can be used.
27
+ attr_accessor :api_key
28
+
29
+ # @return [String, nil] endpoint signing secret (`whsec_…`). Required
30
+ # for webhook signature verification.
31
+ attr_accessor :webhook_secret
32
+
33
+ # @return [String, nil] override the API base URL.
34
+ attr_accessor :base_url
35
+
36
+ # @return [Integer] per-request timeout in seconds. Default 30.
37
+ attr_accessor :timeout
38
+
39
+ # @return [Integer] webhook signature timestamp tolerance in seconds.
40
+ # `0` disables the staleness check. Default 300.
41
+ attr_accessor :signature_tolerance_seconds
42
+
43
+ # @return [String, nil] override the SDK `User-Agent` header.
44
+ attr_accessor :user_agent
45
+
46
+ # @return [String] path the webhook controller is mounted at when the
47
+ # generated routes are used. Default `/rerout/webhooks`.
48
+ attr_accessor :webhook_path
49
+
50
+ def initialize
51
+ @api_key = ENV.fetch('REROUT_API_KEY', nil)
52
+ @webhook_secret = ENV.fetch('REROUT_WEBHOOK_SECRET', nil)
53
+ @base_url = ENV.fetch('REROUT_BASE_URL', nil)
54
+ @timeout = 30
55
+ @signature_tolerance_seconds = Rerout::Webhooks::DEFAULT_TOLERANCE_SECONDS
56
+ @user_agent = nil
57
+ @webhook_path = '/rerout/webhooks'
58
+ @client = nil
59
+ @client_mutex = Mutex.new
60
+ end
61
+
62
+ # The process-wide cached {Rerout::Client}, built from this config.
63
+ #
64
+ # @return [Rerout::Client]
65
+ # @raise [Rerout::Rails::ConfigurationError] when `api_key` is unset.
66
+ def client
67
+ return @client if @client
68
+
69
+ @client_mutex.synchronize do
70
+ @client ||= build_client
71
+ end
72
+ end
73
+
74
+ # Drop the cached client. The next {#client} call rebuilds it. Useful
75
+ # after changing credentials in tests.
76
+ #
77
+ # @return [void]
78
+ def reset_client!
79
+ @client_mutex.synchronize { @client = nil }
80
+ end
81
+
82
+ # The configured webhook signing secret.
83
+ #
84
+ # @return [String]
85
+ # @raise [Rerout::Rails::ConfigurationError] when unset or blank.
86
+ def webhook_secret!
87
+ if webhook_secret.nil? || webhook_secret.to_s.strip.empty?
88
+ raise ConfigurationError,
89
+ 'Rerout::Rails.config.webhook_secret is required to verify ' \
90
+ 'webhook signatures. Set it in config/initializers/rerout.rb ' \
91
+ 'or via the REROUT_WEBHOOK_SECRET environment variable.'
92
+ end
93
+ webhook_secret
94
+ end
95
+
96
+ private
97
+
98
+ def build_client
99
+ if api_key.nil? || api_key.to_s.strip.empty?
100
+ raise ConfigurationError,
101
+ 'Rerout::Rails.config.api_key is required to build a Rerout ' \
102
+ 'client. Set it in config/initializers/rerout.rb or via the ' \
103
+ 'REROUT_API_KEY environment variable.'
104
+ end
105
+
106
+ Rerout::Client.new(
107
+ api_key: api_key,
108
+ base_url: base_url,
109
+ timeout: timeout,
110
+ user_agent: user_agent
111
+ )
112
+ end
113
+ end
114
+
115
+ # Raised when `rerout-rails` is missing required configuration.
116
+ class ConfigurationError < StandardError; end
117
+ end
118
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rerout
4
+ module Rails
5
+ # Maps Rerout webhook event names onto `ActiveSupport::Notifications` topic
6
+ # names and dispatches verified deliveries.
7
+ #
8
+ # Every verified webhook is instrumented under {CATCH_ALL} (`rerout.webhook`)
9
+ # so a single subscriber can see everything. Events with a recognised
10
+ # `event` field are *also* instrumented under a dedicated, more specific
11
+ # topic so subscribers can listen narrowly.
12
+ #
13
+ # Subscribe the Rails way:
14
+ #
15
+ # ActiveSupport::Notifications.subscribe('rerout.link.clicked') do |event|
16
+ # code = event.payload[:body]['code']
17
+ # Rails.logger.info("link #{code} clicked")
18
+ # end
19
+ #
20
+ # The instrumentation payload carries:
21
+ #
22
+ # - `:event` — the event-type string (e.g. `"link.clicked"`), or `""`.
23
+ # - `:body` — the full parsed JSON body as a Hash.
24
+ # - `:request` — the `ActionDispatch::Request` that delivered the webhook.
25
+ module Events
26
+ # Topic every verified webhook is instrumented under.
27
+ CATCH_ALL = 'rerout.webhook'
28
+
29
+ # Known `event` strings mapped onto their dedicated notification topic.
30
+ # Events absent from this map still fire {CATCH_ALL}.
31
+ TOPICS = {
32
+ 'link.created' => 'rerout.link.created',
33
+ 'link.updated' => 'rerout.link.updated',
34
+ 'link.deleted' => 'rerout.link.deleted',
35
+ 'link.clicked' => 'rerout.link.clicked',
36
+ 'qr.scanned' => 'rerout.qr.scanned'
37
+ }.freeze
38
+
39
+ module_function
40
+
41
+ # Instrument a verified webhook delivery.
42
+ #
43
+ # Fires {CATCH_ALL} unconditionally, then the event-specific topic when
44
+ # `event` is one of {TOPICS}.
45
+ #
46
+ # @param event [String] the event-type string; may be empty.
47
+ # @param body [Hash] the parsed JSON body.
48
+ # @param request [ActionDispatch::Request, nil] the delivering request.
49
+ # @return [void]
50
+ def dispatch(event:, body:, request: nil)
51
+ payload = { event: event, body: body, request: request }
52
+
53
+ ActiveSupport::Notifications.instrument(CATCH_ALL, payload)
54
+
55
+ specific = TOPICS[event]
56
+ ActiveSupport::Notifications.instrument(specific, payload) if specific
57
+ end
58
+
59
+ # @return [Array<String>] every notification topic this gem can emit.
60
+ def all_topics
61
+ [CATCH_ALL, *TOPICS.values]
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/railtie'
4
+
5
+ module Rerout
6
+ module Rails
7
+ # Hooks `rerout-rails` into the Rails boot process.
8
+ #
9
+ # The railtie keeps the integration zero-config beyond the initializer:
10
+ # the webhook controller and generators are autoloaded by Rails' standard
11
+ # `lib/` eager-loading once the gem is in the `Gemfile`.
12
+ class Railtie < ::Rails::Railtie
13
+ # Namespaced config accessible as `Rails.application.config.rerout`.
14
+ config.rerout = Rerout::Rails.config
15
+
16
+ # Make `Rerout::Rails::WebhookController` resolvable by the router
17
+ # without the host app having to require it explicitly.
18
+ initializer 'rerout.rails.controller' do
19
+ require 'rerout/rails/webhook_controller'
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rerout
4
+ module Rails
5
+ # Version of the `rerout-rails` gem. Follows semantic versioning and is
6
+ # released in lockstep with the base `rerout` gem.
7
+ VERSION = '0.1.0'
8
+ end
9
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'action_controller'
5
+ require 'rerout'
6
+
7
+ require_relative 'events'
8
+
9
+ module Rerout
10
+ module Rails
11
+ # `ActionController` endpoint that ingests signed Rerout webhooks.
12
+ #
13
+ # The `rerout:install` generator mounts it for you. To wire it manually:
14
+ #
15
+ # # config/routes.rb
16
+ # post '/rerout/webhooks', to: 'rerout/rails/webhook#receive'
17
+ #
18
+ # Behaviour:
19
+ #
20
+ # - Verifies the `X-Rerout-Signature` header against the configured
21
+ # `webhook_secret` using the base SDK's constant-time HMAC check.
22
+ # - `200` — signature valid and the body is a JSON object; the delivery is
23
+ # dispatched through {Rerout::Rails::Events}.
24
+ # - `401` — signature missing, malformed, stale, or wrong.
25
+ # - `400` — signature valid but the body is not a JSON object.
26
+ #
27
+ # CSRF protection is skipped — webhooks are server-to-server and carry no
28
+ # session cookie. Subscribe to {Rerout::Rails::Events} topics to react to
29
+ # deliveries; do not subclass this controller to add handlers.
30
+ class WebhookController < ActionController::Base
31
+ # Header Rerout signs every delivery with.
32
+ SIGNATURE_HEADER = 'X-Rerout-Signature'
33
+
34
+ # Webhooks are unauthenticated server-to-server POSTs — no CSRF token.
35
+ skip_forgery_protection if respond_to?(:skip_forgery_protection)
36
+
37
+ # Verify, parse, and dispatch a single webhook delivery.
38
+ #
39
+ # @return [void]
40
+ def receive
41
+ raw_body = read_raw_body
42
+ signature = request.headers[SIGNATURE_HEADER].to_s
43
+
44
+ unless signature_valid?(raw_body, signature)
45
+ return render(json: { error: 'invalid signature' }, status: :unauthorized)
46
+ end
47
+
48
+ body = parse_object(raw_body)
49
+ if body.nil?
50
+ return render(json: { error: 'body must be a JSON object' },
51
+ status: :bad_request)
52
+ end
53
+
54
+ event = body['event'].is_a?(String) ? body['event'] : ''
55
+ Events.dispatch(event: event, body: body, request: request)
56
+
57
+ render json: { received: true, event: event }, status: :ok
58
+ end
59
+
60
+ private
61
+
62
+ def config
63
+ Rerout::Rails.config
64
+ end
65
+
66
+ # Read the exact request body bytes. `request.body` may already have been
67
+ # consumed by Rails' params parsing, so rewind first.
68
+ def read_raw_body
69
+ body = request.body
70
+ body.rewind if body.respond_to?(:rewind)
71
+ body.read.to_s
72
+ end
73
+
74
+ def signature_valid?(raw_body, signature)
75
+ Rerout::Webhooks.verify_signature(
76
+ raw_body: raw_body,
77
+ signature_header: signature,
78
+ secret: config.webhook_secret!,
79
+ tolerance_seconds: config.signature_tolerance_seconds
80
+ )
81
+ end
82
+
83
+ # Parse `raw_body` and return it only when it is a JSON object. Returns
84
+ # `nil` for non-JSON, JSON arrays, JSON scalars, or an empty body.
85
+ def parse_object(raw_body)
86
+ return nil if raw_body.empty?
87
+
88
+ parsed = JSON.parse(raw_body)
89
+ parsed.is_a?(Hash) ? parsed : nil
90
+ rescue JSON::ParserError
91
+ nil
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rerout'
4
+
5
+ require_relative 'rails/version'
6
+ require_relative 'rails/configuration'
7
+ require_relative 'rails/events'
8
+
9
+ module Rerout
10
+ # Official Rails integration for the Rerout branded-link API.
11
+ #
12
+ # Wraps the base {Rerout} SDK with Rails-native ergonomics:
13
+ #
14
+ # - {Rerout::Rails.client} — a process-wide {Rerout::Client} built from an
15
+ # initializer.
16
+ # - {Rerout::Rails::WebhookController} — verifies the `X-Rerout-Signature`
17
+ # header and instruments an `ActiveSupport::Notifications` event per
18
+ # delivery.
19
+ # - {Rerout::Rails::Events} — the notification topics apps subscribe to.
20
+ #
21
+ # @see https://rerout.co
22
+ # @see https://github.com/ModestNerds-Co/rerout-sdks
23
+ module Rails
24
+ class << self
25
+ # The global {Rerout::Rails::Configuration}.
26
+ #
27
+ # @return [Rerout::Rails::Configuration]
28
+ def config
29
+ @config ||= Configuration.new
30
+ end
31
+
32
+ # Yields the configuration for mutation. Call from an initializer.
33
+ #
34
+ # Rerout::Rails.configure do |c|
35
+ # c.api_key = ENV.fetch('REROUT_API_KEY')
36
+ # end
37
+ #
38
+ # @yieldparam config [Rerout::Rails::Configuration]
39
+ # @return [Rerout::Rails::Configuration]
40
+ def configure
41
+ yield(config) if block_given?
42
+ config
43
+ end
44
+
45
+ # The shared, lazily-built {Rerout::Client}.
46
+ #
47
+ # @return [Rerout::Client]
48
+ # @raise [Rerout::Rails::ConfigurationError] when `api_key` is unset.
49
+ def client
50
+ config.client
51
+ end
52
+
53
+ # Replace the configuration wholesale. Mainly a test seam.
54
+ #
55
+ # @param configuration [Rerout::Rails::Configuration]
56
+ # @return [Rerout::Rails::Configuration]
57
+ attr_writer :config
58
+
59
+ # Drop the global configuration and cached client. Test seam.
60
+ #
61
+ # @return [void]
62
+ def reset!
63
+ @config = nil
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ # The railtie pulls in Rails' boot machinery — load it only when Rails itself
70
+ # is present so the gem stays usable (config + webhook verification) in plain
71
+ # Ruby contexts and test harnesses.
72
+ require 'rerout/rails/railtie' if defined?(Rails::Railtie)
metadata ADDED
@@ -0,0 +1,200 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rerout-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Codecraft Solutions
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: railties
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.0'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '9.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '7.0'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '9.0'
32
+ - !ruby/object:Gem::Dependency
33
+ name: rerout
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '0.1'
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: '0.1'
46
+ - !ruby/object:Gem::Dependency
47
+ name: actionpack
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '7.0'
53
+ - - "<"
54
+ - !ruby/object:Gem::Version
55
+ version: '9.0'
56
+ type: :development
57
+ prerelease: false
58
+ version_requirements: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '7.0'
63
+ - - "<"
64
+ - !ruby/object:Gem::Version
65
+ version: '9.0'
66
+ - !ruby/object:Gem::Dependency
67
+ name: rack-test
68
+ requirement: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - "~>"
71
+ - !ruby/object:Gem::Version
72
+ version: '2.1'
73
+ type: :development
74
+ prerelease: false
75
+ version_requirements: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - "~>"
78
+ - !ruby/object:Gem::Version
79
+ version: '2.1'
80
+ - !ruby/object:Gem::Dependency
81
+ name: rake
82
+ requirement: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - "~>"
85
+ - !ruby/object:Gem::Version
86
+ version: '13.0'
87
+ type: :development
88
+ prerelease: false
89
+ version_requirements: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - "~>"
92
+ - !ruby/object:Gem::Version
93
+ version: '13.0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: rspec
96
+ requirement: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - "~>"
99
+ - !ruby/object:Gem::Version
100
+ version: '3.12'
101
+ type: :development
102
+ prerelease: false
103
+ version_requirements: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - "~>"
106
+ - !ruby/object:Gem::Version
107
+ version: '3.12'
108
+ - !ruby/object:Gem::Dependency
109
+ name: rubocop
110
+ requirement: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - "~>"
113
+ - !ruby/object:Gem::Version
114
+ version: '1.60'
115
+ type: :development
116
+ prerelease: false
117
+ version_requirements: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - "~>"
120
+ - !ruby/object:Gem::Version
121
+ version: '1.60'
122
+ - !ruby/object:Gem::Dependency
123
+ name: rubocop-performance
124
+ requirement: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - "~>"
127
+ - !ruby/object:Gem::Version
128
+ version: '1.20'
129
+ type: :development
130
+ prerelease: false
131
+ version_requirements: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - "~>"
134
+ - !ruby/object:Gem::Version
135
+ version: '1.20'
136
+ - !ruby/object:Gem::Dependency
137
+ name: rubocop-rspec
138
+ requirement: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - "~>"
141
+ - !ruby/object:Gem::Version
142
+ version: '3.0'
143
+ type: :development
144
+ prerelease: false
145
+ version_requirements: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - "~>"
148
+ - !ruby/object:Gem::Version
149
+ version: '3.0'
150
+ description: |
151
+ Rails integration for the Rerout API. Wraps the `rerout` gem with a
152
+ cached, initializer-driven client, an install generator, and a webhook
153
+ controller that verifies signatures and dispatches each delivery through
154
+ ActiveSupport::Notifications.
155
+ email:
156
+ - hello@codecraftsolutions.co.za
157
+ executables: []
158
+ extensions: []
159
+ extra_rdoc_files: []
160
+ files:
161
+ - CHANGELOG.md
162
+ - LICENSE
163
+ - README.md
164
+ - lib/generators/rerout/install_generator.rb
165
+ - lib/generators/rerout/templates/POST_INSTALL
166
+ - lib/generators/rerout/templates/rerout.rb
167
+ - lib/rerout/rails.rb
168
+ - lib/rerout/rails/configuration.rb
169
+ - lib/rerout/rails/events.rb
170
+ - lib/rerout/rails/railtie.rb
171
+ - lib/rerout/rails/version.rb
172
+ - lib/rerout/rails/webhook_controller.rb
173
+ homepage: https://github.com/ModestNerds-Co/rerout-sdks
174
+ licenses:
175
+ - MIT
176
+ metadata:
177
+ homepage_uri: https://github.com/ModestNerds-Co/rerout-sdks
178
+ source_code_uri: https://github.com/ModestNerds-Co/rerout-sdks/tree/main/ruby-rails
179
+ changelog_uri: https://github.com/ModestNerds-Co/rerout-sdks/blob/main/ruby-rails/CHANGELOG.md
180
+ bug_tracker_uri: https://github.com/ModestNerds-Co/rerout-sdks/issues
181
+ documentation_uri: https://rerout.co/docs
182
+ rubygems_mfa_required: 'true'
183
+ rdoc_options: []
184
+ require_paths:
185
+ - lib
186
+ required_ruby_version: !ruby/object:Gem::Requirement
187
+ requirements:
188
+ - - ">="
189
+ - !ruby/object:Gem::Version
190
+ version: 3.0.0
191
+ required_rubygems_version: !ruby/object:Gem::Requirement
192
+ requirements:
193
+ - - ">="
194
+ - !ruby/object:Gem::Version
195
+ version: '0'
196
+ requirements: []
197
+ rubygems_version: 3.6.9
198
+ specification_version: 4
199
+ summary: Official Rails integration for the Rerout branded-link API.
200
+ test_files: []