timely-app 1.3.2 → 1.3.3

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: c82f58f7c3e0713310beb5653d74487822d9f96bb883ae15805c5987896a589e
4
- data.tar.gz: d90c62ee9a7baa1b29468f70997a7b1e5e6236d8553c9d92abfc082855595292
3
+ metadata.gz: 9d8ce8174d129b5c2273101c2c00911260f27c6ace3f2cf674920a5b7b3c20b1
4
+ data.tar.gz: bc5c6f0bb086d3091e820f28ea278786ee4a54562df0bc5b9e151610d7a9b49e
5
5
  SHA512:
6
- metadata.gz: cf7b4edda75f282566b4f38c3e48c88f716056055734dae4bd2a08016e5d1e9d7d148922fc4ddcd5625fde5740376cb6e0088bf0c8a558c5d78e6e4723f84668
7
- data.tar.gz: 3cdb10455d12dbc9f265ea500bccb965775e2d767ae8e798bea35a356ce8065036c0402e60016ff7feb2ea2de2198ea75b901592ac3c45993e3f55c953662ba7
6
+ metadata.gz: 20abf6c65cf3558f079ed82e1b95245a66fac3e7301bdf6f3a8039e7ad20044efce9dddc2dee3c054ba8a18dbdc25437b8de934a37784f9521ce7c30cf34a6be
7
+ data.tar.gz: a84b5732a9a539ee9a2133f370748ee32d2dbff90cf916dc5a2bfa08817e1f90ce53dee7acafef8e887de6a5a40de763e2b250cb7bcc9ac72662fd18d70e3069
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 1.3.3 (2026-10-29)
4
+
5
+ - Preserve HTTP error types when the server returns malformed or unexpected JSON.
6
+ - Keep authorization codes and response contents out of failed sign-in diagnostics.
7
+ - Cancel release preparation when confirmation input is closed.
8
+ - Exit sign-in when the authorization code prompt is closed or blank.
9
+
3
10
  ## 1.3.2 (2026-07-25)
4
11
 
5
12
  - BREAKING: Require Ruby 3.4 or newer
@@ -1,3 +1,6 @@
1
+ require "securerandom"
2
+ require "yaml"
3
+
1
4
  module TimelyApp
2
5
  class CLI
3
6
  attr_reader :options, :client
@@ -37,7 +40,7 @@ module TimelyApp
37
40
  token = fetch_oauth_token(auth_client, client_id, client_secret, code)
38
41
  persist_or_print_token(token)
39
42
  rescue TimelyApp::Error => error
40
- handle_auth_failure(error, code)
43
+ handle_auth_failure(error)
41
44
  end
42
45
 
43
46
  private
@@ -57,7 +60,12 @@ module TimelyApp
57
60
  puts "Visit this URL in your browser:"
58
61
  puts auth_url
59
62
  puts "\nEnter authorization code here:"
60
- gets.chomp
63
+ code = gets&.chomp
64
+ if code.nil? || code.empty?
65
+ puts "Authorization code is required"
66
+ exit 1
67
+ end
68
+ code
61
69
  end
62
70
 
63
71
  def oauth_redirect_uri
@@ -84,15 +92,13 @@ module TimelyApp
84
92
  else
85
93
  puts "\nAccess token:\n#{token.access_token}\n"
86
94
  end
87
- puts "Token details: #{token.to_h}" if options[:verbose]
95
+ puts "Authentication succeeded" if options[:verbose]
88
96
  end
89
97
 
90
- def handle_auth_failure(error, code)
98
+ def handle_auth_failure(error)
91
99
  puts "Authentication failed"
92
100
  if options[:verbose]
93
- puts "Code: #{code}"
94
101
  puts "Response code: #{error.response.code}"
95
- puts "Response: #{error.response.body}"
96
102
  end
97
103
  exit 1
98
104
  end
@@ -102,9 +108,16 @@ module TimelyApp
102
108
  end
103
109
 
104
110
  def read_config_file
105
- if File.exist?(config_file_path)
106
- YAML.load_file(config_file_path)
107
- end
111
+ return unless File.exist?(config_file_path)
112
+
113
+ config = YAML.safe_load_file(
114
+ config_file_path,
115
+ permitted_classes: [Symbol, Time],
116
+ aliases: false
117
+ )
118
+ return {} unless config.is_a?(Hash)
119
+
120
+ config.to_h { |key, value| [key.to_s, value] }
108
121
  end
109
122
 
110
123
  def fetch_account_id
@@ -124,9 +137,30 @@ module TimelyApp
124
137
 
125
138
  def save_config_file(**options)
126
139
  config = read_config_file || {}
127
- config.merge!(options)
128
- File.write(config_file_path, config.to_yaml)
140
+ config.merge!(options.to_h { |key, value| [key.to_s, value] })
141
+ write_config_file(config.to_yaml)
129
142
  puts "Saved to #{config_file_path}"
130
143
  end
144
+
145
+ def write_config_file(content)
146
+ temporary_path = "#{config_file_path}.#{Process.pid}.#{SecureRandom.hex(8)}.tmp"
147
+
148
+ File.open(temporary_path, File::WRONLY | File::CREAT | File::EXCL, 0o600) do |file|
149
+ file.write(content)
150
+ file.flush
151
+ file.fsync
152
+ end
153
+ File.rename(temporary_path, config_file_path)
154
+ File.chmod(0o600, config_file_path)
155
+ sync_config_directory
156
+ ensure
157
+ File.delete(temporary_path) if temporary_path && File.exist?(temporary_path)
158
+ end
159
+
160
+ def sync_config_directory
161
+ File.open(File.dirname(config_file_path), File::RDONLY, &:fsync)
162
+ rescue Errno::EINVAL
163
+ nil
164
+ end
131
165
  end
132
166
  end
@@ -10,6 +10,11 @@ require "json"
10
10
 
11
11
  module TimelyApp
12
12
  class Client
13
+ DEFAULT_OPEN_TIMEOUT = 5
14
+ DEFAULT_READ_TIMEOUT = 30
15
+ FILTERED_VALUE = "[FILTERED]"
16
+ SENSITIVE_FIELD = /authorization|code|credential|password|secret|token/i
17
+
13
18
  attr_accessor :account_id
14
19
 
15
20
  def initialize(options = {})
@@ -18,9 +23,7 @@ module TimelyApp
18
23
  @user_agent = options.fetch(:user_agent) { "timely-app/#{VERSION} ruby/#{RUBY_VERSION}" }
19
24
 
20
25
  @host = "api.timelyapp.com"
21
-
22
- @http = Net::HTTP.new(@host, Net::HTTP.https_default_port)
23
- @http.use_ssl = true
26
+ @http = build_http(options)
24
27
 
25
28
  @account_id = options[:account_id]
26
29
  @verbose = options[:verbose] || !ENV["VERBOSE"].nil? || false
@@ -32,6 +35,14 @@ module TimelyApp
32
35
 
33
36
  private
34
37
 
38
+ def build_http(options)
39
+ http = Net::HTTP.new(@host, Net::HTTP.https_default_port)
40
+ http.use_ssl = true
41
+ http.open_timeout = options.fetch(:open_timeout, DEFAULT_OPEN_TIMEOUT)
42
+ http.read_timeout = options.fetch(:read_timeout, DEFAULT_READ_TIMEOUT)
43
+ http
44
+ end
45
+
35
46
  def verbose?
36
47
  @verbose == true
37
48
  end
@@ -74,8 +85,29 @@ module TimelyApp
74
85
  def log_verbose_exchange(http_request, response)
75
86
  return unless verbose?
76
87
 
77
- puts ">> request: #{http_request.method} #{http_request.path} #{http_request.body}"
78
- puts "<< response: #{http_request.method} #{http_request.path} #{response.code} #{response.body}"
88
+ puts ">> request: #{http_request.method} #{http_request.path} #{filtered_body(http_request.body)}"
89
+ puts "<< response: #{http_request.method} #{http_request.path} #{response.code} #{filtered_body(response.body)}"
90
+ end
91
+
92
+ def filtered_body(body)
93
+ return if body.nil? || body.empty?
94
+
95
+ JSON.generate(filter_sensitive_values(JSON.parse(body)))
96
+ rescue JSON::ParserError
97
+ FILTERED_VALUE
98
+ end
99
+
100
+ def filter_sensitive_values(value)
101
+ case value
102
+ when Hash
103
+ value.to_h { |key, nested_value|
104
+ [key, key.match?(SENSITIVE_FIELD) ? FILTERED_VALUE : filter_sensitive_values(nested_value)]
105
+ }
106
+ when Array
107
+ value.map { |nested_value| filter_sensitive_values(nested_value) }
108
+ else
109
+ value
110
+ end
79
111
  end
80
112
  end
81
113
  end
@@ -27,13 +27,20 @@ module TimelyApp
27
27
  end
28
28
 
29
29
  def error(response)
30
- if response.content_type == "application/json"
31
- body = JSON.parse(response.body)
32
- message = body&.dig("errors", "message") || body&.dig("error_description")
33
- error_class(response).new(message, response: response, errors: body&.dig("errors"))
34
- else
35
- error_class(response).new(response: response)
36
- end
30
+ body = error_body(response)
31
+ errors = body["errors"]
32
+ message = errors.is_a?(Hash) ? errors["message"] : nil
33
+ message ||= body["error_description"]
34
+ error_class(response).new(message, response: response, errors: errors)
35
+ end
36
+
37
+ def error_body(response)
38
+ return {} unless response.content_type == "application/json"
39
+
40
+ body = JSON.parse(response.body.to_s)
41
+ body.is_a?(Hash) ? body : {}
42
+ rescue JSON::ParserError
43
+ {}
37
44
  end
38
45
 
39
46
  def error_class(object)
data/lib/timely-app.rb CHANGED
@@ -1,3 +1,7 @@
1
+ module TimelyApp
2
+ VERSION = "1.3.3"
3
+ end
4
+
1
5
  require "timely-app/client"
2
6
  require "timely-app/client/accounts"
3
7
  require "timely-app/client/auth"
@@ -13,7 +17,4 @@ require "timely-app/client/teams"
13
17
  require "timely-app/client/user_capacities"
14
18
  require "timely-app/client/users"
15
19
  require "timely-app/client/webhooks"
16
-
17
- module TimelyApp
18
- VERSION = "1.3.2"
19
- end
20
+ require "timely-app/cli"
data/timely-app.gemspec CHANGED
@@ -30,9 +30,6 @@ Gem::Specification.new do |gem|
30
30
  gem.add_development_dependency "rspec-core", "~> 3"
31
31
  gem.add_development_dependency "polyrun", ">= 2.2.0"
32
32
  gem.add_development_dependency "rspec-expectations", "~> 3"
33
- gem.add_development_dependency "rspec_junit_formatter", "~> 0.6"
34
- gem.add_development_dependency "simplecov", "~> 0.22"
35
- gem.add_development_dependency "simplecov-cobertura", "~> 3"
36
33
  gem.add_development_dependency "webmock", "~> 3"
37
34
  gem.add_development_dependency "pry", "~> 0.15"
38
35
  gem.add_development_dependency "standard", "~> 1.52"
@@ -41,6 +38,6 @@ Gem::Specification.new do |gem|
41
38
  gem.add_development_dependency "standard-rspec", "~> 0.3"
42
39
  gem.add_development_dependency "rubocop-rspec", "~> 3.8"
43
40
  gem.add_development_dependency "rubocop-thread_safety", "~> 0.7"
44
- gem.add_development_dependency "rbs", "~> 3"
41
+ gem.add_development_dependency "rbs", "~> 4"
45
42
  gem.add_development_dependency "appraisal", "~> 2"
46
43
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: timely-app
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.2
4
+ version: 1.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -51,48 +51,6 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
53
  version: '3'
54
- - !ruby/object:Gem::Dependency
55
- name: rspec_junit_formatter
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - "~>"
59
- - !ruby/object:Gem::Version
60
- version: '0.6'
61
- type: :development
62
- prerelease: false
63
- version_requirements: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - "~>"
66
- - !ruby/object:Gem::Version
67
- version: '0.6'
68
- - !ruby/object:Gem::Dependency
69
- name: simplecov
70
- requirement: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - "~>"
73
- - !ruby/object:Gem::Version
74
- version: '0.22'
75
- type: :development
76
- prerelease: false
77
- version_requirements: !ruby/object:Gem::Requirement
78
- requirements:
79
- - - "~>"
80
- - !ruby/object:Gem::Version
81
- version: '0.22'
82
- - !ruby/object:Gem::Dependency
83
- name: simplecov-cobertura
84
- requirement: !ruby/object:Gem::Requirement
85
- requirements:
86
- - - "~>"
87
- - !ruby/object:Gem::Version
88
- version: '3'
89
- type: :development
90
- prerelease: false
91
- version_requirements: !ruby/object:Gem::Requirement
92
- requirements:
93
- - - "~>"
94
- - !ruby/object:Gem::Version
95
- version: '3'
96
54
  - !ruby/object:Gem::Dependency
97
55
  name: webmock
98
56
  requirement: !ruby/object:Gem::Requirement
@@ -211,14 +169,14 @@ dependencies:
211
169
  requirements:
212
170
  - - "~>"
213
171
  - !ruby/object:Gem::Version
214
- version: '3'
172
+ version: '4'
215
173
  type: :development
216
174
  prerelease: false
217
175
  version_requirements: !ruby/object:Gem::Requirement
218
176
  requirements:
219
177
  - - "~>"
220
178
  - !ruby/object:Gem::Version
221
- version: '3'
179
+ version: '4'
222
180
  - !ruby/object:Gem::Dependency
223
181
  name: appraisal
224
182
  requirement: !ruby/object:Gem::Requirement