airwallex 0.3.0 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aff1a8f847f36f804c6ff9738480a8acd8e9aae867a9a3846311521337544c15
4
- data.tar.gz: af199726a6f9cdcf4eef3d47c1ac1c24fd3ce92f9dd564c57730e75a8e4230ce
3
+ metadata.gz: ec7542a0e82153bbd60acceadb071eba2d48d328d89fa34d72d8a23dc30aeb45
4
+ data.tar.gz: b14a15b3b70878c6291bc6ffd447ccf81d4cab5d86e61b806cc0708bd70590e2
5
5
  SHA512:
6
- metadata.gz: 41ddfe80904d71772a0ed87a6f6f26ff3f857b8502bfde57c6e8b42bd53a162d6235ac6ae99ca52a11f2e4688dbe0682a96806ec2e97f1e2e3f617c038766c45
7
- data.tar.gz: 907a5e84b3a38f3b4664ccf41d5ac4d62e04804073034845dfd9503b880113f2bc32d8c365ca9cca96dd6870ac4ebc1283ca93da93fb59128789b001b556ef47
6
+ metadata.gz: 1e401e23580f1347cbf2caae60b985138c59e73a22ba758578c7ffd99b2318859f766e7c0e0a13f55bff4da29a6796029a359a12a9c1db32b213c972244e70a3
7
+ data.tar.gz: b7779fbdc8227ae72d20eda510799bb6898edb43c74d7aa6a6a30b7110f304be96b2e17673b02e832c2a9638c48f987b6ecf54027fc4b18f754d218fbaec4398
data/CHANGELOG.md CHANGED
@@ -1,15 +1,47 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.6.0] - 2026-08-28
4
+
5
+ ### Fixed
6
+ - `Webhook::Event` now correctly exposes the event type. Airwallex sends this field as `"name"` in
7
+ the webhook payload, not `"type"` — the previous `Event#type` reader was reading a key that doesn't
8
+ exist and always returned `nil`. The accessor is renamed to `Event#name` to match Airwallex's actual
9
+ field. **Breaking change:** `event.type` → `event.name`.
10
+
11
+ ## [0.5.0] - 2026-08-28
12
+
13
+ ### Fixed
14
+ - Webhook signature verification no longer rejects valid webhooks when Airwallex sends
15
+ millisecond-precision timestamps. `verify_timestamp` now detects millisecond values (anything at or
16
+ above `MS_THRESHOLD = 10_000_000_000`, which safely distinguishes seconds from milliseconds until the
17
+ year 2286) and normalizes them to seconds before comparing against the tolerance window.
18
+
19
+ ## [0.4.0] - 2026-08-27
20
+
21
+ ### Fixed
22
+ - `Idempotency` and `AuthRefresh` middleware are now actually registered on the Faraday connection.
23
+ Previously both classes existed but were never wired in, so the "automatic `request_id` generation"
24
+ and "retry once after a 401" behavior documented in the README did not happen at runtime.
25
+ - Fixed a bug in `AuthRefresh` where the 401-retry guard used `env[:request][:auth_retry]`, a key that
26
+ doesn't exist on `Faraday::RequestOptions` and would have raised `NoMethodError` the first time it ran.
27
+ - CI now triggers on pushes to `main` (previously configured for `master`, so pushes never ran CI).
28
+
29
+ ### Changed
30
+ - `Client#request` no longer manually manages the `Authorization` header or calls
31
+ `ensure_authenticated!` directly; this is now owned by the `AuthRefresh` middleware.
32
+
3
33
  ## [0.3.0] - 2025-11-25
4
34
 
5
35
  ### Added
36
+ - BatchTransfer resource (create, retrieve, list) for bulk payout operations
37
+ - Dispute resource (retrieve, list, accept, submit_evidence) for chargeback management
6
38
  - Foreign Exchange resources:
7
39
  - Rate resource (retrieve, list) for real-time exchange rate queries
8
40
  - Quote resource (create, retrieve) for locking exchange rates with expiration helpers
9
41
  - Conversion resource (create, retrieve, list) for executing currency conversions
10
42
  - Balance resource (list, retrieve) for querying account balances across currencies
11
43
  - Enhanced List operation to handle both array responses and paginated responses
12
- - 38 new tests (278 total) covering FX and balance operations
44
+ - 63 new tests (278 total) covering batch transfers, disputes, FX, and balance operations
13
45
  - Comprehensive manual test suite for regression testing
14
46
 
15
47
  ### Changed
@@ -19,13 +51,6 @@
19
51
  ### Fixed
20
52
  - List operation now correctly handles Balance API's direct array response format
21
53
 
22
- ## [0.2.1] - 2025-11-25
23
-
24
- ### Added
25
- - BatchTransfer resource (create, retrieve, list) for bulk payout operations
26
- - Dispute resource (retrieve, list, accept, submit_evidence) for chargeback management
27
- - 25 new tests (240 total) covering batch transfers and disputes
28
-
29
54
  ## [0.2.0] - 2025-11-25
30
55
 
31
56
  ### Added
data/README.md CHANGED
@@ -319,8 +319,8 @@ begin
319
319
  timestamp,
320
320
  tolerance: 300 # 5 minutes
321
321
  )
322
-
323
- case event.type
322
+
323
+ case event.name
324
324
  when 'payment_intent.succeeded'
325
325
  handle_successful_payment(event.data)
326
326
  when 'payout.transfer.failed'
@@ -7,6 +7,8 @@ require "json"
7
7
 
8
8
  module Airwallex
9
9
  class Client
10
+ LOGIN_PATH = "/api/v1/authentication/login"
11
+
10
12
  attr_reader :config, :access_token, :token_expires_at
11
13
 
12
14
  def initialize(config = Airwallex.configuration)
@@ -19,11 +21,7 @@ module Airwallex
19
21
 
20
22
  def connection
21
23
  @connection ||= Faraday.new(url: config.api_url) do |conn|
22
- conn.request :json
23
- conn.request :multipart
24
- conn.request :retry, retry_options
25
- conn.response :json, content_type: /\bjson$/
26
- conn.response :logger, config.logger, { headers: true, bodies: true } if config.logger
24
+ configure_middleware(conn)
27
25
 
28
26
  conn.headers["Content-Type"] = "application/json"
29
27
  conn.headers["User-Agent"] = user_agent
@@ -55,10 +53,9 @@ module Airwallex
55
53
 
56
54
  def authenticate!
57
55
  @token_mutex.synchronize do
58
- response = connection.post("/api/v1/authentication/login") do |req|
56
+ response = connection.post(LOGIN_PATH) do |req|
59
57
  req.headers["x-client-id"] = config.client_id
60
58
  req.headers["x-api-key"] = config.api_key
61
- req.headers.delete("Authorization")
62
59
  end
63
60
 
64
61
  handle_response_errors(response)
@@ -85,12 +82,9 @@ module Airwallex
85
82
  private
86
83
 
87
84
  def request(method, path, data, headers)
88
- ensure_authenticated!
89
-
90
85
  response = connection.public_send(method) do |req|
91
86
  req.url(path)
92
87
  req.headers.merge!(headers)
93
- req.headers["Authorization"] = "Bearer #{access_token}"
94
88
 
95
89
  case method
96
90
  when :get, :delete
@@ -104,6 +98,16 @@ module Airwallex
104
98
  response.body
105
99
  end
106
100
 
101
+ def configure_middleware(conn)
102
+ conn.use Airwallex::Middleware::Idempotency
103
+ conn.request :json
104
+ conn.request :multipart
105
+ conn.request :retry, retry_options
106
+ conn.use Airwallex::Middleware::AuthRefresh, self
107
+ conn.response :json, content_type: /\bjson$/
108
+ conn.response :logger, config.logger, { headers: true, bodies: true } if config.logger
109
+ end
110
+
107
111
  def handle_response_errors(response)
108
112
  return if response.success?
109
113
 
@@ -9,24 +9,38 @@ module Airwallex
9
9
  end
10
10
 
11
11
  def call(env)
12
- # Skip authentication refresh for login endpoint
13
- return @app.call(env) if env[:url].path.include?("/authentication/login")
12
+ # Skip authentication entirely for the login endpoint itself
13
+ return @app.call(env) if login_request?(env)
14
14
 
15
- # Ensure token is valid before making request
16
- @client.ensure_authenticated! unless env[:url].path.include?("/authentication/")
15
+ # Ensure token is valid before making the request, then attach it
16
+ @client.ensure_authenticated! unless authentication_request?(env)
17
+ authorize!(env)
17
18
 
18
19
  response = @app.call(env)
19
20
 
20
21
  # If we get a 401, try refreshing the token and retrying once
21
- if response.status == 401 && !env[:request].fetch(:auth_retry, false)
22
+ if response.status == 401
22
23
  @client.authenticate!
23
- env[:request][:auth_retry] = true
24
- env[:request_headers]["Authorization"] = "Bearer #{@client.access_token}"
24
+ authorize!(env)
25
25
  response = @app.call(env)
26
26
  end
27
27
 
28
28
  response
29
29
  end
30
+
31
+ private
32
+
33
+ def login_request?(env)
34
+ env[:url].path.include?(Client::LOGIN_PATH)
35
+ end
36
+
37
+ def authentication_request?(env)
38
+ env[:url].path.include?("/authentication/")
39
+ end
40
+
41
+ def authorize!(env)
42
+ env[:request_headers]["Authorization"] = "Bearer #{@client.access_token}"
43
+ end
30
44
  end
31
45
  end
32
46
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Airwallex
4
- VERSION = "0.3.0"
4
+ VERSION = "0.6.0"
5
5
  end
@@ -5,6 +5,8 @@ require "openssl"
5
5
  module Airwallex
6
6
  module Webhook
7
7
  DEFAULT_TOLERANCE = 300 # 5 minutes
8
+ # Timestamps below this are seconds, at/above are milliseconds. Valid until year 2286.
9
+ MS_THRESHOLD = 10_000_000_000
8
10
 
9
11
  module_function
10
12
 
@@ -37,6 +39,8 @@ module Airwallex
37
39
  def verify_timestamp(timestamp, tolerance)
38
40
  current_time = Time.now.to_i
39
41
  timestamp_int = timestamp.to_i
42
+ # Airwallex sends x-timestamp in milliseconds; normalize to seconds before comparing.
43
+ timestamp_int /= 1000 if timestamp_int > MS_THRESHOLD
40
44
 
41
45
  if (current_time - timestamp_int).abs > tolerance
42
46
  raise SignatureVerificationError, "Timestamp outside tolerance (#{tolerance}s)"
@@ -54,11 +58,11 @@ module Airwallex
54
58
  private_class_method :compute_signature, :verify_timestamp, :secure_compare
55
59
 
56
60
  class Event
57
- attr_reader :id, :type, :data, :created_at
61
+ attr_reader :id, :name, :data, :created_at
58
62
 
59
63
  def initialize(attributes = {})
60
64
  @id = attributes["id"]
61
- @type = attributes["type"]
65
+ @name = attributes["name"]
62
66
  @data = attributes["data"]
63
67
  @created_at = attributes["created_at"]
64
68
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: airwallex
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chayut Orapinpatipat
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-11-25 00:00:00.000000000 Z
11
+ date: 2026-08-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -122,7 +122,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
122
122
  - !ruby/object:Gem::Version
123
123
  version: '0'
124
124
  requirements: []
125
- rubygems_version: 3.5.11
125
+ rubygems_version: 3.4.10
126
126
  signing_key:
127
127
  specification_version: 4
128
128
  summary: Production-grade Ruby client for the Airwallex API