rail0-sdk 1.7.0 → 1.7.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 861d8ebab53accbd5a962ed1cb040a91461841b63722077b93374291f8de66bd
4
- data.tar.gz: e7d22a5a5d2e9ad6a22149427195b1d7d341e15da84e1d858f453d343859b771
3
+ metadata.gz: 5d88d3da009d4c04853e8fb794eba655913763a1391c6e0bd6915bda6cb68d36
4
+ data.tar.gz: 5cc37022ce71f950c6b11dcf421902518b63caebfd54a13f78eaf909a7eef39c
5
5
  SHA512:
6
- metadata.gz: b926413fd2b987d43b60b78216506baa56eb8954c3d51c7ba2e61a977e597a250c2b961772a27fba93bca1a2f08b036250c15111ecac692823e3ab84548f2004
7
- data.tar.gz: a43b612c63ea9114034c4e80eeccb9670758bfecae1267efe5568f322475adfc8dcdf2f935b35f40d9cfcf8559b94b1e65a00b36093c937bf5ac6a385920ba5c
6
+ metadata.gz: 2512e0a842d7d76749dc4028a0c1d2590b7fe1dd4e95676572a39aa8c41fa9a882c2e4a534e85080f0fe8089c8001c6f2101ff4b9fb7ab9f4e2cc9d7e6d35d62
7
+ data.tar.gz: 7b569639525815722a586f308d45ebc67c964cbd4f6f1ae4c5bac4a8c5a731589b03dffebcfcdb375e3a265f9f1143b52162d5e8685de591e39a364de3257cf8
data/README.md CHANGED
@@ -150,8 +150,16 @@ signs after `create` (and the payee signs in refund phase 1) is the more dangero
150
150
  two artifacts: the **token** honours it on its own, with no RAIL0 involvement, for up to
151
151
  its `value` — so an endpoint that answered with `{from: you, to: attacker, value: your
152
152
  whole balance}` used to get exactly that signed. `sign_signing_payload(signing_payload,
153
- key, payment:, amount: nil)` checks it first, against data you already hold:
154
- `message.from` is your key's address, `domain.chainId` the payment's `chain_id`,
153
+ key, payment:, amount: nil)` checks it first. The typed-data envelope: `primaryType` is
154
+ `ReceiveWithAuthorization` (authorize/charge on v1.4.0+ deployments, and every refund) or
155
+ `TransferWithAuthorization` (authorize/charge on v1.1.0–v1.3.x); `types[primaryType]` is
156
+ **exactly** the EIP-3009 struct — `address from, address to, uint256 value, uint256
157
+ validAfter, uint256 validBefore, bytes32 nonce`, same names, types and order;
158
+ `types.EIP712Domain`, when present, is exactly `string name, string version, uint256
159
+ chainId, address verifyingContract`; and `types` defines nothing else (a local key hashes
160
+ with fixed typehashes, but a browser wallet hashes whatever `types` says, so this is what
161
+ lets `PayloadVerify.verify` guard an `eth_signTypedData_v4` call). Then, against data you
162
+ already hold: `message.from` is your key's address, `domain.chainId` the payment's `chain_id`,
155
163
  `domain.verifyingContract` the payment's `token`, `message.to` its `rail0_contract`, and
156
164
  `message.value` **exactly** the amount — `amount:` scaled by the payment's `decimals` when
157
165
  you pass one (a refund), otherwise the payment's own base-unit `amount` (a created
@@ -827,7 +835,7 @@ Requires the `eth` gem. No private key ever leaves your process.
827
835
  |--------|-----|
828
836
  | `sign_signing_payload(signing_payload, signer, payment:, amount: nil)` | Verify the EIP-3009 payload from a create/refund response against the payment, then sign it verbatim — the recommended path, and the only one that follows the gateway across contract versions |
829
837
  | `sign_payload(signer, signing_payload, payment: nil, amount: nil)` | The same signer; with `payment:` it is `sign_signing_payload`, without it signs blindly (**deprecated**) |
830
- | `Rail0::PayloadVerify.verify(signing_payload, payment:, signer:, amount: nil)` | The payload pre-sign check on its own |
838
+ | `Rail0::PayloadVerify.verify(signing_payload, payment:, signer:, amount: nil)` | The payload pre-sign check on its own — run it before handing the payload to a browser wallet |
831
839
  | `sign_transaction(unsigned_transaction, key, payment:, operation:, amount: nil)` | Verify a prepare step's unsigned EIP-1559 transaction against the payment, then sign it; returns the 0x raw tx. Without `payment:` it signs blindly (**deprecated**) |
832
840
  | `Rail0::TxVerify.verify(unsigned_transaction, payment:, operation:, amount: nil, signer: nil)` | The pre-sign check on its own — for a signer that is not a local key |
833
841
  | `sign_transfer_with_authorization(signer, domain, params)` | Raw EIP-3009 `TransferWithAuthorization` signer, for talking to a token directly |
@@ -846,7 +854,8 @@ client.payments.sign(payment[:rail0_id], { signature: sig.to_hex })
846
854
  **The gateway builds the payload; you sign it verbatim.** `sign_signing_payload` takes the
847
855
  typehash from the payload's `primaryType` and every field from its `message` — only
848
856
  the gateway knows which contract version a payment lives on, and the version selects
849
- the typehash, the EIP-712 domain and the field layout. An unrecognised `primaryType`
857
+ the typehash and the EIP-712 domain (the field layout is EIP-3009's own, the same on
858
+ every version, which is why the check pins it). An unrecognised `primaryType`
850
859
  raises rather than falling back, because a guessed typehash yields a *valid*
851
860
  signature over the wrong digest, which fails only on-chain, after gas.
852
861
 
@@ -27,10 +27,17 @@ module Rail0
27
27
  # 5. +message.value+ is EXACTLY the amount asked for, in base units — not a ceiling:
28
28
  # the gateway asks for the full amount of the operation.
29
29
  #
30
+ # Before those, the typed-data envelope: +primaryType+ is one of the two EIP-3009
31
+ # authorization types, +types[primaryType]+ is EXACTLY the EIP-3009 field list (names,
32
+ # types, order), +types.EIP712Domain+ (when present) is exactly the token domain's
33
+ # field list, and +types+ defines nothing else. {Signing.sign_payload} hashes with
34
+ # fixed typehashes and would not notice, but a browser wallet hashes whatever +types+
35
+ # says — so a caller that verifies here and then hands the payload to
36
+ # +eth_signTypedData_v4+ needs it.
37
+ #
30
38
  # The payload is still signed VERBATIM: the typehash comes from its own +primaryType+
31
39
  # and the digest from its own message. This only refuses a message whose fields
32
- # contradict what the caller asked for. Like the CLI, it does not check +primaryType+
33
- # against the contract version (the signer already refuses an unknown one), the
40
+ # contradict what the caller asked for. Like the CLI, it does not check the
34
41
  # validity window, or +message.nonce+ — recomputing the nonce needs the deployment's
35
42
  # VERSION(), which no payment response carries. The bounded residual is an
36
43
  # authorization for the RIGHT token, on the RIGHT chain, from the RIGHT signer, for
@@ -62,6 +69,7 @@ module Rail0
62
69
  pay = payment_fields(payment)
63
70
  refuse("cannot check the authorization without the address of the signing key") if blank?(signer)
64
71
 
72
+ check_typed_data(sp[:primaryType], sp[:types])
65
73
  check_from(message[:from], signer.to_s)
66
74
  check_chain(domain[:chainId], pay[:chain_id])
67
75
  check_address(domain[:verifyingContract], pay[:token], "token") do |got, want|
@@ -91,6 +99,73 @@ module Rail0
91
99
 
92
100
  private_class_method :parse, :payment_fields
93
101
 
102
+ # The EIP-3009 authorization types the gateway emits. Which one a payment needs is the
103
+ # deployment's contract version, which no payment response carries: 1.4.0 and later
104
+ # use ReceiveWithAuthorization for authorize/charge, 1.1.0–1.3.x used
105
+ # TransferWithAuthorization, and refunds are ReceiveWithAuthorization on every
106
+ # version. So both are accepted and nothing else is.
107
+ PRIMARY_TYPES = %w[TransferWithAuthorization ReceiveWithAuthorization].freeze
108
+
109
+ # The EIP-3009 struct shared by both types: names, types AND order are the typehash.
110
+ AUTHORIZATION_FIELDS = [
111
+ %w[from address], %w[to address], %w[value uint256],
112
+ %w[validAfter uint256], %w[validBefore uint256], %w[nonce bytes32]
113
+ ].freeze
114
+
115
+ # The token's EIP-712 domain, as the gateway declares it and the token hashes it.
116
+ DOMAIN_FIELDS = [
117
+ %w[name string], %w[version string], %w[chainId uint256], %w[verifyingContract address]
118
+ ].freeze
119
+
120
+ private_constant :PRIMARY_TYPES, :AUTHORIZATION_FIELDS, :DOMAIN_FIELDS
121
+
122
+ # The typed-data envelope: refuses an unknown +primaryType+, a +types[primaryType]+
123
+ # that differs from the EIP-3009 struct in any name, type or position, an
124
+ # +EIP712Domain+ that differs from the token domain, or any further type.
125
+ def self.check_typed_data(primary_type, types)
126
+ unless PRIMARY_TYPES.include?(primary_type)
127
+ refuse("the authorization's primaryType #{primary_type.inspect} is not an EIP-3009 authorization type " \
128
+ "(TransferWithAuthorization or ReceiveWithAuthorization)")
129
+ end
130
+ refuse("the signing payload has no types") unless types.is_a?(Hash)
131
+
132
+ types = types.transform_keys(&:to_s)
133
+ extra = types.keys.find { |name| name != primary_type && name != "EIP712Domain" }
134
+ if extra
135
+ refuse("the signing payload's types define #{extra.inspect}, which an EIP-3009 authorization does not use")
136
+ end
137
+ check_type_fields("types.#{primary_type}", types[primary_type], AUTHORIZATION_FIELDS)
138
+ check_type_fields("types.EIP712Domain", types["EIP712Domain"], DOMAIN_FIELDS) if types.key?("EIP712Domain")
139
+ end
140
+
141
+ def self.check_type_fields(label, fields, want)
142
+ return if exact_fields?(fields, want)
143
+
144
+ expected = want.map { |name, type| "#{type} #{name}" }.join(",")
145
+ refuse("#{label} is (#{describe_fields(fields)}) but must be exactly (#{expected})")
146
+ end
147
+
148
+ def self.describe_fields(fields)
149
+ return (fields.nil? ? "null" : fields.inspect) unless fields.is_a?(Array)
150
+
151
+ fields.map { |f| f.is_a?(Hash) ? "#{field_of(f, 'type')} #{field_of(f, 'name')}" : f.inspect }.join(",")
152
+ end
153
+
154
+ def self.exact_fields?(fields, want)
155
+ fields.is_a?(Array) && fields.size == want.size &&
156
+ fields.each_with_index.all? do |f, i|
157
+ f.is_a?(Hash) && f.size == 2 && field_of(f, "name") == want[i][0] && field_of(f, "type") == want[i][1]
158
+ end
159
+ end
160
+
161
+ # A field entry's +name+ / +type+, whether the payload arrived string- or
162
+ # symbol-keyed.
163
+ def self.field_of(field, key)
164
+ field.key?(key) ? field[key] : field[key.to_sym]
165
+ end
166
+
167
+ private_class_method :check_typed_data, :check_type_fields, :describe_fields, :exact_fields?, :field_of
168
+
94
169
  # Check 1: from = the key's own address. An absent +from+ is a mismatch, not a skip.
95
170
  def self.check_from(from, signer)
96
171
  return if same_address?(from, signer)
data/lib/rail0/types.rb CHANGED
@@ -457,17 +457,17 @@ module Rail0
457
457
  keyword_init: true
458
458
  )
459
459
 
460
- # One reported policy value.
461
- PolicySetting = Struct.new(
460
+ # One reported configuration value.
461
+ ConfigSetting = Struct.new(
462
462
  :value, # The effective value (number, boolean, string, list or map).
463
463
  :env, # String — The environment variable that sets it; null for a constant.
464
464
  :overridden, # Boolean — Whether that variable is set in this process.
465
- :description, # String — Plain-text explanation of the entry: what it controls, which part of the gateway reads it and the effect of raising or lowering it, and its unit (from config/policy_descriptions.yml). Every entry has one; older gateways omit the field.
465
+ :description, # String — Plain-text explanation of the entry: what it controls, which part of the gateway reads it and the effect of raising or lowering it, and its unit (from config/config_descriptions.yml). Every entry has one; older gateways omit the field.
466
466
  keyword_init: true
467
467
  )
468
468
 
469
- # GET /admin/policy: the effective policy, grouped. Every leaf is a PolicySetting.
470
- PolicyReport = Struct.new(
469
+ # GET /admin/config: the effective configuration, grouped. Every leaf is a ConfigSetting.
470
+ ConfigReport = Struct.new(
471
471
  :payments, # Hash
472
472
  :recovery, # Hash
473
473
  :retention, # Hash
data/lib/rail0/version.rb CHANGED
@@ -5,5 +5,5 @@ module Rail0
5
5
  # consumed by path from a sibling checkout, where the number bought nothing: no registry
6
6
  # ever served a 1.x, no tag was ever cut, and the one consumer resolves the working tree.
7
7
  # Publishing from 1.2.0 would have implied two releases nobody can install.
8
- VERSION = "1.7.0"
8
+ VERSION = "1.7.1"
9
9
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rail0-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.0
4
+ version: 1.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Commerce Layer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-19 00:00:00.000000000 Z
11
+ date: 2026-10-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logger