basecamp-sdk 0.23.0 → 0.24.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: 970062728951b90280186908c54935e720d7297edf23230a9f0a7ccc57804376
4
- data.tar.gz: d26a1714e8e59778e2563180531d0835f274f53bdab6f2ebf0edba8d79ab353f
3
+ metadata.gz: f300a3079488142de429c2df9e1abe31178cfa71ce8dbaddbf98d1d6b641c977
4
+ data.tar.gz: c277cb71684ca7ee5e60c165a79be00bd3ae123401295ae09f2be5dc072875b2
5
5
  SHA512:
6
- metadata.gz: 8d7e0cda0be190fbc8d4d40cbe27529b45cc2226e462154dfe2509b80acaf2295da8a1593f1832defd0003b7ff59971df9f416c82ab6adf2ef8094cb29a0aa9d
7
- data.tar.gz: d7fb5dfdb49af2eef1740528ef96a88801d0c8e66ef0dc025fc8c9e848456b414fa5187ce56bbaff7fdffdc3967cdb1272325e83ff39f287ffb8d7f8c7ec59e7
6
+ metadata.gz: 5c609aa069c11cc436de1d503cc165fca51bebad3f10024b81dd3a4b9774b96f14e1fe8b9c6bf3171ef6a38dd22584acca22fac2ea028c4eec6361ce4b17fdf9
7
+ data.tar.gz: 90662d68fa6fe592b284ec2767f21a59bd8d94571958e96cb7df50ce2aeb4e8c80daf26aa7afafd7551e66715e7ba938b8d22b103756aa3931ac74f1fe61d0ec
@@ -288,9 +288,16 @@ module Basecamp
288
288
  end
289
289
 
290
290
  def parse_token_response(status, body)
291
- data = JSON.parse(body)
291
+ # A failed request is classified from its status and OAuth error
292
+ # object BEFORE the body is parsed as a token: a refusal whose body is
293
+ # not JSON (a bare 401, an HTML error page) must surface as that
294
+ # refusal, not as a malformed token response.
295
+ handle_error_response(status, body) unless (200..299).cover?(status)
292
296
 
293
- handle_error_response(status, data) unless (200..299).cover?(status)
297
+ data = JSON.parse(body)
298
+ unless data.is_a?(Hash)
299
+ raise OauthError.new("api_error", "Token response is not a JSON object", http_status: status)
300
+ end
294
301
 
295
302
  unless data["access_token"].is_a?(String) && !data["access_token"].empty?
296
303
  raise OauthError.new(
@@ -344,20 +351,49 @@ module Basecamp
344
351
  ), cause: nil
345
352
  end
346
353
 
347
- def handle_error_response(status, data)
348
- error_msg = Basecamp::Security.truncate(data["error_description"] || data["error"] || "Token request failed")
349
-
350
- if status == 401 || data["error"] == "invalid_grant"
354
+ # The OAuth error codes a caller resolves by signing in again: the grant
355
+ # is invalid, expired or revoked (+invalid_grant+), the client failed to
356
+ # authenticate (+invalid_client+) or may not use this grant
357
+ # (+unauthorized_client+), or the resource owner refused
358
+ # (+access_denied+). Classified by code, not status: RFC 6749 §5.2
359
+ # answers +invalid_client+ with a 400 unless the client authenticated
360
+ # through the Authorization header, so a 401 cannot be the only signal.
361
+ # Matches every other SDK's token endpoint (conformance/oauth-token).
362
+ AUTH_ERROR_CODES = %w[invalid_grant invalid_client unauthorized_client access_denied].freeze
363
+
364
+ # Classifies a non-2xx token endpoint response: "auth" for an error code
365
+ # in AUTH_ERROR_CODES on any status, or for any 401 whatever its body
366
+ # (Launchpad and other servers refuse with a bare status); "api_error"
367
+ # for everything else. The body is read only for the RFC 6749 +error+
368
+ # and +error_description+ members and is never echoed — a body that is
369
+ # not an OAuth error object contributes nothing.
370
+ def handle_error_response(status, body)
371
+ data = oauth_error_object(body)
372
+ error_code = data["error"] if data["error"].is_a?(String)
373
+ description = data["error_description"] if data["error_description"].is_a?(String)
374
+ error_msg = Basecamp::Security.truncate(description || error_code || "Token request failed")
375
+
376
+ if status == 401 || AUTH_ERROR_CODES.include?(error_code)
351
377
  raise OauthError.new(
352
378
  "auth",
353
379
  error_msg,
354
380
  http_status: status,
355
- hint: "The authorization code or refresh token may be invalid or expired"
381
+ hint: "The authorization server refused this grant or client: authorize again, or check the client credentials"
356
382
  )
357
383
  end
358
384
 
359
385
  raise OauthError.new("api_error", error_msg, http_status: status)
360
386
  end
387
+
388
+ # The response body as an OAuth error object, or {} when it is not one.
389
+ # The parser error is discarded, never chained: its message embeds the
390
+ # offending input.
391
+ def oauth_error_object(body)
392
+ parsed = JSON.parse(body)
393
+ parsed.is_a?(Hash) ? parsed : {}
394
+ rescue JSON::ParserError
395
+ {}
396
+ end
361
397
  end
362
398
  end
363
399
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Basecamp
4
- VERSION = "0.23.0"
4
+ VERSION = "0.24.0"
5
5
  API_VERSION = "2026-09-15"
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: basecamp-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.23.0
4
+ version: 0.24.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Basecamp
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-05 00:00:00.000000000 Z
11
+ date: 2026-10-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday