basecamp-sdk 0.10.0 → 0.12.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 +4 -4
- data/README.md +106 -10
- data/lib/basecamp/client.rb +28 -8
- data/lib/basecamp/config.rb +4 -3
- data/lib/basecamp/generated/metadata.json +178 -17
- data/lib/basecamp/generated/services/account_service.rb +1 -1
- data/lib/basecamp/generated/services/automation_service.rb +1 -1
- data/lib/basecamp/generated/services/bookmarks_service.rb +49 -0
- data/lib/basecamp/generated/services/boosts_service.rb +3 -3
- data/lib/basecamp/generated/services/calendars_service.rb +30 -0
- data/lib/basecamp/generated/services/campfires_service.rb +7 -7
- data/lib/basecamp/generated/services/card_columns_service.rb +1 -1
- data/lib/basecamp/generated/services/card_steps_service.rb +1 -1
- data/lib/basecamp/generated/services/card_tables_service.rb +1 -1
- data/lib/basecamp/generated/services/cards_service.rb +2 -2
- data/lib/basecamp/generated/services/checkins_service.rb +8 -8
- data/lib/basecamp/generated/services/client_approvals_service.rb +2 -2
- data/lib/basecamp/generated/services/client_correspondences_service.rb +2 -2
- data/lib/basecamp/generated/services/client_replies_service.rb +2 -2
- data/lib/basecamp/generated/services/comments_service.rb +2 -2
- data/lib/basecamp/generated/services/documents_service.rb +2 -2
- data/lib/basecamp/generated/services/drafts_service.rb +21 -0
- data/lib/basecamp/generated/services/events_service.rb +1 -1
- data/lib/basecamp/generated/services/everything_service.rb +69 -46
- data/lib/basecamp/generated/services/forwards_service.rb +5 -5
- data/lib/basecamp/generated/services/gauges_service.rb +3 -3
- data/lib/basecamp/generated/services/hill_charts_service.rb +1 -1
- data/lib/basecamp/generated/services/message_boards_service.rb +1 -1
- data/lib/basecamp/generated/services/message_types_service.rb +2 -2
- data/lib/basecamp/generated/services/messages_service.rb +2 -2
- data/lib/basecamp/generated/services/my_assignments_service.rb +34 -3
- data/lib/basecamp/generated/services/my_notes_service.rb +28 -0
- data/lib/basecamp/generated/services/my_notifications_service.rb +2 -2
- data/lib/basecamp/generated/services/people_service.rb +8 -8
- data/lib/basecamp/generated/services/projects_service.rb +2 -2
- data/lib/basecamp/generated/services/recordings_service.rb +2 -2
- data/lib/basecamp/generated/services/reports_service.rb +5 -5
- data/lib/basecamp/generated/services/schedules_service.rb +4 -4
- data/lib/basecamp/generated/services/search_service.rb +2 -2
- data/lib/basecamp/generated/services/subscriptions_service.rb +1 -1
- data/lib/basecamp/generated/services/templates_service.rb +3 -3
- data/lib/basecamp/generated/services/timeline_service.rb +1 -1
- data/lib/basecamp/generated/services/timesheets_service.rb +4 -4
- data/lib/basecamp/generated/services/todolist_groups_service.rb +1 -1
- data/lib/basecamp/generated/services/todolists_service.rb +2 -2
- data/lib/basecamp/generated/services/todos_service.rb +19 -2
- data/lib/basecamp/generated/services/todosets_service.rb +1 -1
- data/lib/basecamp/generated/services/tools_service.rb +1 -1
- data/lib/basecamp/generated/services/uploads_service.rb +3 -3
- data/lib/basecamp/generated/services/vaults_service.rb +2 -2
- data/lib/basecamp/generated/services/webhooks_service.rb +2 -2
- data/lib/basecamp/generated/types.rb +313 -34
- data/lib/basecamp/http.rb +47 -19
- data/lib/basecamp/oauth/device_authorization.rb +37 -0
- data/lib/basecamp/oauth/device_flow.rb +873 -0
- data/lib/basecamp/oauth/device_flow_error.rb +46 -0
- data/lib/basecamp/oauth/discovery.rb +13 -11
- data/lib/basecamp/oauth/exchange.rb +44 -5
- data/lib/basecamp/oauth/fetcher.rb +626 -56
- data/lib/basecamp/oauth/oauth_error.rb +3 -1
- data/lib/basecamp/oauth/refresh_request.rb +8 -2
- data/lib/basecamp/oauth/resource.rb +13 -10
- data/lib/basecamp/oauth/token.rb +10 -3
- data/lib/basecamp/oauth.rb +47 -2
- data/lib/basecamp/version.rb +2 -2
- data/scripts/generate-services.rb +10 -7
- metadata +9 -2
|
@@ -4,7 +4,9 @@ module Basecamp
|
|
|
4
4
|
module Oauth
|
|
5
5
|
# OAuth-specific error class.
|
|
6
6
|
#
|
|
7
|
-
# @attr type [String] Error type ("validation", "auth", "network",
|
|
7
|
+
# @attr type [String] Error type ("validation", "auth", "network",
|
|
8
|
+
# "api_error", or "usage" — the parent type {DeviceFlowError} derives for
|
|
9
|
+
# a cancelled device flow)
|
|
8
10
|
# @attr http_status [Integer, nil] HTTP status code if applicable
|
|
9
11
|
# @attr hint [String, nil] Helpful hint for resolving the error
|
|
10
12
|
# @attr retryable [Boolean] Whether the request can be retried
|
|
@@ -9,19 +9,25 @@ module Basecamp
|
|
|
9
9
|
# @attr client_id [String, nil] The client identifier (optional)
|
|
10
10
|
# @attr client_secret [String, nil] The client secret (optional)
|
|
11
11
|
# @attr use_legacy_format [Boolean] Use Launchpad's non-standard token format
|
|
12
|
+
# @attr resource [String, nil] RFC 8707 resource indicator to bind the
|
|
13
|
+
# refreshed token to, sent only when set. Echo the stored token's
|
|
14
|
+
# resource: BC5 multi-account refresh tokens hard-require it (SPEC §16).
|
|
15
|
+
# Appended last so earlier members keep their positions.
|
|
12
16
|
RefreshRequest = Data.define(
|
|
13
17
|
:token_endpoint,
|
|
14
18
|
:refresh_token,
|
|
15
19
|
:client_id,
|
|
16
20
|
:client_secret,
|
|
17
|
-
:use_legacy_format
|
|
21
|
+
:use_legacy_format,
|
|
22
|
+
:resource
|
|
18
23
|
) do
|
|
19
24
|
def initialize(
|
|
20
25
|
token_endpoint:,
|
|
21
26
|
refresh_token:,
|
|
22
27
|
client_id: nil,
|
|
23
28
|
client_secret: nil,
|
|
24
|
-
use_legacy_format: false
|
|
29
|
+
use_legacy_format: false,
|
|
30
|
+
resource: nil
|
|
25
31
|
)
|
|
26
32
|
super
|
|
27
33
|
end
|
|
@@ -5,25 +5,28 @@ module Basecamp
|
|
|
5
5
|
# Fetches RFC 9728 protected-resource metadata (hop 1 of resource-first
|
|
6
6
|
# discovery) and binds the returned +resource+ to the requested origin.
|
|
7
7
|
class Resource
|
|
8
|
-
# @param http_client [Faraday::Connection, nil]
|
|
9
|
-
#
|
|
8
|
+
# @param http_client [Faraday::Connection, nil] injected Faraday client
|
|
9
|
+
# (kept, verified redirect-free); nil selects the default headers-first
|
|
10
|
+
# Net::HTTP transport ({Fetcher.stream_http})
|
|
11
|
+
# @param timeout [Numeric] request timeout in seconds (default: 10); an
|
|
12
|
+
# invalid value falls back via {Fetcher.normalize_timeout}
|
|
10
13
|
# @param max_body_bytes [Integer] bounded read cap in bytes
|
|
11
14
|
def initialize(http_client: nil, timeout: 10, max_body_bytes: Fetcher::DEFAULT_MAX_BODY_BYTES)
|
|
12
15
|
Fetcher.ensure_redirects_suppressed!(http_client) if http_client
|
|
13
|
-
# Normalize before
|
|
16
|
+
# Normalize before storing (nil selects the headers-first Net::HTTP
|
|
17
|
+
# transport; an injected client is kept) and before the fetch computes its
|
|
14
18
|
# wall-clock deadline: a non-finite/non-positive timeout must not disable
|
|
15
19
|
# either bound (see Fetcher.normalize_timeout).
|
|
16
20
|
@timeout = Fetcher.normalize_timeout(timeout)
|
|
17
|
-
|
|
21
|
+
# nil selects the headers-first {Fetcher.stream_http} transport (total
|
|
22
|
+
# wall-clock bound incl. the header phase); an injected connection keeps
|
|
23
|
+
# the Faraday path, verified redirect-free above.
|
|
24
|
+
@http_client = http_client
|
|
18
25
|
# Normalize the public cap to a finite non-negative Integer: a nil, float,
|
|
19
26
|
# or Float::INFINITY would otherwise disable the streaming memory bound
|
|
20
27
|
# (an infinite/undefined cap never trips), reintroducing an SSRF/OOM risk.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
max_body_bytes
|
|
24
|
-
else
|
|
25
|
-
Fetcher::DEFAULT_MAX_BODY_BYTES
|
|
26
|
-
end
|
|
28
|
+
# Shared with Discovery and the device flow.
|
|
29
|
+
@max_body_bytes = Fetcher.normalize_body_cap(max_body_bytes)
|
|
27
30
|
end
|
|
28
31
|
|
|
29
32
|
# Discovers protected-resource metadata from
|
data/lib/basecamp/oauth/token.rb
CHANGED
|
@@ -10,13 +10,18 @@ module Basecamp
|
|
|
10
10
|
# @attr expires_in [Integer, nil] Lifetime of the access token in seconds
|
|
11
11
|
# @attr expires_at [Time, nil] Calculated expiration time
|
|
12
12
|
# @attr scope [String, nil] OAuth scope granted
|
|
13
|
+
# @attr resource [String, nil] RFC 8707 resource indicator the token is
|
|
14
|
+
# bound to (BC5: +urn:bc:account:<id>+). Echo it when refreshing —
|
|
15
|
+
# BC5 multi-account refresh tokens reject a refresh without it
|
|
16
|
+
# (SPEC §16). Appended last so earlier members keep their positions.
|
|
13
17
|
Token = Data.define(
|
|
14
18
|
:access_token,
|
|
15
19
|
:token_type,
|
|
16
20
|
:refresh_token,
|
|
17
21
|
:expires_in,
|
|
18
22
|
:expires_at,
|
|
19
|
-
:scope
|
|
23
|
+
:scope,
|
|
24
|
+
:resource
|
|
20
25
|
) do
|
|
21
26
|
def initialize(
|
|
22
27
|
access_token:,
|
|
@@ -24,7 +29,8 @@ module Basecamp
|
|
|
24
29
|
refresh_token: nil,
|
|
25
30
|
expires_in: nil,
|
|
26
31
|
expires_at: nil,
|
|
27
|
-
scope: nil
|
|
32
|
+
scope: nil,
|
|
33
|
+
resource: nil
|
|
28
34
|
)
|
|
29
35
|
# Calculate expires_at from expires_in if not provided
|
|
30
36
|
calculated_expires_at = expires_at || (expires_in ? Time.now + expires_in : nil)
|
|
@@ -34,7 +40,8 @@ module Basecamp
|
|
|
34
40
|
refresh_token: refresh_token,
|
|
35
41
|
expires_in: expires_in,
|
|
36
42
|
expires_at: calculated_expires_at,
|
|
37
|
-
scope: scope
|
|
43
|
+
scope: scope,
|
|
44
|
+
resource: resource
|
|
38
45
|
)
|
|
39
46
|
end
|
|
40
47
|
|
data/lib/basecamp/oauth.rb
CHANGED
|
@@ -133,12 +133,12 @@ module Basecamp
|
|
|
133
133
|
def self.refresh_token(
|
|
134
134
|
token_endpoint:, refresh_token:,
|
|
135
135
|
client_id: nil, client_secret: nil,
|
|
136
|
-
use_legacy_format: false, timeout: 30
|
|
136
|
+
use_legacy_format: false, timeout: 30, resource: nil
|
|
137
137
|
)
|
|
138
138
|
request = RefreshRequest.new(
|
|
139
139
|
token_endpoint: token_endpoint, refresh_token: refresh_token,
|
|
140
140
|
client_id: client_id, client_secret: client_secret,
|
|
141
|
-
use_legacy_format: use_legacy_format
|
|
141
|
+
use_legacy_format: use_legacy_format, resource: resource
|
|
142
142
|
)
|
|
143
143
|
Exchange.new(timeout: timeout).refresh(request)
|
|
144
144
|
end
|
|
@@ -147,6 +147,51 @@ module Basecamp
|
|
|
147
147
|
token.expired?(buffer_seconds)
|
|
148
148
|
end
|
|
149
149
|
|
|
150
|
+
# Requests an RFC 8628 device/user code pair. Thin delegator to
|
|
151
|
+
# {DeviceFlow.request_device_authorization}.
|
|
152
|
+
#
|
|
153
|
+
# @return [DeviceAuthorization]
|
|
154
|
+
def self.request_device_authorization(device_authorization_endpoint:, client_id:, scope: nil, timeout: DeviceFlow::DEVICE_REQUEST_TIMEOUT)
|
|
155
|
+
DeviceFlow.request_device_authorization(
|
|
156
|
+
device_authorization_endpoint: device_authorization_endpoint,
|
|
157
|
+
client_id: client_id, scope: scope, timeout: timeout
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Polls the token endpoint for a device grant (RFC 8628 §3.5). Thin delegator
|
|
162
|
+
# to {DeviceFlow.poll_device_token}; +clock+, +sleeper+, and +cancelled+ are
|
|
163
|
+
# injectable for tests.
|
|
164
|
+
#
|
|
165
|
+
# @return [Token]
|
|
166
|
+
def self.poll_device_token(
|
|
167
|
+
token_endpoint:, client_id:, device_code:, interval:, expires_in:,
|
|
168
|
+
clock: DeviceFlow::DEFAULT_CLOCK, sleeper: DeviceFlow::DEFAULT_SLEEPER,
|
|
169
|
+
cancelled: DeviceFlow::DEFAULT_CANCELLED, timeout: DeviceFlow::DEVICE_REQUEST_TIMEOUT,
|
|
170
|
+
deadline_at: nil
|
|
171
|
+
)
|
|
172
|
+
DeviceFlow.poll_device_token(
|
|
173
|
+
token_endpoint: token_endpoint, client_id: client_id,
|
|
174
|
+
device_code: device_code, interval: interval, expires_in: expires_in,
|
|
175
|
+
clock: clock, sleeper: sleeper, cancelled: cancelled, timeout: timeout,
|
|
176
|
+
deadline_at: deadline_at
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Runs the full device authorization grant against an already-selected
|
|
181
|
+
# {Config} (RFC 8628). Thin delegator to {DeviceFlow.perform_device_login}.
|
|
182
|
+
#
|
|
183
|
+
# @return [Token]
|
|
184
|
+
def self.perform_device_login(
|
|
185
|
+
config:, client_id:, display:, scope: nil,
|
|
186
|
+
clock: DeviceFlow::DEFAULT_CLOCK, sleeper: DeviceFlow::DEFAULT_SLEEPER,
|
|
187
|
+
cancelled: DeviceFlow::DEFAULT_CANCELLED, timeout: DeviceFlow::DEVICE_REQUEST_TIMEOUT
|
|
188
|
+
)
|
|
189
|
+
DeviceFlow.perform_device_login(
|
|
190
|
+
config: config, client_id: client_id, display: display, scope: scope,
|
|
191
|
+
clock: clock, sleeper: sleeper, cancelled: cancelled, timeout: timeout
|
|
192
|
+
)
|
|
193
|
+
end
|
|
194
|
+
|
|
150
195
|
# Selects an issuer from the advertised set and — once a BC5 issuer is
|
|
151
196
|
# committed — binds its AS metadata. From this point on, every failure is
|
|
152
197
|
# fatal: no Launchpad request may be issued.
|
data/lib/basecamp/version.rb
CHANGED
|
@@ -116,7 +116,7 @@ class ServiceGenerator
|
|
|
116
116
|
'ClientVisibility' => %w[SetClientVisibility]
|
|
117
117
|
},
|
|
118
118
|
'Todos' => {
|
|
119
|
-
'Todos' => %w[ListTodos CreateTodo GetTodo ReplaceTodo CompleteTodo UncompleteTodo TrashTodo],
|
|
119
|
+
'Todos' => %w[ListTodos CreateTodo CreateTodosetTodo GetTodo ReplaceTodo CompleteTodo UncompleteTodo TrashTodo],
|
|
120
120
|
'Todolists' => %w[GetTodolistOrGroup UpdateTodolistOrGroup ListTodolists CreateTodolist RepositionTodolist],
|
|
121
121
|
'Todosets' => %w[GetTodoset],
|
|
122
122
|
'HillCharts' => %w[GetHillChart UpdateHillChartSettings],
|
|
@@ -818,9 +818,9 @@ class ServiceGenerator
|
|
|
818
818
|
if op[:query_params].any?
|
|
819
819
|
param_names = op[:query_params].map { |q| "#{to_snake_case(q[:name])}: #{to_snake_case(q[:name])}" }
|
|
820
820
|
lines << " params = compact_query_params(#{param_names.join(', ')})"
|
|
821
|
-
lines << " paginate(#{path_expr}, params: params)"
|
|
821
|
+
lines << " paginate(#{path_expr}, params: params, operation: \"#{op[:operation_id]}\")"
|
|
822
822
|
else
|
|
823
|
-
lines << " paginate(#{path_expr})"
|
|
823
|
+
lines << " paginate(#{path_expr}, operation: \"#{op[:operation_id]}\")"
|
|
824
824
|
end
|
|
825
825
|
|
|
826
826
|
lines
|
|
@@ -832,9 +832,9 @@ class ServiceGenerator
|
|
|
832
832
|
if op[:query_params].any?
|
|
833
833
|
param_names = op[:query_params].map { |q| "#{to_snake_case(q[:name])}: #{to_snake_case(q[:name])}" }
|
|
834
834
|
lines << " params = compact_query_params(#{param_names.join(', ')})"
|
|
835
|
-
lines << " paginate_wrapped(#{path_expr}, key: \"#{pagination_key}\", params: params)"
|
|
835
|
+
lines << " paginate_wrapped(#{path_expr}, key: \"#{pagination_key}\", params: params, operation: \"#{op[:operation_id]}\")"
|
|
836
836
|
else
|
|
837
|
-
lines << " paginate_wrapped(#{path_expr}, key: \"#{pagination_key}\")"
|
|
837
|
+
lines << " paginate_wrapped(#{path_expr}, key: \"#{pagination_key}\", operation: \"#{op[:operation_id]}\")"
|
|
838
838
|
end
|
|
839
839
|
|
|
840
840
|
lines
|
|
@@ -843,6 +843,9 @@ class ServiceGenerator
|
|
|
843
843
|
def generate_get_method_body(op, path_expr)
|
|
844
844
|
lines = []
|
|
845
845
|
http_method = op[:http_method].downcase
|
|
846
|
+
# Only GETs are retry-governed in Ruby, and only Http#get accepts the
|
|
847
|
+
# operation keyword — mutations go through single_request with no retry.
|
|
848
|
+
operation_arg = http_method == 'get' ? ", operation: \"#{op[:operation_id]}\"" : ''
|
|
846
849
|
|
|
847
850
|
if op[:has_binary_body]
|
|
848
851
|
# Binary upload - use raw body and set Content-Type header
|
|
@@ -863,9 +866,9 @@ class ServiceGenerator
|
|
|
863
866
|
lines << " http_#{http_method}(#{path_expr}, body: #{body_expr}).json"
|
|
864
867
|
elsif op[:query_params].any?
|
|
865
868
|
param_names = op[:query_params].map { |q| "#{to_snake_case(q[:name])}: #{to_snake_case(q[:name])}" }
|
|
866
|
-
lines << " http_#{http_method}(#{path_expr}, params: compact_query_params(#{param_names.join(', ')})).json"
|
|
869
|
+
lines << " http_#{http_method}(#{path_expr}, params: compact_query_params(#{param_names.join(', ')})#{operation_arg}).json"
|
|
867
870
|
else
|
|
868
|
-
lines << " http_#{http_method}(#{path_expr}).json"
|
|
871
|
+
lines << " http_#{http_method}(#{path_expr}#{operation_arg}).json"
|
|
869
872
|
end
|
|
870
873
|
|
|
871
874
|
lines
|
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.
|
|
4
|
+
version: 0.12.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-
|
|
11
|
+
date: 2026-08-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -182,7 +182,9 @@ files:
|
|
|
182
182
|
- lib/basecamp/generated/services/attachments_service.rb
|
|
183
183
|
- lib/basecamp/generated/services/automation_service.rb
|
|
184
184
|
- lib/basecamp/generated/services/base_service.rb
|
|
185
|
+
- lib/basecamp/generated/services/bookmarks_service.rb
|
|
185
186
|
- lib/basecamp/generated/services/boosts_service.rb
|
|
187
|
+
- lib/basecamp/generated/services/calendars_service.rb
|
|
186
188
|
- lib/basecamp/generated/services/campfires_service.rb
|
|
187
189
|
- lib/basecamp/generated/services/card_columns_service.rb
|
|
188
190
|
- lib/basecamp/generated/services/card_steps_service.rb
|
|
@@ -195,6 +197,7 @@ files:
|
|
|
195
197
|
- lib/basecamp/generated/services/client_visibility_service.rb
|
|
196
198
|
- lib/basecamp/generated/services/comments_service.rb
|
|
197
199
|
- lib/basecamp/generated/services/documents_service.rb
|
|
200
|
+
- lib/basecamp/generated/services/drafts_service.rb
|
|
198
201
|
- lib/basecamp/generated/services/events_service.rb
|
|
199
202
|
- lib/basecamp/generated/services/everything_service.rb
|
|
200
203
|
- lib/basecamp/generated/services/forwards_service.rb
|
|
@@ -205,6 +208,7 @@ files:
|
|
|
205
208
|
- lib/basecamp/generated/services/message_types_service.rb
|
|
206
209
|
- lib/basecamp/generated/services/messages_service.rb
|
|
207
210
|
- lib/basecamp/generated/services/my_assignments_service.rb
|
|
211
|
+
- lib/basecamp/generated/services/my_notes_service.rb
|
|
208
212
|
- lib/basecamp/generated/services/my_notifications_service.rb
|
|
209
213
|
- lib/basecamp/generated/services/people_service.rb
|
|
210
214
|
- lib/basecamp/generated/services/projects_service.rb
|
|
@@ -234,6 +238,9 @@ files:
|
|
|
234
238
|
- lib/basecamp/not_found_error.rb
|
|
235
239
|
- lib/basecamp/oauth.rb
|
|
236
240
|
- lib/basecamp/oauth/config.rb
|
|
241
|
+
- lib/basecamp/oauth/device_authorization.rb
|
|
242
|
+
- lib/basecamp/oauth/device_flow.rb
|
|
243
|
+
- lib/basecamp/oauth/device_flow_error.rb
|
|
237
244
|
- lib/basecamp/oauth/discovery.rb
|
|
238
245
|
- lib/basecamp/oauth/discovery_result.rb
|
|
239
246
|
- lib/basecamp/oauth/discovery_selection_error.rb
|