deacon-rails 1.0.0 → 1.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 +4 -4
- data/README.md +87 -9
- data/app/controllers/deacon/rails/tokens_controller.rb +52 -0
- data/lib/deacon/rails/current_user.rb +44 -0
- data/lib/deacon/rails/engine.rb +25 -0
- data/lib/deacon/rails/helper.rb +76 -9
- data/lib/deacon/rails/version.rb +1 -1
- data/lib/deacon/rails.rb +2 -1
- metadata +28 -5
- data/lib/deacon/rails/railtie.rb +0 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: da8d6aadcc7b35006f1a904adda812bafc11bd4ae63295547447e5d905b8acbd
|
|
4
|
+
data.tar.gz: 4f7075550ef183de2d723857692b7f52e97ed7f16036d406bc1556ae7db87241
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c6298ba5b22ba7d1e27f84f114213740492f39c1f823e7cfe4e9f9e6a63e1cd0f862231d316f10c3cf96ee44b816366b4fff377325dbdcca471b8c7f060dd077
|
|
7
|
+
data.tar.gz: 5706b3a17954523dea7b58e84dc141b7042e5911c2cd4f287603bec5925d2280eb16d3a247e3149ac7861067b4b9b9b42a0b33d2ce293662802b263a72ec9e74
|
data/README.md
CHANGED
|
@@ -40,31 +40,86 @@ Deacon only runs on the sites listed on Install. A listed domain covers its subd
|
|
|
40
40
|
|
|
41
41
|
When you run `bin/rails server`, Install notices the widget on `localhost:3000` and lets you add it in one click. Chats on localhost don't count towards your plan.
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
## Who is signed in
|
|
44
|
+
|
|
45
|
+
When someone is signed in to your app, the tag tells Deacon their id, email and name, so their conversations show who they are. To have Deacon trust that, rather than take the page's word for it, give the gem your secret in the `DEACON_SECRET` environment variable:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
DEACON_SECRET=deacon_identity_your_secret
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
To make one, open Install in Deacon and use Make a secret under Verify who's signed in. You see it once. The key the gem signs with is the whole string exactly as Install shows it, `deacon_identity_` and all, not decoded or shortened. Keep it on the server: unlike the `pk_` key, anyone who has it can tell Deacon they are any of your users.
|
|
52
|
+
|
|
53
|
+
With the secret set, the gem does the rest:
|
|
54
|
+
|
|
55
|
+
- Above the widget's tag it prints `window.deaconUser`, with the user's id, email and name and a `token` function.
|
|
56
|
+
- When the chat opens, `token` posts to `/deacon/token`, a route the gem adds to your app. It answers with a token signed with `DEACON_SECRET` that names the user and this chat and lasts 10 minutes. With nobody signed in, or no secret, it answers 204 and signs nothing. Any other failure, such as a 404 or a 500, makes `token` throw, so Deacon can tell you the route is broken rather than take it for a sign-out. Its answers are never cached and carry no CORS headers, so only your own pages can read them.
|
|
57
|
+
- On a page where nobody is signed in, it prints `window.deaconUser = null` and `Deacon.identify(null)` instead, so signing out ends what Deacon knew about the last person, and the next person on that computer starts fresh.
|
|
58
|
+
|
|
59
|
+
If your layout sets `window.deaconUser` itself, from an earlier version of Deacon's install guide, take that script out: the tag prints it now.
|
|
60
|
+
|
|
61
|
+
### Finding the user
|
|
62
|
+
|
|
63
|
+
The gem asks your controllers for `current_user`, which Devise and most sign-in gems provide. Rails 8's built-in authentication has no `current_user`, so tell the gem how to find the user with a lambda, which runs inside the controller:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
Rails.application.config.deacon.current_user = -> { Current.user if authenticated? }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Or give it the name of a method, such as `:current_member`. The id it sends is the user's `id`, so it must name one person across your whole app. The email is the user's `email`, or `email_address` with Rails 8's built-in authentication, and the name is their `name`, when they have one.
|
|
70
|
+
|
|
71
|
+
The token route's controller inherits your `ApplicationController`, so it finds the user just as your pages do. It skips `require_authentication` (Rails 8) and `authenticate_user!` (Devise), so someone who has signed out gets a 204 rather than a redirect to your sign-in page. If your `ApplicationController` requires sign-in under another name, skip that for the route too:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
Rails.application.config.to_prepare do
|
|
75
|
+
Deacon::Rails::TokensController.skip_before_action :require_login
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### CSRF
|
|
80
|
+
|
|
81
|
+
The route checks your app's CSRF token, as every other POST in your app does, and the `token` function sends it in the `X-CSRF-Token` header. The route changes nothing, and with no CORS headers another site couldn't read its answer, so exempting it would be safe too. Keeping the check means a page on another site can't get your server to sign anything at all, and nothing in your app is exempt from it.
|
|
82
|
+
|
|
83
|
+
### Staging and development
|
|
84
|
+
|
|
85
|
+
Give staging its own Deacon team, with its own key and secret, or leave `DEACON_SECRET` unset there so the route answers 204. Never copy production's secret to staging or a laptop: whoever runs that server could sign a token for any of your users.
|
|
86
|
+
|
|
87
|
+
### Signing in in the browser
|
|
88
|
+
|
|
89
|
+
If your app learns who is signed in in the browser, such as a React front end on a JSON API, the server sees nobody on every page, and the sign-out line would start everyone afresh on every page load. Turn it off, and tell Deacon from your own code with `Deacon.identify`, as [Deacon's install guide](https://heydeacon.com/agent/install.md) shows:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
Rails.application.config.deacon.signed_in = false
|
|
93
|
+
```
|
|
44
94
|
|
|
45
95
|
## Options
|
|
46
96
|
|
|
47
97
|
Set them on `config.deacon`, in an initializer or in `config/application.rb`.
|
|
48
98
|
|
|
49
|
-
| Option
|
|
50
|
-
|
|
|
51
|
-
| `key`
|
|
52
|
-
| `context`
|
|
53
|
-
| `page_views`
|
|
99
|
+
| Option | Type | Default | What it does |
|
|
100
|
+
| -------------- | ----------------- | -------------- | ------------ |
|
|
101
|
+
| `key` | string | `DEACON_KEY` | Your Deacon key. |
|
|
102
|
+
| `context` | hash | none | Details Deacon takes into account when it answers, the same on every page, such as `{ product: "api", docs_version: "3.2" }`. Up to 12 keys, each value a string, number or boolean. Anyone can read them in the page's HTML, so never put a secret here. |
|
|
103
|
+
| `page_views` | boolean | `true` | The same script counts visits for Deacon's website analytics. Set this to `false` to send no page views or JavaScript errors from your app. Chat still works. |
|
|
104
|
+
| `signed_in` | boolean | `true` | Tell Deacon who is signed in, and sign them out of Deacon on pages where nobody is. Set this to `false` if your app signs people in in the browser. |
|
|
105
|
+
| `current_user` | symbol or lambda | `current_user` | How to find the signed-in user: the name of a method on your controllers, or a lambda that runs inside one. |
|
|
54
106
|
|
|
55
107
|
```ruby
|
|
56
108
|
Rails.application.config.deacon.key = "pk_your_widget_key"
|
|
57
109
|
Rails.application.config.deacon.context = { product: "api", docs_version: "3.2" }
|
|
58
110
|
Rails.application.config.deacon.page_views = false
|
|
111
|
+
Rails.application.config.deacon.current_user = -> { Current.user if authenticated? }
|
|
59
112
|
```
|
|
60
113
|
|
|
114
|
+
The secret has no option, only `DEACON_SECRET`, so it stays out of your code.
|
|
115
|
+
|
|
61
116
|
## Turbo
|
|
62
117
|
|
|
63
|
-
Turbo Drive swaps the `<body>` on each visit and keeps the `<head>`, so in the head the tag runs once. Deacon puts its chat on `<html>`, outside the body, so the widget stays in place as people move between pages.
|
|
118
|
+
Turbo Drive swaps the `<body>` on each visit and keeps the `<head>`, so in the head the tag runs once. Deacon puts its chat on `<html>`, outside the body, so the widget stays in place as people move between pages. The signed-in script is different on every page, so Turbo runs it on each visit, and a sign-in or sign-out without a full page load still reaches Deacon.
|
|
64
119
|
|
|
65
120
|
## Content Security Policy
|
|
66
121
|
|
|
67
|
-
If your app gives each request a nonce, with `config.content_security_policy_nonce_generator`, the tag carries it. The policy also needs Deacon's address, `https://heydeacon.com`, in `script-src`, `connect-src` and `frame-src`, and `'unsafe-inline'` in `style-src`, because the widget writes its styles where a nonce can't reach. Keep the nonce to `script-src`, since a nonce in `style-src` makes browsers ignore `'unsafe-inline'`:
|
|
122
|
+
If your app gives each request a nonce, with `config.content_security_policy_nonce_generator`, the tag carries it, on the signed-in script too. With Turbo, also put `<%= csp_meta_tag %>` in the head, which Turbo reads to run the signed-in script again on each visit. The policy also needs Deacon's address, `https://heydeacon.com`, in `script-src`, `connect-src` and `frame-src`, and `'unsafe-inline'` in `style-src`, because the widget writes its styles where a nonce can't reach. Keep the nonce to `script-src`, since a nonce in `style-src` makes browsers ignore `'unsafe-inline'`:
|
|
68
123
|
|
|
69
124
|
```ruby
|
|
70
125
|
Rails.application.configure do
|
|
@@ -92,7 +147,30 @@ The helper prints the same tag Install gives you:
|
|
|
92
147
|
<script src="https://heydeacon.com/widget/v1/loader.js" data-key="pk_your_widget_key" async></script>
|
|
93
148
|
```
|
|
94
149
|
|
|
95
|
-
It loads in the background without holding up the page.
|
|
150
|
+
It loads in the background without holding up the page. Above it, when someone is signed in, goes the signed-in script, here spread over lines:
|
|
151
|
+
|
|
152
|
+
```html
|
|
153
|
+
<script data-deacon-render="3f9c0a71d2e4b856">
|
|
154
|
+
window.deaconUser = {
|
|
155
|
+
id: "7",
|
|
156
|
+
email: "ana@studio.com",
|
|
157
|
+
name: "Ana Ruiz",
|
|
158
|
+
token: (nonce) => fetch("/deacon/token", {
|
|
159
|
+
method: "POST",
|
|
160
|
+
credentials: "same-origin",
|
|
161
|
+
headers: { "Content-Type": "application/json", "X-CSRF-Token": "…" },
|
|
162
|
+
body: JSON.stringify({ nonce }),
|
|
163
|
+
}).then((r) => {
|
|
164
|
+
if (r.status === 204) return "";
|
|
165
|
+
if (!r.ok) throw new Error("Deacon token route answered " + r.status);
|
|
166
|
+
return r.text();
|
|
167
|
+
}),
|
|
168
|
+
};
|
|
169
|
+
window.Deacon?.identify(window.deaconUser);
|
|
170
|
+
</script>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Works with Rails 8.0 and later, on Ruby 3.3 and later.
|
|
96
174
|
|
|
97
175
|
## License
|
|
98
176
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require "jwt"
|
|
2
|
+
|
|
3
|
+
module Deacon
|
|
4
|
+
module Rails
|
|
5
|
+
# POST /deacon/token, which the page's token function calls when the chat opens: a token
|
|
6
|
+
# saying who is signed in, signed with DEACON_SECRET, for Deacon to check. It inherits the
|
|
7
|
+
# app's ApplicationController, so it finds the user as the app's own pages do and checks the
|
|
8
|
+
# CSRF token as every other POST in the app does.
|
|
9
|
+
class TokensController < ::ApplicationController
|
|
10
|
+
# How long a token lasts. Deacon refuses one that lasts more than 15 minutes.
|
|
11
|
+
LIFETIME = 10 * 60
|
|
12
|
+
|
|
13
|
+
# The widget session's nonce, which the token carries so it proves who is signed in to that
|
|
14
|
+
# session and no other. Deacon's are hex; anything far from that isn't one.
|
|
15
|
+
NONCE_FORMAT = /\A[A-Za-z0-9_-]{1,128}\z/
|
|
16
|
+
|
|
17
|
+
MISSING_SECRET = "deacon-rails: DEACON_SECRET is missing, so the token route signs nobody " \
|
|
18
|
+
"and Deacon can't verify who is signed in. In Deacon, open Install (the " \
|
|
19
|
+
"button at the top of the dashboard) and make a secret under Verify who's " \
|
|
20
|
+
"signed in. It starts with deacon_identity_.".freeze
|
|
21
|
+
|
|
22
|
+
# The route answers for nobody signed in itself, with a 204, so the app's sign-in gates must
|
|
23
|
+
# not send that request to a sign-in page: Rails 8's built-in authentication's and Devise's.
|
|
24
|
+
skip_before_action :require_authentication, :authenticate_user!, raise: false
|
|
25
|
+
|
|
26
|
+
def create
|
|
27
|
+
# A token names one person, so no cache in front of the app may keep it for another.
|
|
28
|
+
response.headers["Cache-Control"] = "no-store, private"
|
|
29
|
+
|
|
30
|
+
nonce = request.request_parameters["nonce"]
|
|
31
|
+
return head(:bad_request) unless nonce.is_a?(String) && NONCE_FORMAT.match?(nonce)
|
|
32
|
+
|
|
33
|
+
user = CurrentUser.find(self)
|
|
34
|
+
return head(:no_content) unless user.is_a?(CurrentUser::Details)
|
|
35
|
+
|
|
36
|
+
# The key is the whole secret as Deacon showed it, deacon_identity_ and all.
|
|
37
|
+
secret = ENV["DEACON_SECRET"].to_s.strip
|
|
38
|
+
|
|
39
|
+
if secret.empty?
|
|
40
|
+
::Rails.logger&.warn(MISSING_SECRET)
|
|
41
|
+
return head(:no_content)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
now = Time.now.to_i
|
|
45
|
+
claims = { sub: user.id, email: user.email, name: user.name, nonce: nonce, iat: now,
|
|
46
|
+
exp: now + LIFETIME }
|
|
47
|
+
|
|
48
|
+
render plain: JWT.encode(claims.compact, secret, "HS256")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Deacon
|
|
2
|
+
module Rails
|
|
3
|
+
# Who is signed in to the app, found the same way for the page and for the token route.
|
|
4
|
+
module CurrentUser
|
|
5
|
+
# The signed-in user as Deacon is told them: the id as a string, and the email and name
|
|
6
|
+
# when the user has them.
|
|
7
|
+
Details = Struct.new(:id, :email, :name)
|
|
8
|
+
|
|
9
|
+
# The signed-in user's Details, nil when nobody is signed in, or :unknown when the app has
|
|
10
|
+
# no way to say who is. config.deacon.current_user names a method on the controller, or is
|
|
11
|
+
# a lambda run inside it; without one, the controller's own current_user is used.
|
|
12
|
+
def self.find(controller)
|
|
13
|
+
lookup = ::Rails.application.config.deacon.current_user
|
|
14
|
+
lookup ||= :current_user if controller.respond_to?(:current_user, true)
|
|
15
|
+
return :unknown if lookup.nil?
|
|
16
|
+
|
|
17
|
+
user = case lookup
|
|
18
|
+
when Proc then controller.instance_exec(&lookup)
|
|
19
|
+
when Symbol, String then controller.send(lookup)
|
|
20
|
+
else
|
|
21
|
+
raise ArgumentError, "deacon-rails: config.deacon.current_user must be a method " \
|
|
22
|
+
"name or a lambda."
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
details(user) if user
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Rails 8's built-in authentication names the address email_address. A user with no id,
|
|
29
|
+
# such as a guest object or one never saved, names nobody, so it counts as nobody.
|
|
30
|
+
def self.details(user)
|
|
31
|
+
id = user.id.to_s
|
|
32
|
+
return if id.empty?
|
|
33
|
+
|
|
34
|
+
email = if user.respond_to?(:email) then user.email
|
|
35
|
+
elsif user.respond_to?(:email_address) then user.email_address
|
|
36
|
+
end
|
|
37
|
+
name = user.name if user.respond_to?(:name)
|
|
38
|
+
|
|
39
|
+
Details.new(id, email.to_s.presence, name.to_s.presence)
|
|
40
|
+
end
|
|
41
|
+
private_class_method :details
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Deacon
|
|
2
|
+
module Rails
|
|
3
|
+
# What config.deacon holds. A Struct rather than ordered options, so a misspelt setting
|
|
4
|
+
# fails loudly at boot instead of being ignored.
|
|
5
|
+
Configuration = Struct.new(:key, :context, :page_views, :signed_in, :current_user)
|
|
6
|
+
|
|
7
|
+
# Adds config.deacon to the app, deacon_widget_tag to its views and POST /deacon/token to its
|
|
8
|
+
# routes. An engine rather than a plain Railtie, so the app loads the token route's
|
|
9
|
+
# controller with its own, after the ApplicationController it inherits.
|
|
10
|
+
class Engine < ::Rails::Engine
|
|
11
|
+
config.deacon = Configuration.new
|
|
12
|
+
|
|
13
|
+
initializer "deacon.view_helper" do
|
|
14
|
+
ActiveSupport.on_load(:action_view) { include Deacon::Rails::Helper }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Ahead of the app's own routes, so a catch-all route of its own never answers instead.
|
|
18
|
+
initializer "deacon.token_route" do |app|
|
|
19
|
+
app.routes.prepend do
|
|
20
|
+
post "/deacon/token" => "deacon/rails/tokens#create", as: :deacon_token
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/deacon/rails/helper.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "erb"
|
|
2
2
|
require "json"
|
|
3
|
+
require "securerandom"
|
|
3
4
|
|
|
4
5
|
module Deacon
|
|
5
6
|
module Rails
|
|
@@ -25,18 +26,85 @@ module Deacon
|
|
|
25
26
|
|
|
26
27
|
context = deacon_config.context
|
|
27
28
|
page_views = deacon_config.page_views
|
|
29
|
+
signed_in = deacon_config.signed_in
|
|
28
30
|
deacon_check_context(context)
|
|
29
|
-
|
|
31
|
+
deacon_check_boolean(:page_views, page_views)
|
|
32
|
+
deacon_check_boolean(:signed_in, signed_in)
|
|
30
33
|
|
|
31
34
|
attributes = { "data-key" => key }
|
|
32
35
|
attributes["data-context"] = JSON.generate(context) unless context.nil?
|
|
33
36
|
attributes["data-no-page-views"] = true if page_views == false
|
|
34
37
|
|
|
35
|
-
|
|
38
|
+
csp_nonce = deacon_csp_nonce
|
|
39
|
+
script = signed_in == false ? nil : deacon_signed_in_script(csp_nonce)
|
|
40
|
+
|
|
41
|
+
"#{script}#{deacon_loader_tag(attributes, csp_nonce)}".html_safe
|
|
36
42
|
end
|
|
37
43
|
|
|
38
44
|
private
|
|
39
45
|
|
|
46
|
+
# Who is signed in, for the loader to read as it starts, so it goes above the loader tag.
|
|
47
|
+
# The token function asks the app's own POST /deacon/token to sign them when the chat
|
|
48
|
+
# opens, sending the CSRF token as every other POST in the app does. With nobody signed
|
|
49
|
+
# in it signs the visitor out, so a sign-out ends Deacon's proof of who they were. An app
|
|
50
|
+
# with no way to say who is signed in gets neither.
|
|
51
|
+
def deacon_signed_in_script(csp_nonce)
|
|
52
|
+
user = CurrentUser.find(controller)
|
|
53
|
+
return if user == :unknown
|
|
54
|
+
|
|
55
|
+
code = if user.nil?
|
|
56
|
+
"window.deaconUser = null; window.Deacon?.identify(null);"
|
|
57
|
+
else
|
|
58
|
+
"window.deaconUser = #{deacon_user_object(user)}; " \
|
|
59
|
+
"window.Deacon?.identify(window.deaconUser);"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
deacon_script(code, csp_nonce)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The user's id, email and name, and the token function: written as code rather than
|
|
66
|
+
# JSON, since a function has no JSON. A 204 is nobody signed in. Any other failure throws,
|
|
67
|
+
# so Deacon can tell the founder the route is broken instead of taking it for a sign-out.
|
|
68
|
+
def deacon_user_object(user)
|
|
69
|
+
headers = { "Content-Type" => "application/json" }
|
|
70
|
+
headers["X-CSRF-Token"] = form_authenticity_token if protect_against_forgery?
|
|
71
|
+
path = ::Rails.application.routes.url_helpers.deacon_token_path
|
|
72
|
+
|
|
73
|
+
token = "(nonce) => fetch(#{deacon_js(path)}, { method: \"POST\", " \
|
|
74
|
+
"credentials: \"same-origin\", headers: #{deacon_js_object(headers)}, " \
|
|
75
|
+
"body: JSON.stringify({ nonce }) })" \
|
|
76
|
+
'.then((r) => { if (r.status === 204) return ""; ' \
|
|
77
|
+
'if (!r.ok) throw new Error("Deacon token route answered " + r.status); ' \
|
|
78
|
+
"return r.text(); })"
|
|
79
|
+
|
|
80
|
+
fields = { id: user.id, email: user.email, name: user.name }.compact
|
|
81
|
+
fields = fields.map { |name, value| "#{name}: #{deacon_js(value)}" }
|
|
82
|
+
|
|
83
|
+
"{ #{[*fields, "token: #{token}"].join(', ')} }"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# A string as JavaScript, escaped so nothing in it can end the script it sits in.
|
|
87
|
+
def deacon_js(value)
|
|
88
|
+
ERB::Util.json_escape(JSON.generate(value.to_s))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def deacon_js_object(hash)
|
|
92
|
+
"{ #{hash.map { |name, value| "#{deacon_js(name)}: #{deacon_js(value)}" }.join(', ')} }"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Turbo Drive keeps the head between visits and runs a script in the new page's head only
|
|
96
|
+
# when none just like it is there, so each page's is made unlike the last one's. Otherwise a
|
|
97
|
+
# sign-in or sign-out back to someone the head has seen before would never reach Deacon.
|
|
98
|
+
def deacon_script(code, csp_nonce)
|
|
99
|
+
render = SecureRandom.hex(8)
|
|
100
|
+
|
|
101
|
+
%(<script data-deacon-render="#{render}"#{deacon_nonce_attribute(csp_nonce)}>#{code}</script>)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def deacon_nonce_attribute(csp_nonce)
|
|
105
|
+
csp_nonce.nil? ? "" : %( nonce="#{ERB::Util.html_escape(csp_nonce)}")
|
|
106
|
+
end
|
|
107
|
+
|
|
40
108
|
def deacon_config
|
|
41
109
|
::Rails.application.config.deacon
|
|
42
110
|
end
|
|
@@ -56,10 +124,10 @@ module Deacon
|
|
|
56
124
|
"are strings, numbers or booleans."
|
|
57
125
|
end
|
|
58
126
|
|
|
59
|
-
def
|
|
60
|
-
return if
|
|
127
|
+
def deacon_check_boolean(name, value)
|
|
128
|
+
return if value.nil? || value == true || value == false
|
|
61
129
|
|
|
62
|
-
raise ArgumentError, "deacon-rails: config.deacon
|
|
130
|
+
raise ArgumentError, "deacon-rails: config.deacon.#{name} must be true or false."
|
|
63
131
|
end
|
|
64
132
|
|
|
65
133
|
def deacon_scalar?(value)
|
|
@@ -68,14 +136,13 @@ module Deacon
|
|
|
68
136
|
|
|
69
137
|
# The tag Install gives, with any of its other attributes between the key and async, and
|
|
70
138
|
# the nonce after them. An attribute that is true is written with no value, as async is.
|
|
71
|
-
def deacon_loader_tag(attributes,
|
|
139
|
+
def deacon_loader_tag(attributes, csp_nonce)
|
|
72
140
|
rendered = attributes.map do |name, value|
|
|
73
141
|
value == true ? " #{name}" : %( #{name}="#{ERB::Util.html_escape(value)}")
|
|
74
142
|
end.join
|
|
75
143
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
%(<script src="#{LOADER_SRC}"#{rendered} async#{nonce_attribute}></script>).html_safe
|
|
144
|
+
%(<script src="#{LOADER_SRC}"#{rendered} async#{deacon_nonce_attribute(csp_nonce)}></script>)
|
|
145
|
+
.html_safe
|
|
79
146
|
end
|
|
80
147
|
end
|
|
81
148
|
end
|
data/lib/deacon/rails/version.rb
CHANGED
data/lib/deacon/rails.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: deacon-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Deacon
|
|
@@ -23,6 +23,26 @@ dependencies:
|
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '8.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: jwt
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.8'
|
|
33
|
+
- - "<"
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: '4'
|
|
36
|
+
type: :runtime
|
|
37
|
+
prerelease: false
|
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '2.8'
|
|
43
|
+
- - "<"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '4'
|
|
26
46
|
- !ruby/object:Gem::Dependency
|
|
27
47
|
name: railties
|
|
28
48
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -38,9 +58,10 @@ dependencies:
|
|
|
38
58
|
- !ruby/object:Gem::Version
|
|
39
59
|
version: '8.0'
|
|
40
60
|
description: A view helper, deacon_widget_tag, that puts the Deacon chat widget on
|
|
41
|
-
every page of a Rails app from one line in the layout
|
|
42
|
-
that
|
|
43
|
-
|
|
61
|
+
every page of a Rails app from one line in the layout, and tells Deacon who is signed
|
|
62
|
+
in, with a token route that signs them with your secret so Deacon can trust it.
|
|
63
|
+
Deacon is an AI support agent that answers questions from your own docs and content,
|
|
64
|
+
and says when it doesn't know.
|
|
44
65
|
email:
|
|
45
66
|
- hello@heydeacon.com
|
|
46
67
|
executables: []
|
|
@@ -49,9 +70,11 @@ extra_rdoc_files: []
|
|
|
49
70
|
files:
|
|
50
71
|
- LICENSE
|
|
51
72
|
- README.md
|
|
73
|
+
- app/controllers/deacon/rails/tokens_controller.rb
|
|
52
74
|
- lib/deacon/rails.rb
|
|
75
|
+
- lib/deacon/rails/current_user.rb
|
|
76
|
+
- lib/deacon/rails/engine.rb
|
|
53
77
|
- lib/deacon/rails/helper.rb
|
|
54
|
-
- lib/deacon/rails/railtie.rb
|
|
55
78
|
- lib/deacon/rails/version.rb
|
|
56
79
|
homepage: https://heydeacon.com
|
|
57
80
|
licenses:
|
data/lib/deacon/rails/railtie.rb
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
module Deacon
|
|
2
|
-
module Rails
|
|
3
|
-
# What config.deacon holds. A Struct rather than ordered options, so a misspelt setting
|
|
4
|
-
# fails loudly at boot instead of being ignored.
|
|
5
|
-
Configuration = Struct.new(:key, :context, :page_views)
|
|
6
|
-
|
|
7
|
-
# Adds config.deacon to the app and deacon_widget_tag to its views.
|
|
8
|
-
class Railtie < ::Rails::Railtie
|
|
9
|
-
config.deacon = Configuration.new
|
|
10
|
-
|
|
11
|
-
initializer "deacon.view_helper" do
|
|
12
|
-
ActiveSupport.on_load(:action_view) { include Deacon::Rails::Helper }
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|