Skip to content

Commit e19d703

Browse files
authored
Load @castleio/castle-js UMD from npm and support 2.x and 3.x (#85)
* Load @castleio/castle-js from /vendor/castle-js and support 2.x and 3.x. * Load castle.umd.js and seed window.Castle for the 3.x UMD global. * Drop app_id. Configure with api_secret and publishable_key only. * Document public/vendor copy for castle-js UMD and add local mise pins. * Let castle_javascript_tag take a hosted UMD src instead of serving node_modules. * Load castle.umd.js by default from /vendor/castle-js.
1 parent 9e367e9 commit e19d703

9 files changed

Lines changed: 169 additions & 41 deletions

File tree

.tool-versions

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ruby 3.4.10

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## [Unreleased][main]
44

5+
- Drop `cdn.castle.io`; load `castle.umd.js` from `/vendor/castle-js` via `castle_javascript_tag`
6+
- Seed `window.Castle` before a UMD script so 3.x (`name: @castleio/castle-js`) attaches to the same global
7+
- Support `@castleio/castle-js` 2.x (`injectTokenOnSubmit` / `createRequestToken`) and 3.x (`configure({ pk })` + `createRequestToken`)
8+
- Add `publishable_key` configuration and drop `app_id`
9+
510
## [0.6.0] - 2026-05-25
611
- Add support for Rails 8.1
712
- Add support for Devise 5

README.md

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ Include `castle_devise` in your Gemfile:
2020
gem 'castle_devise'
2121
```
2222

23-
Create `config/initializers/castle_devise.rb` and fill in your API secret and APP_ID from the [Castle Dashboard](https://dashboard.castle.io/settings/general)
23+
Create `config/initializers/castle_devise.rb` and fill in your API secret and publishable key from the [Castle Dashboard](https://dashboard.castle.io/settings/general)
2424

2525
```ruby
2626
CastleDevise.configure do |config|
2727
config.api_secret = ENV.fetch('CASTLE_API_SECRET')
28-
config.app_id = ENV.fetch('CASTLE_APP_ID')
28+
config.publishable_key = ENV.fetch('CASTLE_PK')
2929

3030
# When monitoring mode is enabled, CastleDevise sends
3131
# requests to Castle but it doesn't act on the "deny" verdicts.
@@ -60,14 +60,20 @@ en:
6060
6161
(See [devise.en.yml in our specs](spec/dummy_app/config/locales/devise.en.yml#L40))
6262
63-
#### Further steps if you're not using Webpacker
63+
#### Browser SDK
6464
65-
Include Castle's c.js script in the head section of your layout:
65+
Install [`@castleio/castle-js`](https://docs.castle.io/docs/sdk-browser) and serve the npm `dist` directory at `/vendor/castle-js` (keep `castle.umd.js` and the worker files together):
6666

6767
```ruby
6868
<%= castle_javascript_tag %>
6969
```
7070

71+
That loads `/vendor/castle-js/castle.umd.js` and seeds `window.Castle` before the script runs (3.x UMD is named `@castleio/castle-js`). Pass `src:` for a different UMD URL:
72+
73+
```ruby
74+
<%= castle_javascript_tag(src: "/assets/castle.umd.js") %>
75+
```
76+
7177
Add the following tag to the the `<form>` tag in both `devise/registrations/new.html.erb` and `devise/sessions/new.html.erb` (if you haven't generated them yet, run `rails generate devise:views`):
7278

7379
```ruby
@@ -76,26 +82,9 @@ Add the following tag to the the `<form>` tag in both `devise/registrations/new.
7682
<% end %>
7783
```
7884

79-
You're set! Now verify that everything works by logging in to your application as any user. You should be able to see that User on the [Castle Users Page](https://dashboard.castle.io/users)
85+
`castle_on_form_submit` uses 2.x `injectTokenOnSubmit` when present, and 2.x/3.x `createRequestToken` otherwise.
8086

81-
82-
#### Further steps if you're using Webpacker
83-
84-
Add `@castleio/castle-js` to your package.json file:
85-
86-
```
87-
yarn add @castleio/castle-js
88-
```
89-
90-
configure castle in your application pack:
91-
92-
```javascript
93-
import * as Castle from '@castleio/castle-js'
94-
95-
Castle.configure(YOUR_APPLICATION_ID);
96-
```
97-
98-
for advanced configuration follow [the readme](https://www.npmjs.com/package/@castleio/castle-js#configuration)
87+
You're set! Now verify that everything works by logging in to your application as any user. You should be able to see that User on the [Castle Users Page](https://dashboard.castle.io/users)
9988

10089
## How-Tos
10190

@@ -213,6 +202,7 @@ end
213202
### Setup
214203

215204
```bash
205+
mise install
216206
bundle install
217207
```
218208

@@ -221,9 +211,11 @@ bundle install
221211
Most of the specs should pass just by running the following command:
222212

223213
```bash
224-
bundle exec rake
214+
mise run test
225215
```
226216

217+
or `bundle exec rake`.
218+
227219
We also have a few VCR tests that will periodically rebuild the cassettes just to make sure that the integration with Castle API is working.
228220
For those, you need to run your specs with a proper Castle API Secret:
229221

lib/castle_devise/configuration.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ class Configuration
1212
# @return [String] Your API secret
1313
attr_accessor :api_secret
1414

15-
# @!attribute app_id
16-
# @return [String] Your Castle App ID
17-
attr_accessor :app_id
15+
# @!attribute publishable_key
16+
# @return [String] Publishable key passed to @castleio/castle-js as { pk: }
17+
attr_accessor :publishable_key
1818

1919
# @!attribute monitoring_mode
2020
# When CastleDevise is in monitoring mode, it sends requests to Castle
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
(function () {
2+
var CastleGlobal = window.Castle || window["@castleio/castle-js"];
3+
var pk = __CASTLE_DEVISE_PK__;
4+
if (!window.__castleDevise && CastleGlobal && typeof CastleGlobal.configure === "function") {
5+
window.__castleDevise = CastleGlobal.configure({ pk: pk });
6+
}
7+
8+
window.castleDeviseOnFormSubmit = function (event) {
9+
var client = window.__castleDevise || window.Castle || window["@castleio/castle-js"];
10+
// 2.x exposes injectTokenOnSubmit; 3.x only has createRequestToken on the configured instance.
11+
if (client && typeof client.injectTokenOnSubmit === "function") {
12+
return client.injectTokenOnSubmit(event);
13+
}
14+
if (!client || typeof client.createRequestToken !== "function") {
15+
event.preventDefault();
16+
return false;
17+
}
18+
var form = event.target;
19+
if (form.getAttribute("data-castle-submitting") === "1") {
20+
return true;
21+
}
22+
event.preventDefault();
23+
client.createRequestToken().then(function (token) {
24+
var field = form.querySelector('input[name="castle_request_token"]');
25+
if (!field) {
26+
field = document.createElement("input");
27+
field.type = "hidden";
28+
field.name = "castle_request_token";
29+
form.appendChild(field);
30+
}
31+
field.value = token || "";
32+
form.setAttribute("data-castle-submitting", "1");
33+
if (typeof form.requestSubmit === "function") {
34+
form.requestSubmit();
35+
} else {
36+
form.submit();
37+
}
38+
}).catch(function () {
39+
form.setAttribute("data-castle-submitting", "1");
40+
if (typeof form.requestSubmit === "function") {
41+
form.requestSubmit();
42+
} else {
43+
form.submit();
44+
}
45+
});
46+
return false;
47+
};
48+
})();

lib/castle_devise/helpers/castle_helper.rb

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,23 @@ module CastleDevise
44
module Helpers
55
# Methods defined here will be available in all your views.
66
module CastleHelper
7-
# Creates a <script> tag that includes our c.js script from a CDN.
8-
# You have to make sure that your app_id is valid, otherwise the script won't work.
9-
#
10-
# You shouldn't call this method if you bundle our c.js script with your other
11-
# JS packages.
7+
BOOTSTRAP_JS = File.read(File.expand_path("castle_devise.js", __dir__)).freeze
8+
# 3.x UMD is named @castleio/castle-js, so seed module.exports as window.Castle before the script tag.
9+
UMD_SHIM = <<~JS
10+
if (!window.Castle) {
11+
window.exports = window.exports || {};
12+
window.module = window.module || { exports: window.exports };
13+
window.Castle = window.module.exports;
14+
}
15+
JS
16+
17+
DEFAULT_UMD_SRC = "/vendor/castle-js/castle.umd.js"
18+
19+
# Loads castle.umd.js and configures it with { pk: }. Pass src: to use a
20+
# different UMD URL, or src: nil when the host app already loaded the SDK.
1221
#
13-
# You should put this in the <head> section of your page:
22+
# @param src [String, nil] URL or path of castle.umd.js (keep workers in the same directory)
23+
# @return [String]
1424
#
1525
# @example
1626
# # app/views/layouts/application.html.erb
@@ -21,14 +31,18 @@ module CastleHelper
2131
# <title>Your app title</title>
2232
#
2333
# <!-- the rest of your layout -->
24-
def castle_javascript_tag
25-
javascript_include_tag(
26-
"https://cdn.castle.io/v2/castle.js?#{CastleDevise.configuration.app_id}"
27-
)
34+
def castle_javascript_tag(src: DEFAULT_UMD_SRC)
35+
parts = []
36+
if src
37+
parts << javascript_tag(UMD_SHIM)
38+
parts << javascript_include_tag(src)
39+
end
40+
parts << javascript_tag(castle_js_bootstrap)
41+
safe_join(parts)
2842
end
2943

30-
# Puts an inline <script> tag that includes a "castle_devise_token" field
31-
# within the current form.
44+
# onsubmit handler that mints castle_request_token via 2.x injectTokenOnSubmit
45+
# or 2.x/3.x createRequestToken.
3246
#
3347
# @example
3448
# <%= form_for(resource, as: resource_name, url: sessions_path(resource_name), html: { onsubmit: castle_on_form_submit }) do |f| %>
@@ -38,7 +52,13 @@ def castle_javascript_tag
3852
#
3953
# @return [String]
4054
def castle_on_form_submit
41-
"typeof(_castle)=='undefined'?event.preventDefault():_castle('onFormSubmit', event)"
55+
"castleDeviseOnFormSubmit(event)"
56+
end
57+
58+
private
59+
60+
def castle_js_bootstrap
61+
BOOTSTRAP_JS.sub("__CASTLE_DEVISE_PK__", CastleDevise.configuration.publishable_key.to_json)
4262
end
4363
end
4464
end

mise.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[tasks.install]
2+
description = "Install Ruby gems"
3+
run = "bundle install"
4+
5+
[tasks.test]
6+
description = "Run the RSpec suite"
7+
run = "bundle exec rspec"
8+
9+
[tasks.lint]
10+
description = "Run Standard Ruby"
11+
run = "bundle exec standardrb"
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe CastleDevise::Helpers::CastleHelper, type: :helper do
4+
describe "#castle_javascript_tag" do
5+
before do
6+
CastleDevise.configuration.publishable_key = "pk_test"
7+
end
8+
9+
after do
10+
CastleDevise.configuration.publishable_key = "pk_spec"
11+
end
12+
13+
it "loads castle.umd.js from /vendor/castle-js" do
14+
html = helper.castle_javascript_tag
15+
16+
expect(html).to include("/vendor/castle-js/castle.umd.js")
17+
expect(html).to include("window.module")
18+
expect(html).to include("window.Castle")
19+
expect(html).to include("@castleio/castle-js")
20+
expect(html).to include(".configure")
21+
expect(html).to include("pk_test")
22+
expect(html).to include("castleDeviseOnFormSubmit")
23+
expect(html).not_to include("cdn.castle.io")
24+
expect(html).not_to include("castle.browser.js")
25+
end
26+
27+
it "loads a custom UMD src when given" do
28+
html = helper.castle_javascript_tag(src: "/assets/castle.umd.js")
29+
30+
expect(html).to include("/assets/castle.umd.js")
31+
expect(html).not_to include("/vendor/castle-js/castle.umd.js")
32+
expect(html).to include("window.module")
33+
expect(html).not_to include("cdn.castle.io")
34+
end
35+
36+
it "skips the script tag when src is nil" do
37+
html = helper.castle_javascript_tag(src: nil)
38+
39+
expect(html).to include(".configure")
40+
expect(html).to include("pk_test")
41+
expect(html).not_to include("castle.umd.js")
42+
expect(html).not_to include("cdn.castle.io")
43+
end
44+
end
45+
46+
describe "#castle_on_form_submit" do
47+
it "delegates to the shared submit helper" do
48+
expect(helper.castle_on_form_submit).to eq("castleDeviseOnFormSubmit(event)")
49+
end
50+
end
51+
end

spec/support/init_castle_device.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@
66
# no real secret is needed, so a fixed dummy value keeps the specs valid
77
# everywhere (including Dependabot runs that don't get repo secrets).
88
config.api_secret = "fake-secret-123"
9-
config.app_id = "123456789"
9+
config.publishable_key = "pk_spec"
1010
config.logger = Logger.new($stdout)
1111
end

0 commit comments

Comments
 (0)