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 +7 -0
- data/CHANGELOG.md +31 -0
- data/LICENSE +21 -0
- data/README.md +166 -0
- data/lib/generators/rerout/install_generator.rb +42 -0
- data/lib/generators/rerout/templates/POST_INSTALL +24 -0
- data/lib/generators/rerout/templates/rerout.rb +34 -0
- data/lib/rerout/rails/configuration.rb +118 -0
- data/lib/rerout/rails/events.rb +65 -0
- data/lib/rerout/rails/railtie.rb +23 -0
- data/lib/rerout/rails/version.rb +9 -0
- data/lib/rerout/rails/webhook_controller.rb +95 -0
- data/lib/rerout/rails.rb +72 -0
- metadata +200 -0
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,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
|
data/lib/rerout/rails.rb
ADDED
|
@@ -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: []
|