hibira-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 +17 -0
- data/LICENSE +21 -0
- data/README.md +215 -0
- data/app/controllers/hibira/channel_auth_controller.rb +38 -0
- data/app/controllers/hibira/push_tokens_controller.rb +17 -0
- data/app/controllers/hibira/turbo_auth_controller.rb +23 -0
- data/app/controllers/hibira/webhooks_controller.rb +31 -0
- data/app/javascript/hibira/turbo.js +144 -0
- data/config/importmap.rb +3 -0
- data/lib/generators/hibira/install/install_generator.rb +85 -0
- data/lib/generators/hibira/install/templates/hibira.rb +27 -0
- data/lib/hibira/check.rb +45 -0
- data/lib/hibira/configuration.rb +134 -0
- data/lib/hibira/engine.rb +56 -0
- data/lib/hibira/streams_helper.rb +72 -0
- data/lib/hibira/turbo.rb +75 -0
- data/lib/hibira/version.rb +5 -0
- data/lib/hibira/webhook.rb +54 -0
- data/lib/hibira-rails.rb +3 -0
- data/lib/hibira.rb +93 -0
- data/lib/tasks/hibira.rake +10 -0
- metadata +150 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ec8dd25bdad390a25709965fcd822ac5882f32a09ba94ae218a61f0f3bf88463
|
|
4
|
+
data.tar.gz: 0f75219446b55e2a1385861a14e69bc33a7f77eefc6642099ba33f9a38326345
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 90e64f04f5b36d52f038dce3c950730b3b85a182981969dd41764bb9626f4e9f03dc2d1f6190959175d294dc4e9c1c7fd05cbe38b13ea63a3a11800da49ef89d
|
|
7
|
+
data.tar.gz: 0743615164b04d789f48c40eac56507c92a8462bdc212f9d2638698f916833ee25083527858103b007b4ec5f6faf8dae85fcd9bc9dc370e710d0ab5b25bc9b9b
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
- Turbo Streams over Hibira: every Turbo broadcast (`broadcasts_to`,
|
|
6
|
+
`broadcast_*_to`, `broadcasts_refreshes`, the `_later` jobs) goes to a
|
|
7
|
+
private Hibira channel, and `turbo_stream_from` subscribes through Hibira.
|
|
8
|
+
No Action Cable server or Redis needed.
|
|
9
|
+
- Broadcasts over the app's event size limit are sent as a Turbo page refresh
|
|
10
|
+
(`config.oversize`).
|
|
11
|
+
- Channel auth for your own private and presence channels
|
|
12
|
+
(`config.authorize_channel`, `POST /hibira/auth`).
|
|
13
|
+
- Verified webhooks, including the previous secret during rotation
|
|
14
|
+
(`Hibira.on_webhook`, `POST /hibira/webhooks`).
|
|
15
|
+
- `Hibira.push` and push user tokens (`GET /hibira/push/token`).
|
|
16
|
+
- `bin/rails generate hibira:install` and `bin/rails hibira:check`.
|
|
17
|
+
- Rails 7.0 to 8.1.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hibira
|
|
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,215 @@
|
|
|
1
|
+
# hibira-rails
|
|
2
|
+
|
|
3
|
+
[](https://github.com/hibira-io/rails/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
Hotwire Turbo Streams over [Hibira](https://hibira.io), with no Action Cable
|
|
6
|
+
server, Redis or Solid Cable to run.
|
|
7
|
+
|
|
8
|
+
Hibira is a hosted realtime service that speaks the Pusher Channels protocol.
|
|
9
|
+
This gem sends every Turbo Stream broadcast your app already makes through
|
|
10
|
+
Hibira, and makes `turbo_stream_from` subscribe through it. Your models and
|
|
11
|
+
views stay as they are. It also signs your own private and presence
|
|
12
|
+
channels, verifies webhooks and serves push notification tokens.
|
|
13
|
+
|
|
14
|
+
Supports Rails 7.0 to 8.1 (Ruby 3.1+) and turbo-rails 1.5 to 2.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
bundle add hibira-rails
|
|
20
|
+
bin/rails generate hibira:install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The generator writes `config/initializers/hibira.rb`, pins `pusher-js` (or
|
|
24
|
+
installs it with your package manager in a jsbundling app) and imports the
|
|
25
|
+
`hibira/turbo` module after `@hotwired/turbo-rails` in
|
|
26
|
+
`app/javascript/application.js`.
|
|
27
|
+
|
|
28
|
+
Copy the app ID, key and secret from your app page on
|
|
29
|
+
[hibira.io/portal](https://hibira.io/portal) (the free plan needs no card)
|
|
30
|
+
into the environment:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
HIBIRA_APP_ID=your-app-id
|
|
34
|
+
HIBIRA_APP_KEY=your-app-key
|
|
35
|
+
HIBIRA_APP_SECRET=your-app-secret
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
or into `bin/rails credentials:edit`:
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
hibira:
|
|
42
|
+
app_id: your-app-id
|
|
43
|
+
key: your-app-key
|
|
44
|
+
secret: your-app-secret
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Then check them:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
bin/rails hibira:check
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Turbo Streams
|
|
54
|
+
|
|
55
|
+
Nothing changes in your code. This model and view, straight from the Turbo
|
|
56
|
+
handbook, now update every open page through Hibira:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
class Message < ApplicationRecord
|
|
60
|
+
belongs_to :room
|
|
61
|
+
broadcasts_to :room
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```erb
|
|
66
|
+
<%= turbo_stream_from @room %>
|
|
67
|
+
<div id="messages"><%= render @room.messages %></div>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`broadcast_append_to`, `broadcast_replace_later_to`, `broadcasts_refreshes`
|
|
71
|
+
and the rest all go through Hibira, from requests and from Active Job alike.
|
|
72
|
+
|
|
73
|
+
Each Turbo stream maps to one private channel. `turbo_stream_from` renders a
|
|
74
|
+
`<hibira-turbo-stream-source>` element holding the signed stream name, and the
|
|
75
|
+
browser can only subscribe with it (checked at `POST /hibira/turbo/auth`), the
|
|
76
|
+
same rule Action Cable applies. The app secret never leaves the server.
|
|
77
|
+
|
|
78
|
+
Turbo goes through Hibira whenever the credentials are set, except in the
|
|
79
|
+
test environment, where Turbo's `assert_turbo_stream_broadcasts` keeps working
|
|
80
|
+
on Action Cable. Force it either way:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
Hibira.configure do |config|
|
|
84
|
+
config.turbo = true # or false
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
A `turbo_stream_from` with a custom Action Cable channel
|
|
89
|
+
(`turbo_stream_from @room, channel: RoomChannel`) still uses Action Cable.
|
|
90
|
+
|
|
91
|
+
### Large broadcasts
|
|
92
|
+
|
|
93
|
+
Each plan has an event size limit (4 KB on Free). When a Turbo Stream is
|
|
94
|
+
larger, the gem sends a Turbo 8 page refresh instead, so open pages still
|
|
95
|
+
catch up. For pages with long lists, `broadcasts_refreshes` with
|
|
96
|
+
`turbo_refreshes_with method: :morph` keeps every broadcast tiny:
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
class Room < ApplicationRecord
|
|
100
|
+
broadcasts_refreshes
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Set `config.oversize = :log` to drop large broadcasts instead, or `:raise`
|
|
105
|
+
to raise `Hibira::PayloadTooLarge`.
|
|
106
|
+
|
|
107
|
+
## Your own channels
|
|
108
|
+
|
|
109
|
+
`Hibira.trigger` is the pusher-http-ruby client, already pointed at Hibira:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
Hibira.trigger("orders", "order-shipped", { id: 1024 })
|
|
113
|
+
Hibira.client.channel_info("presence-room.1", info: "user_count")
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
To sign private and presence channels, set `authorize_channel`. It receives
|
|
117
|
+
the channel name and the controller, which inherits from your
|
|
118
|
+
`ApplicationController`, so `current_user` and CSRF protection work as usual:
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
Hibira.configure do |config|
|
|
122
|
+
config.authorize_channel = lambda do |channel, controller|
|
|
123
|
+
user = controller.send(:current_user)
|
|
124
|
+
case channel
|
|
125
|
+
when /\Aprivate-user\.(\d+)\z/ then user&.id == $1.to_i
|
|
126
|
+
when /\Apresence-room\.(\d+)\z/ then user && { user_id: user.id, user_info: { name: user.name } }
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
In the browser, reuse the client the Turbo module created:
|
|
133
|
+
|
|
134
|
+
```erb
|
|
135
|
+
<%# in your layout, for pages without turbo_stream_from %>
|
|
136
|
+
<%= hibira_meta_tags %>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
import { hibira } from "hibira/turbo"
|
|
141
|
+
|
|
142
|
+
hibira().subscribe("private-user.42").bind("order-shipped", (order) => console.log(order))
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
It authorizes at `POST /hibira/auth` with the page's CSRF token. For presence
|
|
146
|
+
rooms with thousands of members, use a `presence-large-*` channel and
|
|
147
|
+
[`@hibira-io/large-presence`](https://github.com/hibira-io/large-presence-js).
|
|
148
|
+
|
|
149
|
+
## Webhooks
|
|
150
|
+
|
|
151
|
+
Add `https://your-app.example.com/hibira/webhooks` on your app page →
|
|
152
|
+
Webhooks, then handle events in an initializer:
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
Hibira.on_webhook("channel_vacated") do |event|
|
|
156
|
+
Room.find_by(channel: event["channel"])&.close!
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
Hibira.on_webhook { |event| Rails.logger.info("Hibira: #{event['name']}") } # every event
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The signature is checked against your app secret. While a rotated-out secret
|
|
163
|
+
is in its grace period, Hibira also signs with the old one
|
|
164
|
+
(`X-Hibira-Previous-Signature`), so webhooks keep arriving until you
|
|
165
|
+
redeploy. Each event is also instrumented as `webhook.hibira` with
|
|
166
|
+
`ActiveSupport::Notifications`.
|
|
167
|
+
|
|
168
|
+
## Push notifications
|
|
169
|
+
|
|
170
|
+
Enable push on your app page (Push notifications), upload your APNs key or
|
|
171
|
+
FCM service account there, add `gem "pusher-push-notifications"` and set:
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
HIBIRA_PUSH_INSTANCE_ID=your-instance-id
|
|
175
|
+
HIBIRA_PUSH_SECRET_KEY=your-secret-key
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
Hibira.push.publish_to_users(
|
|
180
|
+
users: [order.user_id.to_s],
|
|
181
|
+
payload: {
|
|
182
|
+
apns: { aps: { alert: { title: "Order shipped", body: "Order ##{order.id} is on its way." } } },
|
|
183
|
+
fcm: { notification: { title: "Order shipped", body: "Order ##{order.id} is on its way." } },
|
|
184
|
+
web: { notification: { title: "Order shipped", body: "Order ##{order.id} is on its way.", deep_link: order_url(order) } }
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`GET /hibira/push/token?user_id=…` returns a user token for `setUserId` in the
|
|
190
|
+
web, Android and iOS SDKs. It answers only for the signed-in user
|
|
191
|
+
(`current_user` or `Current.user`; change it with `config.push_user_id`).
|
|
192
|
+
|
|
193
|
+
## Settings
|
|
194
|
+
|
|
195
|
+
| Setting | Environment | Default |
|
|
196
|
+
| --- | --- | --- |
|
|
197
|
+
| `app_id`, `key`, `secret` | `HIBIRA_APP_ID`, `HIBIRA_APP_KEY`, `HIBIRA_APP_SECRET` | |
|
|
198
|
+
| `host` | `HIBIRA_HOST` | `ws.hibira.io` (or `ws-<cluster>.hibira.io`, as shown on your app page) |
|
|
199
|
+
| `port`, `use_tls` | `HIBIRA_PORT`, `HIBIRA_USE_TLS` | `443`, `true` |
|
|
200
|
+
| `turbo` | | `:auto` |
|
|
201
|
+
| `oversize` | | `:refresh` |
|
|
202
|
+
| `authorize_channel` | | refuses all |
|
|
203
|
+
| `push_instance_id`, `push_secret_key` | `HIBIRA_PUSH_INSTANCE_ID`, `HIBIRA_PUSH_SECRET_KEY` | |
|
|
204
|
+
| `push_endpoint` | `HIBIRA_PUSH_ENDPOINT` | `https://<host>` |
|
|
205
|
+
| `push_user_id` | | `current_user&.id` or `Current.user&.id` |
|
|
206
|
+
| `parent_controller` | | `"ApplicationController"` |
|
|
207
|
+
| `draw_routes`, `route_prefix` | | `true`, `"hibira"` |
|
|
208
|
+
| `client_options` | | `{}` (passed to `Pusher::Client.new`) |
|
|
209
|
+
|
|
210
|
+
Values set in `Hibira.configure` win over the environment, which wins over
|
|
211
|
+
credentials.
|
|
212
|
+
|
|
213
|
+
## License
|
|
214
|
+
|
|
215
|
+
MIT
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hibira
|
|
4
|
+
# Signs subscriptions to your own private- and presence- channels through
|
|
5
|
+
# Hibira.config.authorize_channel. Inherits from your ApplicationController
|
|
6
|
+
# (Hibira.config.parent_controller), so current_user, the session and CSRF
|
|
7
|
+
# protection work as in the rest of your app.
|
|
8
|
+
class ChannelAuthController < Hibira.config.parent_controller.constantize
|
|
9
|
+
def create
|
|
10
|
+
channel_name = params[:channel_name].to_s
|
|
11
|
+
socket_id = params[:socket_id].to_s
|
|
12
|
+
authorize = Hibira.config.authorize_channel
|
|
13
|
+
|
|
14
|
+
return head(:forbidden) if authorize.nil? || Hibira::Turbo.turbo_channel?(channel_name)
|
|
15
|
+
return head(:forbidden) unless channel_name.start_with?("private-", "presence-")
|
|
16
|
+
|
|
17
|
+
result = authorize.call(channel_name, self)
|
|
18
|
+
return head(:forbidden) unless result
|
|
19
|
+
|
|
20
|
+
if channel_name.start_with?("presence-")
|
|
21
|
+
return head(:forbidden) unless result.is_a?(Hash) && result.with_indifferent_access[:user_id].present?
|
|
22
|
+
|
|
23
|
+
render json: Hibira.client.authenticate(channel_name, socket_id, presence_data(result))
|
|
24
|
+
else
|
|
25
|
+
render json: Hibira.client.authenticate(channel_name, socket_id)
|
|
26
|
+
end
|
|
27
|
+
rescue ::Pusher::Error
|
|
28
|
+
head :unprocessable_entity
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def presence_data(result)
|
|
34
|
+
result = result.with_indifferent_access
|
|
35
|
+
{ user_id: result[:user_id].to_s, user_info: result[:user_info] || {} }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hibira
|
|
4
|
+
# Returns a Beams user token for the signed-in user, for setUserId in the
|
|
5
|
+
# web, Android and iOS SDKs, at GET /hibira/push/token. The SDKs pass
|
|
6
|
+
# ?user_id=, which must be the signed-in user's push user ID
|
|
7
|
+
# (Hibira.config.push_user_id).
|
|
8
|
+
class PushTokensController < Hibira.config.parent_controller.constantize
|
|
9
|
+
def show
|
|
10
|
+
user_id = Hibira.config.push_user_id.call(self)&.to_s
|
|
11
|
+
return head(:unauthorized) if user_id.blank?
|
|
12
|
+
return head(:unauthorized) unless ActiveSupport::SecurityUtils.secure_compare(user_id, params[:user_id].to_s)
|
|
13
|
+
|
|
14
|
+
render json: Hibira.push.generate_token(user: user_id)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hibira
|
|
4
|
+
# Signs subscriptions to Turbo stream channels. The signed stream name that
|
|
5
|
+
# turbo_stream_from rendered is the credential, as it is for Action Cable,
|
|
6
|
+
# so this needs no session and no CSRF token.
|
|
7
|
+
class TurboAuthController < ActionController::Base
|
|
8
|
+
skip_forgery_protection
|
|
9
|
+
|
|
10
|
+
def create
|
|
11
|
+
stream_name = ::Turbo::StreamsChannel.verified_stream_name(params[:signed_stream_name].to_s)
|
|
12
|
+
channel_name = params[:channel_name].to_s
|
|
13
|
+
|
|
14
|
+
if stream_name && channel_name == Hibira::Turbo.channel_name(stream_name)
|
|
15
|
+
render json: Hibira.client.authenticate(channel_name, params[:socket_id].to_s)
|
|
16
|
+
else
|
|
17
|
+
head :forbidden
|
|
18
|
+
end
|
|
19
|
+
rescue ::Pusher::Error
|
|
20
|
+
head :unprocessable_entity
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hibira
|
|
4
|
+
# Receives Hibira webhooks at POST /hibira/webhooks. Add that URL on your
|
|
5
|
+
# app page → Webhooks. Each verified event runs the Hibira.on_webhook
|
|
6
|
+
# handlers for its name and is instrumented as "webhook.hibira".
|
|
7
|
+
class WebhooksController < ActionController::Base
|
|
8
|
+
skip_forgery_protection
|
|
9
|
+
|
|
10
|
+
def create
|
|
11
|
+
webhook = Hibira::Webhook.new(request.env)
|
|
12
|
+
return head(:unauthorized) unless webhook.valid?
|
|
13
|
+
|
|
14
|
+
events = begin
|
|
15
|
+
webhook.events
|
|
16
|
+
rescue JSON::ParserError
|
|
17
|
+
return head(:bad_request)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
events.each do |event|
|
|
21
|
+
ActiveSupport::Notifications.instrument("webhook.hibira", event: event, time_ms: webhook.time_ms) do
|
|
22
|
+
(Hibira.webhook_handlers[event["name"]] + Hibira.webhook_handlers["*"]).each do |handler|
|
|
23
|
+
handler.call(event)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
head :ok
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Turbo Streams over Hibira.
|
|
2
|
+
//
|
|
3
|
+
// Defines <hibira-turbo-stream-source>, which turbo_stream_from renders when
|
|
4
|
+
// the hibira-rails gem is set up. Each element subscribes to its stream's
|
|
5
|
+
// private channel and hands every "turbo-stream" event to Turbo, the way
|
|
6
|
+
// <turbo-cable-stream-source> does for Action Cable.
|
|
7
|
+
//
|
|
8
|
+
// import "@hotwired/turbo-rails"
|
|
9
|
+
// import "hibira/turbo"
|
|
10
|
+
|
|
11
|
+
import { Turbo } from "@hotwired/turbo-rails"
|
|
12
|
+
import Pusher from "pusher-js"
|
|
13
|
+
|
|
14
|
+
const EVENT = "turbo-stream"
|
|
15
|
+
const UNSUBSCRIBE_DELAY = 1000 // keeps the channel across a Turbo visit to a page with the same stream
|
|
16
|
+
|
|
17
|
+
let client = null
|
|
18
|
+
const streams = new Map() // channel name → { channel, elements, signedStreamName, authEndpoint, timer }
|
|
19
|
+
|
|
20
|
+
const META = { Key: "hibira-key", Host: "hibira-host", Port: "hibira-port", Tls: "hibira-tls", AuthEndpoint: "hibira-auth-endpoint" }
|
|
21
|
+
|
|
22
|
+
// A connection setting from the element's data-hibira-* attributes, falling
|
|
23
|
+
// back to the hibira_meta_tags meta tags.
|
|
24
|
+
function setting(element, name) {
|
|
25
|
+
const value = element?.dataset?.[`hibira${name}`]
|
|
26
|
+
if (value != null && value !== "") return value
|
|
27
|
+
return document.querySelector(`meta[name="${META[name]}"]`)?.content
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function csrfToken() {
|
|
31
|
+
return document.querySelector('meta[name="csrf-token"]')?.content
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function authorize({ socketId, channelName }, callback) {
|
|
35
|
+
const stream = streams.get(channelName)
|
|
36
|
+
const endpoint = stream ? stream.authEndpoint : setting(null, "AuthEndpoint") || "/hibira/auth"
|
|
37
|
+
const body = new URLSearchParams({ socket_id: socketId, channel_name: channelName })
|
|
38
|
+
if (stream) body.set("signed_stream_name", stream.signedStreamName)
|
|
39
|
+
|
|
40
|
+
const headers = { Accept: "application/json" }
|
|
41
|
+
const token = csrfToken()
|
|
42
|
+
if (token) headers["X-CSRF-Token"] = token
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const response = await fetch(endpoint, { method: "POST", headers, body, credentials: "same-origin" })
|
|
46
|
+
if (!response.ok) throw new Error(`Hibira auth for ${channelName} failed with ${response.status}`)
|
|
47
|
+
callback(null, await response.json())
|
|
48
|
+
} catch (error) {
|
|
49
|
+
callback(error, null)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The shared pusher-js client, created from the first stream source element
|
|
54
|
+
// or, without one, from the hibira_meta_tags meta tags. Use it for your own
|
|
55
|
+
// channels too: hibira().subscribe("presence-room.1").
|
|
56
|
+
export function hibira(element = null) {
|
|
57
|
+
if (client) return client
|
|
58
|
+
|
|
59
|
+
const key = setting(element, "Key")
|
|
60
|
+
if (!key) throw new Error("Hibira: no app key. Set HIBIRA_APP_KEY, or add <%= hibira_meta_tags %> to your layout.")
|
|
61
|
+
|
|
62
|
+
const tls = setting(element, "Tls") !== "false"
|
|
63
|
+
const port = Number(setting(element, "Port") || (tls ? 443 : 80))
|
|
64
|
+
client = new Pusher(key, {
|
|
65
|
+
cluster: "hkg", // required by pusher-js, unused once wsHost is set
|
|
66
|
+
wsHost: setting(element, "Host") || "ws.hibira.io",
|
|
67
|
+
wsPort: port,
|
|
68
|
+
wssPort: port,
|
|
69
|
+
forceTLS: tls,
|
|
70
|
+
enabledTransports: ["ws", "wss"],
|
|
71
|
+
channelAuthorization: { customHandler: authorize },
|
|
72
|
+
})
|
|
73
|
+
return client
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
class HibiraTurboStreamSourceElement extends HTMLElement {
|
|
77
|
+
static observedAttributes = ["channel-name", "signed-stream-name"]
|
|
78
|
+
|
|
79
|
+
connectedCallback() {
|
|
80
|
+
Turbo.connectStreamSource(this)
|
|
81
|
+
this.subscribe()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
disconnectedCallback() {
|
|
85
|
+
Turbo.disconnectStreamSource(this)
|
|
86
|
+
this.unsubscribe()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
attributeChangedCallback() {
|
|
90
|
+
if (this.subscribedTo) {
|
|
91
|
+
this.unsubscribe()
|
|
92
|
+
this.subscribe()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
subscribe() {
|
|
97
|
+
const name = this.getAttribute("channel-name")
|
|
98
|
+
if (!name) return
|
|
99
|
+
|
|
100
|
+
let stream = streams.get(name)
|
|
101
|
+
if (!stream) {
|
|
102
|
+
stream = { elements: new Set(), timer: null }
|
|
103
|
+
streams.set(name, stream)
|
|
104
|
+
}
|
|
105
|
+
stream.signedStreamName = this.getAttribute("signed-stream-name")
|
|
106
|
+
stream.authEndpoint = setting(this, "AuthEndpoint")
|
|
107
|
+
clearTimeout(stream.timer)
|
|
108
|
+
stream.channel ||= hibira(this).subscribe(name)
|
|
109
|
+
|
|
110
|
+
this.receive = (data) => this.dispatchEvent(new MessageEvent("message", { data }))
|
|
111
|
+
this.connected = () => this.setAttribute("connected", "")
|
|
112
|
+
stream.channel.bind(EVENT, this.receive)
|
|
113
|
+
stream.channel.bind("pusher:subscription_succeeded", this.connected)
|
|
114
|
+
if (stream.channel.subscribed) this.connected()
|
|
115
|
+
|
|
116
|
+
stream.elements.add(this)
|
|
117
|
+
this.subscribedTo = name
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
unsubscribe() {
|
|
121
|
+
const name = this.subscribedTo
|
|
122
|
+
const stream = name && streams.get(name)
|
|
123
|
+
this.subscribedTo = null
|
|
124
|
+
this.removeAttribute("connected")
|
|
125
|
+
if (!stream) return
|
|
126
|
+
|
|
127
|
+
stream.channel.unbind(EVENT, this.receive)
|
|
128
|
+
stream.channel.unbind("pusher:subscription_succeeded", this.connected)
|
|
129
|
+
stream.elements.delete(this)
|
|
130
|
+
|
|
131
|
+
if (stream.elements.size === 0) {
|
|
132
|
+
clearTimeout(stream.timer)
|
|
133
|
+
stream.timer = setTimeout(() => {
|
|
134
|
+
if (stream.elements.size > 0) return
|
|
135
|
+
client?.unsubscribe(name)
|
|
136
|
+
streams.delete(name)
|
|
137
|
+
}, UNSUBSCRIBE_DELAY)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (customElements.get("hibira-turbo-stream-source") === undefined) {
|
|
143
|
+
customElements.define("hibira-turbo-stream-source", HibiraTurboStreamSourceElement)
|
|
144
|
+
}
|
data/config/importmap.rb
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module Hibira
|
|
6
|
+
module Generators
|
|
7
|
+
# bin/rails generate hibira:install
|
|
8
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
desc "Sets up Hibira: an initializer, pusher-js and the hibira/turbo JavaScript module."
|
|
12
|
+
|
|
13
|
+
class_option :skip_install, type: :boolean, default: false, desc: "Don't install pusher-js"
|
|
14
|
+
|
|
15
|
+
def create_initializer
|
|
16
|
+
template "hibira.rb", "config/initializers/hibira.rb"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def install_javascript
|
|
20
|
+
if importmap?
|
|
21
|
+
run "bin/importmap pin pusher-js" unless options[:skip_install]
|
|
22
|
+
import_in_application_js 'import "hibira/turbo"'
|
|
23
|
+
elsif File.exist?(File.join(destination_root, "package.json"))
|
|
24
|
+
copy_file File.expand_path("../../../../app/javascript/hibira/turbo.js", __dir__), "app/javascript/hibira/turbo.js"
|
|
25
|
+
run "#{package_manager} pusher-js" unless options[:skip_install]
|
|
26
|
+
import_in_application_js 'import "./hibira/turbo"'
|
|
27
|
+
else
|
|
28
|
+
say "No importmap or package.json found. Load pusher-js and the hibira/turbo module " \
|
|
29
|
+
"(#{File.expand_path('../../../../app/javascript/hibira/turbo.js', __dir__)}) yourself.", :yellow
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def show_next_steps
|
|
34
|
+
say <<~TEXT
|
|
35
|
+
|
|
36
|
+
Hibira is installed. Copy the app ID, key and secret from your app page on
|
|
37
|
+
https://hibira.io/portal into the environment:
|
|
38
|
+
|
|
39
|
+
HIBIRA_APP_ID=your-app-id
|
|
40
|
+
HIBIRA_APP_KEY=your-app-key
|
|
41
|
+
HIBIRA_APP_SECRET=your-app-secret
|
|
42
|
+
|
|
43
|
+
or into bin/rails credentials:edit under hibira: (app_id, key, secret).
|
|
44
|
+
Then check them with:
|
|
45
|
+
|
|
46
|
+
bin/rails hibira:check
|
|
47
|
+
TEXT
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def importmap?
|
|
53
|
+
File.exist?(File.join(destination_root, "config/importmap.rb"))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def package_manager
|
|
57
|
+
if File.exist?(File.join(destination_root, "bun.lockb")) || File.exist?(File.join(destination_root, "bun.lock"))
|
|
58
|
+
"bun add"
|
|
59
|
+
elsif File.exist?(File.join(destination_root, "yarn.lock"))
|
|
60
|
+
"yarn add"
|
|
61
|
+
elsif File.exist?(File.join(destination_root, "pnpm-lock.yaml"))
|
|
62
|
+
"pnpm add"
|
|
63
|
+
else
|
|
64
|
+
"npm install"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def import_in_application_js(line)
|
|
69
|
+
path = "app/javascript/application.js"
|
|
70
|
+
full = File.join(destination_root, path)
|
|
71
|
+
unless File.exist?(full)
|
|
72
|
+
say "Add #{line} to your JavaScript entry point after @hotwired/turbo-rails.", :yellow
|
|
73
|
+
return
|
|
74
|
+
end
|
|
75
|
+
return if File.read(full).include?(line)
|
|
76
|
+
|
|
77
|
+
if File.read(full).match?(%r{^import ["']@hotwired/turbo-rails["'].*$})
|
|
78
|
+
inject_into_file path, "\n#{line}", after: %r{^import ["']@hotwired/turbo-rails["'].*$}
|
|
79
|
+
else
|
|
80
|
+
append_to_file path, "#{line}\n"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Hibira reads HIBIRA_APP_ID, HIBIRA_APP_KEY, HIBIRA_APP_SECRET and HIBIRA_HOST
|
|
4
|
+
# from the environment, then hibira: in your credentials. Anything set here
|
|
5
|
+
# wins. See https://github.com/hibira-io/rails for every option.
|
|
6
|
+
Hibira.configure do |config|
|
|
7
|
+
# Turbo Streams go through Hibira whenever the credentials are set (except
|
|
8
|
+
# in tests). Set true or false to force it.
|
|
9
|
+
# config.turbo = :auto
|
|
10
|
+
|
|
11
|
+
# A broadcast over your plan's event size limit is sent as a Turbo page
|
|
12
|
+
# refresh instead. Or :log to drop it, :raise to raise.
|
|
13
|
+
# config.oversize = :refresh
|
|
14
|
+
|
|
15
|
+
# Authorize your own private- and presence- channels (POST /hibira/auth).
|
|
16
|
+
# config.authorize_channel = lambda do |channel, controller|
|
|
17
|
+
# user = controller.send(:current_user)
|
|
18
|
+
# case channel
|
|
19
|
+
# when /\Aprivate-user\.(\d+)\z/ then user&.id == $1.to_i
|
|
20
|
+
# when /\Apresence-room\.(\d+)\z/ then user && { user_id: user.id, user_info: { name: user.name } }
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Hibira.on_webhook("channel_vacated") do |event|
|
|
26
|
+
# Rails.logger.info "#{event['channel']} is empty"
|
|
27
|
+
# end
|
data/lib/hibira/check.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hibira
|
|
4
|
+
# Checks the settings and credentials, for bin/rails hibira:check.
|
|
5
|
+
module Check
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# Returns [ok, lines].
|
|
9
|
+
def run(config = Hibira.config)
|
|
10
|
+
lines = []
|
|
11
|
+
ok = true
|
|
12
|
+
|
|
13
|
+
if config.configured?
|
|
14
|
+
lines << "ok App #{config.app_id} on #{config.host}:#{config.port}"
|
|
15
|
+
begin
|
|
16
|
+
Hibira.client.channels
|
|
17
|
+
lines << "ok Credentials accepted by the REST API"
|
|
18
|
+
rescue ::Pusher::AuthenticationError
|
|
19
|
+
ok = false
|
|
20
|
+
lines << "fail The app key or secret was refused. Copy them again from your app page on hibira.io/portal."
|
|
21
|
+
rescue ::Pusher::Error => e
|
|
22
|
+
ok = false
|
|
23
|
+
lines << "fail Could not reach #{config.host}: #{e.message}"
|
|
24
|
+
end
|
|
25
|
+
else
|
|
26
|
+
ok = false
|
|
27
|
+
lines << "fail Missing #{config.missing.join(', ')} (environment or credentials hibira:)"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
lines << if config.turbo?
|
|
31
|
+
"ok Turbo Streams go through Hibira"
|
|
32
|
+
else
|
|
33
|
+
"info Turbo Streams use Action Cable (turbo: #{config.turbo.inspect})"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
lines << if config.push_configured?
|
|
37
|
+
"ok Push notifications set up (#{config.push_endpoint})"
|
|
38
|
+
else
|
|
39
|
+
"info Push notifications off (set HIBIRA_PUSH_INSTANCE_ID and HIBIRA_PUSH_SECRET_KEY)"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
[ok, lines]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hibira
|
|
4
|
+
# Settings for Hibira. Each one falls back to an environment variable and
|
|
5
|
+
# then to `hibira:` in Rails credentials, so an app that sets HIBIRA_APP_ID,
|
|
6
|
+
# HIBIRA_APP_KEY and HIBIRA_APP_SECRET needs no initializer at all.
|
|
7
|
+
class Configuration
|
|
8
|
+
# The app ID, key and secret from your app page on https://hibira.io.
|
|
9
|
+
attr_writer :app_id, :key, :secret
|
|
10
|
+
|
|
11
|
+
# The WebSocket host shown on the app page: ws.hibira.io for the default
|
|
12
|
+
# cluster, or ws-<cluster>.hibira.io for apps in another region. The
|
|
13
|
+
# REST API is served on the same host.
|
|
14
|
+
attr_writer :host, :port, :use_tls
|
|
15
|
+
|
|
16
|
+
# Send Turbo Stream broadcasts (broadcasts_to, broadcast_append_to,
|
|
17
|
+
# broadcasts_refreshes, ...) through Hibira instead of Action Cable, and
|
|
18
|
+
# make turbo_stream_from subscribe through Hibira. The default, :auto,
|
|
19
|
+
# does so whenever the credentials are set, except in the test
|
|
20
|
+
# environment, where Turbo's own broadcast assertions keep working on
|
|
21
|
+
# Action Cable. true forces it on, false off.
|
|
22
|
+
attr_accessor :turbo
|
|
23
|
+
|
|
24
|
+
# What to do when a Turbo Stream broadcast is larger than the app's event
|
|
25
|
+
# size limit (Hibira answers 413): :refresh sends a Turbo 8 page refresh
|
|
26
|
+
# instead, :log logs and drops it, :raise raises Hibira::PayloadTooLarge.
|
|
27
|
+
attr_accessor :oversize
|
|
28
|
+
|
|
29
|
+
# Called by the channel auth endpoint (POST /hibira/auth) for private-
|
|
30
|
+
# and presence- channels that are not Turbo streams. Receives the channel
|
|
31
|
+
# name and the controller (for current_user, session, params). Return
|
|
32
|
+
# false or nil to refuse, true to allow a private channel, or a hash such
|
|
33
|
+
# as { user_id: 1, user_info: { name: "Aiko" } } for a presence channel.
|
|
34
|
+
attr_accessor :authorize_channel
|
|
35
|
+
|
|
36
|
+
# Push notifications: from the app page → Push notifications.
|
|
37
|
+
attr_writer :push_instance_id, :push_secret_key, :push_endpoint
|
|
38
|
+
|
|
39
|
+
# Resolves the signed-in user's push user ID for GET /hibira/push/token
|
|
40
|
+
# (setUserId in the web, Android and iOS SDKs). Receives the controller;
|
|
41
|
+
# return nil when nobody is signed in. Defaults to the ID of current_user
|
|
42
|
+
# (Devise and most auth gems) or Current.user (the Rails 8 generator).
|
|
43
|
+
attr_accessor :push_user_id
|
|
44
|
+
|
|
45
|
+
# The controller the channel auth and push token endpoints inherit from,
|
|
46
|
+
# for current_user and your sign-in filters. Defaults to
|
|
47
|
+
# "ApplicationController".
|
|
48
|
+
attr_accessor :parent_controller
|
|
49
|
+
|
|
50
|
+
# Extra options passed to Pusher::Client.new (timeouts, http_proxy, ...).
|
|
51
|
+
attr_accessor :client_options
|
|
52
|
+
|
|
53
|
+
# The routes for Turbo and channel auth, webhooks and push tokens are drawn
|
|
54
|
+
# under /hibira. Set draw_routes to false to draw your own.
|
|
55
|
+
attr_accessor :draw_routes, :route_prefix
|
|
56
|
+
|
|
57
|
+
def initialize
|
|
58
|
+
@turbo = :auto
|
|
59
|
+
@oversize = :refresh
|
|
60
|
+
@client_options = {}
|
|
61
|
+
@parent_controller = "ApplicationController"
|
|
62
|
+
@draw_routes = true
|
|
63
|
+
@route_prefix = "hibira"
|
|
64
|
+
@push_user_id = lambda do |controller|
|
|
65
|
+
user = if controller.respond_to?(:current_user, true)
|
|
66
|
+
controller.send(:current_user)
|
|
67
|
+
elsif defined?(::Current) && ::Current.respond_to?(:user)
|
|
68
|
+
::Current.user
|
|
69
|
+
end
|
|
70
|
+
user&.id
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def app_id = setting(:@app_id, "HIBIRA_APP_ID", :app_id)
|
|
75
|
+
def key = setting(:@key, "HIBIRA_APP_KEY", :key)
|
|
76
|
+
def secret = setting(:@secret, "HIBIRA_APP_SECRET", :secret)
|
|
77
|
+
def host = setting(:@host, "HIBIRA_HOST", :host) || "ws.hibira.io"
|
|
78
|
+
def port = Integer(setting(:@port, "HIBIRA_PORT", :port) || (use_tls ? 443 : 80))
|
|
79
|
+
|
|
80
|
+
def use_tls
|
|
81
|
+
value = setting(:@use_tls, "HIBIRA_USE_TLS", :use_tls)
|
|
82
|
+
value.nil? || !%w[false 0 no off].include?(value.to_s.downcase)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def push_instance_id = setting(:@push_instance_id, "HIBIRA_PUSH_INSTANCE_ID", :push_instance_id)
|
|
86
|
+
def push_secret_key = setting(:@push_secret_key, "HIBIRA_PUSH_SECRET_KEY", :push_secret_key)
|
|
87
|
+
|
|
88
|
+
def push_endpoint
|
|
89
|
+
setting(:@push_endpoint, "HIBIRA_PUSH_ENDPOINT", :push_endpoint) || "https://#{host}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Whether Turbo Streams go through Hibira right now (see #turbo).
|
|
93
|
+
def turbo?
|
|
94
|
+
return turbo == true unless turbo == :auto
|
|
95
|
+
|
|
96
|
+
configured? && !(defined?(::Rails) && ::Rails.respond_to?(:env) && ::Rails.env.test?)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# True when the app ID, key and secret are all set.
|
|
100
|
+
def configured?
|
|
101
|
+
[app_id, key, secret].none? { |v| v.nil? || v.to_s.empty? }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def push_configured?
|
|
105
|
+
[push_instance_id, push_secret_key].none? { |v| v.nil? || v.to_s.empty? }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Names of the realtime settings that are still blank.
|
|
109
|
+
def missing
|
|
110
|
+
{ "HIBIRA_APP_ID" => app_id, "HIBIRA_APP_KEY" => key, "HIBIRA_APP_SECRET" => secret }
|
|
111
|
+
.select { |_, v| v.nil? || v.to_s.empty? }.keys
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def setting(ivar, env, credential)
|
|
117
|
+
value = instance_variable_get(ivar)
|
|
118
|
+
return value unless value.nil?
|
|
119
|
+
|
|
120
|
+
value = ENV[env]
|
|
121
|
+
return value unless value.nil? || value.empty?
|
|
122
|
+
|
|
123
|
+
credentials&.dig(credential)&.then { |v| v.to_s.empty? ? nil : v }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def credentials
|
|
127
|
+
return unless defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application
|
|
128
|
+
|
|
129
|
+
::Rails.application.credentials.hibira
|
|
130
|
+
rescue StandardError
|
|
131
|
+
nil
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/engine"
|
|
4
|
+
# Turbo::StreamsChannel subclasses ActionCable::Channel::Base, so the
|
|
5
|
+
# constants must load even in apps generated with --skip-action-cable. No
|
|
6
|
+
# Action Cable server is mounted or needed.
|
|
7
|
+
require "action_cable"
|
|
8
|
+
|
|
9
|
+
module Hibira
|
|
10
|
+
class Engine < ::Rails::Engine
|
|
11
|
+
initializer "hibira.assets" do |app|
|
|
12
|
+
if app.config.respond_to?(:assets)
|
|
13
|
+
app.config.assets.paths << root.join("app/javascript")
|
|
14
|
+
app.config.assets.precompile += %w[hibira/turbo.js] if app.config.assets.respond_to?(:precompile)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
initializer "hibira.importmap", before: "importmap" do |app|
|
|
19
|
+
if app.config.respond_to?(:importmap)
|
|
20
|
+
app.config.importmap.paths << root.join("config/importmap.rb")
|
|
21
|
+
app.config.importmap.cache_sweepers << root.join("app/javascript")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
initializer "hibira.helpers" do
|
|
26
|
+
ActiveSupport.on_load(:action_view) do
|
|
27
|
+
include Hibira::StreamsHelper
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The Hibira routes are drawn straight into the app's routes (like Active
|
|
32
|
+
# Storage's), under Hibira.config.route_prefix.
|
|
33
|
+
initializer "hibira.routes" do |app|
|
|
34
|
+
app.routes.prepend do
|
|
35
|
+
next unless Hibira.config.draw_routes
|
|
36
|
+
|
|
37
|
+
scope Hibira.config.route_prefix, module: "hibira", as: "hibira" do
|
|
38
|
+
post "turbo/auth" => "turbo_auth#create", as: :turbo_auth
|
|
39
|
+
post "auth" => "channel_auth#create", as: :channel_auth
|
|
40
|
+
post "webhooks" => "webhooks#create", as: :webhooks
|
|
41
|
+
get "push/token" => "push_tokens#show", as: :push_token
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
config.to_prepare do
|
|
47
|
+
if defined?(::Turbo::StreamsChannel)
|
|
48
|
+
singleton = ::Turbo::StreamsChannel.singleton_class
|
|
49
|
+
singleton.prepend(Hibira::Turbo::Broadcasts) unless singleton < Hibira::Turbo::Broadcasts
|
|
50
|
+
end
|
|
51
|
+
if defined?(::Turbo::StreamsHelper) && !(::Turbo::StreamsHelper < Hibira::TurboStreamFrom)
|
|
52
|
+
::Turbo::StreamsHelper.prepend(Hibira::TurboStreamFrom)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hibira
|
|
4
|
+
module StreamsHelper
|
|
5
|
+
# Like turbo_stream_from, but subscribes through Hibira:
|
|
6
|
+
#
|
|
7
|
+
# <%= hibira_turbo_stream_from @room %>
|
|
8
|
+
#
|
|
9
|
+
# Renders a <hibira-turbo-stream-source> element, which the hibira/turbo
|
|
10
|
+
# JavaScript module turns into a subscription to the stream's private
|
|
11
|
+
# channel. While Turbo goes through Hibira (config.turbo, on whenever the
|
|
12
|
+
# credentials are set), turbo_stream_from renders this too, so existing
|
|
13
|
+
# views need no change.
|
|
14
|
+
def hibira_turbo_stream_from(*streamables, **attributes)
|
|
15
|
+
raise ArgumentError, "streamables can't be blank" unless streamables.any?(&:present?)
|
|
16
|
+
|
|
17
|
+
stream_name = ::Turbo::StreamsChannel.send(:stream_name_from, streamables)
|
|
18
|
+
attributes.delete(:channel)
|
|
19
|
+
attributes[:"signed-stream-name"] = ::Turbo::StreamsChannel.signed_stream_name(streamables)
|
|
20
|
+
attributes[:"channel-name"] = Hibira::Turbo.channel_name(stream_name)
|
|
21
|
+
|
|
22
|
+
tag.hibira_turbo_stream_source(**hibira_client_attributes, **attributes)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Connection settings for the browser, as data attributes. The key is the
|
|
26
|
+
# public app key; the secret never leaves the server.
|
|
27
|
+
def hibira_client_attributes
|
|
28
|
+
config = Hibira.config
|
|
29
|
+
{
|
|
30
|
+
"data-hibira-key": config.key,
|
|
31
|
+
"data-hibira-host": config.host,
|
|
32
|
+
"data-hibira-port": config.port,
|
|
33
|
+
"data-hibira-tls": config.use_tls.to_s,
|
|
34
|
+
"data-hibira-auth-endpoint": hibira_route_path(:hibira_turbo_auth_path, "turbo/auth")
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Meta tags with the same settings, for your own pusher-js code:
|
|
39
|
+
#
|
|
40
|
+
# <%= hibira_meta_tags %>
|
|
41
|
+
def hibira_meta_tags
|
|
42
|
+
config = Hibira.config
|
|
43
|
+
safe_join([
|
|
44
|
+
tag.meta(name: "hibira-key", content: config.key),
|
|
45
|
+
tag.meta(name: "hibira-host", content: config.host),
|
|
46
|
+
tag.meta(name: "hibira-port", content: config.port),
|
|
47
|
+
tag.meta(name: "hibira-tls", content: config.use_tls.to_s),
|
|
48
|
+
tag.meta(name: "hibira-auth-endpoint", content: hibira_route_path(:hibira_channel_auth_path, "auth"))
|
|
49
|
+
], "\n")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def hibira_route_path(helper, fallback)
|
|
55
|
+
respond_to?(helper) ? public_send(helper) : "/#{Hibira.config.route_prefix}/#{fallback}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Prepended to Turbo::StreamsHelper so turbo_stream_from goes through
|
|
60
|
+
# Hibira while Hibira.config.turbo? is true. A custom Action Cable channel
|
|
61
|
+
# (turbo_stream_from @room, channel: RoomChannel) still uses Action Cable.
|
|
62
|
+
module TurboStreamFrom
|
|
63
|
+
def turbo_stream_from(*streamables, **attributes)
|
|
64
|
+
channel = attributes[:channel]&.to_s
|
|
65
|
+
if Hibira.config.turbo? && (channel.nil? || channel == "Turbo::StreamsChannel")
|
|
66
|
+
hibira_turbo_stream_from(*streamables, **attributes)
|
|
67
|
+
else
|
|
68
|
+
super
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
data/lib/hibira/turbo.rb
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Hibira
|
|
6
|
+
# Turbo Streams over Hibira. Each Turbo stream name maps to one private
|
|
7
|
+
# channel, so a browser can only subscribe with the signed stream name that
|
|
8
|
+
# turbo_stream_from rendered for it, the same rule Action Cable applies.
|
|
9
|
+
module Turbo
|
|
10
|
+
EVENT = "turbo-stream"
|
|
11
|
+
CHANNEL_PREFIX = "private-turbo-"
|
|
12
|
+
REFRESH = '<turbo-stream action="refresh"></turbo-stream>'
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
# Stream names hold characters channel names cannot ("gid://app/Room/1"),
|
|
16
|
+
# so the channel is named after a digest of the stream name.
|
|
17
|
+
def channel_name(stream_name)
|
|
18
|
+
"#{CHANNEL_PREFIX}#{Digest::SHA256.hexdigest(stream_name.to_s)}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def turbo_channel?(channel_name)
|
|
22
|
+
channel_name.to_s.start_with?(CHANNEL_PREFIX)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def broadcast(stream_name, content)
|
|
26
|
+
Hibira.trigger(channel_name(stream_name), EVENT, content.to_s)
|
|
27
|
+
rescue ::Pusher::Error => e
|
|
28
|
+
raise unless e.message.include?("Payload Too Large")
|
|
29
|
+
|
|
30
|
+
oversized(stream_name, content, e)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def oversized(stream_name, content, error)
|
|
36
|
+
message = "[Hibira] Turbo Stream for #{stream_name.inspect} is #{content.to_s.bytesize} bytes, " \
|
|
37
|
+
"over the app's event size limit"
|
|
38
|
+
|
|
39
|
+
case Hibira.config.oversize
|
|
40
|
+
when :raise
|
|
41
|
+
raise PayloadTooLarge, message
|
|
42
|
+
when :refresh
|
|
43
|
+
if content.to_s.include?('action="refresh"')
|
|
44
|
+
logger&.warn("#{message}; dropped")
|
|
45
|
+
else
|
|
46
|
+
logger&.warn("#{message}; sent a page refresh instead")
|
|
47
|
+
Hibira.trigger(channel_name(stream_name), EVENT, REFRESH)
|
|
48
|
+
end
|
|
49
|
+
else
|
|
50
|
+
logger&.warn("#{message}; dropped")
|
|
51
|
+
end
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def logger
|
|
56
|
+
defined?(::Rails) && ::Rails.respond_to?(:logger) ? ::Rails.logger : nil
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Prepended to Turbo::StreamsChannel's singleton class. Every Turbo
|
|
61
|
+
# broadcast (broadcast_*_to, broadcasts_to, broadcasts_refreshes and the
|
|
62
|
+
# _later jobs) ends in broadcast_stream_to, so this one override sends all
|
|
63
|
+
# of them to Hibira.
|
|
64
|
+
module Broadcasts
|
|
65
|
+
def broadcast_stream_to(*streamables, content:)
|
|
66
|
+
return super unless Hibira.config.turbo?
|
|
67
|
+
|
|
68
|
+
streamables = streamables.flatten.compact_blank
|
|
69
|
+
return if streamables.empty?
|
|
70
|
+
|
|
71
|
+
Hibira::Turbo.broadcast(stream_name_from(streamables), content)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "openssl"
|
|
5
|
+
require "active_support/security_utils"
|
|
6
|
+
|
|
7
|
+
module Hibira
|
|
8
|
+
# A webhook post from Hibira. X-Pusher-Signature is the hex HMAC-SHA256 of
|
|
9
|
+
# the body made with the app's current secret. While a rotated-out secret
|
|
10
|
+
# is in its grace period, X-Hibira-Previous-Signature carries the same HMAC
|
|
11
|
+
# made with the old one, so an app that still has the old secret keeps
|
|
12
|
+
# accepting webhooks until it is redeployed.
|
|
13
|
+
class Webhook
|
|
14
|
+
SIGNATURE_HEADERS = %w[HTTP_X_PUSHER_SIGNATURE HTTP_X_HIBIRA_PREVIOUS_SIGNATURE].freeze
|
|
15
|
+
|
|
16
|
+
attr_reader :body
|
|
17
|
+
|
|
18
|
+
def initialize(env, key: Hibira.config.key, secret: Hibira.config.secret)
|
|
19
|
+
@env = env
|
|
20
|
+
@key = key.to_s
|
|
21
|
+
@secret = secret.to_s
|
|
22
|
+
input = env["rack.input"]
|
|
23
|
+
input.rewind if input.respond_to?(:rewind)
|
|
24
|
+
@body = input ? input.read.to_s : ""
|
|
25
|
+
input.rewind if input.respond_to?(:rewind)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def valid?
|
|
29
|
+
return false if @key.empty? || @secret.empty?
|
|
30
|
+
return false unless secure_compare(@key, @env["HTTP_X_PUSHER_KEY"].to_s)
|
|
31
|
+
|
|
32
|
+
expected = OpenSSL::HMAC.hexdigest("SHA256", @secret, body)
|
|
33
|
+
SIGNATURE_HEADERS.any? { |h| secure_compare(expected, @env[h].to_s) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def payload
|
|
37
|
+
@payload ||= JSON.parse(body)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def events
|
|
41
|
+
Array(payload["events"]).select { |e| e.is_a?(Hash) && e["name"].is_a?(String) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def time_ms
|
|
45
|
+
payload["time_ms"].to_i
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def secure_compare(a, b)
|
|
51
|
+
a.bytesize == b.bytesize && ActiveSupport::SecurityUtils.secure_compare(a, b)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
data/lib/hibira-rails.rb
ADDED
data/lib/hibira.rb
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pusher"
|
|
4
|
+
require "hibira/version"
|
|
5
|
+
require "hibira/configuration"
|
|
6
|
+
require "hibira/webhook"
|
|
7
|
+
require "hibira/turbo"
|
|
8
|
+
require "hibira/streams_helper"
|
|
9
|
+
require "hibira/check"
|
|
10
|
+
|
|
11
|
+
# Rails integration for Hibira (https://hibira.io): Turbo Streams, private and
|
|
12
|
+
# presence channel auth, webhooks and push notifications over the Pusher
|
|
13
|
+
# Channels protocol.
|
|
14
|
+
module Hibira
|
|
15
|
+
class Error < StandardError; end
|
|
16
|
+
class MissingCredentials < Error; end
|
|
17
|
+
class PayloadTooLarge < Error; end
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def config
|
|
21
|
+
@config ||= Configuration.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def configure
|
|
25
|
+
yield config
|
|
26
|
+
reset!
|
|
27
|
+
config
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The pusher-http-ruby client, pointed at Hibira.
|
|
31
|
+
def client
|
|
32
|
+
@client ||= begin
|
|
33
|
+
unless config.configured?
|
|
34
|
+
raise MissingCredentials, "Hibira is not configured: set #{config.missing.join(', ')} " \
|
|
35
|
+
"(run bin/rails hibira:check)"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
::Pusher::Client.new(
|
|
39
|
+
app_id: config.app_id.to_s,
|
|
40
|
+
key: config.key,
|
|
41
|
+
secret: config.secret,
|
|
42
|
+
host: config.host,
|
|
43
|
+
port: config.port,
|
|
44
|
+
use_tls: config.use_tls,
|
|
45
|
+
**config.client_options
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Hibira.trigger("orders", "order-shipped", { id: 1024 })
|
|
51
|
+
def trigger(channels, event, data, params = {})
|
|
52
|
+
client.trigger(channels, event, data, params)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The Beams-compatible push client, pointed at Hibira. Needs the
|
|
56
|
+
# pusher-push-notifications gem in your Gemfile.
|
|
57
|
+
def push
|
|
58
|
+
begin
|
|
59
|
+
require "pusher/push_notifications"
|
|
60
|
+
rescue LoadError
|
|
61
|
+
raise Error, "Add gem \"pusher-push-notifications\" to your Gemfile to send push notifications"
|
|
62
|
+
end
|
|
63
|
+
raise MissingCredentials, "Set HIBIRA_PUSH_INSTANCE_ID and HIBIRA_PUSH_SECRET_KEY" unless config.push_configured?
|
|
64
|
+
|
|
65
|
+
push = ::Pusher::PushNotifications
|
|
66
|
+
push.configure do |c|
|
|
67
|
+
c.instance_id = config.push_instance_id
|
|
68
|
+
c.secret_key = config.push_secret_key
|
|
69
|
+
c.endpoint = config.push_endpoint
|
|
70
|
+
end
|
|
71
|
+
push
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Runs the block for each verified webhook event with that name ("*" for
|
|
75
|
+
# all). Events are also instrumented as "webhook.hibira".
|
|
76
|
+
#
|
|
77
|
+
# Hibira.on_webhook("channel_vacated") { |event| Room.close(event["channel"]) }
|
|
78
|
+
def on_webhook(name = "*", &block)
|
|
79
|
+
webhook_handlers[name.to_s] << block
|
|
80
|
+
block
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def webhook_handlers
|
|
84
|
+
@webhook_handlers ||= Hash.new { |h, k| h[k] = [] }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def reset!
|
|
88
|
+
@client = nil
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
require "hibira/engine" if defined?(Rails::Engine)
|
metadata
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: hibira-rails
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Hibira
|
|
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: actioncable
|
|
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'
|
|
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'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: pusher
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '2.0'
|
|
39
|
+
- - "<"
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '3'
|
|
42
|
+
type: :runtime
|
|
43
|
+
prerelease: false
|
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '2.0'
|
|
49
|
+
- - "<"
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '3'
|
|
52
|
+
- !ruby/object:Gem::Dependency
|
|
53
|
+
name: railties
|
|
54
|
+
requirement: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '7.0'
|
|
59
|
+
- - "<"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '9'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '7.0'
|
|
69
|
+
- - "<"
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '9'
|
|
72
|
+
- !ruby/object:Gem::Dependency
|
|
73
|
+
name: turbo-rails
|
|
74
|
+
requirement: !ruby/object:Gem::Requirement
|
|
75
|
+
requirements:
|
|
76
|
+
- - ">="
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: '1.5'
|
|
79
|
+
- - "<"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3'
|
|
82
|
+
type: :runtime
|
|
83
|
+
prerelease: false
|
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '1.5'
|
|
89
|
+
- - "<"
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: '3'
|
|
92
|
+
description: Sends Hotwire Turbo Stream broadcasts through Hibira, a hosted realtime
|
|
93
|
+
service that speaks the Pusher Channels protocol, with no Action Cable server or
|
|
94
|
+
Redis to run. Also signs private and presence channels, verifies webhooks and serves
|
|
95
|
+
push tokens.
|
|
96
|
+
email:
|
|
97
|
+
- github@hibira.io
|
|
98
|
+
executables: []
|
|
99
|
+
extensions: []
|
|
100
|
+
extra_rdoc_files: []
|
|
101
|
+
files:
|
|
102
|
+
- CHANGELOG.md
|
|
103
|
+
- LICENSE
|
|
104
|
+
- README.md
|
|
105
|
+
- app/controllers/hibira/channel_auth_controller.rb
|
|
106
|
+
- app/controllers/hibira/push_tokens_controller.rb
|
|
107
|
+
- app/controllers/hibira/turbo_auth_controller.rb
|
|
108
|
+
- app/controllers/hibira/webhooks_controller.rb
|
|
109
|
+
- app/javascript/hibira/turbo.js
|
|
110
|
+
- config/importmap.rb
|
|
111
|
+
- lib/generators/hibira/install/install_generator.rb
|
|
112
|
+
- lib/generators/hibira/install/templates/hibira.rb
|
|
113
|
+
- lib/hibira-rails.rb
|
|
114
|
+
- lib/hibira.rb
|
|
115
|
+
- lib/hibira/check.rb
|
|
116
|
+
- lib/hibira/configuration.rb
|
|
117
|
+
- lib/hibira/engine.rb
|
|
118
|
+
- lib/hibira/streams_helper.rb
|
|
119
|
+
- lib/hibira/turbo.rb
|
|
120
|
+
- lib/hibira/version.rb
|
|
121
|
+
- lib/hibira/webhook.rb
|
|
122
|
+
- lib/tasks/hibira.rake
|
|
123
|
+
homepage: https://hibira.io
|
|
124
|
+
licenses:
|
|
125
|
+
- MIT
|
|
126
|
+
metadata:
|
|
127
|
+
homepage_uri: https://hibira.io
|
|
128
|
+
source_code_uri: https://github.com/hibira-io/rails
|
|
129
|
+
changelog_uri: https://github.com/hibira-io/rails/blob/main/CHANGELOG.md
|
|
130
|
+
bug_tracker_uri: https://github.com/hibira-io/rails/issues
|
|
131
|
+
rubygems_mfa_required: 'true'
|
|
132
|
+
rdoc_options: []
|
|
133
|
+
require_paths:
|
|
134
|
+
- lib
|
|
135
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
136
|
+
requirements:
|
|
137
|
+
- - ">="
|
|
138
|
+
- !ruby/object:Gem::Version
|
|
139
|
+
version: '3.1'
|
|
140
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - ">="
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: '0'
|
|
145
|
+
requirements: []
|
|
146
|
+
rubygems_version: 3.6.9
|
|
147
|
+
specification_version: 4
|
|
148
|
+
summary: Turbo Streams, channel auth, webhooks and push notifications over Hibira
|
|
149
|
+
for Rails
|
|
150
|
+
test_files: []
|