patient_http 1.0.0 → 1.1.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: c5cda06fbc0db9bc2beb2264a718afb1dedc8c2b27fee4f8e8bd9c417086a6b2
4
- data.tar.gz: edaa46086b479884d618fc144aeb5bd231196424df7061c71fa242c61de4a564
3
+ metadata.gz: bf333a4e860c7ec2acbabd0b2d99b050a2574fd3fe97e7d9cb10b5ba08868fed
4
+ data.tar.gz: ea8fdac974792444d4f82ded653beeb644d39a58a88b2c1d7796029266dd0e39
5
5
  SHA512:
6
- metadata.gz: 617a2dbbdb9df31eb19f9b48d852cfb15f1c43ed26e4d552f0674fcced748c88b90cbbc7d47e7b8e2a0b7b88b8b4285c3cbca5dd09fec6fbee91e6e7b050378c
7
- data.tar.gz: e8ea8de4be59c91cb686b1e4249d57a5c4dc50bc1ccf94e6c0dfd8a091d9b3ceb94544eb580924578d845b074d7a478fddce8a1e694706a814e62bd3d03fd604
6
+ metadata.gz: 1ca730c46c12d7be338959fbb45b4c0863e7b81fc2c09ec43aa5e27f073cf4e6023df63441ffc7fa4b244e7fe560a26041872fc4013ed8dfaf8dfdd5b9390fb4
7
+ data.tar.gz: 4073af1b84aac90f73e41efaded611eff0c46d2521221eedcdbb3dd640269d4fcf2865cb881e88134897825adc0899b022cf9ca041d0ec3a4818c93fade351f3
data/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 1.1.0
8
+
9
+ ### Added
10
+
11
+ - Secret manager for referencing sensitive headers and query parameters indirectly. Register secrets on the `Configuration` with `register_secret` (static value or lazy block), then reference them when building a request via `PatientHttp.secret(name)`. The serialized request stores only a `{"$secret" => name}` reference; the value is resolved by the processor when the request is sent, keeping sensitive values out of the job queue and logs.
12
+
7
13
  ## 1.0.0
8
14
 
9
15
  ### Added
data/README.md CHANGED
@@ -485,6 +485,39 @@ end
485
485
 
486
486
  Encrypted data is stored as `{"__encrypted__" => true, "value" => "<base64>"}`. The `Encryptor` JSON-serializes the original hash, passes the bytes to your callable, and Base64-encodes the result. Decryption reverses the process. Hashes without the `"__encrypted__"` key are passed through unchanged, so un-encrypted historical data continues to work while you roll out encryption.
487
487
 
488
+ ## Secrets
489
+
490
+ Requests are serialized into your job queue before they run. If you put a sensitive value — an API token in an `Authorization` header, or an API key in a query parameter — directly on the request, that value is written into the queue. Requests can be encrypted in the queue, but a better practice is to avoid putting sensitive values on the request at all.
491
+
492
+ The secret manager lets you reference a sensitive values in headers or query parameters by name instead. The serialized request stores only a reference marker (`{"$secret" => "name"}`), never the value. The actual value lives on the `Configuration` (which exists on the processor side) and is resolved at the moment the request is sent.
493
+
494
+ ### Defining secrets
495
+
496
+ Register named secrets on the `Configuration`. A value can be given directly, or as a block that is evaluated lazily each time the secret is resolved (useful for reading from the environment on demand):
497
+
498
+ ```ruby
499
+ config = PatientHttp::Configuration.new
500
+ config.register_secret(:authorization, "Bearer #{ENV['API_TOKEN']}") # static value
501
+ config.register_secret(:api_key) { ENV["MY_API_KEY"] } # lazy block
502
+ ```
503
+
504
+ I a secret is not found when resolving a request, a `PatientHttp::SecretManager::SecretNotFoundError` is raised, which surfaces through the normal request error path.
505
+
506
+ ### Referencing secrets when building a request
507
+
508
+ Use `PatientHttp.secret(name)` anywhere you would put a sensitive header or query parameter value. No value is needed (or available) at build time:
509
+
510
+ ```ruby
511
+ PatientHttp.get(
512
+ "https://api.example.com/data",
513
+ callback: MyCallback,
514
+ headers: {"Authorization" => PatientHttp.secret(:api_token)},
515
+ params: {"api_key" => PatientHttp.secret(:api_key), "page" => 2}
516
+ )
517
+ ```
518
+
519
+ The request serializes the secret header as `{"$secret" => "api_token"}` and keeps the secret query parameter out of the URL (non-secret params like `page` are still folded into the URL as usual). The processor dereferences both just before sending: the header is set to its resolved value and the resolved query parameter is appended to the URL.
520
+
488
521
  ## Configuration
489
522
 
490
523
  ```ruby
@@ -525,6 +558,9 @@ config = PatientHttp::Configuration.new(
525
558
  # Logger instance (default: Logger to STDERR at ERROR level)
526
559
  logger: Logger.new($stdout)
527
560
  )
561
+
562
+ # Register named secrets to reference sensitive headers/params indirectly (see Secrets)
563
+ config.register_secret(:api_token, ENV["MY_API_TOKEN"])
528
564
  ```
529
565
 
530
566
  ### Tuning Tips
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.0
1
+ 1.1.0
@@ -23,11 +23,12 @@ module PatientHttp
23
23
 
24
24
  begin
25
25
  headers = request_headers(request, request_id)
26
+ url = config.secret_manager.resolve_url(request.url, request.secret_params)
26
27
  body = Protocol::HTTP::Body::Buffered.wrap([request.body.to_s]) if request.body
27
28
  timeout = request.timeout || config.request_timeout
28
29
 
29
30
  Async::Task.current.with_timeout(timeout) do
30
- async_response = @client_pool.request(request.http_method, request.url, headers, body)
31
+ async_response = @client_pool.request(request.http_method, url, headers, body)
31
32
  headers_hash = async_response.headers.to_h.transform_values(&:to_s)
32
33
  body = @response_reader.read_body(async_response, headers_hash)
33
34
 
@@ -72,7 +73,8 @@ module PatientHttp
72
73
  end
73
74
 
74
75
  def request_headers(request, request_id)
75
- headers = request.headers.to_h.merge("x-request-id" => request_id)
76
+ headers = config.secret_manager.resolve_headers(request.headers.to_h)
77
+ headers["x-request-id"] = request_id
76
78
  headers["user-agent"] ||= config.user_agent if config.user_agent
77
79
  headers
78
80
  end
@@ -46,6 +46,9 @@ module PatientHttp
46
46
  # @return [Integer] Number of retries for failed requests
47
47
  attr_reader :retries
48
48
 
49
+ # @return [SecretManager] the secret manager instance
50
+ attr_reader :secret_manager
51
+
49
52
  # Initializes a new Configuration with the specified options.
50
53
  #
51
54
  # @param max_connections [Integer] Maximum number of concurrent connections
@@ -75,10 +78,15 @@ module PatientHttp
75
78
  retries: 3,
76
79
  encryption_key: nil
77
80
  )
81
+ @mutex = Mutex.new
82
+
78
83
  # Initialize payload store configuration
79
84
  @payload_stores = {}
80
85
  @default_payload_store_name = nil
81
- @payload_store_mutex = Mutex.new
86
+
87
+ # Initialize secret configuration
88
+ @secrets = {}
89
+ @secret_manager = SecretManager.new
82
90
 
83
91
  @encryptor = nil
84
92
 
@@ -215,6 +223,33 @@ module PatientHttp
215
223
  @encryptor ||= Encryptor.new(encryption: @encryption, decryption: @decryption)
216
224
  end
217
225
 
226
+ # Register a named secret whose value can be referenced indirectly when building
227
+ # requests via {PatientHttp.secret}.
228
+ #
229
+ # The value can be provided directly or as a block (callable). A block is invoked
230
+ # lazily with the secret name each time the secret is resolved, which is useful for
231
+ # values that should be read on demand (for example, from the environment).
232
+ #
233
+ # @param name [String, Symbol] the secret name
234
+ # @param value [Object, nil] the secret value (omit when providing a block)
235
+ # @yield [name] a block that returns the secret value (omit when providing a value)
236
+ # @raise [ArgumentError] if neither or both of value and block are provided
237
+ # @return [void]
238
+ def register_secret(name, value = nil, &block)
239
+ if value.nil? && block.nil?
240
+ raise ArgumentError.new("register_secret requires a value or a block")
241
+ end
242
+
243
+ if !value.nil? && block
244
+ raise ArgumentError.new("register_secret accepts either a value or a block, not both")
245
+ end
246
+
247
+ @mutex.synchronize do
248
+ @secrets[name.to_s] = block || value
249
+ @secret_manager = SecretManager.new(secrets: @secrets.dup)
250
+ end
251
+ end
252
+
218
253
  # Register a payload store for external storage of large payloads.
219
254
  #
220
255
  # The name is included in the serialized references to the stored data.
@@ -243,8 +278,8 @@ module PatientHttp
243
278
 
244
279
  store = PayloadStore::Base.create(adapter, **options)
245
280
 
246
- @payload_store_mutex.synchronize do
247
- @payload_stores[name] = store
281
+ @mutex.synchronize do
282
+ @payload_stores = @payload_stores.merge(name => store)
248
283
  @default_payload_store_name = name
249
284
  end
250
285
  end
@@ -254,33 +289,25 @@ module PatientHttp
254
289
  # @param name [Symbol, String, nil] Store name. If nil, returns the default store.
255
290
  # @return [PayloadStore::Base, nil] The store instance or nil if not found
256
291
  def payload_store(name = nil)
257
- @payload_store_mutex.synchronize do
258
- if name.nil?
259
- return nil unless @default_payload_store_name
292
+ if name.nil?
293
+ return nil unless @default_payload_store_name
260
294
 
261
- @payload_stores[@default_payload_store_name]
262
- else
263
- @payload_stores[name.to_sym]
264
- end
295
+ @payload_stores[@default_payload_store_name]
296
+ else
297
+ @payload_stores[name.to_sym]
265
298
  end
266
299
  end
267
300
 
268
301
  # Get the name of the default payload store.
269
302
  #
270
303
  # @return [Symbol, nil] The default store name or nil if none registered
271
- def default_payload_store_name
272
- @payload_store_mutex.synchronize do
273
- @default_payload_store_name
274
- end
275
- end
304
+ attr_reader :default_payload_store_name
276
305
 
277
306
  # Get all registered payload stores.
278
307
  #
279
308
  # @return [Hash{Symbol => PayloadStore::Base}] Copy of registered stores
280
309
  def payload_stores
281
- @payload_store_mutex.synchronize do
282
- @payload_stores.dup
283
- end
310
+ @payload_stores.dup
284
311
  end
285
312
 
286
313
  # Convert to hash for inspection
@@ -300,7 +327,8 @@ module PatientHttp
300
327
  "proxy_url" => proxy_url,
301
328
  "retries" => retries,
302
329
  "payload_stores" => payload_stores.keys,
303
- "default_payload_store" => default_payload_store_name
330
+ "default_payload_store" => default_payload_store_name,
331
+ "secrets" => @mutex.synchronize { @secrets.keys }
304
332
  }
305
333
  end
306
334
 
@@ -34,6 +34,10 @@ module PatientHttp
34
34
  # @return [Integer, nil] Maximum number of redirects to follow (nil uses config default, 0 disables)
35
35
  attr_reader :max_redirects
36
36
 
37
+ # @return [Hash{String, Symbol => SecretReference}] Query parameters whose values are
38
+ # secret references, kept out of the serialized URL and resolved at send time
39
+ attr_reader :secret_params
40
+
37
41
  class << self
38
42
  # Reconstruct a Request from a hash
39
43
  #
@@ -43,12 +47,31 @@ module PatientHttp
43
47
  new(
44
48
  hash["http_method"].to_sym,
45
49
  hash["url"],
46
- headers: hash["headers"],
50
+ headers: load_headers(hash["headers"]),
47
51
  body: Payload.load(hash["body"])&.value,
52
+ params: load_secret_params(hash["secret_params"]),
48
53
  timeout: hash["timeout"],
49
54
  max_redirects: hash["max_redirects"]
50
55
  )
51
56
  end
57
+
58
+ private
59
+
60
+ # Convert serialized secret-reference header markers back into SecretReference
61
+ # objects, leaving plain header values unchanged.
62
+ def load_headers(headers)
63
+ return headers if headers.nil?
64
+
65
+ headers.transform_values { |value| SecretReference.load(value) }
66
+ end
67
+
68
+ # Reconstruct secret params from their serialized markers. Returned as a params
69
+ # hash so the constructor folds them back into the request's secret params.
70
+ def load_secret_params(secret_params)
71
+ return nil if secret_params.nil? || secret_params.empty?
72
+
73
+ secret_params.transform_values { |value| SecretReference.load(value) }
74
+ end
52
75
  end
53
76
 
54
77
  # Initializes a new Request.
@@ -77,6 +100,7 @@ module PatientHttp
77
100
  raise ArgumentError.new("url must be a String or URI, got: #{url.class}")
78
101
  end
79
102
 
103
+ @secret_params = {}
80
104
  @url = normalized_url(url, params)
81
105
  @headers = headers.is_a?(HttpHeaders) ? headers : HttpHeaders.new(headers)
82
106
  @body = (body == "") ? nil : body
@@ -109,23 +133,49 @@ module PatientHttp
109
133
  #
110
134
  # @return [Hash]
111
135
  def as_json
112
- {
136
+ hash = {
113
137
  "http_method" => @http_method.to_s,
114
138
  "url" => @url.to_s,
115
- "headers" => @headers.to_h,
139
+ "headers" => serialized_headers,
116
140
  "body" => @payload&.as_json,
117
141
  "timeout" => @timeout,
118
142
  "max_redirects" => @max_redirects
119
143
  }
144
+
145
+ if @secret_params.any?
146
+ hash["secret_params"] = @secret_params.transform_values(&:as_json)
147
+ end
148
+
149
+ hash
120
150
  end
121
151
 
122
152
  private
123
153
 
154
+ # Header values may be SecretReference objects; serialize those as markers.
155
+ def serialized_headers
156
+ @headers.to_h.transform_values do |value|
157
+ value.is_a?(SecretReference) ? value.as_json : value
158
+ end
159
+ end
160
+
124
161
  def normalized_url(url, params)
125
162
  uri = url.is_a?(URI::Generic) ? url.dup : URI(url.to_s)
126
163
  return uri.to_s unless params&.any?
127
164
 
128
- serialized_params = URI.encode_www_form(params)
165
+ # Partition out secret params: they are kept off the serialized URL and resolved
166
+ # at send time by the processor. Only non-secret params are folded into the URL.
167
+ regular_params = {}
168
+ params.each do |key, value|
169
+ if SecretReference.reference?(value)
170
+ @secret_params[key] = SecretReference.load(value)
171
+ else
172
+ regular_params[key] = value
173
+ end
174
+ end
175
+
176
+ return uri.to_s if regular_params.empty?
177
+
178
+ serialized_params = URI.encode_www_form(regular_params)
129
179
  uri.query = [uri.query, serialized_params].compact.reject(&:empty?).join("&")
130
180
  uri.to_s
131
181
  end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PatientHttp
4
+ # Resolves {SecretReference} values into their actual secret values when a request
5
+ # is sent by the processor.
6
+ #
7
+ # A SecretManager is built from the secrets registered on the {Configuration}.
8
+ #
9
+ # @see Configuration#secret_manager
10
+ class SecretManager
11
+ # Raised when a referenced secret cannot be resolved.
12
+ class SecretNotFoundError < StandardError; end
13
+
14
+ # Initialize a new SecretManager.
15
+ #
16
+ # @param secrets [Hash{String => Object}] static registry mapping names to values
17
+ # (a value may be a callable, which is invoked with the name to produce the value)
18
+ # secret not found in the static registry
19
+ def initialize(secrets: {})
20
+ @secrets = secrets || {}
21
+ end
22
+
23
+ # Check if a secret name is registered in the static registry.
24
+ #
25
+ # @param name [String, Symbol] the secret name
26
+ # @return [Boolean] true if the name is registered, false otherwise
27
+ def include?(name)
28
+ @secrets.include?(name.to_s)
29
+ end
30
+
31
+ # Resolve a secret by name.
32
+ #
33
+ # The static registry is checked first; if the registered value responds to #call
34
+ # it is invoked with the name. If the name is not in the registry, an error is raised.
35
+ #
36
+ # @param name [String, Symbol] the secret name
37
+ # @return [String] the resolved secret value
38
+ # @raise [SecretNotFoundError] if the secret cannot be resolved
39
+ def resolve(name)
40
+ name = name.to_s
41
+
42
+ unless @secrets.include?(name)
43
+ raise SecretNotFoundError.new("No secret registered for #{name.inspect}")
44
+ end
45
+
46
+ value = @secrets[name]
47
+ value = value.call(name) if value.respond_to?(:call)
48
+ value&.to_s
49
+ end
50
+
51
+ # Resolve any secret references in a headers hash, returning a new hash.
52
+ #
53
+ # @param headers [Hash, nil] header name/value pairs
54
+ # @return [Hash, nil] a new hash with secret references replaced by resolved values
55
+ def resolve_headers(headers)
56
+ resolve_values(headers)
57
+ end
58
+
59
+ # Resolve any secret references in a params hash, returning a new hash.
60
+ #
61
+ # @param params [Hash, nil] param name/value pairs
62
+ # @return [Hash, nil] a new hash with secret references replaced by resolved values
63
+ def resolve_params(params)
64
+ resolve_values(params)
65
+ end
66
+
67
+ # Append resolved secret params to a URL's query string.
68
+ #
69
+ # @param url [String] the request URL
70
+ # @param secret_params [Hash, nil] secret param name/value (SecretReference) pairs
71
+ # @return [String] the URL with resolved secret params appended (unchanged if none)
72
+ def resolve_url(url, secret_params)
73
+ return url if secret_params.nil? || secret_params.empty?
74
+
75
+ serialized_params = URI.encode_www_form(resolve_params(secret_params))
76
+ uri = URI(url)
77
+ uri.query = [uri.query, serialized_params].compact.reject(&:empty?).join("&")
78
+ uri.to_s
79
+ end
80
+
81
+ private
82
+
83
+ # Return a new hash with any secret-reference values replaced by their resolved
84
+ # values. Non-secret values are passed through unchanged.
85
+ def resolve_values(hash)
86
+ return hash if hash.nil?
87
+
88
+ hash.each_with_object({}) do |(key, value), result|
89
+ result[key] = if SecretReference.reference?(value)
90
+ resolve(SecretReference.load(value).name)
91
+ else
92
+ value
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PatientHttp
4
+ # A reference to a named secret that can be used as a header or query parameter
5
+ # value when building a {Request}.
6
+ #
7
+ # A SecretReference holds only the secret's name -- never its value. When a request
8
+ # is serialized (for example, to be enqueued in a job system), the reference is
9
+ # serialized as a lightweight marker (`{"$secret" => name}`) so the sensitive value
10
+ # is never written to the queue or logs. The actual value is resolved on the
11
+ # processor side at the moment the request is sent, using the secrets registered on
12
+ # the {Configuration}.
13
+ #
14
+ # @example Referencing a secret when building a request
15
+ # PatientHttp.get(
16
+ # "https://api.example.com/data",
17
+ # callback: MyCallback,
18
+ # headers: {"Authorization" => PatientHttp.secret(:api_token)},
19
+ # params: {"api_key" => PatientHttp.secret(:api_key)}
20
+ # )
21
+ class SecretReference
22
+ # Key used in serialized JSON to indicate a secret reference.
23
+ REFERENCE_KEY = "$secret"
24
+
25
+ # @return [String] the name of the referenced secret
26
+ attr_reader :name
27
+
28
+ class << self
29
+ # Check if a value is a secret reference (either a SecretReference instance or a
30
+ # serialized marker hash).
31
+ #
32
+ # @param value [Object] the value to check
33
+ # @return [Boolean] true if the value is a secret reference
34
+ def reference?(value)
35
+ value.is_a?(SecretReference) ||
36
+ (value.is_a?(Hash) && value.key?(REFERENCE_KEY))
37
+ end
38
+
39
+ # Reconstruct a SecretReference from a serialized marker hash. Any other value
40
+ # (including an existing SecretReference) is returned unchanged.
41
+ #
42
+ # @param value [Object] a serialized marker hash or any other value
43
+ # @return [Object] a SecretReference for a marker hash, otherwise the original value
44
+ def load(value)
45
+ return value unless value.is_a?(Hash) && value.key?(REFERENCE_KEY)
46
+
47
+ new(value[REFERENCE_KEY])
48
+ end
49
+ end
50
+
51
+ # Initialize a new SecretReference.
52
+ #
53
+ # @param name [String, Symbol] the name of the secret to reference
54
+ # @raise [ArgumentError] if the name is empty
55
+ def initialize(name)
56
+ @name = name.to_s
57
+ raise ArgumentError.new("secret name cannot be empty") if @name.empty?
58
+ end
59
+
60
+ # Serialize to a marker hash. Only the name is included; the value is never present.
61
+ #
62
+ # @return [Hash] the marker hash
63
+ def as_json
64
+ {REFERENCE_KEY => name}
65
+ end
66
+
67
+ def ==(other)
68
+ other.is_a?(SecretReference) && other.name == name
69
+ end
70
+ alias_method :eql?, :==
71
+
72
+ def hash
73
+ [self.class, name].hash
74
+ end
75
+
76
+ # Inspect the reference. Only the name is shown (there is no value to leak).
77
+ #
78
+ # @return [String]
79
+ def inspect
80
+ "#<PatientHttp::SecretReference name=#{name.inspect}>"
81
+ end
82
+ end
83
+ end
@@ -39,11 +39,12 @@ module PatientHttp
39
39
  timeout = @task.request.timeout || @config.request_timeout
40
40
 
41
41
  response_data = Async::Task.current.with_timeout(timeout) do
42
- headers = @task.request.headers.to_h.merge("x-request-id" => @task.id)
42
+ headers = @config.secret_manager.resolve_headers(@task.request.headers.to_h)
43
+ headers["x-request-id"] = @task.id
43
44
  headers["user-agent"] ||= @config.user_agent if @config.user_agent
44
45
  body = Protocol::HTTP::Body::Buffered.wrap([@task.request.body.to_s]) if @task.request.body
45
46
 
46
- endpoint = Async::HTTP::Endpoint.parse(@task.request.url)
47
+ endpoint = Async::HTTP::Endpoint.parse(request_url)
47
48
  endpoint = configure_endpoint(endpoint) if @config.connection_timeout
48
49
 
49
50
  verb = @task.request.http_method.to_s.upcase
@@ -124,11 +125,18 @@ module PatientHttp
124
125
 
125
126
  private
126
127
 
128
+ # Resolve the current task's request URL, appending any secret query params.
129
+ #
130
+ # @return [String] the resolved request URL
131
+ def request_url
132
+ @config.secret_manager.resolve_url(@task.request.url, @task.request.secret_params)
133
+ end
134
+
127
135
  # Create HTTP client with config settings (retries, proxy, connection timeout).
128
136
  #
129
137
  # @return [Protocol::HTTP::AcceptEncoding] wrapped HTTP client
130
138
  def create_http_client
131
- endpoint = Async::HTTP::Endpoint.parse(@task.request.url)
139
+ endpoint = Async::HTTP::Endpoint.parse(request_url)
132
140
  endpoint = configure_endpoint(endpoint) if @config.connection_timeout
133
141
 
134
142
  client = if @config.proxy_url
data/lib/patient_http.rb CHANGED
@@ -66,6 +66,8 @@ module PatientHttp
66
66
  autoload :RequestTemplate, File.join(__dir__, "patient_http/request_template")
67
67
  autoload :Response, File.join(__dir__, "patient_http/response")
68
68
  autoload :ResponseReader, File.join(__dir__, "patient_http/response_reader")
69
+ autoload :SecretManager, File.join(__dir__, "patient_http/secret_manager")
70
+ autoload :SecretReference, File.join(__dir__, "patient_http/secret_reference")
69
71
  autoload :ServerError, File.join(__dir__, "patient_http/http_error")
70
72
  autoload :SynchronousExecutor, File.join(__dir__, "patient_http/synchronous_executor")
71
73
  autoload :TaskHandler, File.join(__dir__, "patient_http/task_handler")
@@ -258,6 +260,19 @@ module PatientHttp
258
260
  )
259
261
  end
260
262
 
263
+ # Build a reference to a named secret for use as a sensitive header or query
264
+ # parameter value when building a request.
265
+ #
266
+ # The reference holds only the secret's name; the value is resolved on the
267
+ # processor side at send time using the secrets registered on the configuration.
268
+ #
269
+ # @param name [String, Symbol] the name of the secret to reference
270
+ # @return [SecretReference] a reference to the named secret
271
+ # @see Configuration#register_secret
272
+ def secret(name)
273
+ SecretReference.new(name)
274
+ end
275
+
261
276
  private
262
277
 
263
278
  # Validates that the handler accepts the required keyword arguments.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: patient_http
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Durand
@@ -129,6 +129,8 @@ files:
129
129
  - lib/patient_http/request_template.rb
130
130
  - lib/patient_http/response.rb
131
131
  - lib/patient_http/response_reader.rb
132
+ - lib/patient_http/secret_manager.rb
133
+ - lib/patient_http/secret_reference.rb
132
134
  - lib/patient_http/synchronous_executor.rb
133
135
  - lib/patient_http/task_handler.rb
134
136
  - lib/patient_http/time_helper.rb