siwe-rb 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1344345a5daec54f2ab4b8e0da7dd6476561afd6669f23f4a85a8bdcdb2c751c
4
+ data.tar.gz: ba59b351b355dc16dffdd08ca99214f895acdaa933a492f746d4916634887b51
5
+ SHA512:
6
+ metadata.gz: 0a3d065c0830b39ceb0d4b36650671cb7e92ad48907caa305219956d394c155f53fc322c91052b1609667ec6fefcadab23203449f311631d924977fbe525aab2
7
+ data.tar.gz: 47e4a640616be6de51ca67b78cf90650bbbc043ff9b3468021ae749fd0c5b403c7bc7d94b406dfc8b30735f869c7455898aee01fb2d251c5dfabed0e0849b7dc
data/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] — 2026-05-04
4
+
5
+ Initial release of `siwe-rb`. Hard fork of the abandoned `siwe` gem (last published 0.1.0, 2021).
6
+
7
+ - Modern Ruby (≥ 3.2), no Ruby 2.x cruft.
8
+ - ABNF-aligned EIP-4361 parser; passes the shared `@signinwithethereum/test-vectors` suite (parsing, grammar, objects, verification).
9
+ - 17-character alphanumeric nonce, matching the TypeScript reference (`Siwe.generate_nonce`).
10
+ - Optional `scheme` field for `https://`-prefixed messages (ERC-4361 erratum).
11
+ - Single structured `Siwe::Error` with 27 typed error codes (`Siwe::ErrorType`) and `expected:` / `received:` context, replacing the legacy one-class-per-error layout.
12
+ - `Siwe::Response` value object returned by `Message#verify`; `Message#verify!` raises on failure for the common Rails-controller path.
13
+ - ERC-1271 + EIP-6492 smart-wallet support via the off-chain universal validator (single `eth_call` covers deployed wallets like Safe and counterfactual wallets like Coinbase Smart Wallet).
14
+ - Built-in `Siwe::Rpc::HttpClient` (Net::HTTP-based JSON-RPC) plus a duck-typed plug-in surface — drop in `web3.rb`, `eth-rpc`, or any object responding to `eth_call(to:, data:, block:)`.
15
+ - Pluggable `Siwe::Adapter` (defaults to the `eth` gem) for crypto / signature recovery.
16
+ - Modern toolchain: RuboCop, RSpec, WebMock, GitHub Actions matrix on Ruby 3.2 / 3.3 / 3.4, optional gated live-RPC integration job.
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ group :development, :test do
8
+ gem "rake", "~> 13.0"
9
+ gem "rspec", "~> 3.13"
10
+ gem "rubocop", "~> 1.65"
11
+ gem "simplecov", "~> 0.22"
12
+ gem "webmock", "~> 3.25"
13
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,211 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ siwe-rb (0.1.0)
5
+ eth (>= 0.5.11, < 1.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ addressable (2.9.0)
11
+ public_suffix (>= 2.0.2, < 8.0)
12
+ ast (2.4.3)
13
+ base64 (0.3.0)
14
+ bigdecimal (3.3.1)
15
+ bls12-381 (0.3.1)
16
+ bigdecimal
17
+ h2c (>= 0.2.1, < 0.3)
18
+ crack (1.0.1)
19
+ bigdecimal
20
+ rexml
21
+ diff-lcs (1.6.2)
22
+ docile (1.4.1)
23
+ ecdsa (1.2.0)
24
+ eth (0.5.17)
25
+ base64 (~> 0.1)
26
+ bigdecimal (~> 3.1)
27
+ bls12-381 (~> 0.3)
28
+ forwardable (~> 1.3)
29
+ httpx (~> 1.6)
30
+ keccak (~> 1.3)
31
+ konstructor (~> 1.0)
32
+ openssl (~> 3.3)
33
+ rbsecp256k1 (~> 6.0)
34
+ scrypt (~> 3.0)
35
+ ffi (1.17.4)
36
+ ffi (1.17.4-aarch64-linux-gnu)
37
+ ffi (1.17.4-aarch64-linux-musl)
38
+ ffi (1.17.4-arm-linux-gnu)
39
+ ffi (1.17.4-arm-linux-musl)
40
+ ffi (1.17.4-arm64-darwin)
41
+ ffi (1.17.4-x86-linux-gnu)
42
+ ffi (1.17.4-x86-linux-musl)
43
+ ffi (1.17.4-x86_64-darwin)
44
+ ffi (1.17.4-x86_64-linux-gnu)
45
+ ffi (1.17.4-x86_64-linux-musl)
46
+ ffi-compiler (1.4.2)
47
+ ffi (>= 1.15.5)
48
+ rake
49
+ forwardable (1.4.0)
50
+ h2c (0.2.1)
51
+ ecdsa (~> 1.2.0)
52
+ hashdiff (1.2.1)
53
+ http-2 (1.1.3)
54
+ httpx (1.7.6)
55
+ http-2 (>= 1.1.3)
56
+ json (2.19.5)
57
+ keccak (1.3.3)
58
+ konstructor (1.0.2)
59
+ language_server-protocol (3.17.0.5)
60
+ lint_roller (1.1.0)
61
+ mini_portile2 (2.8.9)
62
+ openssl (3.3.2)
63
+ parallel (2.1.0)
64
+ parser (3.3.11.1)
65
+ ast (~> 2.4.1)
66
+ racc
67
+ pkg-config (1.6.5)
68
+ prism (1.9.0)
69
+ public_suffix (7.0.5)
70
+ racc (1.8.1)
71
+ rainbow (3.1.1)
72
+ rake (13.4.2)
73
+ rbsecp256k1 (6.0.0)
74
+ mini_portile2 (~> 2.8)
75
+ pkg-config (~> 1.5)
76
+ rubyzip (~> 2.3)
77
+ regexp_parser (2.12.0)
78
+ rexml (3.4.4)
79
+ rspec (3.13.2)
80
+ rspec-core (~> 3.13.0)
81
+ rspec-expectations (~> 3.13.0)
82
+ rspec-mocks (~> 3.13.0)
83
+ rspec-core (3.13.6)
84
+ rspec-support (~> 3.13.0)
85
+ rspec-expectations (3.13.5)
86
+ diff-lcs (>= 1.2.0, < 2.0)
87
+ rspec-support (~> 3.13.0)
88
+ rspec-mocks (3.13.8)
89
+ diff-lcs (>= 1.2.0, < 2.0)
90
+ rspec-support (~> 3.13.0)
91
+ rspec-support (3.13.7)
92
+ rubocop (1.86.1)
93
+ json (~> 2.3)
94
+ language_server-protocol (~> 3.17.0.2)
95
+ lint_roller (~> 1.1.0)
96
+ parallel (>= 1.10)
97
+ parser (>= 3.3.0.2)
98
+ rainbow (>= 2.2.2, < 4.0)
99
+ regexp_parser (>= 2.9.3, < 3.0)
100
+ rubocop-ast (>= 1.49.0, < 2.0)
101
+ ruby-progressbar (~> 1.7)
102
+ unicode-display_width (>= 2.4.0, < 4.0)
103
+ rubocop-ast (1.49.1)
104
+ parser (>= 3.3.7.2)
105
+ prism (~> 1.7)
106
+ ruby-progressbar (1.13.0)
107
+ rubyzip (2.4.1)
108
+ scrypt (3.1.0)
109
+ ffi-compiler (>= 1.0, < 2.0)
110
+ rake (~> 13)
111
+ simplecov (0.22.0)
112
+ docile (~> 1.1)
113
+ simplecov-html (~> 0.11)
114
+ simplecov_json_formatter (~> 0.1)
115
+ simplecov-html (0.13.2)
116
+ simplecov_json_formatter (0.1.4)
117
+ unicode-display_width (3.2.0)
118
+ unicode-emoji (~> 4.1)
119
+ unicode-emoji (4.2.0)
120
+ webmock (3.26.2)
121
+ addressable (>= 2.8.0)
122
+ crack (>= 0.3.2)
123
+ hashdiff (>= 0.4.0, < 2.0.0)
124
+
125
+ PLATFORMS
126
+ aarch64-linux-gnu
127
+ aarch64-linux-musl
128
+ arm-linux-gnu
129
+ arm-linux-musl
130
+ arm64-darwin
131
+ ruby
132
+ x86-linux-gnu
133
+ x86-linux-musl
134
+ x86_64-darwin
135
+ x86_64-linux-gnu
136
+ x86_64-linux-musl
137
+
138
+ DEPENDENCIES
139
+ rake (~> 13.0)
140
+ rspec (~> 3.13)
141
+ rubocop (~> 1.65)
142
+ simplecov (~> 0.22)
143
+ siwe-rb!
144
+ webmock (~> 3.25)
145
+
146
+ CHECKSUMS
147
+ addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af
148
+ ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
149
+ base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
150
+ bigdecimal (3.3.1) sha256=eaa01e228be54c4f9f53bf3cc34fe3d5e845c31963e7fcc5bedb05a4e7d52218
151
+ bls12-381 (0.3.1) sha256=d1525cd319c53f14178d54f355dfe1b4c572fbf5625965c1afccdc425b9896dc
152
+ crack (1.0.1) sha256=ff4a10390cd31d66440b7524eb1841874db86201d5b70032028553130b6d4c7e
153
+ diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
154
+ docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
155
+ ecdsa (1.2.0) sha256=b8f7c9541b6c587cbe37e705a1b76be6dc1e85336aa23ac3cf2f07b038bcec55
156
+ eth (0.5.17) sha256=5be9caa85b0643613010ae33c92e746a02ce376a35245e6eb5f491e80531f137
157
+ ffi (1.17.4) sha256=bcd1642e06f0d16fc9e09ac6d49c3a7298b9789bcb58127302f934e437d60acf
158
+ ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df
159
+ ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39
160
+ ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564
161
+ ffi (1.17.4-arm-linux-musl) sha256=9d4838ded0465bef6e2426935f6bcc93134b6616785a84ffd2a3d82bc3cf6f95
162
+ ffi (1.17.4-arm64-darwin) sha256=19071aaf1419251b0a46852abf960e77330a3b334d13a4ab51d58b31a937001b
163
+ ffi (1.17.4-x86-linux-gnu) sha256=38e150df5f4ca555e25beca4090823ae09657bceded154e3c52f8631c1ed72cf
164
+ ffi (1.17.4-x86-linux-musl) sha256=fbeec0fc7c795bcf86f623bb18d31ea1820f7bd580e1703a3d3740d527437809
165
+ ffi (1.17.4-x86_64-darwin) sha256=aa70390523cf3235096cf64962b709b4cfbd5c082a2cb2ae714eb0fe2ccda496
166
+ ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d
167
+ ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e
168
+ ffi-compiler (1.4.2) sha256=a9d69d5d9ced6f64776ddafa535867ad90e5740ad37bed7afb4de6e450da126e
169
+ forwardable (1.4.0) sha256=f1cd40cc9812937980e1c76f1aa053660990a7c9b6a98fc37d945468afcce838
170
+ h2c (0.2.1) sha256=a53b7dfeb95660c97b615235ea1b29ec8e1856edc571845685682d94b6e8e227
171
+ hashdiff (1.2.1) sha256=9c079dbc513dfc8833ab59c0c2d8f230fa28499cc5efb4b8dd276cf931457cd1
172
+ http-2 (1.1.3) sha256=1b2f379d35a11dbae94f8a1a52c053d8c161eb4a0c98b5d1605ff1b2bf171c9c
173
+ httpx (1.7.6) sha256=82d825abc9876a132adc3492c56a0c528478ac238dd6f74d3422ab0036c6b5c8
174
+ json (2.19.5) sha256=218a18553e4801d579ca7e0f5bc72bafd776d7397238a1fb4e74db5b0a812c59
175
+ keccak (1.3.3) sha256=970dcb1e78b8c3129ba2baff8a5baa5cebf391136db1f4018e8ccc9130794557
176
+ konstructor (1.0.2) sha256=fd6ac9eb1dadc1e520e06042aa2ef0122d6a070e06cde86db5a54c0c7bfe2e31
177
+ language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
178
+ lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
179
+ mini_portile2 (2.8.9) sha256=0cd7c7f824e010c072e33f68bc02d85a00aeb6fce05bb4819c03dfd3c140c289
180
+ openssl (3.3.2) sha256=7f4e01215dc9c4be1fca71d692406be3e6340b39c1f71a47fea9c497decd0f6c
181
+ parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356
182
+ parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54
183
+ pkg-config (1.6.5) sha256=33f9f81c5322983d22b439b8b672f27777b406fea23bfec74ff14bbeb42ec733
184
+ prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
185
+ public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623
186
+ racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
187
+ rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
188
+ rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
189
+ rbsecp256k1 (6.0.0) sha256=d38f563bfc6bcf3ce20c336a4798cc6990427f291e74c6817360363a72f11669
190
+ regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
191
+ rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
192
+ rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
193
+ rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
194
+ rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
195
+ rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
196
+ rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
197
+ rubocop (1.86.1) sha256=44415f3f01d01a21e01132248d2fd0867572475b566ca188a0a42133a08d4531
198
+ rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035
199
+ ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
200
+ rubyzip (2.4.1) sha256=8577c88edc1fde8935eb91064c5cb1aef9ad5494b940cf19c775ee833e075615
201
+ scrypt (3.1.0) sha256=67fde35bc7e3b7fe906c5be9c242acf95fe4a886c89572f405f6b11df30aa6af
202
+ simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5
203
+ simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246
204
+ simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428
205
+ siwe-rb (0.1.0)
206
+ unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
207
+ unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
208
+ webmock (3.26.2) sha256=774556f2ea6371846cca68c01769b2eac0d134492d21f6d0ab5dd643965a4c90
209
+
210
+ BUNDLED WITH
211
+ 4.0.3
data/LICENSE-MIT ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 EthID.org
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,142 @@
1
+ # siwe-rb
2
+
3
+ Sign-In with Ethereum (EIP-4361) for Ruby — message construction, parsing, and signature verification, with built-in support for ERC-1271 and EIP-6492 smart contract wallets.
4
+
5
+ `siwe-rb` is the Ruby companion to the [TypeScript](https://github.com/signinwithethereum/siwe), [Python](https://github.com/signinwithethereum/siwe-py), and [Rust](https://github.com/signinwithethereum/siwe-rs) implementations under the [@signinwithethereum](https://github.com/signinwithethereum) organisation. It runs against the same shared test-vector suite.
6
+
7
+ ## Installation
8
+
9
+ ```ruby
10
+ # Gemfile
11
+ gem "siwe-rb", "~> 0.1"
12
+ ```
13
+
14
+ ```ruby
15
+ require "siwe"
16
+ ```
17
+
18
+ Requires Ruby ≥ 3.2.
19
+
20
+ ## Usage
21
+
22
+ ### Construct and sign a message
23
+
24
+ ```ruby
25
+ require "siwe"
26
+ require "eth"
27
+
28
+ key = Eth::Key.new
29
+
30
+ message = Siwe::Message.new(
31
+ domain: "example.com",
32
+ address: key.address.to_s,
33
+ uri: "https://example.com/login",
34
+ chain_id: 1,
35
+ nonce: Siwe.generate_nonce,
36
+ issued_at: Time.now.utc.iso8601,
37
+ statement: "Sign in to example.com"
38
+ )
39
+
40
+ text = message.prepare_message # → EIP-4361 message string
41
+ signature = key.personal_sign(text)
42
+ ```
43
+
44
+ ### Parse a message
45
+
46
+ ```ruby
47
+ message = Siwe::Message.parse(text)
48
+ message.domain # => "example.com"
49
+ message.address # => "0x..."
50
+ message.warnings # => [] (e.g. ["address is not EIP-55 checksummed - 0x…"])
51
+ ```
52
+
53
+ ### Verify a signature
54
+
55
+ `verify` returns a `Siwe::Response`; `verify!` raises a `Siwe::Error` on failure.
56
+
57
+ ```ruby
58
+ response = message.verify(
59
+ signature: signature,
60
+ domain: "example.com",
61
+ nonce: message.nonce
62
+ )
63
+
64
+ if response.success?
65
+ # signed in
66
+ else
67
+ Rails.logger.warn("siwe failed: #{response.error.type}")
68
+ end
69
+
70
+ # or, idiomatic Ruby:
71
+ message.verify!(signature: signature, domain: "example.com", nonce: message.nonce)
72
+ ```
73
+
74
+ ### Smart-wallet support (ERC-1271, EIP-6492)
75
+
76
+ Configure an Ethereum RPC URL once at boot, and `verify` will automatically fall through to a single deploy-and-call against the EIP-6492 universal validator when EOA recovery fails. This handles both deployed ERC-1271 wallets (e.g. Safe) and counterfactual EIP-6492-wrapped signatures (e.g. Coinbase Smart Wallet) in one call.
77
+
78
+ ```ruby
79
+ Siwe.configure do |c|
80
+ c.rpc_url = ENV["ETH_RPC_URL"] # e.g. https://ethereum-rpc.publicnode.com
81
+ end
82
+
83
+ message.verify!(signature: sig, domain: "example.com", nonce: message.nonce)
84
+ ```
85
+
86
+ You can also pass an RPC client per call, or inject your own client (anything responding to `eth_call(to:, data:, block:)`):
87
+
88
+ ```ruby
89
+ custom_rpc = MyOwnRpcClient.new(...)
90
+ config = Siwe::Config.new(rpc: custom_rpc)
91
+ message.verify!(signature: sig, domain: domain, nonce: nonce, config: config)
92
+ ```
93
+
94
+ ### Error handling
95
+
96
+ All failures raise (or, for `verify`, surface as `response.error`) a single `Siwe::Error` carrying a `type` symbol from `Siwe::ErrorType`:
97
+
98
+ ```ruby
99
+ begin
100
+ message.verify!(signature: sig, domain: domain, nonce: nonce)
101
+ rescue Siwe::Error => e
102
+ case e.type
103
+ when :expired_message then render_expired
104
+ when :nonce_mismatch then render_replay
105
+ when :invalid_signature then render_unauthorized
106
+ when :rpc_error then retry_or_fail
107
+ else render_generic_error
108
+ end
109
+ end
110
+ ```
111
+
112
+ The full set of error types mirrors `SiweErrorType` in the TypeScript reference (27 codes including the Ruby-specific `:rpc_error`). See `lib/siwe/error_type.rb`.
113
+
114
+ ## Comparison with the TypeScript implementation
115
+
116
+ | Feature | TS | Ruby |
117
+ | ---------------------------------- | ------------- | -------------- |
118
+ | EIP-4361 v1 parsing & rendering | ✓ | ✓ |
119
+ | Optional `scheme` field | ✓ | ✓ |
120
+ | EIP-55 checksum + warning | ✓ | ✓ |
121
+ | 17-char alphanumeric nonce | ✓ | ✓ |
122
+ | `verify` / response object | Promise | sync `Response`|
123
+ | EOA verification | ✓ | ✓ |
124
+ | ERC-1271 verification | ✓ (via viem) | ✓ (built-in) |
125
+ | EIP-6492 verification | ✓ (via viem) | ✓ (built-in) |
126
+ | Pluggable provider | viem / ethers | duck-typed RPC |
127
+ | Shared test-vector suite | ✓ | ✓ |
128
+
129
+ ## Development
130
+
131
+ ```bash
132
+ git submodule update --init # pulls in the shared test-vectors repo
133
+ bundle install
134
+ bundle exec rake # runs rspec + rubocop
135
+
136
+ # Live RPC integration tests (Argent, Loopring, EIP-6492 universal validator):
137
+ SIWE_RPC_URL=https://ethereum-rpc.publicnode.com bundle exec rspec --tag live_rpc
138
+ ```
139
+
140
+ ## License
141
+
142
+ MIT or Apache-2.0, at your option.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "eth"
4
+
5
+ require_relative "error"
6
+ require_relative "error_type"
7
+
8
+ module Siwe
9
+ module Adapter
10
+ # Default crypto adapter using the eth gem.
11
+ # Implements the SiweConfig interface from the TS reference implementation:
12
+ # verify_message → recover signer address from EIP-191 signed message
13
+ # hash_message → EIP-191 personal_sign hash
14
+ # get_address → normalize to EIP-55 checksum address
15
+ class EthGem
16
+ def verify_message(message, signature)
17
+ public_key = Eth::Signature.personal_recover(message, signature)
18
+ Eth::Util.public_key_to_address(public_key).to_s
19
+ end
20
+
21
+ def hash_message(message)
22
+ prefixed = Eth::Signature.prefix_message(message)
23
+ Eth::Util.keccak256(prefixed)
24
+ end
25
+
26
+ def get_address(addr)
27
+ Eth::Address.new(addr).to_s
28
+ end
29
+ end
30
+
31
+ DEFAULT = EthGem.new
32
+ end
33
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "adapter"
4
+
5
+ module Siwe
6
+ # Verification config — pluggable adapter (crypto), RPC URL or RPC client (smart wallet),
7
+ # and optional verification fallback.
8
+ #
9
+ # Set globally via Siwe.configure { |c| ... } or per-call as `verify(config: ...)`.
10
+ class Config
11
+ attr_reader :rpc_url, :rpc, :adapter, :verification_fallback
12
+
13
+ def initialize(rpc_url: nil, rpc: nil, adapter: nil, verification_fallback: nil)
14
+ @rpc_url = rpc_url
15
+ @rpc = rpc
16
+ @adapter = adapter || Siwe::Adapter::DEFAULT
17
+ @verification_fallback = verification_fallback
18
+ end
19
+
20
+ def to_h
21
+ {
22
+ rpc_url: @rpc_url,
23
+ rpc: @rpc,
24
+ adapter: @adapter,
25
+ verification_fallback: @verification_fallback
26
+ }
27
+ end
28
+
29
+ # Mutable struct used inside Siwe.configure { |c| c.rpc_url = ... }.
30
+ # Caller mutates fields, then build returns a frozen Config.
31
+ class Builder
32
+ attr_accessor :rpc_url, :rpc, :adapter, :verification_fallback
33
+
34
+ def initialize(rpc_url: nil, rpc: nil, adapter: nil, verification_fallback: nil)
35
+ @rpc_url = rpc_url
36
+ @rpc = rpc
37
+ @adapter = adapter
38
+ @verification_fallback = verification_fallback
39
+ end
40
+
41
+ def build
42
+ Config.new(
43
+ rpc_url: @rpc_url,
44
+ rpc: @rpc,
45
+ adapter: @adapter,
46
+ verification_fallback: @verification_fallback
47
+ )
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Siwe
4
+ # EIP-6492 — Signatures for Predeploy Contracts.
5
+ #
6
+ # Wraps an inner ERC-1271 signature with a deploy-and-call shim so that signers
7
+ # whose contract has not yet been deployed can still produce verifiable signatures.
8
+ # See https://eips.ethereum.org/EIPS/eip-6492.
9
+ module Eip6492
10
+ # 32-byte magic suffix appended to EIP-6492 wrapped signatures.
11
+ MAGIC_SUFFIX = "6492649264926492649264926492649264926492649264926492649264926492"
12
+
13
+ # ERC-1271 magic value returned by `isValidSignature(bytes32,bytes)`.
14
+ EIP1271_MAGIC_VALUE = "1626ba7e"
15
+
16
+ # Off-chain universal signature validator. Deployed via eth_call (no actual
17
+ # deployment) to verify EOA, ERC-1271, and EIP-6492 signatures in one call.
18
+ #
19
+ # Constructor: (address signer, bytes32 hash, bytes signature)
20
+ # Returns: 0x01 if valid, 0x00 if invalid.
21
+ #
22
+ # Source: viem `erc6492SignatureValidatorByteCode`. Mirror of
23
+ # libs/ts/packages/siwe/lib/eip6492.ts:16. Without the 0x prefix.
24
+ # rubocop:disable Layout/LineLength
25
+ VALIDATOR_BYTECODE = "608060405234801561001057600080fd5b5060405161069438038061069483398101604081905261002f9161051e565b600061003c848484610048565b9050806000526001601ff35b60007f64926492649264926492649264926492649264926492649264926492649264926100748361040c565b036101e7576000606080848060200190518101906100929190610577565b60405192955090935091506000906001600160a01b038516906100b69085906105dd565b6000604051808303816000865af19150503d80600081146100f3576040519150601f19603f3d011682016040523d82523d6000602084013e6100f8565b606091505b50509050876001600160a01b03163b60000361016057806101605760405162461bcd60e51b815260206004820152601e60248201527f5369676e617475726556616c696461746f723a206465706c6f796d656e74000060448201526064015b60405180910390fd5b604051630b135d3f60e11b808252906001600160a01b038a1690631626ba7e90610190908b9087906004016105f9565b602060405180830381865afa1580156101ad573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101d19190610633565b6001600160e01b03191614945050505050610405565b6001600160a01b0384163b1561027a57604051630b135d3f60e11b808252906001600160a01b03861690631626ba7e9061022790879087906004016105f9565b602060405180830381865afa158015610244573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102689190610633565b6001600160e01b031916149050610405565b81516041146102df5760405162461bcd60e51b815260206004820152603a602482015260008051602061067483398151915260448201527f3a20696e76616c6964207369676e6174757265206c656e6774680000000000006064820152608401610157565b6102e7610425565b5060208201516040808401518451859392600091859190811061030c5761030c61065d565b016020015160f81c9050601b811480159061032b57508060ff16601c14155b1561038c5760405162461bcd60e51b815260206004820152603b602482015260008051602061067483398151915260448201527f3a20696e76616c6964207369676e617475726520762076616c756500000000006064820152608401610157565b60408051600081526020810180835289905260ff83169181019190915260608101849052608081018390526001600160a01b0389169060019060a0016020604051602081039080840390855afa1580156103ea573d6000803e3d6000fd5b505050602060405103516001600160a01b0316149450505050505b9392505050565b600060208251101561041d57600080fd5b508051015190565b60405180606001604052806003906020820280368337509192915050565b6001600160a01b038116811461045857600080fd5b50565b634e487b7160e01b600052604160045260246000fd5b60005b8381101561048c578181015183820152602001610474565b50506000910152565b600082601f8301126104a657600080fd5b81516001600160401b038111156104bf576104bf61045b565b604051601f8201601f19908116603f011681016001600160401b03811182821017156104ed576104ed61045b565b60405281815283820160200185101561050557600080fd5b610516826020830160208701610471565b949350505050565b60008060006060848603121561053357600080fd5b835161053e81610443565b6020850151604086015191945092506001600160401b0381111561056157600080fd5b61056d86828701610495565b9150509250925092565b60008060006060848603121561058c57600080fd5b835161059781610443565b60208501519093506001600160401b038111156105b357600080fd5b6105bf86828701610495565b604086015190935090506001600160401b0381111561056157600080fd5b600082516105ef818460208701610471565b9190910192915050565b828152604060208201526000825180604084015261061e816060850160208701610471565b601f01601f1916919091016060019392505050565b60006020828403121561064557600080fd5b81516001600160e01b03198116811461040557600080fd5b634e487b7160e01b600052603260045260246000fdfe5369676e617475726556616c696461746f72237265636f7665725369676e6572"
26
+ # rubocop:enable Layout/LineLength
27
+
28
+ # Predicate: does this signature carry the EIP-6492 magic suffix?
29
+ def self.signature?(hex)
30
+ return false if hex.nil?
31
+
32
+ stripped = hex.start_with?("0x") ? hex[2..] : hex
33
+ return false if stripped.length < MAGIC_SUFFIX.length
34
+
35
+ stripped.end_with?(MAGIC_SUFFIX)
36
+ end
37
+ end
38
+ end
data/lib/siwe/error.rb ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "error_type"
4
+
5
+ module Siwe
6
+ class Error < StandardError
7
+ attr_reader :type, :expected, :received
8
+
9
+ def initialize(type, expected: nil, received: nil, message: nil)
10
+ raise ArgumentError, "unknown SIWE error type: #{type.inspect}" unless ErrorType::MESSAGES.key?(type)
11
+
12
+ @type = type
13
+ @expected = expected
14
+ @received = received
15
+ super(message || ErrorType::MESSAGES.fetch(type))
16
+ end
17
+
18
+ def to_h
19
+ { type: @type, expected: @expected, received: @received, message: message }
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Siwe
4
+ module ErrorType
5
+ EXPIRED_MESSAGE = :expired_message
6
+ INVALID_DOMAIN = :invalid_domain
7
+ SCHEME_MISMATCH = :scheme_mismatch
8
+ DOMAIN_MISMATCH = :domain_mismatch
9
+ NONCE_MISMATCH = :nonce_mismatch
10
+ URI_MISMATCH = :uri_mismatch
11
+ CHAIN_ID_MISMATCH = :chain_id_mismatch
12
+ REQUEST_ID_MISMATCH = :request_id_mismatch
13
+ INVALID_ADDRESS = :invalid_address
14
+ INVALID_URI = :invalid_uri
15
+ INVALID_NONCE = :invalid_nonce
16
+ NOT_YET_VALID_MESSAGE = :not_yet_valid_message
17
+ INVALID_SIGNATURE = :invalid_signature
18
+ INVALID_SIGNATURE_CHAIN_ID = :invalid_signature_chain_id
19
+ INVALID_TIME_FORMAT = :invalid_time_format
20
+ INVALID_MESSAGE_VERSION = :invalid_message_version
21
+ UNABLE_TO_PARSE = :unable_to_parse
22
+ MISSING_DOMAIN = :missing_domain
23
+ MISSING_NONCE = :missing_nonce
24
+ MISSING_URI = :missing_uri
25
+ MISSING_CHAIN_ID = :missing_chain_id
26
+ MISSING_CONFIG = :missing_config
27
+ MISSING_PROVIDER_LIBRARY = :missing_provider_library
28
+ NONCE_GENERATION_FAILED = :nonce_generation_failed
29
+ INVALID_PARAMS = :invalid_params
30
+ MALFORMED_MESSAGE = :malformed_message
31
+ RPC_ERROR = :rpc_error
32
+
33
+ MESSAGES = {
34
+ EXPIRED_MESSAGE => "Expired message.",
35
+ INVALID_DOMAIN => "Invalid domain.",
36
+ SCHEME_MISMATCH => "Scheme does not match provided scheme for verification.",
37
+ DOMAIN_MISMATCH => "Domain does not match provided domain for verification.",
38
+ NONCE_MISMATCH => "Nonce does not match provided nonce for verification.",
39
+ URI_MISMATCH => "URI does not match provided URI for verification.",
40
+ CHAIN_ID_MISMATCH => "Chain ID does not match provided chain ID for verification.",
41
+ REQUEST_ID_MISMATCH => "Request ID does not match provided request ID for verification.",
42
+ INVALID_ADDRESS => "Invalid address.",
43
+ INVALID_URI => "URI does not conform to RFC 3986.",
44
+ INVALID_NONCE => "Nonce size smaller than 8 characters or is not alphanumeric.",
45
+ NOT_YET_VALID_MESSAGE => "Message is not valid yet.",
46
+ INVALID_SIGNATURE => "Signature does not match address of the message.",
47
+ INVALID_SIGNATURE_CHAIN_ID => "Contract wallet verification provider chain does not match message chain ID.",
48
+ INVALID_TIME_FORMAT => "Invalid time format.",
49
+ INVALID_MESSAGE_VERSION => "Invalid message version.",
50
+ UNABLE_TO_PARSE => "Unable to parse the message.",
51
+ MISSING_DOMAIN => "Domain is required for verification.",
52
+ MISSING_NONCE => "Nonce is required for verification.",
53
+ MISSING_URI => "URI is required in strict mode.",
54
+ MISSING_CHAIN_ID => "Chain ID is required in strict mode.",
55
+ MISSING_CONFIG => "No verification configuration found.",
56
+ MISSING_PROVIDER_LIBRARY => "Required provider library is not installed.",
57
+ NONCE_GENERATION_FAILED => "Nonce generation failed.",
58
+ INVALID_PARAMS => "Invalid parameters passed to verify.",
59
+ MALFORMED_MESSAGE => "Message could not be prepared for signing.",
60
+ RPC_ERROR => "RPC call failed."
61
+ }.freeze
62
+
63
+ ALL = MESSAGES.keys.freeze
64
+ end
65
+ end