boldsign 0.3.0 → 0.5.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/CHANGELOG.md +48 -1
- data/README.md +36 -7
- data/lib/boldsign/access_token.rb +95 -0
- data/lib/boldsign/case_convert.rb +23 -14
- data/lib/boldsign/client.rb +68 -10
- data/lib/boldsign/resources/document.rb +22 -7
- data/lib/boldsign/version.rb +1 -1
- data/lib/boldsign.rb +38 -4
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9fc744443078d2ffab4156204b7921ef617c3788d373253956a0fe9f3455a14a
|
|
4
|
+
data.tar.gz: 133c334403bb3a2655f61ab7c14f8d9d75c9e3adf5212f9bdf7e69811a2cda2a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 96e5622f76bf23b2df6be9663542fb0db5da070f40480e6649960da33d336c14afcafc36c308b5dad867fa4ce90ab3faf574c3c956011c330c7599713cf3afe2
|
|
7
|
+
data.tar.gz: 640539d3b260f4b50de92bddca66ed45a794c0c54c722d32d807134eee505d06f737542d077403ab71f5699f23e106567d1bf19dfb4954420398904e9cd756e2
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] — 2026-06-02
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- OAuth 2.0 authentication via the **client credentials** grant. Configure
|
|
14
|
+
`client_id` / `client_secret` (on `Boldsign.configure` or `Client.new`) and
|
|
15
|
+
the client fetches a bearer token from the region's account host
|
|
16
|
+
(`https://account.boldsign.com/connect/token` for `:us`), caches it until
|
|
17
|
+
shortly before `expires_in` lapses, and sends `Authorization: Bearer …`
|
|
18
|
+
instead of `X-API-KEY`. Token fetches are mutex-guarded for shared clients.
|
|
19
|
+
- `scope:` (defaults to `BoldSign.Documents.All`) and `token_url:` options to
|
|
20
|
+
override the requested scope and token endpoint.
|
|
21
|
+
- `access_token:` option to supply a pre-obtained bearer token that is used
|
|
22
|
+
as-is (not refreshed).
|
|
23
|
+
- `Boldsign::AccessToken` encapsulating the client-credentials token lifecycle.
|
|
24
|
+
- `TOKEN_REGIONS` / `DEFAULT_TOKEN_BASE_URL` constants and `Client#auth_mode`.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- `Client.new` no longer requires an API key. It accepts any one of
|
|
28
|
+
`client_id`/`client_secret`, `access_token`, or `api_key`, and raises
|
|
29
|
+
`ConfigurationError` only when none is provided. The API-key path is
|
|
30
|
+
unchanged, so existing `api_key:` callers keep working.
|
|
31
|
+
|
|
32
|
+
## [0.4.0] — 2026-05-22
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
- Request body hash keys are now recursively converted to **camelCase** (not
|
|
36
|
+
PascalCase). This matches the casing used by the BoldSign OpenAPI/Swagger
|
|
37
|
+
schema for all request body DTOs, including nested objects like
|
|
38
|
+
`DocumentSigner` and `FormField`. Callers pass idiomatic snake_case (or
|
|
39
|
+
camelCase / PascalCase) Ruby keys and the gem emits the casing BoldSign
|
|
40
|
+
validates against. **Breaking** for callers that relied on the 0.3.0
|
|
41
|
+
PascalCase output.
|
|
42
|
+
- Multipart file uploads are still keyed `Files` (capital F) for the
|
|
43
|
+
document send endpoint; only request body field names are camelCased.
|
|
44
|
+
- Multipart `send_document`: single-element arrays for both `signers:` and
|
|
45
|
+
`files:` are now unwrapped before encoding. BoldSign's POST
|
|
46
|
+
`/v1/document/send` rejects `signers=[{...}]` (JSON-array string) and
|
|
47
|
+
`Files[]=…` (Faraday's automatic array-bracket form). The gem now emits
|
|
48
|
+
`signers={...}` (single JSON object) and `Files=<file>` (single FilePart)
|
|
49
|
+
for the one-signer/one-file case. Multi-signer / multi-file uploads are
|
|
50
|
+
not yet supported via this helper and raise `NotImplementedError`.
|
|
51
|
+
|
|
52
|
+
### Removed
|
|
53
|
+
- `Boldsign::CaseConvert.pascalize` / `.pascalize_key`. Replaced by
|
|
54
|
+
`.camelize` / `.camelize_key`.
|
|
55
|
+
|
|
10
56
|
## [0.3.0] — 2026-05-22
|
|
11
57
|
|
|
12
58
|
### Changed
|
|
@@ -44,5 +90,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
44
90
|
- 100% line and branch test coverage enforced via SimpleCov.
|
|
45
91
|
- GitHub Actions CI on Ruby 3.4 and 4.0.
|
|
46
92
|
|
|
47
|
-
[Unreleased]: https://github.com/kleinjm/boldsign-ruby/compare/v0.
|
|
93
|
+
[Unreleased]: https://github.com/kleinjm/boldsign-ruby/compare/v0.5.0...HEAD
|
|
94
|
+
[0.5.0]: https://github.com/kleinjm/boldsign-ruby/compare/v0.4.0...v0.5.0
|
|
48
95
|
[0.1.0]: https://github.com/kleinjm/boldsign-ruby/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -17,6 +17,32 @@ gem "boldsign"
|
|
|
17
17
|
|
|
18
18
|
## Configuration
|
|
19
19
|
|
|
20
|
+
The client authenticates with either an API key or OAuth 2.0.
|
|
21
|
+
|
|
22
|
+
### OAuth 2.0 (client credentials)
|
|
23
|
+
|
|
24
|
+
Register an OAuth app in the BoldSign dashboard (API → OAuth Apps) and configure
|
|
25
|
+
its `client_id` / `client_secret`. The client fetches a bearer token from the
|
|
26
|
+
region's account host, caches it until shortly before it expires, and refreshes
|
|
27
|
+
automatically — no token plumbing on your side.
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
Boldsign.configure do |c|
|
|
31
|
+
c.client_id = ENV["BOLDSIGN_CLIENT_ID"]
|
|
32
|
+
c.client_secret = ENV["BOLDSIGN_CLIENT_SECRET"]
|
|
33
|
+
c.region = :us # :us, :eu, :ca, :au
|
|
34
|
+
# c.scope = "BoldSign.Documents.All" # optional; empty grants all the app allows
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
You can also pass a token you obtained yourself (used as-is, not refreshed):
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
Boldsign::Client.new(access_token: "…", region: :us)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### API key
|
|
45
|
+
|
|
20
46
|
```ruby
|
|
21
47
|
Boldsign.configure do |c|
|
|
22
48
|
c.api_key = ENV["BOLDSIGN_API_KEY"]
|
|
@@ -30,14 +56,17 @@ Or instantiate a client directly:
|
|
|
30
56
|
client = Boldsign::Client.new(api_key: "…", region: :us)
|
|
31
57
|
```
|
|
32
58
|
|
|
33
|
-
Region
|
|
59
|
+
Region hosts:
|
|
60
|
+
|
|
61
|
+
| Region | API base URL | OAuth account host |
|
|
62
|
+
| ------ | ------------ | ------------------ |
|
|
63
|
+
| `:us` | `https://api.boldsign.com` | `https://account.boldsign.com` |
|
|
64
|
+
| `:eu` | `https://api-eu.boldsign.com` | `https://account-eu.boldsign.com` |
|
|
65
|
+
| `:ca` | `https://api-ca.boldsign.com` | `https://account-ca.boldsign.com` |
|
|
66
|
+
| `:au` | `https://api-au.boldsign.com` | `https://account-au.boldsign.com` |
|
|
34
67
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
| `:us` | `https://api.boldsign.com` |
|
|
38
|
-
| `:eu` | `https://api-eu.boldsign.com` |
|
|
39
|
-
| `:ca` | `https://api-ca.boldsign.com` |
|
|
40
|
-
| `:au` | `https://api-au.boldsign.com` |
|
|
68
|
+
The token endpoint is the account host + `/connect/token`; override the full URL
|
|
69
|
+
with `token_url:` if a region differs.
|
|
41
70
|
|
|
42
71
|
## Usage
|
|
43
72
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
module Boldsign
|
|
2
|
+
# Fetches and caches an OAuth 2.0 access token using the client credentials
|
|
3
|
+
# grant (server-to-server; no user interaction).
|
|
4
|
+
#
|
|
5
|
+
# A single instance is shared by a {Client} and may be hit from multiple
|
|
6
|
+
# threads, so token reads/refreshes are guarded by a mutex. The token is
|
|
7
|
+
# cached until shortly before it expires ({EXPIRY_LEEWAY_SECONDS}), then a new
|
|
8
|
+
# one is requested on the next call. The client credentials grant does not
|
|
9
|
+
# issue refresh tokens — expiry is handled by re-requesting.
|
|
10
|
+
#
|
|
11
|
+
# @see https://developers.boldsign.com/authentication/oauth-2-0/
|
|
12
|
+
class AccessToken
|
|
13
|
+
# Token endpoint path, appended to the region's account host.
|
|
14
|
+
TOKEN_PATH = "/connect/token".freeze
|
|
15
|
+
|
|
16
|
+
# Default scope requested for the access token. Covers the document /
|
|
17
|
+
# signature endpoints this gem is primarily used for. Pass a different
|
|
18
|
+
# `scope:` to request others (an empty scope grants all the app is allowed).
|
|
19
|
+
DEFAULT_SCOPE = "BoldSign.Documents.All".freeze
|
|
20
|
+
|
|
21
|
+
# Refresh this many seconds before the token actually expires, so an
|
|
22
|
+
# in-flight request never carries a token that lapses server-side mid-call.
|
|
23
|
+
EXPIRY_LEEWAY_SECONDS = 60
|
|
24
|
+
|
|
25
|
+
# Fallback lifetime (seconds) when the token response omits `expires_in`.
|
|
26
|
+
DEFAULT_EXPIRES_IN = 3600
|
|
27
|
+
|
|
28
|
+
# @param client_id [String] OAuth app client ID.
|
|
29
|
+
# @param client_secret [String] OAuth app client secret.
|
|
30
|
+
# @param token_url [String] Full token endpoint URL (host + {TOKEN_PATH}).
|
|
31
|
+
# @param scope [String] OAuth scope to request.
|
|
32
|
+
# @param adapter [Symbol] Faraday adapter.
|
|
33
|
+
def initialize(client_id:, client_secret:, token_url:, scope: DEFAULT_SCOPE,
|
|
34
|
+
adapter: Faraday.default_adapter)
|
|
35
|
+
@client_id = client_id
|
|
36
|
+
@client_secret = client_secret
|
|
37
|
+
@token_url = token_url
|
|
38
|
+
@scope = scope
|
|
39
|
+
@adapter = adapter
|
|
40
|
+
@mutex = Mutex.new
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @return [String] a currently-valid bearer token, fetching or refreshing
|
|
44
|
+
# one if the cached token is missing or near expiry.
|
|
45
|
+
def value
|
|
46
|
+
@mutex.synchronize do
|
|
47
|
+
refresh! if expired?
|
|
48
|
+
@token
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def expired?
|
|
55
|
+
@token.nil? || Time.now >= @expires_at
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def refresh!
|
|
59
|
+
response = connection.post(@token_url) do |req|
|
|
60
|
+
req.headers["Content-Type"] = "application/x-www-form-urlencoded"
|
|
61
|
+
req.body = URI.encode_www_form(
|
|
62
|
+
grant_type: "client_credentials",
|
|
63
|
+
client_id: @client_id,
|
|
64
|
+
client_secret: @client_secret,
|
|
65
|
+
scope: @scope
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
raise_token_error(response) unless response.success?
|
|
69
|
+
|
|
70
|
+
body = JSON.parse(response.body)
|
|
71
|
+
@token = body["access_token"]
|
|
72
|
+
expires_in = (body["expires_in"] || DEFAULT_EXPIRES_IN).to_i
|
|
73
|
+
@expires_at = Time.now + expires_in - EXPIRY_LEEWAY_SECONDS
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def connection
|
|
77
|
+
Faraday.new do |f|
|
|
78
|
+
f.adapter @adapter
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def raise_token_error(response)
|
|
83
|
+
body = begin
|
|
84
|
+
JSON.parse(response.body)
|
|
85
|
+
rescue StandardError
|
|
86
|
+
response.body
|
|
87
|
+
end
|
|
88
|
+
message = body.is_a?(Hash) ? (body["error_description"] || body["error"] || body.to_s) : body.to_s
|
|
89
|
+
raise AuthenticationError.new(
|
|
90
|
+
"BoldSign OAuth token request failed (#{response.status}): #{message}",
|
|
91
|
+
status: response.status, body: body, response: response
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -1,36 +1,39 @@
|
|
|
1
1
|
module Boldsign
|
|
2
|
-
# Converts arbitrary Ruby hash keys (snake_case
|
|
3
|
-
# String) to
|
|
4
|
-
# BoldSign REST API expects in request bodies. Non-Hash and
|
|
5
|
-
# — including binary IO objects, FilePart instances,
|
|
6
|
-
# pass through untouched.
|
|
2
|
+
# Converts arbitrary Ruby hash keys (snake_case, camelCase, or PascalCase;
|
|
3
|
+
# Symbol or String) to camelCase string keys recursively, matching the
|
|
4
|
+
# casing the BoldSign REST API expects in request bodies. Non-Hash and
|
|
5
|
+
# non-Array values — including binary IO objects, FilePart instances,
|
|
6
|
+
# scalars, dates, etc. — pass through untouched.
|
|
7
7
|
module CaseConvert
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
-
# Recursively
|
|
10
|
+
# Recursively camelCase every Hash key reachable from `obj`.
|
|
11
11
|
# @param obj [Object]
|
|
12
|
-
# @return [Object] new object with the same shape and
|
|
13
|
-
def
|
|
12
|
+
# @return [Object] new object with the same shape and camelCase keys
|
|
13
|
+
def camelize(obj)
|
|
14
14
|
case obj
|
|
15
15
|
when Hash
|
|
16
|
-
obj.each_with_object({}) { |(k, v), acc| acc[
|
|
16
|
+
obj.each_with_object({}) { |(k, v), acc| acc[camelize_key(k)] = camelize(v) }
|
|
17
17
|
when Array
|
|
18
|
-
obj.map { |v|
|
|
18
|
+
obj.map { |v| camelize(v) }
|
|
19
19
|
else
|
|
20
20
|
obj
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
# @param key [String, Symbol]
|
|
25
|
-
# @return [String]
|
|
26
|
-
def
|
|
25
|
+
# @return [String] camelCase string
|
|
26
|
+
def camelize_key(key)
|
|
27
27
|
str = key.to_s
|
|
28
28
|
return str if str.empty?
|
|
29
29
|
|
|
30
30
|
if str.include?("_")
|
|
31
|
-
str.split("_").
|
|
31
|
+
parts = str.split("_").reject(&:empty?)
|
|
32
|
+
return str if parts.empty?
|
|
33
|
+
|
|
34
|
+
([parts.first.downcase] + parts.drop(1).map { |part| capitalize_word(part) }).join
|
|
32
35
|
else
|
|
33
|
-
|
|
36
|
+
lowercase_first(str)
|
|
34
37
|
end
|
|
35
38
|
end
|
|
36
39
|
|
|
@@ -39,5 +42,11 @@ module Boldsign
|
|
|
39
42
|
|
|
40
43
|
str[0].upcase + str[1..]
|
|
41
44
|
end
|
|
45
|
+
|
|
46
|
+
def lowercase_first(str)
|
|
47
|
+
return str if str.empty?
|
|
48
|
+
|
|
49
|
+
str[0].downcase + str[1..]
|
|
50
|
+
end
|
|
42
51
|
end
|
|
43
52
|
end
|
data/lib/boldsign/client.rb
CHANGED
|
@@ -8,9 +8,21 @@ module Boldsign
|
|
|
8
8
|
# Low-level verb methods ({#get}, {#post}, {#put}, {#patch}, {#delete},
|
|
9
9
|
# {#download}) are also available for hitting any endpoint directly.
|
|
10
10
|
#
|
|
11
|
-
#
|
|
11
|
+
# Authenticates with either an API key (`X-API-KEY`) or OAuth 2.0
|
|
12
|
+
# (`Authorization: Bearer`). For OAuth, pass `client_id`/`client_secret` and
|
|
13
|
+
# the client fetches + caches a token via the client credentials grant; or
|
|
14
|
+
# pass a `access_token` you obtained yourself.
|
|
15
|
+
#
|
|
16
|
+
# @example API key
|
|
12
17
|
# client = Boldsign::Client.new(api_key: ENV["BOLDSIGN_API_KEY"], region: :us)
|
|
13
18
|
# client.documents.send_document(title: "NDA", signers: [...])
|
|
19
|
+
#
|
|
20
|
+
# @example OAuth client credentials
|
|
21
|
+
# client = Boldsign::Client.new(
|
|
22
|
+
# client_id: ENV["BOLDSIGN_CLIENT_ID"],
|
|
23
|
+
# client_secret: ENV["BOLDSIGN_CLIENT_SECRET"],
|
|
24
|
+
# region: :us
|
|
25
|
+
# )
|
|
14
26
|
class Client
|
|
15
27
|
# Default region (US).
|
|
16
28
|
DEFAULT_BASE_URL = "https://api.boldsign.com".freeze
|
|
@@ -18,25 +30,50 @@ module Boldsign
|
|
|
18
30
|
# User-Agent header value sent on every request.
|
|
19
31
|
USER_AGENT = "boldsign-ruby/#{Boldsign::VERSION}".freeze
|
|
20
32
|
|
|
21
|
-
# @return [String] the API key in use.
|
|
33
|
+
# @return [String, nil] the API key in use, when authenticating via API key.
|
|
22
34
|
attr_reader :api_key
|
|
23
35
|
|
|
36
|
+
# @return [Symbol] the resolved auth mode (`:api_key`, `:oauth`, or `:bearer`).
|
|
37
|
+
attr_reader :auth_mode
|
|
38
|
+
|
|
24
39
|
# @return [String] the resolved base URL (region or explicit override).
|
|
25
40
|
attr_reader :base_url
|
|
26
41
|
|
|
27
42
|
# @param api_key [String, nil] BoldSign API key. Falls back to `ENV["BOLDSIGN_API_KEY"]`.
|
|
28
|
-
# @param
|
|
29
|
-
# @param
|
|
43
|
+
# @param client_id [String, nil] OAuth app client ID (enables the client credentials grant).
|
|
44
|
+
# @param client_secret [String, nil] OAuth app client secret (required with `client_id`).
|
|
45
|
+
# @param access_token [String, nil] A pre-obtained OAuth bearer token (used as-is, not refreshed).
|
|
46
|
+
# @param scope [String, nil] OAuth scope to request (defaults to {AccessToken::DEFAULT_SCOPE}).
|
|
47
|
+
# @param token_url [String, nil] Override for the full OAuth token endpoint URL.
|
|
48
|
+
# @param region [Symbol, nil] One of `:us`, `:eu`, `:ca`, `:au`. Ignored if `base_url`/`token_url` is given.
|
|
49
|
+
# @param base_url [String, nil] Explicit API base URL override.
|
|
30
50
|
# @param adapter [Symbol] Faraday adapter (defaults to `Faraday.default_adapter`).
|
|
31
51
|
# @param logger [Logger, nil] Optional logger; when present, Faraday's logger middleware is enabled.
|
|
32
|
-
# @raise [ConfigurationError] when no
|
|
33
|
-
def initialize(api_key: nil,
|
|
52
|
+
# @raise [ConfigurationError] when no usable credentials are provided.
|
|
53
|
+
def initialize(api_key: nil, client_id: nil, client_secret: nil, access_token: nil,
|
|
54
|
+
scope: nil, token_url: nil, region: nil, base_url: nil,
|
|
55
|
+
adapter: Faraday.default_adapter, logger: nil)
|
|
34
56
|
@api_key = api_key || ENV["BOLDSIGN_API_KEY"]
|
|
35
|
-
|
|
36
|
-
|
|
57
|
+
@static_access_token = access_token
|
|
37
58
|
@base_url = base_url || Boldsign::REGIONS[region&.to_sym] || DEFAULT_BASE_URL
|
|
38
59
|
@adapter = adapter
|
|
39
60
|
@logger = logger
|
|
61
|
+
|
|
62
|
+
if present?(client_id) && present?(client_secret)
|
|
63
|
+
@auth_mode = :oauth
|
|
64
|
+
@access_token = AccessToken.new(
|
|
65
|
+
client_id: client_id, client_secret: client_secret,
|
|
66
|
+
token_url: token_url || default_token_url(region),
|
|
67
|
+
scope: scope || AccessToken::DEFAULT_SCOPE, adapter: adapter
|
|
68
|
+
)
|
|
69
|
+
elsif present?(@static_access_token)
|
|
70
|
+
@auth_mode = :bearer
|
|
71
|
+
elsif present?(@api_key)
|
|
72
|
+
@auth_mode = :api_key
|
|
73
|
+
else
|
|
74
|
+
raise ConfigurationError,
|
|
75
|
+
"Missing BoldSign credentials: provide client_id/client_secret, access_token, or api_key"
|
|
76
|
+
end
|
|
40
77
|
end
|
|
41
78
|
|
|
42
79
|
# @return [Resources::Brand]
|
|
@@ -125,7 +162,7 @@ module Boldsign
|
|
|
125
162
|
req.body = body
|
|
126
163
|
else
|
|
127
164
|
req.headers["Content-Type"] = "application/json"
|
|
128
|
-
req.body = body.is_a?(String) ? body : JSON.generate(CaseConvert.
|
|
165
|
+
req.body = body.is_a?(String) ? body : JSON.generate(CaseConvert.camelize(body))
|
|
129
166
|
end
|
|
130
167
|
end
|
|
131
168
|
end
|
|
@@ -137,7 +174,7 @@ module Boldsign
|
|
|
137
174
|
Faraday.new(url: @base_url) do |f|
|
|
138
175
|
f.request :multipart if multipart
|
|
139
176
|
f.request :url_encoded
|
|
140
|
-
f.headers
|
|
177
|
+
apply_auth(f.headers)
|
|
141
178
|
f.headers["Accept"] = "application/json"
|
|
142
179
|
f.headers["User-Agent"] = USER_AGENT
|
|
143
180
|
f.response :logger, @logger if @logger
|
|
@@ -145,6 +182,27 @@ module Boldsign
|
|
|
145
182
|
end
|
|
146
183
|
end
|
|
147
184
|
|
|
185
|
+
def apply_auth(headers)
|
|
186
|
+
if @auth_mode == :api_key
|
|
187
|
+
headers["X-API-KEY"] = @api_key
|
|
188
|
+
else
|
|
189
|
+
headers["Authorization"] = "Bearer #{bearer_token}"
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def bearer_token
|
|
194
|
+
@auth_mode == :oauth ? @access_token.value : @static_access_token
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def present?(value)
|
|
198
|
+
!value.nil? && !value.empty?
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def default_token_url(region)
|
|
202
|
+
base = Boldsign::TOKEN_REGIONS[region&.to_sym] || Boldsign::DEFAULT_TOKEN_BASE_URL
|
|
203
|
+
"#{base}#{AccessToken::TOKEN_PATH}"
|
|
204
|
+
end
|
|
205
|
+
|
|
148
206
|
def parse_body(response)
|
|
149
207
|
return nil if response.body.nil? || response.body.empty?
|
|
150
208
|
content_type = response.headers["content-type"].to_s
|
|
@@ -73,21 +73,36 @@ module Boldsign
|
|
|
73
73
|
|
|
74
74
|
private
|
|
75
75
|
|
|
76
|
+
# BoldSign's POST /v1/document/send multipart endpoint is fussy about
|
|
77
|
+
# repeated/array-valued parts. Single signers must be sent as one
|
|
78
|
+
# multipart part whose value is the signer JSON OBJECT (not a JSON array
|
|
79
|
+
# containing one object). Files must be sent as a single FilePart value
|
|
80
|
+
# for one upload; Faraday's automatic `Files[]` bracketing breaks the
|
|
81
|
+
# request. For the single-signer, single-file flow (the common case) we
|
|
82
|
+
# unwrap one-element arrays here. Multi-signer / multi-file is not yet
|
|
83
|
+
# supported via this helper and will raise.
|
|
76
84
|
def multipart_send_body(body, files)
|
|
77
85
|
parts = body.each_with_object({}) do |(key, value), acc|
|
|
78
86
|
next if value.nil?
|
|
79
87
|
|
|
80
|
-
|
|
81
|
-
acc[
|
|
82
|
-
value.to_s
|
|
83
|
-
else
|
|
84
|
-
JSON.generate(Boldsign::CaseConvert.pascalize(value))
|
|
85
|
-
end
|
|
88
|
+
camel_key = Boldsign::CaseConvert.camelize_key(key)
|
|
89
|
+
acc[camel_key] = encode_multipart_value(value)
|
|
86
90
|
end
|
|
87
|
-
|
|
91
|
+
files_array = Array(files)
|
|
92
|
+
raise NotImplementedError, "multi-file uploads are not supported yet" if files_array.size > 1
|
|
93
|
+
|
|
94
|
+
parts["Files"] = file_part(files_array.first)
|
|
88
95
|
parts
|
|
89
96
|
end
|
|
90
97
|
|
|
98
|
+
def encode_multipart_value(value)
|
|
99
|
+
return value.to_s if scalar?(value)
|
|
100
|
+
return JSON.generate(Boldsign::CaseConvert.camelize(value)) unless value.is_a?(Array)
|
|
101
|
+
raise NotImplementedError, "multi-element arrays are not supported in multipart bodies yet" if value.size > 1
|
|
102
|
+
|
|
103
|
+
JSON.generate(Boldsign::CaseConvert.camelize(value.first))
|
|
104
|
+
end
|
|
105
|
+
|
|
91
106
|
def file_part(file)
|
|
92
107
|
return file if file.is_a?(Faraday::Multipart::FilePart)
|
|
93
108
|
|
data/lib/boldsign/version.rb
CHANGED
data/lib/boldsign.rb
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
require "faraday"
|
|
2
2
|
require "faraday/multipart"
|
|
3
3
|
require "json"
|
|
4
|
+
require "uri"
|
|
4
5
|
|
|
5
6
|
require_relative "boldsign/version"
|
|
6
7
|
require_relative "boldsign/error"
|
|
7
8
|
require_relative "boldsign/case_convert"
|
|
9
|
+
require_relative "boldsign/access_token"
|
|
8
10
|
require_relative "boldsign/client"
|
|
9
11
|
require_relative "boldsign/resource"
|
|
10
12
|
require_relative "boldsign/resources/brand"
|
|
@@ -24,10 +26,11 @@ require_relative "boldsign/resources/user"
|
|
|
24
26
|
# Configure once at boot and access a shared {Client} via {Boldsign.client}, or
|
|
25
27
|
# instantiate {Client} directly when you need multiple credentials/regions.
|
|
26
28
|
#
|
|
27
|
-
# @example Configure and use the shared client
|
|
29
|
+
# @example Configure with OAuth and use the shared client
|
|
28
30
|
# Boldsign.configure do |c|
|
|
29
|
-
# c.
|
|
30
|
-
# c.
|
|
31
|
+
# c.client_id = ENV["BOLDSIGN_CLIENT_ID"]
|
|
32
|
+
# c.client_secret = ENV["BOLDSIGN_CLIENT_SECRET"]
|
|
33
|
+
# c.region = :us
|
|
31
34
|
# end
|
|
32
35
|
# Boldsign.client.documents.list
|
|
33
36
|
module Boldsign
|
|
@@ -39,10 +42,37 @@ module Boldsign
|
|
|
39
42
|
au: "https://api-au.boldsign.com"
|
|
40
43
|
}.freeze
|
|
41
44
|
|
|
45
|
+
# Region → account host map for the OAuth token endpoint. Non-US hosts follow
|
|
46
|
+
# BoldSign's regional naming; override with `token_url:` if a region differs.
|
|
47
|
+
TOKEN_REGIONS = {
|
|
48
|
+
us: "https://account.boldsign.com",
|
|
49
|
+
eu: "https://account-eu.boldsign.com",
|
|
50
|
+
ca: "https://account-ca.boldsign.com",
|
|
51
|
+
au: "https://account-au.boldsign.com"
|
|
52
|
+
}.freeze
|
|
53
|
+
|
|
54
|
+
# Default account host (US) for the OAuth token endpoint.
|
|
55
|
+
DEFAULT_TOKEN_BASE_URL = "https://account.boldsign.com".freeze
|
|
56
|
+
|
|
42
57
|
class << self
|
|
43
58
|
# @return [String, nil] API key used by the shared {client}.
|
|
44
59
|
attr_accessor :api_key
|
|
45
60
|
|
|
61
|
+
# @return [String, nil] OAuth app client ID used by the shared {client}.
|
|
62
|
+
attr_accessor :client_id
|
|
63
|
+
|
|
64
|
+
# @return [String, nil] OAuth app client secret used by the shared {client}.
|
|
65
|
+
attr_accessor :client_secret
|
|
66
|
+
|
|
67
|
+
# @return [String, nil] Pre-obtained OAuth bearer token (used as-is, not refreshed).
|
|
68
|
+
attr_accessor :access_token
|
|
69
|
+
|
|
70
|
+
# @return [String, nil] OAuth scope to request (defaults to {AccessToken::DEFAULT_SCOPE}).
|
|
71
|
+
attr_accessor :scope
|
|
72
|
+
|
|
73
|
+
# @return [String, nil] Override for the full OAuth token endpoint URL.
|
|
74
|
+
attr_accessor :token_url
|
|
75
|
+
|
|
46
76
|
# @return [Symbol, nil] Region key (`:us`, `:eu`, `:ca`, `:au`).
|
|
47
77
|
attr_accessor :region
|
|
48
78
|
|
|
@@ -58,7 +88,11 @@ module Boldsign
|
|
|
58
88
|
|
|
59
89
|
# @return [Client] memoized shared client built from module-level config.
|
|
60
90
|
def client
|
|
61
|
-
@client ||= Client.new(
|
|
91
|
+
@client ||= Client.new(
|
|
92
|
+
api_key: api_key, client_id: client_id, client_secret: client_secret,
|
|
93
|
+
access_token: access_token, scope: scope, token_url: token_url,
|
|
94
|
+
region: region, base_url: base_url
|
|
95
|
+
)
|
|
62
96
|
end
|
|
63
97
|
|
|
64
98
|
# Clears the memoized shared client so the next call to {client} rebuilds it.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: boldsign
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- James Klein
|
|
@@ -92,6 +92,7 @@ files:
|
|
|
92
92
|
- README.md
|
|
93
93
|
- boldsign.gemspec
|
|
94
94
|
- lib/boldsign.rb
|
|
95
|
+
- lib/boldsign/access_token.rb
|
|
95
96
|
- lib/boldsign/case_convert.rb
|
|
96
97
|
- lib/boldsign/client.rb
|
|
97
98
|
- lib/boldsign/error.rb
|