basecamp-sdk 0.23.0 → 0.25.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: 7713550a3bc2d6b5dd732e3bdadb679d5016316e9f4004677c028a30daa65371
4
+ data.tar.gz: 71b74a2b499c484f245669d9b317df4f826c799f2d2bbda0cce140a75b22884f
5
5
  SHA512:
6
- metadata.gz: 8d7e0cda0be190fbc8d4d40cbe27529b45cc2226e462154dfe2509b80acaf2295da8a1593f1832defd0003b7ff59971df9f416c82ab6adf2ef8094cb29a0aa9d
7
- data.tar.gz: d7fb5dfdb49af2eef1740528ef96a88801d0c8e66ef0dc025fc8c9e848456b414fa5187ce56bbaff7fdffdc3967cdb1272325e83ff39f287ffb8d7f8c7ec59e7
6
+ metadata.gz: a5f430f32c8ef3e13f0b3690ec31223bc942f06bc907708fe33fbfa84b99a86dcfe56a2734ef0e8224f5cfdfdc41ce4c28703e8321c6ba88330bba4bedf775f0
7
+ data.tar.gz: 834ea9c748c9d64ce67089e765875bf8c6c56a52ce0d895629b1c02f4d6e2acbdcb63b566a563c19823ce1c3199016b02fba3c92136bbbd5cb67cf6d950a0744
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://basecamp.com/schemas/sdk-metadata.json",
3
3
  "version": "1.0.0",
4
- "generated": "2026-10-05T19:11:15Z",
4
+ "generated": "2026-10-15T16:04:42Z",
5
5
  "operations": {
6
6
  "GetAccount": {
7
7
  "retry": {
@@ -48,7 +48,7 @@ module Basecamp
48
48
  end
49
49
  end
50
50
 
51
- # List boosts on a specific event within a recording
51
+ # List boosts on a specific event within a recording. Only boostable events carry boosts: those whose action is completed, adopted, column_changed or moved_in (their JSON includes boosts_count and boosts_url). Any other event answers 403 Forbidden; an event on a recording the person cannot see answers 404.
52
52
  # @param recording_id [Integer] recording id ID
53
53
  # @param event_id [Integer] event id ID
54
54
  # @param page [Integer, nil] Page number for paginating through results. Defaults to 1. A positive value selects exactly that page, not a starting offset; see SPEC section 8.
@@ -61,7 +61,7 @@ module Basecamp
61
61
  end
62
62
  end
63
63
 
64
- # Create a boost on a specific event within a recording
64
+ # Create a boost on a specific event within a recording. Only boostable events accept one: those whose action is completed, adopted, column_changed or moved_in (their JSON includes boosts_count and boosts_url). Any other event answers 403 Forbidden, as does an event on a recording whose comments are closed; an event on a recording the person cannot see answers 404.
65
65
  # @param recording_id [Integer] recording id ID
66
66
  # @param event_id [Integer] event id ID
67
67
  # @param content [String] content
@@ -89,11 +89,14 @@ module Basecamp
89
89
  end
90
90
  end
91
91
 
92
- # Move a column within a card table
92
+ # Move a column to a slot within its card table. target_id is the card table's own id (the same as cardTableId), not a column; position is the 1-based slot among the columns, not counting Triage, Not Now or Done.
93
93
  # @param card_table_id [Integer] card table id ID
94
- # @param source_id [Integer] source id
95
- # @param target_id [Integer] target id
96
- # @param position [Integer, nil] position
94
+ # @param source_id [Integer] The column to move.
95
+ # @param target_id [Integer] The card table the column moves within: the same id as `cardTableId`.
96
+ # Basecamp refuses any other destination, a column included, with 422.
97
+ # @param position [Integer, nil] 1-based slot among the card table's columns, not counting Triage,
98
+ # Not Now or Done. 1 puts the column first; a slot past the last puts it
99
+ # last. Default: 1.
97
100
  # @return [void]
98
101
  def move(card_table_id:, source_id:, target_id:, position: nil)
99
102
  with_operation(service: "cardcolumns", operation: "move", is_mutation: true, resource_id: card_table_id) do
@@ -81,15 +81,14 @@ module Basecamp
81
81
  end
82
82
  end
83
83
 
84
- # Update an existing question
84
+ # Update an existing question's title, schedule or both. A schedule replaces the whole schedule, so send every part of it: one without frequency or time_of_day is refused, and one without start_date re-anchors the recurrence on today. Pausing and resuming are PauseQuestion and ResumeQuestion.
85
85
  # @param question_id [Integer] question id ID
86
- # @param title [String, nil] title
86
+ # @param title [String, nil] The question text. An update carries a title, a schedule or both.
87
87
  # @param schedule [Hash, nil] schedule
88
- # @param paused [Boolean, nil] paused
89
88
  # @return [Hash] response data
90
- def update_question(question_id:, title: nil, schedule: nil, paused: nil)
89
+ def update_question(question_id:, title: nil, schedule: nil)
91
90
  with_operation(service: "checkins", operation: "update_question", is_mutation: true, resource_id: question_id) do
92
- http_put("/questions/#{question_id}", body: compact_params(title: title, schedule: schedule, paused: paused)).json(operation: "UpdateQuestion")
91
+ http_put("/questions/#{question_id}", body: compact_params(title: title, schedule: schedule)).json(operation: "UpdateQuestion")
93
92
  end
94
93
  end
95
94
 
@@ -15,26 +15,24 @@ module Basecamp
15
15
  end
16
16
  end
17
17
 
18
- # Create a new lineup marker
18
+ # Create a new lineup marker. Returns the created marker.
19
19
  # @param name [String] name
20
20
  # @param date [String] date
21
- # @return [void]
21
+ # @return [Hash] response data
22
22
  def create(name:, date:)
23
23
  with_operation(service: "lineup", operation: "create", is_mutation: true) do
24
- http_post("/lineup/markers.json", body: compact_params(name: name, date: date))
25
- nil
24
+ http_post("/lineup/markers.json", body: compact_params(name: name, date: date)).json(operation: "CreateLineupMarker")
26
25
  end
27
26
  end
28
27
 
29
- # Update an existing lineup marker
28
+ # Update an existing lineup marker. Returns the updated marker.
30
29
  # @param marker_id [Integer] marker id ID
31
30
  # @param name [String, nil] name
32
31
  # @param date [String, nil] date
33
- # @return [void]
32
+ # @return [Hash] response data
34
33
  def update(marker_id:, name: nil, date: nil)
35
34
  with_operation(service: "lineup", operation: "update", is_mutation: true, resource_id: marker_id) do
36
- http_put("/lineup/markers/#{marker_id}", body: compact_params(name: name, date: date))
37
- nil
35
+ http_put("/lineup/markers/#{marker_id}", body: compact_params(name: name, date: date)).json(operation: "UpdateLineupMarker")
38
36
  end
39
37
  end
40
38
 
@@ -7,7 +7,7 @@ module Basecamp
7
7
  # @generated from OpenAPI spec
8
8
  class MyNotificationsService < BaseService
9
9
 
10
- # Get the current user's notification inbox (the "Hey!" menu).
10
+ # Get the current user's notifications (the "Hey!" menu in Basecamp 4).
11
11
  # @param page [Integer, nil] Page number for paginating through read items. Defaults to 1. This
12
12
  # operation is not auto-paginated in any SDK, so a page is returned as
13
13
  # asked for and later pages are not followed.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Auto-generated from OpenAPI spec. Do not edit manually.
4
- # Generated: 2026-10-05T19:11:15Z
4
+ # Generated: 2026-10-15T16:04:42Z
5
5
 
6
6
  require "json"
7
7
  require "time"
@@ -3299,7 +3299,7 @@ module Basecamp
3299
3299
  include TypeHelpers
3300
3300
  # @!attribute [rw] clientside
3301
3301
  # @deprecated This shape is deprecated since 2024-01: Use Client Visibility feature instead
3302
- attr_accessor :app_url, :created_at, :id, :name, :status, :updated_at, :url, :bookmark_url, :bookmarked, :client_company, :clients_enabled, :clientside, :description, :dock, :end_date, :purpose, :star_url, :starred, :start_date
3302
+ attr_accessor :app_url, :created_at, :id, :name, :status, :updated_at, :url, :bookmark_url, :bookmarked, :client_company, :clients_enabled, :clientside, :description, :dock, :end_date, :purpose, :star_url, :starred, :start_date, :timesheet_enabled
3303
3303
 
3304
3304
  # @return [Array<Symbol>]
3305
3305
  def self.required_fields
@@ -3326,6 +3326,7 @@ module Basecamp
3326
3326
  @star_url = data["star_url"]
3327
3327
  @starred = parse_boolean(data["starred"])
3328
3328
  @start_date = data["start_date"]
3329
+ @timesheet_enabled = parse_boolean(data["timesheet_enabled"])
3329
3330
  end
3330
3331
 
3331
3332
  def to_h
@@ -3349,6 +3350,7 @@ module Basecamp
3349
3350
  "star_url" => @star_url,
3350
3351
  "starred" => @starred,
3351
3352
  "start_date" => @start_date,
3353
+ "timesheet_enabled" => @timesheet_enabled,
3352
3354
  }.compact
3353
3355
  end
3354
3356
 
@@ -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.25.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.25.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-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday