basecamp-sdk 0.9.0 → 0.11.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 +13 -8
- data/lib/basecamp/config.rb +4 -3
- data/lib/basecamp/generated/metadata.json +280 -12
- data/lib/basecamp/generated/services/account_service.rb +1 -1
- data/lib/basecamp/generated/services/automation_service.rb +2 -2
- data/lib/basecamp/generated/services/boosts_service.rb +3 -3
- 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 +4 -4
- 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/events_service.rb +1 -1
- data/lib/basecamp/generated/services/everything_service.rb +169 -0
- 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 +20 -15
- data/lib/basecamp/generated/services/messages_service.rb +2 -2
- data/lib/basecamp/generated/services/my_assignments_service.rb +5 -5
- data/lib/basecamp/generated/services/my_notifications_service.rb +16 -2
- data/lib/basecamp/generated/services/people_service.rb +9 -9
- data/lib/basecamp/generated/services/projects_service.rb +2 -2
- data/lib/basecamp/generated/services/recordings_service.rb +3 -3
- data/lib/basecamp/generated/services/reports_service.rb +5 -5
- data/lib/basecamp/generated/services/schedules_service.rb +13 -5
- 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 +5 -5
- 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 +2 -2
- data/lib/basecamp/generated/services/todosets_service.rb +1 -1
- data/lib/basecamp/generated/services/tools_service.rb +4 -3
- 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 +374 -18
- data/lib/basecamp/http.rb +47 -18
- 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/services/cards_extensions.rb +67 -0
- data/lib/basecamp/version.rb +2 -2
- data/lib/basecamp.rb +5 -0
- data/scripts/generate-services.rb +24 -6
- data/scripts/generate-types.rb +29 -2
- metadata +7 -2
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Basecamp
|
|
4
|
+
module Oauth
|
|
5
|
+
# A terminal RFC 8628 device-flow outcome. Carries a +reason+; the parent
|
|
6
|
+
# {OauthError} +type+ is DERIVED from that reason (SPEC.md §16) so callers can
|
|
7
|
+
# branch on either the precise +reason+ or the coarse +type+.
|
|
8
|
+
#
|
|
9
|
+
# | reason | parent type |
|
|
10
|
+
# |------------------|----------------------|
|
|
11
|
+
# | +:access_denied+ | +auth+ |
|
|
12
|
+
# | +:expired+ | +auth+ |
|
|
13
|
+
# | +:transport+ | +network+ (retryable)|
|
|
14
|
+
# | +:unavailable+ | +validation+ |
|
|
15
|
+
# | +:cancelled+ | +usage+ |
|
|
16
|
+
#
|
|
17
|
+
# @attr reason [Symbol] the device-flow termination reason
|
|
18
|
+
class DeviceFlowError < OauthError
|
|
19
|
+
# Maps a device-flow reason to its parent {OauthError} type.
|
|
20
|
+
REASON_TYPES = {
|
|
21
|
+
access_denied: "auth",
|
|
22
|
+
expired: "auth",
|
|
23
|
+
transport: "network",
|
|
24
|
+
unavailable: "validation",
|
|
25
|
+
cancelled: "usage"
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
attr_reader :reason
|
|
29
|
+
|
|
30
|
+
# @param reason [Symbol] the device-flow termination reason
|
|
31
|
+
# @param message [String] human-readable description
|
|
32
|
+
# @param http_status [Integer, nil] HTTP status code, if applicable
|
|
33
|
+
# @param hint [String, nil] helpful hint for resolving the error
|
|
34
|
+
def initialize(reason, message, http_status: nil, hint: nil)
|
|
35
|
+
super(
|
|
36
|
+
REASON_TYPES.fetch(reason, "api_error"),
|
|
37
|
+
message,
|
|
38
|
+
http_status: http_status,
|
|
39
|
+
hint: hint,
|
|
40
|
+
retryable: reason == :transport
|
|
41
|
+
)
|
|
42
|
+
@reason = reason
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -18,26 +18,28 @@ module Basecamp
|
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
# @param http_client [Faraday::Connection, nil]
|
|
22
|
-
#
|
|
21
|
+
# @param http_client [Faraday::Connection, nil] injected Faraday client
|
|
22
|
+
# (kept, verified redirect-free); nil selects the default headers-first
|
|
23
|
+
# Net::HTTP transport ({Fetcher.stream_http})
|
|
24
|
+
# @param timeout [Numeric] request timeout in seconds (default: 10); an
|
|
25
|
+
# invalid value falls back via {Fetcher.normalize_timeout}
|
|
23
26
|
# @param max_body_bytes [Integer] bounded read cap in bytes
|
|
24
27
|
def initialize(http_client: nil, timeout: 10, max_body_bytes: Fetcher::DEFAULT_MAX_BODY_BYTES)
|
|
25
28
|
Fetcher.ensure_redirects_suppressed!(http_client) if http_client
|
|
26
|
-
# Normalize before
|
|
29
|
+
# Normalize before storing (nil selects the headers-first Net::HTTP
|
|
30
|
+
# transport; an injected client is kept) and before the fetch computes its
|
|
27
31
|
# wall-clock deadline: a non-finite/non-positive timeout must not disable
|
|
28
32
|
# either bound (see Fetcher.normalize_timeout).
|
|
29
33
|
@timeout = Fetcher.normalize_timeout(timeout)
|
|
30
|
-
|
|
34
|
+
# nil selects the headers-first {Fetcher.stream_http} transport (total
|
|
35
|
+
# wall-clock bound incl. the header phase); an injected connection keeps
|
|
36
|
+
# the Faraday path, verified redirect-free above.
|
|
37
|
+
@http_client = http_client
|
|
31
38
|
# Normalize the public cap to a finite non-negative Integer: a nil, float,
|
|
32
39
|
# or Float::INFINITY would otherwise disable the streaming memory bound
|
|
33
40
|
# (an infinite/undefined cap never trips +total > max_body_bytes+),
|
|
34
|
-
# reintroducing an SSRF/OOM risk.
|
|
35
|
-
@max_body_bytes =
|
|
36
|
-
if max_body_bytes.is_a?(Integer) && max_body_bytes >= 0
|
|
37
|
-
max_body_bytes
|
|
38
|
-
else
|
|
39
|
-
Fetcher::DEFAULT_MAX_BODY_BYTES
|
|
40
|
-
end
|
|
41
|
+
# reintroducing an SSRF/OOM risk. Shared with Resource and the device flow.
|
|
42
|
+
@max_body_bytes = Fetcher.normalize_body_cap(max_body_bytes)
|
|
41
43
|
end
|
|
42
44
|
|
|
43
45
|
# Discovers OAuth configuration from
|
|
@@ -134,6 +134,10 @@ module Basecamp
|
|
|
134
134
|
params["refresh_token"] = request.refresh_token
|
|
135
135
|
params["client_id"] = request.client_id if request.client_id
|
|
136
136
|
params["client_secret"] = request.client_secret if request.client_secret
|
|
137
|
+
# An empty string is truthy in Ruby but an empty resource is not a
|
|
138
|
+
# binding — treat it as unset (omit) per the send-only-when-set
|
|
139
|
+
# contract, matching Go/TS/Kotlin.
|
|
140
|
+
params["resource"] = request.resource unless request.resource.to_s.empty?
|
|
137
141
|
|
|
138
142
|
params
|
|
139
143
|
end
|
|
@@ -161,21 +165,56 @@ module Basecamp
|
|
|
161
165
|
|
|
162
166
|
handle_error_response(response.status, data) unless response.success?
|
|
163
167
|
|
|
164
|
-
|
|
168
|
+
unless data["access_token"].is_a?(String) && !data["access_token"].empty?
|
|
169
|
+
raise OauthError.new(
|
|
170
|
+
"api_error", "Token response missing or non-string access_token",
|
|
171
|
+
http_status: response.status
|
|
172
|
+
)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# resource: absent and JSON null are unset; when present it must be a
|
|
176
|
+
# non-empty string (SPEC §16) — an empty binding is not a binding.
|
|
177
|
+
resource = data["resource"]
|
|
178
|
+
unless resource.nil? || (resource.is_a?(String) && !resource.empty?)
|
|
179
|
+
raise OauthError.new(
|
|
180
|
+
"api_error",
|
|
181
|
+
"Token response resource must be a non-empty string when present",
|
|
182
|
+
http_status: response.status
|
|
183
|
+
)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# token_type: absent/JSON-null defaults to Bearer; present must be a
|
|
187
|
+
# non-empty String ("" is truthy in Ruby, so || alone would admit it) —
|
|
188
|
+
# matching the device-flow parser and SPEC §16.
|
|
189
|
+
token_type = data["token_type"]
|
|
190
|
+
unless token_type.nil? || (token_type.is_a?(String) && !token_type.empty?)
|
|
191
|
+
raise OauthError.new(
|
|
192
|
+
"api_error",
|
|
193
|
+
"Token response token_type must be a non-empty string when present",
|
|
194
|
+
http_status: response.status
|
|
195
|
+
)
|
|
196
|
+
end
|
|
165
197
|
|
|
166
198
|
Token.new(
|
|
167
199
|
access_token: data["access_token"],
|
|
168
200
|
refresh_token: data["refresh_token"],
|
|
169
|
-
token_type:
|
|
201
|
+
token_type: token_type || "Bearer",
|
|
170
202
|
expires_in: data["expires_in"],
|
|
171
|
-
scope: data["scope"]
|
|
203
|
+
scope: data["scope"],
|
|
204
|
+
resource: resource
|
|
172
205
|
)
|
|
173
206
|
rescue JSON::ParserError
|
|
207
|
+
# A token response that fails to parse may still contain credential
|
|
208
|
+
# material (a syntactically-broken body carrying an access_token) —
|
|
209
|
+
# never echo ANY of it into an error message, where it would reach
|
|
210
|
+
# logs and exception telemetry. The status is diagnosis enough.
|
|
211
|
+
# cause: nil — the parser error's message embeds the offending input,
|
|
212
|
+
# so the implicit cause chain would leak it via full_message.
|
|
174
213
|
raise OauthError.new(
|
|
175
214
|
"api_error",
|
|
176
|
-
"Failed to parse token response
|
|
215
|
+
"Failed to parse token response",
|
|
177
216
|
http_status: response.status
|
|
178
|
-
)
|
|
217
|
+
), cause: nil
|
|
179
218
|
end
|
|
180
219
|
|
|
181
220
|
def handle_error_response(status, data)
|