tcat 0.3.5 → 0.4.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: f40a1e40fc125273e5928860f2afa18f22ad88a22a2b02d63f441b9197b0153d
4
- data.tar.gz: 6e9d0c2f7bf65f9cc81a9764761092b76dce77faedc57836f3d9f4866b327b00
3
+ metadata.gz: 588b8dd9b49b05b35d28384cb85c3bc4e87b38852a04c3da8ce5b35ce59739bc
4
+ data.tar.gz: cd704ade306674058f6fd4c8de240d7570f54dba29ad7e1dfe30afb3b0119d40
5
5
  SHA512:
6
- metadata.gz: c402bffc3cc32eb481d930958107d08e278b362390cc4d6f62bbe9b8357ebc5501e74dfce68ca1326040a32ef9bc51274b8853c9b57e6cd7b0b98ca19c52cbf4
7
- data.tar.gz: 83fb6ec4ec1e9731fbd7d8fc0288221676afd14904101c3063304ae687cb54cc83112a9baf28eb616251d3618452976e0e7bc6ba284ef49d3a49d20b19a4b6e9
6
+ metadata.gz: 8617ad7c2b5bec16c3d7c94fd39175b45417a869feb6a74f0a1fab3590860c44d4a7e61a49b908982ae65f4b856504de3e61c9f96ba082c0ef0736cf936a59ca
7
+ data.tar.gz: '059ea7b7711cae4a2d146a23470016048edfb6e5c5d8951822ba17be8bd792355b1f9d29a73cc1b40e604884b332ac1548e453fb66f3cc22cff6c43198c510c7'
data/README.md CHANGED
@@ -64,11 +64,16 @@ Tcat.configure do |config|
64
64
  config.secret_key = 'your_secret_key'
65
65
  end
66
66
 
67
- # Create a query instance
68
- query = Tcat::Query.new('your_tracking_number')
67
+ # Two equivalent shapes — pick whichever fits your code:
69
68
 
70
- # Get shipment status
69
+ # A) tracking number bound at construction (one-shot)
70
+ query = Tcat::Query.new('your_tracking_number')
71
71
  status = query.status_code
72
+
73
+ # B) tracking number per call (reuse one client for many lookups,
74
+ # matches Tcat::WorkerClient's shape)
75
+ query = Tcat::Query.new
76
+ status = query.status_code('your_tracking_number')
72
77
  # Returns one of the following:
73
78
  # :done - Successfully delivered
74
79
  # :delivering - Out for delivery
@@ -265,6 +270,19 @@ This gem is available as open source under the terms of the [MIT License](https:
265
270
 
266
271
  ## Changelog
267
272
 
273
+ ### 0.4.1
274
+
275
+ - Security hardening: `Tcat::Query` now uses `SecureRandom` (CSPRNG) instead of `rand` for the per-request freshness component embedded in the encrypted `secret` payload
276
+ - Encryption: `Tcat::EncryptionService` explicitly disables OpenSSL's built-in cipher padding so the manually applied PKCS#7 padding cannot be double-applied if the cipher pipeline ever changes
277
+ - `Tcat::WorkerClient` warns when `worker_url` uses `http://` against a non-localhost host so a misconfigured deployment cannot silently leak the Bearer token over plaintext
278
+ - Worker: added `[observability.logs]` (`enabled = false`, `invocation_logs = true`) to `wrangler.toml`
279
+
280
+ ### 0.4.0
281
+
282
+ - `Tcat::Query#status_code`, `#history`, `#latest_status` now accept an optional tracking-number argument, mirroring `Tcat::WorkerClient`'s shape
283
+ - `Tcat::Query.new` may be called without a tracking number for stateless reuse: `Tcat::Query.new.status_code('1234567890')`
284
+ - Existing one-shot construction (`Tcat::Query.new(tn).status_code`) is still supported
285
+
268
286
  ### 0.3.5
269
287
 
270
288
  - Added optional `worker_url` and `worker_token` to `Tcat.configure`; explicit `Tcat::WorkerClient.new(url, token:)` arguments still take precedence
@@ -34,6 +34,9 @@ module Tcat
34
34
  def setup_cipher
35
35
  cipher = OpenSSL::Cipher.new(CIPHER_ALGORITHM)
36
36
  cipher.encrypt
37
+ # PKCS#7 padding is applied manually in #pad_message; turn off OpenSSL's
38
+ # built-in padding so a future cipher.final call cannot double-pad.
39
+ cipher.padding = 0
37
40
  cipher.key = @secret_key
38
41
  cipher
39
42
  end
data/lib/tcat/query.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'net/http'
4
4
  require 'base64'
5
+ require 'securerandom'
5
6
  require 'ox'
6
7
  require_relative 'http_client'
7
8
  require_relative 'encryption_service'
@@ -11,7 +12,11 @@ module Tcat
11
12
  class Query
12
13
  DeliveryItem = Struct.new(:status, :status_code, :time, :office, :last_update, keyword_init: true)
13
14
 
14
- def initialize(tracking_number)
15
+ # @param tracking_number [String, nil] Default tracking number used when
16
+ # the per-call methods are invoked without an explicit argument. Pass nil
17
+ # to construct a stateless client and supply the tracking number on each
18
+ # call (matching Tcat::WorkerClient's shape).
19
+ def initialize(tracking_number = nil)
15
20
  @secret_string = Tcat.configuration.secret_string
16
21
  @secret_key = Tcat.configuration.secret_key
17
22
  validate_secrets!
@@ -25,22 +30,27 @@ module Tcat
25
30
  end
26
31
 
27
32
  # Get current delivery status code
33
+ # @param tracking_number [String, nil] Overrides the constructor value.
28
34
  # @return [Symbol] Status code (:done, :delivering, :collected, :in_transit, :unknown)
29
- def status_code
30
- response_body = @http_client.post(data)
35
+ def status_code(tracking_number = nil)
36
+ tn = resolve_tracking_number(tracking_number)
37
+ response_body = @http_client.post(data(tn))
31
38
  parse_status_code(response_body) if response_body
32
- rescue HttpClient::RequestError => e
39
+ rescue HttpClient::RequestError
33
40
  # Log error or handle it appropriately
34
41
  nil
35
42
  end
36
43
 
37
44
  # Get complete delivery history
45
+ # @param tracking_number [String, nil] Overrides the constructor value.
38
46
  # @return [Array<DeliveryItem>] Array of delivery status items, sorted by time (newest first)
39
- def history
40
- response_body = @http_client.post(data)
47
+ def history(tracking_number = nil)
48
+ tn = resolve_tracking_number(tracking_number)
49
+ response_body = @http_client.post(data(tn))
41
50
  if response_body
42
51
  result = Ox.load(response_body, mode: :hash, with_cdata: true)
43
52
  return [] if result.dig(:Result, :Status) != '0'
53
+
44
54
  extract_delivery_history(result)
45
55
  end
46
56
  rescue StandardError => e
@@ -49,10 +59,12 @@ module Tcat
49
59
  end
50
60
 
51
61
  # Get latest delivery status with details
62
+ # @param tracking_number [String, nil] Overrides the constructor value.
52
63
  # @return [DeliveryItem, nil] Latest delivery status or nil if no history
53
- def latest_status
54
- items = history
64
+ def latest_status(tracking_number = nil)
65
+ items = history(tracking_number)
55
66
  return nil if items.empty?
67
+
56
68
  items.first
57
69
  end
58
70
 
@@ -75,9 +87,16 @@ module Tcat
75
87
  warn e.backtrace.first(5).join("\n") if $DEBUG
76
88
  end
77
89
 
78
- def data
90
+ def resolve_tracking_number(arg)
91
+ tn = arg || @tracking_number
92
+ raise ArgumentError, 'tracking number required' if tn.nil? || tn.to_s.empty?
93
+
94
+ tn
95
+ end
96
+
97
+ def data(tracking_number)
79
98
  {
80
- ConsignmentNo: @tracking_number,
99
+ ConsignmentNo: tracking_number,
81
100
  f: 5,
82
101
  isForeign: 'N',
83
102
  secret: generate_secret,
@@ -90,7 +109,8 @@ module Tcat
90
109
  end
91
110
 
92
111
  def random
93
- rand(10_000..99_999).to_s
112
+ # CSPRNG so the per-request freshness component is unpredictable.
113
+ (SecureRandom.random_number(90_000) + 10_000).to_s
94
114
  end
95
115
 
96
116
  def source_string
data/lib/tcat/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tcat
4
- VERSION = '0.3.5'
4
+ VERSION = '0.4.1'
5
5
  end
@@ -99,15 +99,29 @@ module Tcat
99
99
 
100
100
  private
101
101
 
102
+ LOCAL_HOSTS = %w[localhost 127.0.0.1 ::1].freeze
103
+ private_constant :LOCAL_HOSTS
104
+
102
105
  def validate_url!
103
106
  uri = URI.parse(@worker_url)
104
107
  unless %w[http https].include?(uri.scheme)
105
108
  raise ArgumentError, 'Invalid Worker URL: must be http or https'
106
109
  end
110
+
111
+ warn_if_insecure(uri)
107
112
  rescue URI::InvalidURIError => e
108
113
  raise ArgumentError, "Invalid Worker URL: #{e.message}"
109
114
  end
110
115
 
116
+ def warn_if_insecure(uri)
117
+ return if uri.scheme == 'https'
118
+ return if LOCAL_HOSTS.include?(uri.host)
119
+
120
+ warn '[Tcat::WorkerClient] WARNING: worker_url uses http://; ' \
121
+ 'requests and any Authorization token will be sent in plaintext. ' \
122
+ 'Use https:// in production.'
123
+ end
124
+
111
125
  def make_request(uri)
112
126
  response = setup_http(uri).request(build_request(uri))
113
127
  raise APIError, "HTTP #{response.code}: #{response.message}" unless response.is_a?(Net::HTTPSuccess)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tcat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.5
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zac